전략평가 수정
This commit is contained in:
+20
-12
@@ -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);
|
||||
|
||||
@@ -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<ChartMagnifierProps> = ({
|
||||
(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<ChartMagnifierProps> = ({
|
||||
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<ChartMagnifierProps> = ({
|
||||
>×</button>
|
||||
</div>
|
||||
|
||||
<canvas ref={canvasRef} className="chart-magnifier-canvas" />
|
||||
<div className="chart-magnifier-body">
|
||||
<canvas ref={canvasRef} className="chart-magnifier-canvas" />
|
||||
</div>
|
||||
|
||||
{HANDLES.map(h => (
|
||||
<div key={h.id} className="chart-magnifier-handle"
|
||||
style={{ ...h.style, cursor: h.cursor, position: 'absolute', width: 10, height: 10 }}
|
||||
<div
|
||||
key={h.id}
|
||||
className={`chart-magnifier-handle chart-magnifier-handle--${h.id}`}
|
||||
style={{ ...h.style, cursor: h.cursor }}
|
||||
onPointerDown={e => onHandleDown(e, h.id)}
|
||||
onPointerMove={onHandleMove}
|
||||
onPointerUp={onHandleUp}
|
||||
|
||||
@@ -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<HTMLDivElement>) => {
|
||||
const startX = e.clientX;
|
||||
const start = rightRef.current;
|
||||
@@ -474,14 +488,25 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
</main>
|
||||
|
||||
<div
|
||||
className="btd-splitter btd-splitter--v"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label="우측 패널 너비"
|
||||
onPointerDown={onRightSplit}
|
||||
/>
|
||||
|
||||
<aside className="btd-right seval-right-panel" style={{ width: rightWidth, flex: `0 0 ${rightWidth}px` }}>
|
||||
className={`seval-right-wrap${rightOpen ? ' seval-right-wrap--open' : ''}`}
|
||||
style={rightWrapStyle}
|
||||
>
|
||||
<SePanelCollapseHandle
|
||||
side="right"
|
||||
open={rightOpen}
|
||||
onToggle={toggleRightPanel}
|
||||
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">
|
||||
<span className="seval-signal-market">{getKoreanName(market)}</span>
|
||||
<span
|
||||
@@ -523,7 +548,8 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
</span>
|
||||
<span className="seval-strategy-chip">{strategyLabel}</span>
|
||||
</footer>
|
||||
</aside>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{editorModalOpen && (
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user