전략편집기 목록방식 오류 수정
This commit is contained in:
@@ -355,12 +355,18 @@ export default function StrategyEditorPage({
|
||||
const sellConditionRef = useRef(sellCondition);
|
||||
const buyOrphansRef = useRef(buyOrphans);
|
||||
const sellOrphansRef = useRef(sellOrphans);
|
||||
const selectedIdRef = useRef(selectedId);
|
||||
const stratNameRef = useRef(stratName);
|
||||
const stratDescRef = useRef(stratDesc);
|
||||
buyLayoutRef.current = buyLayout;
|
||||
sellLayoutRef.current = sellLayout;
|
||||
buyConditionRef.current = buyCondition;
|
||||
sellConditionRef.current = sellCondition;
|
||||
buyOrphansRef.current = buyOrphans;
|
||||
sellOrphansRef.current = sellOrphans;
|
||||
selectedIdRef.current = selectedId;
|
||||
stratNameRef.current = stratName;
|
||||
stratDescRef.current = stratDesc;
|
||||
const layoutRevisionRef = useRef(0);
|
||||
const strategyAutosaveTimerRef = useRef<number | null>(null);
|
||||
const layoutPersistReadyRef = useRef(false);
|
||||
@@ -434,6 +440,8 @@ export default function StrategyEditorPage({
|
||||
[sellCondition, sellLayout],
|
||||
);
|
||||
const currentEditorState = signalTab === 'buy' ? buyEditorState : sellEditorState;
|
||||
const currentEditorStateRef = useRef(currentEditorState);
|
||||
currentEditorStateRef.current = currentEditorState;
|
||||
|
||||
const selectedLogicNode = useMemo(() => {
|
||||
if (!selectedNodeId) return null;
|
||||
@@ -496,6 +504,38 @@ export default function StrategyEditorPage({
|
||||
: s));
|
||||
}, [selectedId, stratName, stratDesc, buildCurrentFlowLayout]);
|
||||
|
||||
/** 전략 전환·새 전략 전 — ref 스냅샷을 지정 id 로 저장 (selectedId 변경 후 빈 DSL 덮어쓰기 방지) */
|
||||
const persistStrategySnapshot = useCallback(async (
|
||||
strategyId: number,
|
||||
name: string,
|
||||
description: string,
|
||||
) => {
|
||||
const buy = toEditorState(buyConditionRef.current, buyLayoutRef.current);
|
||||
const sell = toEditorState(sellConditionRef.current, sellLayoutRef.current);
|
||||
const aligned = alignBuySellStartCandleTypesForSave(buy, sell);
|
||||
const encodedBuy = encodeConditionForSave(aligned.buy);
|
||||
const encodedSell = encodeConditionForSave(aligned.sell);
|
||||
if (!encodedBuy && !encodedSell) return;
|
||||
const flowLayout = buildCurrentFlowLayout();
|
||||
const saved = await persistStrategyEditorState(
|
||||
strategyId,
|
||||
name,
|
||||
description,
|
||||
aligned.buy,
|
||||
aligned.sell,
|
||||
flowLayout,
|
||||
);
|
||||
setStrategies(prev => prev.map(s => s.id === strategyId
|
||||
? {
|
||||
...s,
|
||||
buyCondition: encodedBuy,
|
||||
sellCondition: encodedSell,
|
||||
flowLayout,
|
||||
updatedAt: saved?.updatedAt ?? s.updatedAt,
|
||||
}
|
||||
: s));
|
||||
}, [buildCurrentFlowLayout]);
|
||||
|
||||
const flushStrategyPersist = useCallback(() => {
|
||||
if (strategyAutosaveTimerRef.current != null) {
|
||||
window.clearTimeout(strategyAutosaveTimerRef.current);
|
||||
@@ -732,14 +772,19 @@ export default function StrategyEditorPage({
|
||||
};
|
||||
}, [selectedNodeId, selectedLogicNode, stochPairForest, applyStochPairSecondary]);
|
||||
|
||||
const handleEditorStateChange = useCallback((next: EditorConditionState) => {
|
||||
setCurrentRoot(next.root);
|
||||
const handleEditorStateChange = useCallback((
|
||||
next: EditorConditionState | ((prev: EditorConditionState) => EditorConditionState),
|
||||
) => {
|
||||
const resolved = typeof next === 'function'
|
||||
? next(currentEditorStateRef.current)
|
||||
: next;
|
||||
setCurrentRoot(resolved.root);
|
||||
setCurrentLayout(prev => ({
|
||||
...prev,
|
||||
startMeta: next.startMeta,
|
||||
extraStartIds: next.extraStartIds,
|
||||
extraRoots: next.extraRoots,
|
||||
startCombineOp: normalizeStartCombineOp(next.startCombineOp),
|
||||
startMeta: resolved.startMeta,
|
||||
extraStartIds: resolved.extraStartIds,
|
||||
extraRoots: resolved.extraRoots,
|
||||
startCombineOp: normalizeStartCombineOp(resolved.startCombineOp),
|
||||
}));
|
||||
scheduleStrategyPersist();
|
||||
}, [setCurrentRoot, setCurrentLayout, scheduleStrategyPersist]);
|
||||
@@ -784,21 +829,20 @@ export default function StrategyEditorPage({
|
||||
};
|
||||
}, [signalTab, layoutSeedKey, buyLayout, sellLayout]);
|
||||
|
||||
const persistStrategyToDbRef = useRef(persistStrategyToDb);
|
||||
persistStrategyToDbRef.current = persistStrategyToDb;
|
||||
useEffect(() => {
|
||||
const id = selectedId;
|
||||
const name = stratName;
|
||||
return () => {
|
||||
if (strategyAutosaveTimerRef.current != null) {
|
||||
window.clearTimeout(strategyAutosaveTimerRef.current);
|
||||
strategyAutosaveTimerRef.current = null;
|
||||
}
|
||||
if (id != null && name.trim()) {
|
||||
void persistStrategyToDbRef.current().catch(() => { /* unmount flush */ });
|
||||
}
|
||||
};
|
||||
}, [selectedId, stratName]);
|
||||
const persistStrategySnapshotRef = useRef(persistStrategySnapshot);
|
||||
persistStrategySnapshotRef.current = persistStrategySnapshot;
|
||||
useEffect(() => () => {
|
||||
if (strategyAutosaveTimerRef.current != null) {
|
||||
window.clearTimeout(strategyAutosaveTimerRef.current);
|
||||
strategyAutosaveTimerRef.current = null;
|
||||
}
|
||||
const id = selectedIdRef.current;
|
||||
const name = stratNameRef.current;
|
||||
const desc = stratDescRef.current;
|
||||
if (id != null && name.trim()) {
|
||||
void persistStrategySnapshotRef.current(id, name, desc).catch(() => { /* unmount flush */ });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!layoutPersistReadyRef.current) {
|
||||
@@ -861,7 +905,24 @@ export default function StrategyEditorPage({
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const flushBeforeStrategySwitch = useCallback(() => {
|
||||
if (editorMode === 'graph') layoutFlushRef.current?.();
|
||||
if (strategyAutosaveTimerRef.current != null) {
|
||||
window.clearTimeout(strategyAutosaveTimerRef.current);
|
||||
strategyAutosaveTimerRef.current = null;
|
||||
}
|
||||
const prevId = selectedIdRef.current;
|
||||
const prevName = stratNameRef.current;
|
||||
const prevDesc = stratDescRef.current;
|
||||
if (prevId != null && prevName.trim()) {
|
||||
void persistStrategySnapshot(prevId, prevName, prevDesc).catch(() => { /* switch flush */ });
|
||||
}
|
||||
}, [editorMode, persistStrategySnapshot]);
|
||||
|
||||
const handleSelectStrategy = (s: StrategyDto) => {
|
||||
if (selectedId !== s.id) {
|
||||
flushBeforeStrategySwitch();
|
||||
}
|
||||
const buyDecoded = decodeConditionForEditor(s.buyCondition ?? null);
|
||||
const sellDecoded = decodeConditionForEditor(s.sellCondition ?? null);
|
||||
let stored = normalizeStrategyFlowLayout(s.flowLayout);
|
||||
@@ -982,13 +1043,17 @@ export default function StrategyEditorPage({
|
||||
}, [variant, initialStrategyId, strategies]);
|
||||
|
||||
const handleNew = () => {
|
||||
flushBeforeStrategySwitch();
|
||||
setSelectedId(null);
|
||||
setStratName('');
|
||||
setStratDesc('');
|
||||
setBuyCondition(null);
|
||||
setSellCondition(null);
|
||||
setBuyOrphans([]);
|
||||
setSellOrphans([]);
|
||||
setSelectedNodeId(null);
|
||||
resetFlowLayout('draft', signalTab);
|
||||
resetFlowLayout('draft', 'buy');
|
||||
setSignalTab('buy');
|
||||
};
|
||||
|
||||
const openSaveDialog = useCallback((mode: SaveDialogMode) => {
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
import StartTimeframeCheckboxes from './StartTimeframeCheckboxes';
|
||||
import LogicGateOpToggle from './LogicGateOpToggle';
|
||||
import { formatStartCandleTypesLabel } from '../../utils/strategyStartNodes';
|
||||
import { START_NODE_ID } from '../../utils/strategyFlowLayout';
|
||||
import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage';
|
||||
import {
|
||||
findPaletteDropKeyAtPoint,
|
||||
@@ -293,7 +294,7 @@ interface StartSectionBlockProps {
|
||||
def: DefType;
|
||||
dragOverKey: string | null;
|
||||
setDragOverKey: (key: string | null) => void;
|
||||
onRootChange: (root: LogicNode | null) => void;
|
||||
onRootChange: (root: LogicNode | null | ((prev: LogicNode | null) => LogicNode | null)) => void;
|
||||
onCandleTypesChange: (candleTypes: string[]) => void;
|
||||
onDeleteStart?: () => void;
|
||||
onAddStart?: () => void;
|
||||
@@ -324,16 +325,14 @@ function StartSectionBlock({
|
||||
return;
|
||||
}
|
||||
const newNode = makeNode(data.type, data.value, signalTab, def, makeNodeOptionsFromPalette(data));
|
||||
if (!root) {
|
||||
onRootChange(newNode);
|
||||
return;
|
||||
}
|
||||
if (!targetId) {
|
||||
onRootChange(mergeAtRoot(root, newNode, data.type === 'operator'));
|
||||
return;
|
||||
}
|
||||
onRootChange(addChild(root, targetId, newNode));
|
||||
}, [root, signalTab, def, onRootChange, onAddStart]);
|
||||
onRootChange(prev => {
|
||||
if (!prev) return newNode;
|
||||
if (!targetId) {
|
||||
return mergeAtRoot(prev, newNode, data.type === 'operator');
|
||||
}
|
||||
return addChild(prev, targetId, newNode);
|
||||
});
|
||||
}, [signalTab, def, onRootChange, onAddStart]);
|
||||
|
||||
const handleSectionDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -408,7 +407,9 @@ interface Props {
|
||||
editorState: EditorConditionState;
|
||||
signalTab: 'buy' | 'sell';
|
||||
def: DefType;
|
||||
onEditorStateChange: (next: EditorConditionState) => void;
|
||||
onEditorStateChange: (
|
||||
next: EditorConditionState | ((prev: EditorConditionState) => EditorConditionState),
|
||||
) => void;
|
||||
onAddStart?: () => void;
|
||||
onOrphansChange?: (nodes: LogicNode[]) => void;
|
||||
orphans?: LogicNode[];
|
||||
@@ -438,24 +439,35 @@ export default function StrategyListEditor({
|
||||
onAddStart();
|
||||
return;
|
||||
}
|
||||
onEditorStateChange(addExtraStartSection(editorState));
|
||||
}, [onAddStart, onEditorStateChange, editorState]);
|
||||
onEditorStateChange(prev => addExtraStartSection(prev));
|
||||
}, [onAddStart, onEditorStateChange]);
|
||||
|
||||
const handleRootChange = useCallback((startId: string, root: LogicNode | null) => {
|
||||
onEditorStateChange(updateBranchRoot(editorState, startId, root));
|
||||
}, [editorState, onEditorStateChange]);
|
||||
const handleRootChange = useCallback((
|
||||
startId: string,
|
||||
root: LogicNode | null | ((prev: LogicNode | null) => LogicNode | null),
|
||||
) => {
|
||||
onEditorStateChange(prev => {
|
||||
const currentRoot = startId === START_NODE_ID
|
||||
? prev.root
|
||||
: (prev.extraRoots[startId] ?? null);
|
||||
const resolved = typeof root === 'function' ? root(currentRoot) : root;
|
||||
return updateBranchRoot(prev, startId, resolved);
|
||||
});
|
||||
}, [onEditorStateChange]);
|
||||
|
||||
const handleCandleTypesChange = useCallback((startId: string, candleTypes: string[]) => {
|
||||
onEditorStateChange(updateStartCandleTypes(editorState, startId, candleTypes));
|
||||
}, [editorState, onEditorStateChange]);
|
||||
onEditorStateChange(prev => updateStartCandleTypes(prev, startId, candleTypes));
|
||||
}, [onEditorStateChange]);
|
||||
|
||||
const handleDeleteStart = useCallback((startId: string) => {
|
||||
const { state, orphanedNodes } = removeStartSection(editorState, startId);
|
||||
onEditorStateChange(state);
|
||||
if (orphanedNodes.length && onOrphansChange) {
|
||||
onOrphansChange([...orphans, ...orphanedNodes]);
|
||||
}
|
||||
}, [editorState, onEditorStateChange, onOrphansChange, orphans]);
|
||||
onEditorStateChange(prev => {
|
||||
const { state, orphanedNodes } = removeStartSection(prev, startId);
|
||||
if (orphanedNodes.length && onOrphansChange) {
|
||||
onOrphansChange([...orphans, ...orphanedNodes]);
|
||||
}
|
||||
return state;
|
||||
});
|
||||
}, [onEditorStateChange, onOrphansChange, orphans]);
|
||||
|
||||
const resolvePaletteNode = useCallback((
|
||||
data: { type: string; value: string; label: string; composite?: boolean },
|
||||
@@ -487,26 +499,25 @@ export default function StrategyListEditor({
|
||||
return;
|
||||
}
|
||||
|
||||
const section = collectStartSections(editorState).find(s => s.startId === startId);
|
||||
if (!section) return;
|
||||
|
||||
const newNode = resolvePaletteNode(data);
|
||||
if (!newNode) return;
|
||||
|
||||
const root = section.root;
|
||||
if (!root) {
|
||||
handleRootChange(startId, newNode);
|
||||
return;
|
||||
}
|
||||
if (!targetId) {
|
||||
handleRootChange(startId, mergeAtRoot(root, newNode, data.type === 'operator'));
|
||||
return;
|
||||
}
|
||||
handleRootChange(startId, addChild(root, targetId, newNode));
|
||||
onEditorStateChange(prev => {
|
||||
const section = collectStartSections(prev).find(s => s.startId === startId);
|
||||
if (!section) return prev;
|
||||
|
||||
const root = section.root;
|
||||
if (!root) {
|
||||
return updateBranchRoot(prev, startId, newNode);
|
||||
}
|
||||
if (!targetId) {
|
||||
return updateBranchRoot(prev, startId, mergeAtRoot(root, newNode, data.type === 'operator'));
|
||||
}
|
||||
return updateBranchRoot(prev, startId, addChild(root, targetId, newNode));
|
||||
});
|
||||
}, [
|
||||
editorState,
|
||||
handleAddStart,
|
||||
handleRootChange,
|
||||
onEditorStateChange,
|
||||
resolvePaletteNode,
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user