diff --git a/frontend/src/components/StrategyEvaluationPage.tsx b/frontend/src/components/StrategyEvaluationPage.tsx index d137d33..2a644fa 100644 --- a/frontend/src/components/StrategyEvaluationPage.tsx +++ b/frontend/src/components/StrategyEvaluationPage.tsx @@ -20,7 +20,8 @@ import type { VirtualIndicatorSnapshot } from '../hooks/useVirtualIndicatorSnaps import VirtualTargetSignalPanel from './virtual/VirtualTargetSignalPanel'; import StrategyEvaluationChart from './strategyEvaluation/StrategyEvaluationChart'; import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab'; -import { readStoredSize, storeSize, usePanelResize } from './strategyEditor/usePanelResize'; +import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle'; +import { readStoredSize, readStoredBool, storeBool, storeSize, usePanelResize } from './strategyEditor/usePanelResize'; import { getKoreanName } from '../utils/marketNameCache'; import { useIndicatorSettings } from '../hooks/useIndicatorSettings'; import { @@ -36,6 +37,7 @@ import { 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 LEFT_DEFAULT = 380; const RIGHT_DEFAULT = 440; @@ -68,6 +70,9 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { const [evalError, setEvalError] = useState(null); const [appliedIndicatorParams, setAppliedIndicatorParams] = useState(null); const [paramsRevision, setParamsRevision] = useState(0); + const [chartLoading, setChartLoading] = useState(false); + const evalSessionRef = useRef(0); + const paramsRevisionRef = useRef(paramsRevision); const getEvalParams = useMemo( () => mergeEvalGetParams(baseGetParams, appliedIndicatorParams), @@ -75,6 +80,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 leftRef = useRef(leftWidth); const rightRef = useRef(rightWidth); @@ -82,6 +88,19 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { rightRef.current = rightWidth; const onLeftSplit = usePanelResize('vertical', setLeftWidth, () => leftRef.current, 300, 520, v => storeSize(LEFT_KEY, v)); + + const toggleLeftPanel = useCallback(() => { + setLeftOpen(prev => { + const next = !prev; + storeBool(LEFT_OPEN_KEY, next); + return next; + }); + }, []); + + const leftWrapStyle = useMemo( + () => ({ '--seval-left-width': `${leftWidth}px` }) as React.CSSProperties, + [leftWidth], + ); const onRightSplit = useCallback((e: React.PointerEvent) => { const startX = e.clientX; const start = rightRef.current; @@ -121,6 +140,27 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { [strategyPrimaryTimeframe], ); + /** 종목·시간봉·전략 변경 — 차트·평가 컨텍스트 초기화 */ + useEffect(() => { + evalSessionRef.current += 1; + setSnapshot(undefined); + setEvalError(null); + setEvalLoading(true); + setBars([]); + setSelectedBarIndex(0); + }, [market, chartTimeframe, selectedStrategyId]); + + /** 전략설정 파라미터 저장·초기화 */ + useEffect(() => { + if (paramsRevisionRef.current === paramsRevision) return; + paramsRevisionRef.current = paramsRevision; + if (paramsRevision === 0) return; + evalSessionRef.current += 1; + setSnapshot(undefined); + setEvalError(null); + setEvalLoading(true); + }, [paramsRevision]); + useEffect(() => { void loadStrategies().then(list => setStrategies(list ?? [])); }, []); @@ -150,10 +190,17 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { const selectedBarTimeSec = bars[selectedBarIndex]?.time ?? null; const refreshEvaluation = useCallback(async () => { - if (!selectedStrategyId || !selectedStrategy || selectedBarTimeSec == null) { + if (!selectedStrategyId || !selectedStrategy) { + setSnapshot(undefined); + setEvalLoading(false); + return; + } + if (chartLoading || bars.length === 0 || selectedBarTimeSec == null) { setSnapshot(undefined); return; } + + const session = evalSessionRef.current; setEvalLoading(true); setEvalError(null); try { @@ -164,14 +211,26 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { selectedBarTimeSec, appliedIndicatorParams, ); + if (session !== evalSessionRef.current) return; setSnapshot(snap ?? undefined); } catch (e) { + if (session !== evalSessionRef.current) return; setEvalError(e instanceof Error ? e.message : '조건 평가 실패'); setSnapshot(undefined); } finally { - setEvalLoading(false); + if (session === evalSessionRef.current) { + setEvalLoading(false); + } } - }, [market, selectedStrategyId, selectedStrategy, selectedBarTimeSec, appliedIndicatorParams]); + }, [ + chartLoading, + market, + selectedStrategyId, + selectedStrategy, + selectedBarTimeSec, + bars.length, + appliedIndicatorParams, + ]); useEffect(() => { void refreshEvaluation(); @@ -225,8 +284,12 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
- + +
+ {leftOpen && (
+ )} -
+
@@ -351,19 +425,33 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {