전략편집기 수정
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user