From c7b92dbec5d634c9b8ae60ee06a4cdad2c6a79fb Mon Sep 17 00:00:00 2001 From: Macbook Date: Tue, 16 Jun 2026 10:18:30 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B7=B8=EB=9E=98=ED=94=84=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=20=EC=A1=B0=EA=B1=B4=EC=84=A0=ED=83=9D=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/strategyEditor/FlowNodes.tsx | 37 ++++++++++++++++- .../strategyEditor/StrategyEditorCanvas.tsx | 23 ++++++++++- frontend/src/styles/strategyEditor.css | 41 ++++++++++++++++++- frontend/src/utils/strategyFlowLayout.ts | 9 +++- 4 files changed, 105 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/strategyEditor/FlowNodes.tsx b/frontend/src/components/strategyEditor/FlowNodes.tsx index f32674d..920b0b5 100644 --- a/frontend/src/components/strategyEditor/FlowNodes.tsx +++ b/frontend/src/components/strategyEditor/FlowNodes.tsx @@ -33,6 +33,7 @@ import { stochPairDisplayName, } from '../../utils/stochOverboughtPair'; import ConditionNodeSettings from './ConditionNodeSettings'; +import ConditionPresetPicker from './ConditionPresetPicker'; import StochPairNodeSettings from './StochPairNodeSettings'; import PriceExtremePairNodeSettings from './PriceExtremePairNodeSettings'; import { @@ -415,7 +416,9 @@ export const ConditionFlowNode = memo(function ConditionFlowNode({ id, data, sel const isSell = d.signalTab === 'sell'; const { onDragEnter, onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d); const [settingsOpen, setSettingsOpen] = useState(false); + const [presetOpen, setPresetOpen] = useState(false); const showSettings = cond && hasNodeSettings(cond); + const showPreset = cond && !cond.composite; const handleSettingsChange = useCallback((next: NonNullable) => { d.onUpdateCondition?.(id, next); @@ -423,7 +426,7 @@ export const ConditionFlowNode = memo(function ConditionFlowNode({ id, data, sel return (
{indLabel}
+ {showPreset && ( + + )} {showSettings && (
{d.label ?? indLabel}
+ {presetOpen && cond && d.def && ( + d.onReplaceLogicNode?.(id, replaced)} + onClose={() => setPresetOpen(false)} + popoverClassName="se-flow-preset-pop" + /> + )} {settingsOpen && cond && d.def && ( { + if (isOrphanNode(orphans, id)) { + onOrphansChange(orphans.map(o => (o.id === id ? replaced : o))); + return; + } + if (root && findNodeInTree(root, id)) { + onChange(updateNode(root, id, () => replaced)); + return; + } + for (const [sid, branch] of Object.entries(extraRoots)) { + if (branch && findNodeInTree(branch, id)) { + onExtraRootsChange?.({ + ...extraRoots, + [sid]: updateNode(branch, id, () => replaced), + }); + return; + } + } + }, [root, extraRoots, orphans, onChange, onOrphansChange, onExtraRootsChange]); + const handleChangeLogicGateType = useCallback((id: string, gateType: 'AND' | 'OR') => { const patchGate = (n: LogicNode) => ( n.type === 'AND' || n.type === 'OR' @@ -868,6 +888,7 @@ function StrategyEditorCanvasInner({ const flowCallbacks = useMemo(() => ({ onDelete: handleDelete, onUpdateCondition: handleUpdateCondition, + onReplaceLogicNode: handleReplaceLogicNode, onUpdateStochPairSecondary: handleUpdateStochPairSecondary, onUpdatePriceExtremeFilterLevel: handleUpdatePriceExtremeFilterLevel, onUpdateInflection33FilterLevel: handleUpdateInflection33FilterLevel, @@ -879,7 +900,7 @@ function StrategyEditorCanvasInner({ onStartCandleTypesChange: handleStartCandleTypesChange, onDeleteStart: handleDeleteStart, }), [ - handleDelete, handleUpdateCondition, handleUpdateStochPairSecondary, handleUpdatePriceExtremeFilterLevel, handleUpdateInflection33FilterLevel, handleUpdateStableStrategyFilterLevel, handleChangeLogicGateType, + handleDelete, handleUpdateCondition, handleReplaceLogicNode, handleUpdateStochPairSecondary, handleUpdatePriceExtremeFilterLevel, handleUpdateInflection33FilterLevel, handleUpdateStableStrategyFilterLevel, handleChangeLogicGateType, handleDropTarget, handleDragOverTarget, handleDragLeaveTarget, handleStartCandleTypesChange, handleDeleteStart, ]); diff --git a/frontend/src/styles/strategyEditor.css b/frontend/src/styles/strategyEditor.css index 55b2739..5847ac7 100644 --- a/frontend/src/styles/strategyEditor.css +++ b/frontend/src/styles/strategyEditor.css @@ -1218,18 +1218,57 @@ transition: opacity 0.12s, background 0.12s; flex-shrink: 0; } +.se-flow-preset { + border: none; + background: rgba(167, 139, 250, 0.2); + color: #a78bfa; + width: 18px; height: 18px; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + opacity: 0; + transition: opacity 0.12s, background 0.12s; + flex-shrink: 0; +} +.se-flow-preset:hover { + background: rgba(167, 139, 250, 0.35); +} .se-flow-settings:hover { background: rgba(230, 194, 0, 0.2); } .se-flow-node:hover .se-flow-del, .se-flow-node:hover .se-flow-settings, +.se-flow-node:hover .se-flow-preset, .se-flow-node--selected .se-flow-del, .se-flow-node--selected .se-flow-settings, +.se-flow-node--selected .se-flow-preset, .se-flow-node--settings-open .se-flow-del, -.se-flow-node--settings-open .se-flow-settings { +.se-flow-node--settings-open .se-flow-settings, +.se-flow-node--settings-open .se-flow-preset, +.se-flow-node--preset-open .se-flow-del, +.se-flow-node--preset-open .se-flow-settings, +.se-flow-node--preset-open .se-flow-preset { opacity: 1; } +.se-flow-preset-pop { + position: absolute; + top: 6px; + right: 6px; + z-index: 40; + width: min(320px, calc(100vw - 24px)); + max-height: min(420px, 60vh); + overflow: auto; + padding: 8px 10px; + border-radius: 8px; + border: 1px solid color-mix(in srgb, #a78bfa 45%, transparent); + background: var(--se-bg-elevated); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); +} + .se-flow-settings-pop { position: absolute; top: 6px; diff --git a/frontend/src/utils/strategyFlowLayout.ts b/frontend/src/utils/strategyFlowLayout.ts index 5f8f0f1..231ba26 100644 --- a/frontend/src/utils/strategyFlowLayout.ts +++ b/frontend/src/utils/strategyFlowLayout.ts @@ -40,6 +40,8 @@ export type StrategyFlowNodeData = { onDeleteStart?: (startId: string) => void; onDelete?: (id: string) => void; onUpdateCondition?: (nodeId: string, condition: ConditionDSL) => void; + /** 조건 프리셋(복합) — 노드 전체 교체 (CONDITION → AND 등) */ + onReplaceLogicNode?: (nodeId: string, node: LogicNode) => void; /** Stoch 과열×보조 복합 — 보조지표 변경 */ onUpdateStochPairSecondary?: (nodeId: string, secondaryIndicator: string) => void; /** 9·20 신고가/신저가 복합 — 필터 강도 변경 */ @@ -480,7 +482,7 @@ function layoutTree( signalTab: 'buy' | 'sell', callbacks: Pick< StrategyFlowNodeData, - 'onDelete' | 'onUpdateCondition' | 'onUpdateStochPairSecondary' | 'onUpdatePriceExtremeFilterLevel' | 'onUpdateInflection33FilterLevel' | 'onUpdateStableStrategyFilterLevel' | 'onChangeLogicGateType' + 'onDelete' | 'onUpdateCondition' | 'onReplaceLogicNode' | 'onUpdateStochPairSecondary' | 'onUpdatePriceExtremeFilterLevel' | 'onUpdateInflection33FilterLevel' | 'onUpdateStableStrategyFilterLevel' | 'onChangeLogicGateType' | 'onDropTarget' | 'onDragOverTarget' | 'onDragLeaveTarget' >, dragOverId: string | null, @@ -502,6 +504,7 @@ function layoutTree( signalTab, onDelete: callbacks.onDelete, onUpdateCondition: callbacks.onUpdateCondition, + onReplaceLogicNode: callbacks.onReplaceLogicNode, onUpdateStochPairSecondary: callbacks.onUpdateStochPairSecondary, onUpdatePriceExtremeFilterLevel: callbacks.onUpdatePriceExtremeFilterLevel, onUpdateInflection33FilterLevel: callbacks.onUpdateInflection33FilterLevel, @@ -635,7 +638,7 @@ function appendOrphanFlowNodes( signalTab: 'buy' | 'sell', callbacks: Pick< StrategyFlowNodeData, - 'onDelete' | 'onUpdateCondition' | 'onUpdateStochPairSecondary' | 'onUpdatePriceExtremeFilterLevel' | 'onUpdateInflection33FilterLevel' | 'onUpdateStableStrategyFilterLevel' | 'onChangeLogicGateType' + 'onDelete' | 'onUpdateCondition' | 'onReplaceLogicNode' | 'onUpdateStochPairSecondary' | 'onUpdatePriceExtremeFilterLevel' | 'onUpdateInflection33FilterLevel' | 'onUpdateStableStrategyFilterLevel' | 'onChangeLogicGateType' | 'onDropTarget' | 'onDragOverTarget' | 'onDragLeaveTarget' >, ): void { @@ -657,6 +660,7 @@ function appendOrphanFlowNodes( signalTab, onDelete: callbacks.onDelete, onUpdateCondition: callbacks.onUpdateCondition, + onReplaceLogicNode: callbacks.onReplaceLogicNode, onUpdateStochPairSecondary: callbacks.onUpdateStochPairSecondary, onUpdatePriceExtremeFilterLevel: callbacks.onUpdatePriceExtremeFilterLevel, onUpdateInflection33FilterLevel: callbacks.onUpdateInflection33FilterLevel, @@ -684,6 +688,7 @@ export function logicNodeToFlow( StrategyFlowNodeData, | 'onDelete' | 'onUpdateCondition' + | 'onReplaceLogicNode' | 'onUpdateStochPairSecondary' | 'onUpdatePriceExtremeFilterLevel' | 'onUpdateInflection33FilterLevel'