From 97a154964759705209f9622113e194b1ebe7363b Mon Sep 17 00:00:00 2001 From: Macbook Date: Thu, 18 Jun 2026 10:40:59 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EB=9E=B5=ED=8F=89=EA=B0=80=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20=EC=A0=84=EB=9E=B5?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/StrategyEvaluationPage.tsx | 73 ++- ...trategyEvaluationIndicatorPalettePanel.tsx | 210 ++++++++ .../StrategyEvaluationStrategyPanel.tsx | 178 ++++++ .../src/hooks/useStrategyEvaluationEditor.ts | 509 ++++++++++++++++++ frontend/src/styles/strategyEvaluation.css | 101 +++- 5 files changed, 1064 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/strategyEvaluation/StrategyEvaluationIndicatorPalettePanel.tsx create mode 100644 frontend/src/components/strategyEvaluation/StrategyEvaluationStrategyPanel.tsx create mode 100644 frontend/src/hooks/useStrategyEvaluationEditor.ts diff --git a/frontend/src/components/StrategyEvaluationPage.tsx b/frontend/src/components/StrategyEvaluationPage.tsx index 04bc16e..6fc76d7 100644 --- a/frontend/src/components/StrategyEvaluationPage.tsx +++ b/frontend/src/components/StrategyEvaluationPage.tsx @@ -23,7 +23,8 @@ import { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicat import { fetchEvaluationSnapshotAtBar } from '../utils/strategyEvaluationSnapshot'; import StrategyEvaluationSignalPanel from './strategyEvaluation/StrategyEvaluationSignalPanel'; import StrategyEvaluationChart from './strategyEvaluation/StrategyEvaluationChart'; -import StrategyEvaluationConditionPanel from './strategyEvaluation/StrategyEvaluationConditionPanel'; +import StrategyEvaluationStrategyPanel from './strategyEvaluation/StrategyEvaluationStrategyPanel'; +import StrategyEvaluationIndicatorPalettePanel from './strategyEvaluation/StrategyEvaluationIndicatorPalettePanel'; import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab'; import StrategyEvaluationMarketTab from './strategyEvaluation/StrategyEvaluationMarketTab'; import StrategyEditorModal from './strategyEvaluation/StrategyEditorModal'; @@ -51,6 +52,9 @@ import { buildChartBacktestReportModel } from '../utils/backtestReportModel'; import { BACKTEST_DISPLAY_BAR_COUNT } from '../utils/backtestWarmup'; import StrategyEvaluationAiVerifyPanel from './strategyEvaluation/StrategyEvaluationAiVerifyPanel'; import StrategyEvaluationChartSettingsTab from './strategyEvaluation/StrategyEvaluationChartSettingsTab'; +import PaletteDragOverlay from './strategyEditor/PaletteDragOverlay'; +import { needsPointerPaletteDrag } from '../utils/paletteDragSession'; +import { useStrategyEvaluationEditor } from '../hooks/useStrategyEvaluationEditor'; import { buildStrategyEvaluationAiVerifyContext, requestStrategyEvaluationAiVerify, @@ -66,7 +70,7 @@ const LEFT_DEFAULT = 380; const RIGHT_DEFAULT = 440; type LeftTab = 'strategy' | 'market' | 'settings'; -type RightTab = 'signal' | 'chart' | 'ai'; +type RightTab = 'signal' | 'indicators' | 'chart' | 'ai'; function readMinWidth(key: string, fallback: number): number { return Math.max(readStoredSize(key, fallback), fallback); @@ -139,6 +143,34 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { [baseGetParams, appliedIndicatorParams], ); + const handleStrategyEditorSaved = useCallback((saved: StrategyDto, meta?: { created?: boolean }) => { + const hydrated = hydrateStrategyDto(saved); + setSelectedStrategy(hydrated); + setStrategies(prev => { + const exists = prev.some(s => s.id === hydrated.id); + if (exists) return prev.map(s => (s.id === hydrated.id ? hydrated : s)); + return [...prev, hydrated]; + }); + if (meta?.created && hydrated.id != null) { + setSelectedStrategyId(hydrated.id); + } + evalSessionRef.current += 1; + setSnapshot(undefined); + setEvalError(null); + setEvalLoading(true); + reportCacheKeyRef.current = null; + setReportModel(null); + }, []); + + const strategyEditor = useStrategyEvaluationEditor({ + strategy: selectedStrategy, + strategies, + getParams: baseGetParams, + getVisualConfig, + settingsRevision, + onStrategySaved: handleStrategyEditorSaved, + }); + 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)); @@ -784,10 +816,9 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { refreshMarketTickers={marketFeed.refreshAllTickers} onMarketSearchOpenChange={setMarketSearchOpen} /> - @@ -800,7 +831,13 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { side="right" open={rightOpen} onToggle={toggleRightPanel} - label={rightTab === 'chart' ? '차트설정 패널' : '시그널 패널'} + label={ + rightTab === 'chart' + ? '차트설정 패널' + : rightTab === 'indicators' + ? '전략지표 패널' + : '시그널 패널' + } /> {rightOpen && (
시그널 + + + +
+ {indicatorSubTab === 'auxiliary' ? ( + setSelectedPaletteKey(id ? paletteKey('auxiliary', id) : null)} + onAddToCanvas={item => { + selectPalette('auxiliary', item.id); + applyPaletteItem(item); + }} + /> + ) : indicatorSubTab === 'composite' ? ( + setSelectedPaletteKey(id ? paletteKey('composite', id) : null)} + onAddToCanvas={item => { + selectPalette('composite', item.id); + applyPaletteItem(item); + }} + /> + ) : ( + setSelectedPaletteKey(id ? paletteKey('range', id) : null)} + onAddToCanvas={item => { + selectPalette('range', item.id); + applySidewaysFilter(item.id); + }} + /> + )} + + ); +}; + +export default StrategyEvaluationIndicatorPalettePanel; diff --git a/frontend/src/components/strategyEvaluation/StrategyEvaluationStrategyPanel.tsx b/frontend/src/components/strategyEvaluation/StrategyEvaluationStrategyPanel.tsx new file mode 100644 index 0000000..f03b19e --- /dev/null +++ b/frontend/src/components/strategyEvaluation/StrategyEvaluationStrategyPanel.tsx @@ -0,0 +1,178 @@ +/** + * 전략 평가 — 중앙 하단 목록 방식 전략 설정 + */ +import React from 'react'; +import type { StrategyDto } from '../../utils/backendApi'; +import { repairUtf8Mojibake } from '../../utils/textEncoding'; +import StrategyListEditor from '../strategyEditor/StrategyListEditor'; +import StartCombineOpControl from '../strategyEditor/StartCombineOpControl'; +import DraggableModalFrame from '../DraggableModalFrame'; +import { hasMultipleStartSections, normalizeStartCombineOp } from '../../utils/strategyConditionSerde'; +import type { useStrategyEvaluationEditor } from '../../hooks/useStrategyEvaluationEditor'; + +type EditorApi = ReturnType; + +interface Props { + strategy: StrategyDto | null; + editor: EditorApi; +} + +const StrategyEvaluationStrategyPanel: React.FC = ({ strategy, editor }) => { + const { + def, + signalTab, + switchSignalTab, + currentEditorState, + handleEditorStateChange, + handleStartCombineOpChange, + handleAddStartSection, + currentOrphans, + handleOrphansChange, + saving, + saveNow, + openSaveDialog, + closeSaveDialog, + confirmSaveDialog, + saveDialogOpen, + saveDialogMode, + saveDraftName, + setSaveDraftName, + saveDraftDesc, + setSaveDraftDesc, + saveNameError, + saveDuplicateError, + saveToast, + clearSaveFieldErrors, + } = editor; + + if (!strategy) { + return ( +
+

전략을 선택하면 목록 방식으로 조건을 편집할 수 있습니다.

+
+ ); + } + + const strategyName = repairUtf8Mojibake(strategy.name ?? `전략 #${strategy.id}`); + + return ( +
+
+
+
+ + +
+
+ + +
+
+
+ {strategyName && {strategyName}} + {saving && 저장 중…} +
+ {hasMultipleStartSections(currentEditorState) && ( + + )} +
+
+ +
+ + {saveToast && ( +
{saveToast}
+ )} + + {saveDialogOpen && ( + +
+ {saveDialogMode === 'saveAs' && ( +

+ 현재 편집 중인 조건을 새 전략으로 저장합니다. + 기존 전략「{strategyName}」은 변경되지 않습니다. +

+ )} + + { + setSaveDraftName(e.target.value); + clearSaveFieldErrors(); + }} + placeholder="예: Golden_RSI_V1" + /> + {saveNameError && ( +

전략 이름을 입력하세요

+ )} + {saveDuplicateError && ( +

같은 이름의 전략이 이미 있습니다

+ )} + +