전략평가 수정

This commit is contained in:
Macbook
2026-06-16 15:02:59 +09:00
parent 8eb6e4d47b
commit 779bc31694
4 changed files with 182 additions and 37 deletions
+20 -12
View File
@@ -5608,15 +5608,14 @@ html.desktop-client .tmb-logo-version {
position: fixed; /* wrapper 밖으로도 자유롭게 이동 */ position: fixed; /* wrapper 밖으로도 자유롭게 이동 */
z-index: 2000; z-index: 2000;
border-radius: 6px; border-radius: 6px;
cursor: grab; cursor: default;
overflow: hidden; overflow: visible;
box-shadow: box-shadow:
0 4px 24px rgba(0,0,0,0.55), 0 4px 24px rgba(0,0,0,0.55),
0 0 0 1px rgba(255,200,50,0.45); 0 0 0 1px rgba(255,200,50,0.45);
background: #111218; background: #111218;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
/* pointer-events: all — 내부 drag/resize 가 동작하도록 */
user-select: none; user-select: none;
} }
@@ -5634,8 +5633,6 @@ html.desktop-client .tmb-logo-version {
} }
.chart-magnifier-header:active { cursor: grabbing; } .chart-magnifier-header:active { cursor: grabbing; }
.chart-magnifier:active { cursor: default; } .chart-magnifier:active { cursor: default; }
/* 리사이즈 핸들 위에서는 grab 커서 대신 방향 커서 표시 */
.chart-magnifier-handle { cursor: inherit; }
.chart-magnifier-icon { .chart-magnifier-icon {
opacity: 0.75; 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-close:hover { background: rgba(220,60,60,0.5); color: #fff; }
/* 확대 캔버스 */ /* 확대 캔버스 */
.chart-magnifier-canvas { .chart-magnifier-body {
flex: 1; flex: 1;
display: block; min-height: 0;
width: 100%; overflow: hidden;
height: 0; /* flex:1 로 채워짐 */ border-radius: 0 0 6px 6px;
} }
/* 리사이즈 핸들 */ .chart-magnifier-canvas {
display: block;
width: 100%;
height: 100%;
}
/* 리사이즈 핸들 — 8방향 (모서리·변) */
.chart-magnifier-handle { .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; border-radius: 2px;
z-index: 10; z-index: 20;
touch-action: none;
} }
.chart-magnifier-handle:hover { .chart-magnifier-handle:hover {
background: rgba(255,200,50,1); background: rgba(255,200,50,1);
+48 -16
View File
@@ -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'; 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 }[] = [ const HANDLES: { id: ResizeDir; cursor: string; style: React.CSSProperties }[] = [
{ id:'nw', cursor:'nw-resize', style:{ top:-5, left:-5 } }, { id:'nw', cursor:'nw-resize', style:{ top:HANDLE_OFFSET, left:HANDLE_OFFSET } },
{ id:'n', cursor:'n-resize', style:{ top:-5, left:'50%', transform:'translateX(-50%)' } }, { id:'n', cursor:'n-resize', style:{ top:HANDLE_OFFSET, left:'50%', transform:'translateX(-50%)' } },
{ id:'ne', cursor:'ne-resize', style:{ top:-5, right:-5 } }, { id:'ne', cursor:'ne-resize', style:{ top:HANDLE_OFFSET, right:HANDLE_OFFSET } },
{ id:'e', cursor:'e-resize', style:{ top:'50%', right:-5, transform:'translateY(-50%)' } }, { id:'e', cursor:'e-resize', style:{ top:'50%', right:HANDLE_OFFSET, transform:'translateY(-50%)' } },
{ id:'se', cursor:'se-resize', style:{ bottom:-5, right:-5 } }, { id:'se', cursor:'se-resize', style:{ bottom:HANDLE_OFFSET, right:HANDLE_OFFSET } },
{ id:'s', cursor:'s-resize', style:{ bottom:-5, left:'50%',transform:'translateX(-50%)' } }, { id:'s', cursor:'s-resize', style:{ bottom:HANDLE_OFFSET, left:'50%', transform:'translateX(-50%)' } },
{ id:'sw', cursor:'sw-resize', style:{ bottom:-5, left:-5 } }, { id:'sw', cursor:'sw-resize', style:{ bottom:HANDLE_OFFSET, left:HANDLE_OFFSET } },
{ id:'w', cursor:'w-resize', style:{ top:'50%', left:-5, transform:'translateY(-50%)' } }, { id:'w', cursor:'w-resize', style:{ top:'50%', left:HANDLE_OFFSET, transform:'translateY(-50%)' } },
]; ];
function nearestStepIndex(steps: number[], cur: number): number { function nearestStepIndex(steps: number[], cur: number): number {
@@ -257,14 +260,12 @@ const ChartMagnifier: React.FC<ChartMagnifierProps> = ({
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
}, []); }, []);
const onHandleMove = useCallback((e: React.PointerEvent) => { const applyResize = useCallback((clientX: number, clientY: number) => {
const rs = resizeStart.current; const rs = resizeStart.current;
if (!rs) return; if (!rs) return;
e.preventDefault();
e.stopPropagation();
const { dir, mx, my, px, py, pw, ph } = rs; const { dir, mx, my, px, py, pw, ph } = rs;
const dx = e.clientX - mx; const dx = clientX - mx;
const dy = e.clientY - my; const dy = clientY - my;
let nx = px, ny = py, nw = pw, nh = ph; let nx = px, ny = py, nw = pw, nh = ph;
if (dir.includes('e')) { nw = Math.max(MIN_W, pw + dx); } 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; } if (dir.includes('w')) { nw = Math.max(MIN_W, pw - dx); nx = px + pw - nw; }
@@ -278,11 +279,38 @@ const ChartMagnifier: React.FC<ChartMagnifierProps> = ({
setSize({ w: nw, h: nh }); 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) => { const onHandleUp = useCallback((e: React.PointerEvent) => {
e.stopPropagation(); e.stopPropagation();
resizeStart.current = null; 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) => { const stopChartPointer = useCallback((e: React.PointerEvent | React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@@ -379,11 +407,15 @@ const ChartMagnifier: React.FC<ChartMagnifierProps> = ({
>×</button> >×</button>
</div> </div>
<canvas ref={canvasRef} className="chart-magnifier-canvas" /> <div className="chart-magnifier-body">
<canvas ref={canvasRef} className="chart-magnifier-canvas" />
</div>
{HANDLES.map(h => ( {HANDLES.map(h => (
<div key={h.id} className="chart-magnifier-handle" <div
style={{ ...h.style, cursor: h.cursor, position: 'absolute', width: 10, height: 10 }} key={h.id}
className={`chart-magnifier-handle chart-magnifier-handle--${h.id}`}
style={{ ...h.style, cursor: h.cursor }}
onPointerDown={e => onHandleDown(e, h.id)} onPointerDown={e => onHandleDown(e, h.id)}
onPointerMove={onHandleMove} onPointerMove={onHandleMove}
onPointerUp={onHandleUp} onPointerUp={onHandleUp}
@@ -43,6 +43,7 @@ import type { OHLCVBar } from '../types';
const LEFT_KEY = 'seval-left-width'; const LEFT_KEY = 'seval-left-width';
const LEFT_OPEN_KEY = 'seval-left-open'; const LEFT_OPEN_KEY = 'seval-left-open';
const RIGHT_KEY = 'seval-right-width'; const RIGHT_KEY = 'seval-right-width';
const RIGHT_OPEN_KEY = 'seval-right-open';
const LEFT_DEFAULT = 380; const LEFT_DEFAULT = 380;
const RIGHT_DEFAULT = 440; 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 [leftWidth, setLeftWidth] = useState(() => readMinWidth(LEFT_KEY, LEFT_DEFAULT));
const [leftOpen, setLeftOpen] = useState(() => readStoredBool(LEFT_OPEN_KEY, true)); const [leftOpen, setLeftOpen] = useState(() => readStoredBool(LEFT_OPEN_KEY, true));
const [rightWidth, setRightWidth] = useState(() => readMinWidth(RIGHT_KEY, RIGHT_DEFAULT)); const [rightWidth, setRightWidth] = useState(() => readMinWidth(RIGHT_KEY, RIGHT_DEFAULT));
const [rightOpen, setRightOpen] = useState(() => readStoredBool(RIGHT_OPEN_KEY, true));
const leftRef = useRef(leftWidth); const leftRef = useRef(leftWidth);
const rightRef = useRef(rightWidth); const rightRef = useRef(rightWidth);
leftRef.current = leftWidth; 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( const leftWrapStyle = useMemo(
() => ({ '--seval-left-width': `${leftWidth}px` }) as React.CSSProperties, () => ({ '--seval-left-width': `${leftWidth}px` }) as React.CSSProperties,
[leftWidth], [leftWidth],
); );
const rightWrapStyle = useMemo(
() => ({ '--seval-right-width': `${rightWidth}px` }) as React.CSSProperties,
[rightWidth],
);
const onRightSplit = useCallback((e: React.PointerEvent<HTMLDivElement>) => { const onRightSplit = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
const startX = e.clientX; const startX = e.clientX;
const start = rightRef.current; const start = rightRef.current;
@@ -474,14 +488,25 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
</main> </main>
<div <div
className="btd-splitter btd-splitter--v" className={`seval-right-wrap${rightOpen ? ' seval-right-wrap--open' : ''}`}
role="separator" style={rightWrapStyle}
aria-orientation="vertical" >
aria-label="우측 패널 너비" <SePanelCollapseHandle
onPointerDown={onRightSplit} side="right"
/> open={rightOpen}
onToggle={toggleRightPanel}
<aside className="btd-right seval-right-panel" style={{ width: rightWidth, flex: `0 0 ${rightWidth}px` }}> label="시그널 패널"
/>
{rightOpen && (
<div
className="btd-splitter btd-splitter--v seval-right-splitter"
role="separator"
aria-orientation="vertical"
aria-label="우측 패널 너비"
onPointerDown={onRightSplit}
/>
)}
<aside className={`btd-right seval-right-panel seval-right-sidebar${rightOpen ? ' seval-right-sidebar--open' : ''}`}>
<div className="seval-signal-head"> <div className="seval-signal-head">
<span className="seval-signal-market">{getKoreanName(market)}</span> <span className="seval-signal-market">{getKoreanName(market)}</span>
<span <span
@@ -523,7 +548,8 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
</span> </span>
<span className="seval-strategy-chip">{strategyLabel}</span> <span className="seval-strategy-chip">{strategyLabel}</span>
</footer> </footer>
</aside> </aside>
</div>
</div> </div>
{editorModalOpen && ( {editorModalOpen && (
@@ -39,6 +39,7 @@
flex-shrink: 0; flex-shrink: 0;
position: relative; position: relative;
z-index: 20; z-index: 20;
pointer-events: auto;
width: 16px; width: 16px;
height: 56px; height: 56px;
border: 1px solid var(--se-border); border: 1px solid var(--se-border);
@@ -76,6 +77,84 @@
min-width: 0; 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 { .seval-left-panel {
height: 100%; height: 100%;
} }