diff --git a/frontend/src/components/StrategyEditorPage.tsx b/frontend/src/components/StrategyEditorPage.tsx index d61731b..7f09eb1 100644 --- a/frontend/src/components/StrategyEditorPage.tsx +++ b/frontend/src/components/StrategyEditorPage.tsx @@ -42,6 +42,7 @@ import { persistStrategyEditorState } from '../utils/strategyTimeframeSync'; import { START_NODE_ID, defaultStartMeta, + isStartNodeId, type StartCombineOp, } from '../utils/strategyStartNodes'; import IndicatorPaletteTab from './strategyEditor/IndicatorPaletteTab'; @@ -114,7 +115,7 @@ import { } from '../utils/strategyEditorLayoutStorage'; import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview'; import LogicExpressionNarrative from './strategyEditor/LogicExpressionNarrative'; -import StrategyEditorCanvas, { type FlowLayoutSeed } from './strategyEditor/StrategyEditorCanvas'; +import StrategyEditorCanvas, { type FlowLayoutSeed, type StrategyEditorCanvasHandle } from './strategyEditor/StrategyEditorCanvas'; import StrategyListEditor from './strategyEditor/StrategyListEditor'; import StartCombineOpControl from './strategyEditor/StartCombineOpControl'; import { layoutFlushRef } from './strategyEditor/strategyEditorCallbacks'; @@ -333,6 +334,7 @@ export default function StrategyEditorPage({ const [descOpen, setDescOpen] = useState(false); const [deleteId, setDeleteId] = useState(null); const [selectedNodeId, setSelectedNodeId] = useState(null); + const canvasRef = useRef(null); const [buyOrphans, setBuyOrphans] = useState([]); const [sellOrphans, setSellOrphans] = useState([]); const [snack, setSnack] = useState<{ msg: string; ok: boolean } | null>(null); @@ -460,6 +462,11 @@ export default function StrategyEditorPage({ ); }, [selectedNodeId, currentRoot, currentOrphans, currentLayout.extraRoots]); + const handleDeleteSelectedNode = useCallback(() => { + if (!selectedNodeId || isStartNodeId(selectedNodeId)) return; + canvasRef.current?.deleteNode(selectedNodeId); + }, [selectedNodeId]); + const stochPairForest = useMemo( () => [ currentRoot, @@ -2041,6 +2048,7 @@ export default function StrategyEditorPage({ <> - - {stochPairEditForSelected && !stochPairEditForSelected.stochLocked - ? 'Stoch 과열×보조' - : selectedLogicNode.condition.indicatorType} - +
+ + {stochPairEditForSelected && !stochPairEditForSelected.stochLocked + ? 'Stoch 과열×보조' + : selectedLogicNode.condition.indicatorType} + + +
- Stoch 과열×보조 +
+ Stoch 과열×보조 + +
- 9·20 신고가/신저가 필터 +
+ 9·20 신고가/신저가 필터 + +
- 33변곡 필터 +
+ 33변곡 필터 + +
- 추천 전략 필터 +
+ 추천 전략 필터 + +
) => void; } +export interface StrategyEditorCanvasHandle { + deleteNode: (id: string) => void; +} + function collectTreeIds(node: LogicNode | null): string[] { if (!node) return []; const ids = [node.id]; @@ -310,7 +314,7 @@ function StrategyEditorCanvasInner({ onStartCandleTypesChange, onExtraStartIdsChange, onExtraRootsChange, -}: Props) { +}: Props, ref: React.ForwardedRef) { const { fitView } = useReactFlow(); const canvasWrapRef = useRef(null); const reactFlowRef = useRef(null); @@ -500,6 +504,8 @@ function StrategyEditorCanvasInner({ onSelectNode, selectedNodeId, scheduleLayoutEmit, ]); + useImperativeHandle(ref, () => ({ deleteNode: handleDelete }), [handleDelete]); + const deleteSelectedNodes = useCallback(() => { const ids = nodesRef.current .filter(n => n.selected && !isStartNodeId(n.id)) @@ -1586,6 +1592,8 @@ function StrategyEditorCanvasInner({ ); } -export default function StrategyEditorCanvas(props: Props) { - return ; -} +const StrategyEditorCanvasInnerForwarded = forwardRef(StrategyEditorCanvasInner); + +export default forwardRef(function StrategyEditorCanvas(props, ref) { + return ; +}); diff --git a/frontend/src/styles/strategyEditor.css b/frontend/src/styles/strategyEditor.css index ec8bc12..ca4fa66 100644 --- a/frontend/src/styles/strategyEditor.css +++ b/frontend/src/styles/strategyEditor.css @@ -1517,6 +1517,13 @@ background: var(--se-toolbar-bg); box-shadow: 0 0 24px color-mix(in srgb, var(--se-accent) 8%, transparent); } +.se-node-config-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 8px; +} .se-node-config-label { display: inline-block; font-size: 0.68rem; @@ -1525,7 +1532,29 @@ padding: 2px 8px; border: 1px solid color-mix(in srgb, var(--se-accent) 35%, transparent); border-radius: 999px; - margin-bottom: 8px; + margin-bottom: 0; +} +.se-node-config-del { + border: 1px solid color-mix(in srgb, var(--se-danger) 45%, transparent); + background: color-mix(in srgb, var(--se-danger) 24%, transparent); + color: color-mix(in srgb, var(--se-danger) 88%, #fff 12%); + width: 22px; + height: 22px; + border-radius: 4px; + cursor: pointer; + font-size: 0.95rem; + line-height: 1; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + transition: background 0.12s, border-color 0.12s, color 0.12s; +} +.se-node-config-del:hover { + background: color-mix(in srgb, var(--se-danger) 42%, transparent); + border-color: color-mix(in srgb, var(--se-danger) 72%, transparent); + color: #fff; } .se-sync-tip { display: block; @@ -1551,6 +1580,9 @@ align-items: flex-start; gap: 8px 12px; } +.se-node-config-bar--stoch-pair .se-node-config-head { + flex: 1 1 100%; +} .se-stoch-pair-inline-settings { display: flex; flex: 1;