From 779bc316940dc0be7117fe12ee06cade090c71c8 Mon Sep 17 00:00:00 2001 From: Macbook Date: Tue, 16 Jun 2026 15:02:59 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EB=9E=B5=ED=8F=89=EA=B0=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.css | 32 +++++--- frontend/src/components/ChartMagnifier.tsx | 64 +++++++++++---- .../src/components/StrategyEvaluationPage.tsx | 44 ++++++++--- frontend/src/styles/strategyEvaluation.css | 79 +++++++++++++++++++ 4 files changed, 182 insertions(+), 37 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index 54818ca..6392f51 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -5608,15 +5608,14 @@ html.desktop-client .tmb-logo-version { position: fixed; /* wrapper 밖으로도 자유롭게 이동 */ z-index: 2000; border-radius: 6px; - cursor: grab; - overflow: hidden; + cursor: default; + overflow: visible; box-shadow: 0 4px 24px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,200,50,0.45); background: #111218; display: flex; flex-direction: column; - /* pointer-events: all — 내부 drag/resize 가 동작하도록 */ user-select: none; } @@ -5634,8 +5633,6 @@ html.desktop-client .tmb-logo-version { } .chart-magnifier-header:active { cursor: grabbing; } .chart-magnifier:active { cursor: default; } -/* 리사이즈 핸들 위에서는 grab 커서 대신 방향 커서 표시 */ -.chart-magnifier-handle { cursor: inherit; } .chart-magnifier-icon { opacity: 0.75; @@ -5737,18 +5734,29 @@ html.desktop-client .tmb-logo-version { .chart-magnifier-close:hover { background: rgba(220,60,60,0.5); color: #fff; } /* 확대 캔버스 */ -.chart-magnifier-canvas { +.chart-magnifier-body { flex: 1; - display: block; - width: 100%; - height: 0; /* flex:1 로 채워짐 */ + min-height: 0; + overflow: hidden; + border-radius: 0 0 6px 6px; } -/* 리사이즈 핸들 */ +.chart-magnifier-canvas { + display: block; + width: 100%; + height: 100%; +} + +/* 리사이즈 핸들 — 8방향 (모서리·변) */ .chart-magnifier-handle { - background: rgba(255,200,50,0.6); + position: absolute; + width: 14px; + height: 14px; + background: rgba(255,200,50,0.75); + border: 1px solid rgba(255,200,50,0.95); border-radius: 2px; - z-index: 10; + z-index: 20; + touch-action: none; } .chart-magnifier-handle:hover { background: rgba(255,200,50,1); diff --git a/frontend/src/components/ChartMagnifier.tsx b/frontend/src/components/ChartMagnifier.tsx index f9e3007..e251dc0 100644 --- a/frontend/src/components/ChartMagnifier.tsx +++ b/frontend/src/components/ChartMagnifier.tsx @@ -32,15 +32,18 @@ const Y_STEPS: number[] = [1, 1.5, 2, 3, 5, 8, 12]; type ResizeDir = 'nw'|'n'|'ne'|'e'|'se'|'s'|'sw'|'w'; +const HANDLE_SIZE = 14; +const HANDLE_OFFSET = -Math.round(HANDLE_SIZE / 2); + const HANDLES: { id: ResizeDir; cursor: string; style: React.CSSProperties }[] = [ - { id:'nw', cursor:'nw-resize', style:{ top:-5, left:-5 } }, - { id:'n', cursor:'n-resize', style:{ top:-5, left:'50%', transform:'translateX(-50%)' } }, - { id:'ne', cursor:'ne-resize', style:{ top:-5, right:-5 } }, - { id:'e', cursor:'e-resize', style:{ top:'50%', right:-5, transform:'translateY(-50%)' } }, - { id:'se', cursor:'se-resize', style:{ bottom:-5, right:-5 } }, - { id:'s', cursor:'s-resize', style:{ bottom:-5, left:'50%',transform:'translateX(-50%)' } }, - { id:'sw', cursor:'sw-resize', style:{ bottom:-5, left:-5 } }, - { id:'w', cursor:'w-resize', style:{ top:'50%', left:-5, transform:'translateY(-50%)' } }, + { id:'nw', cursor:'nw-resize', style:{ top:HANDLE_OFFSET, left:HANDLE_OFFSET } }, + { id:'n', cursor:'n-resize', style:{ top:HANDLE_OFFSET, left:'50%', transform:'translateX(-50%)' } }, + { id:'ne', cursor:'ne-resize', style:{ top:HANDLE_OFFSET, right:HANDLE_OFFSET } }, + { id:'e', cursor:'e-resize', style:{ top:'50%', right:HANDLE_OFFSET, transform:'translateY(-50%)' } }, + { id:'se', cursor:'se-resize', style:{ bottom:HANDLE_OFFSET, right:HANDLE_OFFSET } }, + { id:'s', cursor:'s-resize', style:{ bottom:HANDLE_OFFSET, left:'50%', transform:'translateX(-50%)' } }, + { id:'sw', cursor:'sw-resize', style:{ bottom:HANDLE_OFFSET, left:HANDLE_OFFSET } }, + { id:'w', cursor:'w-resize', style:{ top:'50%', left:HANDLE_OFFSET, transform:'translateY(-50%)' } }, ]; function nearestStepIndex(steps: number[], cur: number): number { @@ -257,14 +260,12 @@ const ChartMagnifier: React.FC = ({ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); }, []); - const onHandleMove = useCallback((e: React.PointerEvent) => { + const applyResize = useCallback((clientX: number, clientY: number) => { const rs = resizeStart.current; if (!rs) return; - e.preventDefault(); - e.stopPropagation(); const { dir, mx, my, px, py, pw, ph } = rs; - const dx = e.clientX - mx; - const dy = e.clientY - my; + const dx = clientX - mx; + const dy = clientY - my; let nx = px, ny = py, nw = pw, nh = ph; if (dir.includes('e')) { nw = Math.max(MIN_W, pw + dx); } if (dir.includes('w')) { nw = Math.max(MIN_W, pw - dx); nx = px + pw - nw; } @@ -278,11 +279,38 @@ const ChartMagnifier: React.FC = ({ setSize({ w: nw, h: nh }); }, []); + const onHandleMove = useCallback((e: React.PointerEvent) => { + if (!resizeStart.current) return; + e.preventDefault(); + e.stopPropagation(); + applyResize(e.clientX, e.clientY); + }, [applyResize]); + const onHandleUp = useCallback((e: React.PointerEvent) => { e.stopPropagation(); resizeStart.current = null; }, []); + useEffect(() => { + if (!enabled) return; + const onWindowMove = (e: PointerEvent) => { + if (!resizeStart.current) return; + e.preventDefault(); + applyResize(e.clientX, e.clientY); + }; + const onWindowUp = () => { + resizeStart.current = null; + }; + window.addEventListener('pointermove', onWindowMove, { passive: false }); + window.addEventListener('pointerup', onWindowUp); + window.addEventListener('pointercancel', onWindowUp); + return () => { + window.removeEventListener('pointermove', onWindowMove); + window.removeEventListener('pointerup', onWindowUp); + window.removeEventListener('pointercancel', onWindowUp); + }; + }, [enabled, applyResize]); + const stopChartPointer = useCallback((e: React.PointerEvent | React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); @@ -379,11 +407,15 @@ const ChartMagnifier: React.FC = ({ >× - +
+ +
{HANDLES.map(h => ( -
onHandleDown(e, h.id)} onPointerMove={onHandleMove} onPointerUp={onHandleUp} diff --git a/frontend/src/components/StrategyEvaluationPage.tsx b/frontend/src/components/StrategyEvaluationPage.tsx index 774e9fa..97c4575 100644 --- a/frontend/src/components/StrategyEvaluationPage.tsx +++ b/frontend/src/components/StrategyEvaluationPage.tsx @@ -43,6 +43,7 @@ import type { OHLCVBar } from '../types'; const LEFT_KEY = 'seval-left-width'; const LEFT_OPEN_KEY = 'seval-left-open'; const RIGHT_KEY = 'seval-right-width'; +const RIGHT_OPEN_KEY = 'seval-right-open'; const LEFT_DEFAULT = 380; const RIGHT_DEFAULT = 440; @@ -88,6 +89,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { const [leftWidth, setLeftWidth] = useState(() => readMinWidth(LEFT_KEY, LEFT_DEFAULT)); const [leftOpen, setLeftOpen] = useState(() => readStoredBool(LEFT_OPEN_KEY, true)); const [rightWidth, setRightWidth] = useState(() => readMinWidth(RIGHT_KEY, RIGHT_DEFAULT)); + const [rightOpen, setRightOpen] = useState(() => readStoredBool(RIGHT_OPEN_KEY, true)); const leftRef = useRef(leftWidth); const rightRef = useRef(rightWidth); leftRef.current = leftWidth; @@ -103,10 +105,22 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { }); }, []); + const toggleRightPanel = useCallback(() => { + setRightOpen(prev => { + const next = !prev; + storeBool(RIGHT_OPEN_KEY, next); + return next; + }); + }, []); + const leftWrapStyle = useMemo( () => ({ '--seval-left-width': `${leftWidth}px` }) as React.CSSProperties, [leftWidth], ); + const rightWrapStyle = useMemo( + () => ({ '--seval-right-width': `${rightWidth}px` }) as React.CSSProperties, + [rightWidth], + ); const onRightSplit = useCallback((e: React.PointerEvent) => { const startX = e.clientX; const start = rightRef.current; @@ -474,14 +488,25 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
- - +
{editorModalOpen && ( diff --git a/frontend/src/styles/strategyEvaluation.css b/frontend/src/styles/strategyEvaluation.css index 713e45d..0044338 100644 --- a/frontend/src/styles/strategyEvaluation.css +++ b/frontend/src/styles/strategyEvaluation.css @@ -39,6 +39,7 @@ flex-shrink: 0; position: relative; z-index: 20; + pointer-events: auto; width: 16px; height: 56px; border: 1px solid var(--se-border); @@ -76,6 +77,84 @@ min-width: 0; } +/* ── 우측 패널 접기 (좌측과 대칭: 핸들 → 스플리터 → 패널) ─────────────────── */ +.seval-right-wrap { + display: flex; + flex-direction: row; + align-items: stretch; + flex-shrink: 0; + min-width: 16px; + min-height: 0; + height: 100%; + position: relative; + z-index: 8; +} + +.seval-right-splitter { + align-self: stretch; + flex-shrink: 0; +} + +.seval-right-sidebar.btd-right { + width: 0; + flex: 0 0 0; + min-width: 0; + max-width: 0; + padding: 0; + border-left: none; + overflow: hidden; + transition: + width 0.22s cubic-bezier(0.4, 0, 0.2, 1), + flex-basis 0.22s cubic-bezier(0.4, 0, 0.2, 1), + max-width 0.22s cubic-bezier(0.4, 0, 0.2, 1); +} + +.seval-right-sidebar--open { + width: var(--seval-right-width, 440px); + flex: 0 0 var(--seval-right-width, 440px); + max-width: var(--seval-right-width, 440px); + padding: 10px 8px; + border-left: 1px solid var(--btd-divider, var(--se-border)); +} + +.seval-right-wrap .se-panel-handle { + align-self: center; + flex-shrink: 0; + position: relative; + z-index: 20; + pointer-events: auto; + width: 16px; + height: 56px; + border: 1px solid var(--se-border); + color: var(--se-text-muted); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s, color 0.15s, box-shadow 0.15s, border-color 0.15s; + padding: 0; + background: transparent; +} + +.seval-right-wrap .se-panel-handle--right { + margin-left: -1px; + background: var(--se-bg-elevated, var(--bg2)); + border-right: none; + border-radius: 8px 0 0 8px; + box-shadow: -2px 0 10px rgba(0, 0, 0, 0.35); +} + +.seval-right-wrap .se-panel-handle--right:hover { + color: var(--se-text); + background: var(--se-bg-muted, var(--bg3)); + box-shadow: -3px 0 14px rgba(0, 0, 0, 0.45); +} + +.seval-right-wrap--open .se-panel-handle--right, +.seval-right-wrap .se-panel-handle--right.se-panel-handle--open { + border-color: color-mix(in srgb, var(--se-accent, #3d9be9) 30%, var(--se-border)); +} + .seval-left-panel { height: 100%; }