전략평가 수정
This commit is contained in:
+20
-12
@@ -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);
|
||||||
|
|||||||
@@ -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%;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user