전략편집기 수정

This commit is contained in:
Macbook
2026-05-24 12:25:17 +09:00
parent ea39f7df27
commit 1465cb2255
10 changed files with 714 additions and 83 deletions
+156 -10
View File
@@ -19,8 +19,18 @@ import {
type StrategyDto,
} from '../utils/strategyEditorShared';
import { findLogicNode, getIndicatorPeriodLabel } from '../utils/strategyFlowLayout';
import {
emptySignalFlowLayout,
loadStrategyFlowLayout,
migrateStrategyFlowLayout,
saveStrategyFlowLayout,
deleteStrategyFlowLayout,
type SignalFlowLayoutSnapshot,
type FlowLayoutChangePayload,
} from '../utils/strategyEditorLayoutStorage';
import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview';
import StrategyEditorCanvas from './strategyEditor/StrategyEditorCanvas';
import StrategyEditorCanvas, { type FlowLayoutSeed } from './strategyEditor/StrategyEditorCanvas';
import { layoutFlushRef } from './strategyEditor/strategyEditorCallbacks';
import PaletteChip from './strategyEditor/PaletteChip';
import { readStoredSize, storeSize, usePanelResize } from './strategyEditor/usePanelResize';
import '../styles/strategyEditor.css';
@@ -38,6 +48,17 @@ interface Props {
activeIndicators?: IndicatorConfig[];
}
function readTabLayout(strategyKey: string, tab: 'buy' | 'sell'): SignalFlowLayoutSnapshot {
const stored = loadStrategyFlowLayout(strategyKey);
const snap = stored?.[tab];
if (!snap) return emptySignalFlowLayout();
return {
positions: snap.positions ?? {},
edgeHandles: snap.edgeHandles ?? {},
orphans: snap.orphans ?? [],
};
}
export default function StrategyEditorPage({ theme, activeIndicators = [] }: Props) {
const DEF = useMemo(() => buildDef(activeIndicators), [activeIndicators]);
@@ -55,17 +76,36 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
const [deleteOpen, setDeleteOpen] = useState(false);
const [deleteId, setDeleteId] = useState<number | null>(null);
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
const [buyOrphans, setBuyOrphans] = useState<LogicNode[]>([]);
const [sellOrphans, setSellOrphans] = useState<LogicNode[]>([]);
const initialDraftBuy = readTabLayout('draft', 'buy');
const initialDraftSell = readTabLayout('draft', 'sell');
const [buyOrphans, setBuyOrphans] = useState<LogicNode[]>(() => initialDraftBuy.orphans ?? []);
const [sellOrphans, setSellOrphans] = useState<LogicNode[]>(() => initialDraftSell.orphans ?? []);
const [snack, setSnack] = useState<{ msg: string; ok: boolean } | null>(null);
const [saveToast, setSaveToast] = useState(false);
const [leftWidth, setLeftWidth] = useState(() => readStoredSize('se-left-width', LEFT_PANEL_DEFAULT));
const [terminalHeight, setTerminalHeight] = useState(() => readStoredSize('se-terminal-height', TERMINAL_DEFAULT));
const leftWidthRef = useRef(leftWidth);
const terminalHeightRef = useRef(terminalHeight);
const [buyLayout, setBuyLayout] = useState(() => ({
positions: initialDraftBuy.positions,
edgeHandles: initialDraftBuy.edgeHandles,
}));
const [sellLayout, setSellLayout] = useState(() => ({
positions: initialDraftSell.positions,
edgeHandles: initialDraftSell.edgeHandles,
}));
const buyLayoutRef = useRef(buyLayout);
const sellLayoutRef = useRef(sellLayout);
buyLayoutRef.current = buyLayout;
sellLayoutRef.current = sellLayout;
const layoutRevisionRef = useRef(0);
const persistLayoutTimerRef = useRef<number | null>(null);
const layoutPersistReadyRef = useRef(false);
leftWidthRef.current = leftWidth;
terminalHeightRef.current = terminalHeight;
const [layoutSeedKey, setLayoutSeedKey] = useState('draft:buy:0');
const onLeftSplitter = usePanelResize(
'vertical',
setLeftWidth,
@@ -106,6 +146,105 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
const orphanTotal = buyOrphans.length + sellOrphans.length;
const layoutStrategyKey = selectedId != null ? String(selectedId) : 'draft';
const persistFlowLayout = useCallback((strategyKey: string) => {
saveStrategyFlowLayout(strategyKey, {
buy: { ...buyLayoutRef.current, orphans: buyOrphans },
sell: { ...sellLayoutRef.current, orphans: sellOrphans },
});
}, [buyOrphans, sellOrphans]);
const schedulePersistFlowLayout = useCallback((strategyKey: string) => {
if (persistLayoutTimerRef.current != null) window.clearTimeout(persistLayoutTimerRef.current);
persistLayoutTimerRef.current = window.setTimeout(() => {
persistLayoutTimerRef.current = null;
persistFlowLayout(strategyKey);
}, 150);
}, [persistFlowLayout]);
const bumpLayoutSeed = useCallback((strategyKey: string, tab: 'buy' | 'sell') => {
layoutRevisionRef.current += 1;
setLayoutSeedKey(`${strategyKey}:${tab}:${layoutRevisionRef.current}`);
}, []);
const resetFlowLayout = useCallback((strategyKey: string, tab: 'buy' | 'sell' = 'buy') => {
const emptyBuy = emptySignalFlowLayout();
const emptySell = emptySignalFlowLayout();
setBuyLayout(emptyBuy);
setSellLayout(emptySell);
setBuyOrphans([]);
setSellOrphans([]);
saveStrategyFlowLayout(strategyKey, { buy: emptyBuy, sell: emptySell });
bumpLayoutSeed(strategyKey, tab);
}, [bumpLayoutSeed]);
const applyStoredFlowLayout = useCallback((strategyKey: string, tab: 'buy' | 'sell' = 'buy') => {
const stored = loadStrategyFlowLayout(strategyKey);
if (stored) {
const nextBuy = {
positions: stored.buy.positions ?? {},
edgeHandles: stored.buy.edgeHandles ?? {},
};
const nextSell = {
positions: stored.sell.positions ?? {},
edgeHandles: stored.sell.edgeHandles ?? {},
};
setBuyLayout(nextBuy);
setSellLayout(nextSell);
setBuyOrphans(stored.buy.orphans ?? []);
setSellOrphans(stored.sell.orphans ?? []);
} else {
const emptyBuy = emptySignalFlowLayout();
const emptySell = emptySignalFlowLayout();
setBuyLayout(emptyBuy);
setSellLayout(emptySell);
setBuyOrphans([]);
setSellOrphans([]);
}
bumpLayoutSeed(strategyKey, tab);
}, [bumpLayoutSeed]);
const handleLayoutChange = useCallback((snapshot: FlowLayoutChangePayload) => {
const next = {
positions: snapshot.positions,
edgeHandles: snapshot.edgeHandles,
};
if (snapshot.tab === 'buy') setBuyLayout(next);
else setSellLayout(next);
schedulePersistFlowLayout(layoutStrategyKey);
}, [layoutStrategyKey, schedulePersistFlowLayout]);
const switchSignalTab = useCallback((tab: 'buy' | 'sell') => {
layoutFlushRef.current?.();
persistFlowLayout(layoutStrategyKey);
layoutRevisionRef.current += 1;
setLayoutSeedKey(`${layoutStrategyKey}:${tab}:${layoutRevisionRef.current}`);
setSignalTab(tab);
setSelectedNodeId(null);
}, [layoutStrategyKey, persistFlowLayout]);
const layoutSeed = useMemo((): FlowLayoutSeed => {
const source = signalTab === 'buy' ? buyLayout : sellLayout;
return {
seedKey: layoutSeedKey,
positions: source.positions,
edgeHandles: source.edgeHandles,
};
}, [signalTab, layoutSeedKey, buyLayout, sellLayout]);
useEffect(() => () => {
if (persistLayoutTimerRef.current != null) window.clearTimeout(persistLayoutTimerRef.current);
}, []);
useEffect(() => {
if (!layoutPersistReadyRef.current) {
layoutPersistReadyRef.current = true;
return;
}
schedulePersistFlowLayout(layoutStrategyKey);
}, [buyOrphans, sellOrphans, layoutStrategyKey, schedulePersistFlowLayout]);
useEffect(() => { saveStratsLocal(strategies); }, [strategies]);
useEffect(() => {
@@ -159,9 +298,8 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
setStratDesc(s.description ?? '');
setBuyCondition(s.buyCondition ?? null);
setSellCondition(s.sellCondition ?? null);
setBuyOrphans([]);
setSellOrphans([]);
setSelectedNodeId(null);
applyStoredFlowLayout(String(s.id), signalTab);
};
const handleNew = () => {
@@ -170,9 +308,8 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
setStratDesc('');
setBuyCondition(null);
setSellCondition(null);
setBuyOrphans([]);
setSellOrphans([]);
setSelectedNodeId(null);
resetFlowLayout('draft', signalTab);
};
const handleSave = async () => {
@@ -191,6 +328,7 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
const saved = await saveStrategy(payload);
const now = new Date().toISOString();
const dbId = saved?.id ?? selectedId ?? Date.now();
const wasDraft = selectedId == null;
setStrategies(prev => {
const existing = prev.find(s => s.id === selectedId);
if (existing && selectedId) {
@@ -202,6 +340,11 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
return [...prev, { id: dbId, name: stratName, description: stratDesc, buyCondition, sellCondition, enabled: true, createdAt: saved?.createdAt ?? now, updatedAt: saved?.updatedAt ?? now }];
});
if (!selectedId) setSelectedId(dbId);
if (wasDraft) {
migrateStrategyFlowLayout('draft', String(dbId));
}
persistFlowLayout(String(dbId));
bumpLayoutSeed(String(dbId), signalTab);
setSaveOpen(false);
setSaveToast(true);
setTimeout(() => setSaveToast(false), 2500);
@@ -216,6 +359,7 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
const handleDeleteConfirm = async () => {
if (!deleteId) return;
try { await deleteStrategy(deleteId); } catch { /* local */ }
deleteStrategyFlowLayout(String(deleteId));
setStrategies(prev => prev.filter(s => s.id !== deleteId));
if (selectedId === deleteId) handleNew();
setDeleteOpen(false);
@@ -245,7 +389,7 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
id: genId(), type: 'CONDITION',
condition: { indicatorType: tmpl.ind, conditionType: tmpl.cond, leftField: tmpl.l, rightField: tmpl.r, candleRange: 1 },
};
if (tmpl.signal !== signalTab) setSignalTab(tmpl.signal);
if (tmpl.signal !== signalTab) switchSignalTab(tmpl.signal);
const root = tmpl.signal === 'buy' ? buyCondition : sellCondition;
const setRoot = tmpl.signal === 'buy' ? setBuyCondition : setSellCondition;
if (!root) setRoot(newNode);
@@ -380,14 +524,14 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
<button
type="button"
className={`se-signal-tab${signalTab === 'buy' ? ' se-signal-tab--buy-on' : ''}`}
onClick={() => { setSignalTab('buy'); setSelectedNodeId(null); }}
onClick={() => switchSignalTab('buy')}
>
(Entry)
</button>
<button
type="button"
className={`se-signal-tab${signalTab === 'sell' ? ' se-signal-tab--sell-on' : ''}`}
onClick={() => { setSignalTab('sell'); setSelectedNodeId(null); }}
onClick={() => switchSignalTab('sell')}
>
(Exit)
</button>
@@ -406,6 +550,8 @@ export default function StrategyEditorPage({ theme, activeIndicators = [] }: Pro
onChange={setCurrentRoot}
selectedNodeId={selectedNodeId}
onSelectNode={setSelectedNodeId}
layoutSeed={layoutSeed}
onLayoutChange={handleLayoutChange}
/>
</ReactFlowProvider>