그래프방식 조건선택 기능 추가
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
|||||||
stochPairDisplayName,
|
stochPairDisplayName,
|
||||||
} from '../../utils/stochOverboughtPair';
|
} from '../../utils/stochOverboughtPair';
|
||||||
import ConditionNodeSettings from './ConditionNodeSettings';
|
import ConditionNodeSettings from './ConditionNodeSettings';
|
||||||
|
import ConditionPresetPicker from './ConditionPresetPicker';
|
||||||
import StochPairNodeSettings from './StochPairNodeSettings';
|
import StochPairNodeSettings from './StochPairNodeSettings';
|
||||||
import PriceExtremePairNodeSettings from './PriceExtremePairNodeSettings';
|
import PriceExtremePairNodeSettings from './PriceExtremePairNodeSettings';
|
||||||
import {
|
import {
|
||||||
@@ -415,7 +416,9 @@ export const ConditionFlowNode = memo(function ConditionFlowNode({ id, data, sel
|
|||||||
const isSell = d.signalTab === 'sell';
|
const isSell = d.signalTab === 'sell';
|
||||||
const { onDragEnter, onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d);
|
const { onDragEnter, onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d);
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
|
const [presetOpen, setPresetOpen] = useState(false);
|
||||||
const showSettings = cond && hasNodeSettings(cond);
|
const showSettings = cond && hasNodeSettings(cond);
|
||||||
|
const showPreset = cond && !cond.composite;
|
||||||
|
|
||||||
const handleSettingsChange = useCallback((next: NonNullable<typeof cond>) => {
|
const handleSettingsChange = useCallback((next: NonNullable<typeof cond>) => {
|
||||||
d.onUpdateCondition?.(id, next);
|
d.onUpdateCondition?.(id, next);
|
||||||
@@ -423,7 +426,7 @@ export const ConditionFlowNode = memo(function ConditionFlowNode({ id, data, sel
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`se-flow-node se-flow-node--cond se-flow-node--${tone}${isSell ? ' se-flow-node--sell-mode' : ''}${d.isOrphan ? ' se-flow-node--orphan' : ''} ${selected ? 'se-flow-node--selected' : ''} ${d.dragOver ? 'se-flow-node--drop' : ''}${settingsOpen ? ' se-flow-node--settings-open' : ''}`}
|
className={`se-flow-node se-flow-node--cond se-flow-node--${tone}${isSell ? ' se-flow-node--sell-mode' : ''}${d.isOrphan ? ' se-flow-node--orphan' : ''} ${selected ? 'se-flow-node--selected' : ''} ${d.dragOver ? 'se-flow-node--drop' : ''}${settingsOpen ? ' se-flow-node--settings-open' : ''}${presetOpen ? ' se-flow-node--preset-open' : ''}`}
|
||||||
onDragEnter={onDragEnter}
|
onDragEnter={onDragEnter}
|
||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
onDragLeave={onDragLeave}
|
onDragLeave={onDragLeave}
|
||||||
@@ -437,6 +440,25 @@ export const ConditionFlowNode = memo(function ConditionFlowNode({ id, data, sel
|
|||||||
<div className="se-flow-cond-top">
|
<div className="se-flow-cond-top">
|
||||||
<div className="se-flow-cond-badge">{indLabel}</div>
|
<div className="se-flow-cond-badge">{indLabel}</div>
|
||||||
<div className="se-flow-cond-actions">
|
<div className="se-flow-cond-actions">
|
||||||
|
{showPreset && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="se-flow-preset"
|
||||||
|
title="조건 선택"
|
||||||
|
aria-label="조건 선택"
|
||||||
|
aria-expanded={presetOpen}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setPresetOpen(v => !v);
|
||||||
|
setSettingsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M3 17l4-5 4 3 5-8 5 6" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
<circle cx="18" cy="6" r="2" fill="currentColor" stroke="none" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{showSettings && (
|
{showSettings && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -446,6 +468,7 @@ export const ConditionFlowNode = memo(function ConditionFlowNode({ id, data, sel
|
|||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setSettingsOpen(v => !v);
|
setSettingsOpen(v => !v);
|
||||||
|
setPresetOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
@@ -458,6 +481,18 @@ export const ConditionFlowNode = memo(function ConditionFlowNode({ id, data, sel
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="se-flow-cond-text">{d.label ?? indLabel}</div>
|
<div className="se-flow-cond-text">{d.label ?? indLabel}</div>
|
||||||
|
{presetOpen && cond && d.def && (
|
||||||
|
<ConditionPresetPicker
|
||||||
|
condition={cond}
|
||||||
|
def={d.def}
|
||||||
|
signalType={d.signalTab ?? 'buy'}
|
||||||
|
nodeId={id}
|
||||||
|
onChange={handleSettingsChange}
|
||||||
|
onReplaceNode={replaced => d.onReplaceLogicNode?.(id, replaced)}
|
||||||
|
onClose={() => setPresetOpen(false)}
|
||||||
|
popoverClassName="se-flow-preset-pop"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{settingsOpen && cond && d.def && (
|
{settingsOpen && cond && d.def && (
|
||||||
<ConditionNodeSettings
|
<ConditionNodeSettings
|
||||||
condition={cond}
|
condition={cond}
|
||||||
|
|||||||
@@ -737,6 +737,26 @@ function StrategyEditorCanvasInner({
|
|||||||
}
|
}
|
||||||
}, [root, extraRoots, orphans, onChange, onOrphansChange, onExtraRootsChange]);
|
}, [root, extraRoots, orphans, onChange, onOrphansChange, onExtraRootsChange]);
|
||||||
|
|
||||||
|
const handleReplaceLogicNode = useCallback((id: string, replaced: LogicNode) => {
|
||||||
|
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 handleChangeLogicGateType = useCallback((id: string, gateType: 'AND' | 'OR') => {
|
||||||
const patchGate = (n: LogicNode) => (
|
const patchGate = (n: LogicNode) => (
|
||||||
n.type === 'AND' || n.type === 'OR'
|
n.type === 'AND' || n.type === 'OR'
|
||||||
@@ -868,6 +888,7 @@ function StrategyEditorCanvasInner({
|
|||||||
const flowCallbacks = useMemo(() => ({
|
const flowCallbacks = useMemo(() => ({
|
||||||
onDelete: handleDelete,
|
onDelete: handleDelete,
|
||||||
onUpdateCondition: handleUpdateCondition,
|
onUpdateCondition: handleUpdateCondition,
|
||||||
|
onReplaceLogicNode: handleReplaceLogicNode,
|
||||||
onUpdateStochPairSecondary: handleUpdateStochPairSecondary,
|
onUpdateStochPairSecondary: handleUpdateStochPairSecondary,
|
||||||
onUpdatePriceExtremeFilterLevel: handleUpdatePriceExtremeFilterLevel,
|
onUpdatePriceExtremeFilterLevel: handleUpdatePriceExtremeFilterLevel,
|
||||||
onUpdateInflection33FilterLevel: handleUpdateInflection33FilterLevel,
|
onUpdateInflection33FilterLevel: handleUpdateInflection33FilterLevel,
|
||||||
@@ -879,7 +900,7 @@ function StrategyEditorCanvasInner({
|
|||||||
onStartCandleTypesChange: handleStartCandleTypesChange,
|
onStartCandleTypesChange: handleStartCandleTypesChange,
|
||||||
onDeleteStart: handleDeleteStart,
|
onDeleteStart: handleDeleteStart,
|
||||||
}), [
|
}), [
|
||||||
handleDelete, handleUpdateCondition, handleUpdateStochPairSecondary, handleUpdatePriceExtremeFilterLevel, handleUpdateInflection33FilterLevel, handleUpdateStableStrategyFilterLevel, handleChangeLogicGateType,
|
handleDelete, handleUpdateCondition, handleReplaceLogicNode, handleUpdateStochPairSecondary, handleUpdatePriceExtremeFilterLevel, handleUpdateInflection33FilterLevel, handleUpdateStableStrategyFilterLevel, handleChangeLogicGateType,
|
||||||
handleDropTarget, handleDragOverTarget, handleDragLeaveTarget,
|
handleDropTarget, handleDragOverTarget, handleDragLeaveTarget,
|
||||||
handleStartCandleTypesChange, handleDeleteStart,
|
handleStartCandleTypesChange, handleDeleteStart,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1218,18 +1218,57 @@
|
|||||||
transition: opacity 0.12s, background 0.12s;
|
transition: opacity 0.12s, background 0.12s;
|
||||||
flex-shrink: 0;
|
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 {
|
.se-flow-settings:hover {
|
||||||
background: rgba(230, 194, 0, 0.2);
|
background: rgba(230, 194, 0, 0.2);
|
||||||
}
|
}
|
||||||
.se-flow-node:hover .se-flow-del,
|
.se-flow-node:hover .se-flow-del,
|
||||||
.se-flow-node:hover .se-flow-settings,
|
.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-del,
|
||||||
.se-flow-node--selected .se-flow-settings,
|
.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-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;
|
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 {
|
.se-flow-settings-pop {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 6px;
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export type StrategyFlowNodeData = {
|
|||||||
onDeleteStart?: (startId: string) => void;
|
onDeleteStart?: (startId: string) => void;
|
||||||
onDelete?: (id: string) => void;
|
onDelete?: (id: string) => void;
|
||||||
onUpdateCondition?: (nodeId: string, condition: ConditionDSL) => void;
|
onUpdateCondition?: (nodeId: string, condition: ConditionDSL) => void;
|
||||||
|
/** 조건 프리셋(복합) — 노드 전체 교체 (CONDITION → AND 등) */
|
||||||
|
onReplaceLogicNode?: (nodeId: string, node: LogicNode) => void;
|
||||||
/** Stoch 과열×보조 복합 — 보조지표 변경 */
|
/** Stoch 과열×보조 복합 — 보조지표 변경 */
|
||||||
onUpdateStochPairSecondary?: (nodeId: string, secondaryIndicator: string) => void;
|
onUpdateStochPairSecondary?: (nodeId: string, secondaryIndicator: string) => void;
|
||||||
/** 9·20 신고가/신저가 복합 — 필터 강도 변경 */
|
/** 9·20 신고가/신저가 복합 — 필터 강도 변경 */
|
||||||
@@ -480,7 +482,7 @@ function layoutTree(
|
|||||||
signalTab: 'buy' | 'sell',
|
signalTab: 'buy' | 'sell',
|
||||||
callbacks: Pick<
|
callbacks: Pick<
|
||||||
StrategyFlowNodeData,
|
StrategyFlowNodeData,
|
||||||
'onDelete' | 'onUpdateCondition' | 'onUpdateStochPairSecondary' | 'onUpdatePriceExtremeFilterLevel' | 'onUpdateInflection33FilterLevel' | 'onUpdateStableStrategyFilterLevel' | 'onChangeLogicGateType'
|
'onDelete' | 'onUpdateCondition' | 'onReplaceLogicNode' | 'onUpdateStochPairSecondary' | 'onUpdatePriceExtremeFilterLevel' | 'onUpdateInflection33FilterLevel' | 'onUpdateStableStrategyFilterLevel' | 'onChangeLogicGateType'
|
||||||
| 'onDropTarget' | 'onDragOverTarget' | 'onDragLeaveTarget'
|
| 'onDropTarget' | 'onDragOverTarget' | 'onDragLeaveTarget'
|
||||||
>,
|
>,
|
||||||
dragOverId: string | null,
|
dragOverId: string | null,
|
||||||
@@ -502,6 +504,7 @@ function layoutTree(
|
|||||||
signalTab,
|
signalTab,
|
||||||
onDelete: callbacks.onDelete,
|
onDelete: callbacks.onDelete,
|
||||||
onUpdateCondition: callbacks.onUpdateCondition,
|
onUpdateCondition: callbacks.onUpdateCondition,
|
||||||
|
onReplaceLogicNode: callbacks.onReplaceLogicNode,
|
||||||
onUpdateStochPairSecondary: callbacks.onUpdateStochPairSecondary,
|
onUpdateStochPairSecondary: callbacks.onUpdateStochPairSecondary,
|
||||||
onUpdatePriceExtremeFilterLevel: callbacks.onUpdatePriceExtremeFilterLevel,
|
onUpdatePriceExtremeFilterLevel: callbacks.onUpdatePriceExtremeFilterLevel,
|
||||||
onUpdateInflection33FilterLevel: callbacks.onUpdateInflection33FilterLevel,
|
onUpdateInflection33FilterLevel: callbacks.onUpdateInflection33FilterLevel,
|
||||||
@@ -635,7 +638,7 @@ function appendOrphanFlowNodes(
|
|||||||
signalTab: 'buy' | 'sell',
|
signalTab: 'buy' | 'sell',
|
||||||
callbacks: Pick<
|
callbacks: Pick<
|
||||||
StrategyFlowNodeData,
|
StrategyFlowNodeData,
|
||||||
'onDelete' | 'onUpdateCondition' | 'onUpdateStochPairSecondary' | 'onUpdatePriceExtremeFilterLevel' | 'onUpdateInflection33FilterLevel' | 'onUpdateStableStrategyFilterLevel' | 'onChangeLogicGateType'
|
'onDelete' | 'onUpdateCondition' | 'onReplaceLogicNode' | 'onUpdateStochPairSecondary' | 'onUpdatePriceExtremeFilterLevel' | 'onUpdateInflection33FilterLevel' | 'onUpdateStableStrategyFilterLevel' | 'onChangeLogicGateType'
|
||||||
| 'onDropTarget' | 'onDragOverTarget' | 'onDragLeaveTarget'
|
| 'onDropTarget' | 'onDragOverTarget' | 'onDragLeaveTarget'
|
||||||
>,
|
>,
|
||||||
): void {
|
): void {
|
||||||
@@ -657,6 +660,7 @@ function appendOrphanFlowNodes(
|
|||||||
signalTab,
|
signalTab,
|
||||||
onDelete: callbacks.onDelete,
|
onDelete: callbacks.onDelete,
|
||||||
onUpdateCondition: callbacks.onUpdateCondition,
|
onUpdateCondition: callbacks.onUpdateCondition,
|
||||||
|
onReplaceLogicNode: callbacks.onReplaceLogicNode,
|
||||||
onUpdateStochPairSecondary: callbacks.onUpdateStochPairSecondary,
|
onUpdateStochPairSecondary: callbacks.onUpdateStochPairSecondary,
|
||||||
onUpdatePriceExtremeFilterLevel: callbacks.onUpdatePriceExtremeFilterLevel,
|
onUpdatePriceExtremeFilterLevel: callbacks.onUpdatePriceExtremeFilterLevel,
|
||||||
onUpdateInflection33FilterLevel: callbacks.onUpdateInflection33FilterLevel,
|
onUpdateInflection33FilterLevel: callbacks.onUpdateInflection33FilterLevel,
|
||||||
@@ -684,6 +688,7 @@ export function logicNodeToFlow(
|
|||||||
StrategyFlowNodeData,
|
StrategyFlowNodeData,
|
||||||
| 'onDelete'
|
| 'onDelete'
|
||||||
| 'onUpdateCondition'
|
| 'onUpdateCondition'
|
||||||
|
| 'onReplaceLogicNode'
|
||||||
| 'onUpdateStochPairSecondary'
|
| 'onUpdateStochPairSecondary'
|
||||||
| 'onUpdatePriceExtremeFilterLevel'
|
| 'onUpdatePriceExtremeFilterLevel'
|
||||||
| 'onUpdateInflection33FilterLevel'
|
| 'onUpdateInflection33FilterLevel'
|
||||||
|
|||||||
Reference in New Issue
Block a user