전략편집기 수정

This commit is contained in:
Macbook
2026-05-25 17:56:25 +09:00
parent aac6454724
commit 02d14e4b2b
39 changed files with 1974 additions and 288 deletions
@@ -1,6 +1,12 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import type { Theme } from '../../types';
import { readStoredSize, storeSize, usePanelResize } from '../strategyEditor/usePanelResize';
import {
readStoredBool,
readStoredSize,
storeBool,
storeSize,
usePanelResize,
} from '../strategyEditor/usePanelResize';
import '../../styles/strategyEditorTheme.css';
import '../../styles/builderPageShell.css';
@@ -36,10 +42,58 @@ export interface BuilderPageShellProps {
rightStorageKey?: string;
rightDefaultWidth?: number;
footerStorageKey?: string;
/** 실시간 차트처럼 좌·우 패널 접기/펼치기 */
collapsiblePanels?: boolean;
leftCollapsedStorageKey?: string;
rightCollapsedStorageKey?: string;
loading?: boolean;
loadingText?: string;
}
function PanelCollapseHandle({
side,
open,
onToggle,
label,
}: {
side: 'left' | 'right';
open: boolean;
onToggle: () => void;
label: string;
}) {
return (
<button
type="button"
className={`bps-panel-handle bps-panel-handle--${side}${open ? ' bps-panel-handle--open' : ''}`}
onClick={e => {
e.stopPropagation();
onToggle();
}}
title={open ? `${label} 닫기` : `${label} 열기`}
aria-expanded={open}
aria-label={open ? `${label} 닫기` : `${label} 열기`}
>
<svg
width="8"
height="14"
viewBox="0 0 8 14"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
{side === 'left' ? (
open ? <polyline points="6,1 2,7 6,13" /> : <polyline points="2,1 6,7 2,13" />
) : (
open ? <polyline points="2,1 6,7 2,13" /> : <polyline points="6,1 2,7 6,13" />
)}
</svg>
</button>
);
}
export default function BuilderPageShell({
theme,
title,
@@ -62,11 +116,24 @@ export default function BuilderPageShell({
rightStorageKey = 'bps-right-width',
rightDefaultWidth = RIGHT_DEFAULT,
footerStorageKey = 'bps-footer-height',
collapsiblePanels = false,
leftCollapsedStorageKey,
rightCollapsedStorageKey,
loading = false,
loadingText = '로딩 중…',
}: BuilderPageShellProps) {
const [leftWidth, setLeftWidth] = useState(() => readStoredSize(leftStorageKey, leftDefaultWidth));
const [rightWidth, setRightWidth] = useState(() => readStoredSize(rightStorageKey, rightDefaultWidth));
const [leftOpen, setLeftOpen] = useState(() =>
collapsiblePanels && leftCollapsedStorageKey
? readStoredBool(leftCollapsedStorageKey, true)
: true,
);
const [rightOpen, setRightOpen] = useState(() =>
collapsiblePanels && rightCollapsedStorageKey
? readStoredBool(rightCollapsedStorageKey, true)
: true,
);
const [footerHeight, setFooterHeight] = useState(() => readStoredSize(footerStorageKey, FOOTER_DEFAULT));
const leftWidthRef = useRef(leftWidth);
const rightWidthRef = useRef(rightWidth);
@@ -124,9 +191,34 @@ export default function BuilderPageShell({
v => storeSize(footerStorageKey, v),
);
const toggleLeft = useCallback(() => {
setLeftOpen(prev => {
const next = !prev;
if (leftCollapsedStorageKey) storeBool(leftCollapsedStorageKey, next);
return next;
});
}, [leftCollapsedStorageKey]);
const toggleRight = useCallback(() => {
setRightOpen(prev => {
const next = !prev;
if (rightCollapsedStorageKey) storeBool(rightCollapsedStorageKey, next);
return next;
});
}, [rightCollapsedStorageKey]);
const bodyStyle = useMemo(() => ({
'--bps-footer-height': `${footerHeight}px`,
}) as React.CSSProperties, [footerHeight]);
'--bps-left-width': leftOpen ? `${leftWidth}px` : '0px',
'--bps-right-width': rightOpen ? `${rightWidth}px` : '0px',
}) as React.CSSProperties, [footerHeight, leftOpen, leftWidth, rightOpen, rightWidth]);
const centerContentClass = [
'bps-center-content',
collapsiblePanels && !leftOpen ? 'bps-center-content--left-collapsed' : '',
collapsiblePanels && !rightOpen ? 'bps-center-content--right-collapsed' : '',
collapsiblePanels && !leftOpen && !rightOpen ? 'bps-center-content--both-collapsed' : '',
].filter(Boolean).join(' ');
if (loading) {
return (
@@ -155,39 +247,76 @@ export default function BuilderPageShell({
{banner}
<div className="bps-body" style={bodyStyle}>
<aside className="bps-left" style={{ width: leftWidth }}>
<div className="bps-panel">
{leftTabs ? (
<>
{leftTabs}
<div className="bps-panel-body bps-panel-body--tabs">{left}</div>
</>
) : (
<>
<div className="bps-panel-head">
<h2 className="bps-panel-title">{leftTitle}</h2>
{leftActions && <div className="bps-panel-actions">{leftActions}</div>}
</div>
<div className="bps-panel-body">{left}</div>
</>
)}
{collapsiblePanels ? (
<div className={`bps-side-wrap bps-side-wrap--left${leftOpen ? ' bps-side-wrap--open' : ''}`}>
<aside
className={`bps-left bps-left--collapsible${leftOpen ? ' bps-left--collapsible--open' : ''}`}
>
<div className="bps-panel">
{leftTabs ? (
<>
{leftTabs}
<div className="bps-panel-body bps-panel-body--tabs">{left}</div>
</>
) : (
<>
<div className="bps-panel-head">
<h2 className="bps-panel-title">{leftTitle}</h2>
{leftActions && <div className="bps-panel-actions">{leftActions}</div>}
</div>
<div className="bps-panel-body">{left}</div>
</>
)}
</div>
</aside>
<PanelCollapseHandle side="left" open={leftOpen} onToggle={toggleLeft} label="좌측 패널" />
</div>
</aside>
<div
className="bps-splitter bps-splitter--v se-splitter se-splitter--v"
role="separator"
aria-orientation="vertical"
aria-label="좌측 패널 너비 조절"
onPointerDown={onLeftSplitter}
/>
) : null}
{collapsiblePanels && leftOpen ? (
<div
className="bps-splitter bps-splitter--v se-splitter se-splitter--v"
role="separator"
aria-orientation="vertical"
aria-label="좌측 패널 너비 조절"
onPointerDown={onLeftSplitter}
/>
) : null}
{!collapsiblePanels ? (
<>
<aside className="bps-left" style={{ width: leftWidth }}>
<div className="bps-panel">
{leftTabs ? (
<>
{leftTabs}
<div className="bps-panel-body bps-panel-body--tabs">{left}</div>
</>
) : (
<>
<div className="bps-panel-head">
<h2 className="bps-panel-title">{leftTitle}</h2>
{leftActions && <div className="bps-panel-actions">{leftActions}</div>}
</div>
<div className="bps-panel-body">{left}</div>
</>
)}
</div>
</aside>
<div
className="bps-splitter bps-splitter--v se-splitter se-splitter--v"
role="separator"
aria-orientation="vertical"
aria-label="좌측 패널 너비 조절"
onPointerDown={onLeftSplitter}
/>
</>
) : null}
<div className="bps-main">
<div className="bps-main-row">
<main className="bps-center">
<div className="bps-center-work">
{centerHead && <div className="bps-center-head">{centerHead}</div>}
<div className="bps-center-content">{center}</div>
<div className={centerContentClass}>{center}</div>
</div>
{footer && (
@@ -207,7 +336,30 @@ export default function BuilderPageShell({
)}
</main>
{right && (
{right && collapsiblePanels && rightOpen ? (
<div
className="bps-splitter bps-splitter--v se-splitter se-splitter--v"
role="separator"
aria-orientation="vertical"
aria-label="우측 패널 너비 조절"
onPointerDown={handleRightSplitter}
/>
) : null}
{right && (collapsiblePanels ? (
<div className={`bps-side-wrap bps-side-wrap--right${rightOpen ? ' bps-side-wrap--open' : ''}`}>
<PanelCollapseHandle side="right" open={rightOpen} onToggle={toggleRight} label="우측 패널" />
<aside
className={`bps-right bps-right--collapsible${rightOpen ? ' bps-right--collapsible--open' : ''}`}
>
{rightTabs ?? (rightTitle ? (
<div className="bps-panel-head" style={{ margin: 0, padding: '12px 12px 10px', borderBottom: '1px solid var(--se-border)' }}>
<h2 className="bps-panel-title">{rightTitle}</h2>
</div>
) : null)}
<div className="bps-right-body">{right}</div>
</aside>
</div>
) : (
<>
<div
className="bps-splitter bps-splitter--v se-splitter se-splitter--v"
@@ -228,7 +380,7 @@ export default function BuilderPageShell({
<div className="bps-right-body">{right}</div>
</aside>
</>
)}
))}
</div>
</div>
</div>