전략지표 설정 삭제버튼

This commit is contained in:
Macbook
2026-06-24 01:06:36 +09:00
parent e46ffe838f
commit e5086b2c92
3 changed files with 119 additions and 16 deletions
+64 -1
View File
@@ -42,6 +42,7 @@ import { persistStrategyEditorState } from '../utils/strategyTimeframeSync';
import { import {
START_NODE_ID, START_NODE_ID,
defaultStartMeta, defaultStartMeta,
isStartNodeId,
type StartCombineOp, type StartCombineOp,
} from '../utils/strategyStartNodes'; } from '../utils/strategyStartNodes';
import IndicatorPaletteTab from './strategyEditor/IndicatorPaletteTab'; import IndicatorPaletteTab from './strategyEditor/IndicatorPaletteTab';
@@ -114,7 +115,7 @@ import {
} from '../utils/strategyEditorLayoutStorage'; } from '../utils/strategyEditorLayoutStorage';
import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview'; import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview';
import LogicExpressionNarrative from './strategyEditor/LogicExpressionNarrative'; 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 StrategyListEditor from './strategyEditor/StrategyListEditor';
import StartCombineOpControl from './strategyEditor/StartCombineOpControl'; import StartCombineOpControl from './strategyEditor/StartCombineOpControl';
import { layoutFlushRef } from './strategyEditor/strategyEditorCallbacks'; import { layoutFlushRef } from './strategyEditor/strategyEditorCallbacks';
@@ -333,6 +334,7 @@ export default function StrategyEditorPage({
const [descOpen, setDescOpen] = useState(false); const [descOpen, setDescOpen] = useState(false);
const [deleteId, setDeleteId] = useState<number | null>(null); const [deleteId, setDeleteId] = useState<number | null>(null);
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null); const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
const canvasRef = useRef<StrategyEditorCanvasHandle>(null);
const [buyOrphans, setBuyOrphans] = useState<LogicNode[]>([]); const [buyOrphans, setBuyOrphans] = useState<LogicNode[]>([]);
const [sellOrphans, setSellOrphans] = useState<LogicNode[]>([]); const [sellOrphans, setSellOrphans] = useState<LogicNode[]>([]);
const [snack, setSnack] = useState<{ msg: string; ok: boolean } | null>(null); const [snack, setSnack] = useState<{ msg: string; ok: boolean } | null>(null);
@@ -460,6 +462,11 @@ export default function StrategyEditorPage({
); );
}, [selectedNodeId, currentRoot, currentOrphans, currentLayout.extraRoots]); }, [selectedNodeId, currentRoot, currentOrphans, currentLayout.extraRoots]);
const handleDeleteSelectedNode = useCallback(() => {
if (!selectedNodeId || isStartNodeId(selectedNodeId)) return;
canvasRef.current?.deleteNode(selectedNodeId);
}, [selectedNodeId]);
const stochPairForest = useMemo( const stochPairForest = useMemo(
() => [ () => [
currentRoot, currentRoot,
@@ -2041,6 +2048,7 @@ export default function StrategyEditorPage({
<> <>
<ReactFlowProvider> <ReactFlowProvider>
<StrategyEditorCanvas <StrategyEditorCanvas
ref={canvasRef}
theme={theme} theme={theme}
root={currentRoot} root={currentRoot}
orphans={currentOrphans} orphans={currentOrphans}
@@ -2064,11 +2072,22 @@ export default function StrategyEditorPage({
{selectedLogicNode?.type === 'CONDITION' && selectedLogicNode.condition && ( {selectedLogicNode?.type === 'CONDITION' && selectedLogicNode.condition && (
<div className="se-node-config-bar"> <div className="se-node-config-bar">
<div className="se-node-config-head">
<span className="se-node-config-label"> <span className="se-node-config-label">
{stochPairEditForSelected && !stochPairEditForSelected.stochLocked {stochPairEditForSelected && !stochPairEditForSelected.stochLocked
? 'Stoch 과열×보조' ? 'Stoch 과열×보조'
: selectedLogicNode.condition.indicatorType} : selectedLogicNode.condition.indicatorType}
</span> </span>
<button
type="button"
className="se-node-config-del"
title="삭제"
aria-label="지표 삭제"
onClick={handleDeleteSelectedNode}
>
×
</button>
</div>
<CondEditor <CondEditor
cond={selectedLogicNode.condition} cond={selectedLogicNode.condition}
signalType={signalTab} signalType={signalTab}
@@ -2106,7 +2125,18 @@ export default function StrategyEditorPage({
{selectedLogicNode && isStochOverboughtPairRoot(selectedLogicNode) && selectedLogicNode.stochPair && ( {selectedLogicNode && isStochOverboughtPairRoot(selectedLogicNode) && selectedLogicNode.stochPair && (
<div className="se-node-config-bar se-node-config-bar--stoch-pair"> <div className="se-node-config-bar se-node-config-bar--stoch-pair">
<div className="se-node-config-head">
<span className="se-node-config-label">Stoch ×</span> <span className="se-node-config-label">Stoch ×</span>
<button
type="button"
className="se-node-config-del"
title="삭제"
aria-label="지표 삭제"
onClick={handleDeleteSelectedNode}
>
×
</button>
</div>
<StochPairNodeSettings <StochPairNodeSettings
variant="inline" variant="inline"
secondaryIndicator={selectedLogicNode.stochPair.secondaryIndicatorType} secondaryIndicator={selectedLogicNode.stochPair.secondaryIndicatorType}
@@ -2118,7 +2148,18 @@ export default function StrategyEditorPage({
{selectedLogicNode && isPriceExtremeBreakoutPairRoot(selectedLogicNode) && selectedLogicNode.priceExtremePair && ( {selectedLogicNode && isPriceExtremeBreakoutPairRoot(selectedLogicNode) && selectedLogicNode.priceExtremePair && (
<div className="se-node-config-bar se-node-config-bar--price-extreme"> <div className="se-node-config-bar se-node-config-bar--price-extreme">
<div className="se-node-config-head">
<span className="se-node-config-label">9·20 / </span> <span className="se-node-config-label">9·20 / </span>
<button
type="button"
className="se-node-config-del"
title="삭제"
aria-label="지표 삭제"
onClick={handleDeleteSelectedNode}
>
×
</button>
</div>
<PriceExtremePairNodeSettings <PriceExtremePairNodeSettings
variant="inline" variant="inline"
mode={selectedLogicNode.priceExtremePair.mode} mode={selectedLogicNode.priceExtremePair.mode}
@@ -2132,7 +2173,18 @@ export default function StrategyEditorPage({
)} )}
{selectedLogicNode && isInflection33PairRoot(selectedLogicNode) && selectedLogicNode.inflection33Pair && ( {selectedLogicNode && isInflection33PairRoot(selectedLogicNode) && selectedLogicNode.inflection33Pair && (
<div className="se-node-config-bar se-node-config-bar--inflection33"> <div className="se-node-config-bar se-node-config-bar--inflection33">
<div className="se-node-config-head">
<span className="se-node-config-label">33 </span> <span className="se-node-config-label">33 </span>
<button
type="button"
className="se-node-config-del"
title="삭제"
aria-label="지표 삭제"
onClick={handleDeleteSelectedNode}
>
×
</button>
</div>
<Inflection33PairNodeSettings <Inflection33PairNodeSettings
variant="inline" variant="inline"
mode={selectedLogicNode.inflection33Pair.mode} mode={selectedLogicNode.inflection33Pair.mode}
@@ -2145,7 +2197,18 @@ export default function StrategyEditorPage({
)} )}
{selectedLogicNode && isStableStrategyPairRoot(selectedLogicNode) && selectedLogicNode.stableStrategyPair && ( {selectedLogicNode && isStableStrategyPairRoot(selectedLogicNode) && selectedLogicNode.stableStrategyPair && (
<div className="se-node-config-bar se-node-config-bar--stable-strategy"> <div className="se-node-config-bar se-node-config-bar--stable-strategy">
<div className="se-node-config-head">
<span className="se-node-config-label"> </span> <span className="se-node-config-label"> </span>
<button
type="button"
className="se-node-config-del"
title="삭제"
aria-label="지표 삭제"
onClick={handleDeleteSelectedNode}
>
×
</button>
</div>
<StableStrategyPairNodeSettings <StableStrategyPairNodeSettings
variant="inline" variant="inline"
strategyId={selectedLogicNode.stableStrategyPair.strategyId as StableStrategyId} strategyId={selectedLogicNode.stableStrategyPair.strategyId as StableStrategyId}
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, forwardRef, useImperativeHandle } from 'react';
import { import {
ReactFlow, ReactFlow,
Background, Background,
@@ -148,6 +148,10 @@ interface Props {
onExtraRootsChange?: (roots: Record<string, LogicNode | null>) => void; onExtraRootsChange?: (roots: Record<string, LogicNode | null>) => void;
} }
export interface StrategyEditorCanvasHandle {
deleteNode: (id: string) => void;
}
function collectTreeIds(node: LogicNode | null): string[] { function collectTreeIds(node: LogicNode | null): string[] {
if (!node) return []; if (!node) return [];
const ids = [node.id]; const ids = [node.id];
@@ -310,7 +314,7 @@ function StrategyEditorCanvasInner({
onStartCandleTypesChange, onStartCandleTypesChange,
onExtraStartIdsChange, onExtraStartIdsChange,
onExtraRootsChange, onExtraRootsChange,
}: Props) { }: Props, ref: React.ForwardedRef<StrategyEditorCanvasHandle>) {
const { fitView } = useReactFlow(); const { fitView } = useReactFlow();
const canvasWrapRef = useRef<HTMLDivElement>(null); const canvasWrapRef = useRef<HTMLDivElement>(null);
const reactFlowRef = useRef<ReactFlowInstance | null>(null); const reactFlowRef = useRef<ReactFlowInstance | null>(null);
@@ -500,6 +504,8 @@ function StrategyEditorCanvasInner({
onSelectNode, selectedNodeId, scheduleLayoutEmit, onSelectNode, selectedNodeId, scheduleLayoutEmit,
]); ]);
useImperativeHandle(ref, () => ({ deleteNode: handleDelete }), [handleDelete]);
const deleteSelectedNodes = useCallback(() => { const deleteSelectedNodes = useCallback(() => {
const ids = nodesRef.current const ids = nodesRef.current
.filter(n => n.selected && !isStartNodeId(n.id)) .filter(n => n.selected && !isStartNodeId(n.id))
@@ -1586,6 +1592,8 @@ function StrategyEditorCanvasInner({
); );
} }
export default function StrategyEditorCanvas(props: Props) { const StrategyEditorCanvasInnerForwarded = forwardRef(StrategyEditorCanvasInner);
return <StrategyEditorCanvasInner {...props} />;
} export default forwardRef<StrategyEditorCanvasHandle, Props>(function StrategyEditorCanvas(props, ref) {
return <StrategyEditorCanvasInnerForwarded {...props} ref={ref} />;
});
+33 -1
View File
@@ -1517,6 +1517,13 @@
background: var(--se-toolbar-bg); background: var(--se-toolbar-bg);
box-shadow: 0 0 24px color-mix(in srgb, var(--se-accent) 8%, transparent); 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 { .se-node-config-label {
display: inline-block; display: inline-block;
font-size: 0.68rem; font-size: 0.68rem;
@@ -1525,7 +1532,29 @@
padding: 2px 8px; padding: 2px 8px;
border: 1px solid color-mix(in srgb, var(--se-accent) 35%, transparent); border: 1px solid color-mix(in srgb, var(--se-accent) 35%, transparent);
border-radius: 999px; 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 { .se-sync-tip {
display: block; display: block;
@@ -1551,6 +1580,9 @@
align-items: flex-start; align-items: flex-start;
gap: 8px 12px; gap: 8px 12px;
} }
.se-node-config-bar--stoch-pair .se-node-config-head {
flex: 1 1 100%;
}
.se-stoch-pair-inline-settings { .se-stoch-pair-inline-settings {
display: flex; display: flex;
flex: 1; flex: 1;