전략편집기 병합

This commit is contained in:
Macbook
2026-05-24 12:38:18 +09:00
parent 1465cb2255
commit 03fab44cb3
6 changed files with 628 additions and 81 deletions
+86 -38
View File
@@ -30,7 +30,13 @@ import {
} from '../utils/strategyEditorLayoutStorage';
import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview';
import StrategyEditorCanvas, { type FlowLayoutSeed } from './strategyEditor/StrategyEditorCanvas';
import StrategyListEditor from './strategyEditor/StrategyListEditor';
import { layoutFlushRef } from './strategyEditor/strategyEditorCallbacks';
import {
loadEditorMode,
saveEditorMode,
type StrategyEditorMode,
} from '../utils/strategyEditorModeStorage';
import PaletteChip from './strategyEditor/PaletteChip';
import { readStoredSize, storeSize, usePanelResize } from './strategyEditor/usePanelResize';
import '../styles/strategyEditor.css';
@@ -105,6 +111,7 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
terminalHeightRef.current = terminalHeight;
const [layoutSeedKey, setLayoutSeedKey] = useState('draft:buy:0');
const [editorMode, setEditorMode] = useState<StrategyEditorMode>(() => loadEditorMode());
const onLeftSplitter = usePanelResize(
'vertical',
@@ -216,13 +223,27 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
}, [layoutStrategyKey, schedulePersistFlowLayout]);
const switchSignalTab = useCallback((tab: 'buy' | 'sell') => {
layoutFlushRef.current?.();
if (editorMode === 'graph') layoutFlushRef.current?.();
persistFlowLayout(layoutStrategyKey);
layoutRevisionRef.current += 1;
setLayoutSeedKey(`${layoutStrategyKey}:${tab}:${layoutRevisionRef.current}`);
setSignalTab(tab);
setSelectedNodeId(null);
}, [layoutStrategyKey, persistFlowLayout]);
}, [layoutStrategyKey, persistFlowLayout, editorMode]);
const handleEditorModeChange = useCallback((mode: StrategyEditorMode) => {
if (mode === editorMode) return;
if (editorMode === 'graph') {
layoutFlushRef.current?.();
persistFlowLayout(layoutStrategyKey);
}
if (mode === 'list' && (buyOrphans.length > 0 || sellOrphans.length > 0)) {
showSnack('목록 방식에서는 미연결(고아) 노드가 표시되지 않습니다', false);
}
setEditorMode(mode);
saveEditorMode(mode);
setSelectedNodeId(null);
}, [editorMode, layoutStrategyKey, persistFlowLayout, buyOrphans.length, sellOrphans.length]);
const layoutSeed = useMemo((): FlowLayoutSeed => {
const source = signalTab === 'buy' ? buyLayout : sellLayout;
@@ -444,6 +465,22 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
<span className="se-subtitle">Strategy Builder</span>
</div>
<div className="se-header-actions">
<div className="se-editor-mode" role="group" aria-label="편집 방식">
<button
type="button"
className={`se-editor-mode-btn${editorMode === 'graph' ? ' se-editor-mode-btn--on' : ''}`}
onClick={() => handleEditorModeChange('graph')}
>
</button>
<button
type="button"
className={`se-editor-mode-btn${editorMode === 'list' ? ' se-editor-mode-btn--on' : ''}`}
onClick={() => handleEditorModeChange('list')}
>
</button>
</div>
<button type="button" className="se-btn se-btn--ghost" onClick={handleNew}> </button>
<button type="button" className="se-btn se-btn--gold" onClick={() => setSaveOpen(true)}></button>
</div>
@@ -539,43 +576,54 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
{stratName && <span className="se-editing-name">{stratName}</span>}
</div>
<ReactFlowProvider>
<StrategyEditorCanvas
theme={theme}
root={currentRoot}
orphans={currentOrphans}
onOrphansChange={setCurrentOrphans}
def={DEF}
signalTab={signalTab}
onChange={setCurrentRoot}
selectedNodeId={selectedNodeId}
onSelectNode={setSelectedNodeId}
layoutSeed={layoutSeed}
onLayoutChange={handleLayoutChange}
/>
</ReactFlowProvider>
{editorMode === 'graph' ? (
<>
<ReactFlowProvider>
<StrategyEditorCanvas
theme={theme}
root={currentRoot}
orphans={currentOrphans}
onOrphansChange={setCurrentOrphans}
def={DEF}
signalTab={signalTab}
onChange={setCurrentRoot}
selectedNodeId={selectedNodeId}
onSelectNode={setSelectedNodeId}
layoutSeed={layoutSeed}
onLayoutChange={handleLayoutChange}
/>
</ReactFlowProvider>
{selectedLogicNode?.type === 'CONDITION' && selectedLogicNode.condition && (
<div className="se-node-config-bar">
<span className="se-node-config-label">{selectedLogicNode.condition.indicatorType}</span>
<CondEditor
cond={selectedLogicNode.condition}
signalType={signalTab}
def={DEF}
onChange={c => {
if (!selectedNodeId) return;
const inOrphans = currentOrphans.some(o => o.id === selectedNodeId);
if (inOrphans) {
setCurrentOrphans(currentOrphans.map(o => (
o.id === selectedNodeId ? updateNode(o, selectedNodeId, n => ({ ...n, condition: c })) : o
)));
} else if (currentRoot) {
setCurrentRoot(updateNode(currentRoot, selectedNodeId, n => ({ ...n, condition: c })));
}
}}
/>
<span className="se-sync-tip"> · {getIndicatorPeriodLabel(selectedLogicNode.condition.indicatorType, DEF) || '—'}</span>
</div>
{selectedLogicNode?.type === 'CONDITION' && selectedLogicNode.condition && (
<div className="se-node-config-bar">
<span className="se-node-config-label">{selectedLogicNode.condition.indicatorType}</span>
<CondEditor
cond={selectedLogicNode.condition}
signalType={signalTab}
def={DEF}
onChange={c => {
if (!selectedNodeId) return;
const inOrphans = currentOrphans.some(o => o.id === selectedNodeId);
if (inOrphans) {
setCurrentOrphans(currentOrphans.map(o => (
o.id === selectedNodeId ? updateNode(o, selectedNodeId, n => ({ ...n, condition: c })) : o
)));
} else if (currentRoot) {
setCurrentRoot(updateNode(currentRoot, selectedNodeId, n => ({ ...n, condition: c })));
}
}}
/>
<span className="se-sync-tip"> · {getIndicatorPeriodLabel(selectedLogicNode.condition.indicatorType, DEF) || '—'}</span>
</div>
)}
</>
) : (
<StrategyListEditor
root={currentRoot}
signalTab={signalTab}
def={DEF}
onChange={setCurrentRoot}
/>
)}
</div>