전략평가 차트 전체화면

This commit is contained in:
Macbook
2026-06-22 00:36:15 +09:00
parent e664642341
commit 649a4ab5e8
2 changed files with 106 additions and 1 deletions
@@ -181,6 +181,8 @@ const StrategyEvaluationChart: React.FC<Props> = ({
const [backtestRunning, setBacktestRunning] = useState(false); const [backtestRunning, setBacktestRunning] = useState(false);
const [backtestError, setBacktestError] = useState<string | null>(null); const [backtestError, setBacktestError] = useState<string | null>(null);
const [magnifierActive, setMagnifierActive] = useState(false); const [magnifierActive, setMagnifierActive] = useState(false);
const [chartFullscreen, setChartFullscreen] = useState(false);
const chartRootRef = useRef<HTMLDivElement>(null);
const marketBtnRef = useRef<HTMLButtonElement>(null); const marketBtnRef = useRef<HTMLButtonElement>(null);
const managerRef = useRef<ChartManager | null>(null); const managerRef = useRef<ChartManager | null>(null);
const [internalOverlayVisibility, setInternalOverlayVisibility] = useState<ChartOverlayVisibility>( const [internalOverlayVisibility, setInternalOverlayVisibility] = useState<ChartOverlayVisibility>(
@@ -680,8 +682,47 @@ const StrategyEvaluationChart: React.FC<Props> = ({
const selectedBar = bars[selectedBarIndex] ?? null; const selectedBar = bars[selectedBarIndex] ?? null;
useEffect(() => {
const cls = 'seval-chart-fs-lock';
if (chartFullscreen) {
document.body.classList.add(cls);
} else {
document.body.classList.remove(cls);
}
return () => document.body.classList.remove(cls);
}, [chartFullscreen]);
useEffect(() => {
if (!chartFullscreen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key !== 'Escape' || isKeyboardTypingTarget(e.target)) return;
setChartFullscreen(false);
};
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [chartFullscreen]);
useEffect(() => {
const raf = requestAnimationFrame(() => {
requestAnimationFrame(() => {
const root = chartRootRef.current;
const mgr = managerRef.current;
if (!root || !mgr) return;
const wrap = root.querySelector('.tv-chart-wrap') as HTMLElement | null;
const w = root.clientWidth;
const h = wrap?.clientHeight ?? root.clientHeight;
if (w > 0 && h > 0) mgr.resize(w, h);
mgr.refreshPaneSeparatorOverlay();
});
});
return () => cancelAnimationFrame(raf);
}, [chartFullscreen, bars.length]);
return ( return (
<div className="seval-chart btd-analysis-chart"> <div
ref={chartRootRef}
className={`seval-chart btd-analysis-chart${chartFullscreen ? ' seval-chart--fullscreen' : ''}`}
>
<div className="seval-chart-toolbar btd-analysis-toolbar"> <div className="seval-chart-toolbar btd-analysis-toolbar">
<div className="seval-chart-toolbar-run"> <div className="seval-chart-toolbar-run">
<button <button
@@ -737,6 +778,31 @@ const StrategyEvaluationChart: React.FC<Props> = ({
)} )}
</div> </div>
<div className="seval-chart-toolbar-actions"> <div className="seval-chart-toolbar-actions">
<button
type="button"
className={`btd-analysis-tool seval-chart-fullscreen-btn${chartFullscreen ? ' seval-chart-fullscreen-btn--active' : ''}`}
title={chartFullscreen ? '전체화면 종료 (Esc)' : '차트 전체화면 (캔들·보조지표)'}
aria-label={chartFullscreen ? '전체화면 종료' : '차트 전체화면'}
aria-pressed={chartFullscreen}
disabled={loading || bars.length === 0}
onClick={() => setChartFullscreen(v => !v)}
>
{chartFullscreen ? (
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<polyline points="6,6 1,1" />
<polyline points="10,6 15,1" />
<polyline points="6,10 1,15" />
<polyline points="10,10 15,15" />
</svg>
) : (
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<polyline points="1,6 1,1 6,1" />
<polyline points="10,1 15,1 15,6" />
<polyline points="15,10 15,15 10,15" />
<polyline points="6,15 1,15 1,10" />
</svg>
)}
</button>
{onOpenAiVerify && ( {onOpenAiVerify && (
<button <button
type="button" type="button"
@@ -503,6 +503,45 @@
flex-shrink: 0; flex-shrink: 0;
} }
/* 차트 전체화면 — 캔들·보조지표 pane 전체 뷰포트 */
.seval-chart--fullscreen {
position: fixed;
inset: 0;
z-index: 10050;
background: var(--se-bg, var(--bg, #131722));
min-height: 0;
}
.seval-chart--fullscreen .btd-analysis-main {
flex: 1;
min-height: 0;
}
.seval-chart--fullscreen .seval-chart-body {
flex: 1;
min-height: 0;
}
body.seval-chart-fs-lock {
overflow: hidden;
}
.seval-chart-fullscreen-btn {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
border-radius: 6px;
}
.seval-chart-fullscreen-btn--active {
color: var(--se-accent, #3d9be9);
background: color-mix(in srgb, var(--se-accent, #3d9be9) 14%, transparent);
border-color: color-mix(in srgb, var(--se-accent, #3d9be9) 45%, var(--se-border, var(--border)));
}
.seval-chart-report-btn { .seval-chart-report-btn {
width: 30px; width: 30px;
height: 30px; height: 30px;