전략지표 설정 삭제버튼
This commit is contained in:
@@ -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} />;
|
||||||
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user