그래프방식 조건선택 기능 추가

This commit is contained in:
Macbook
2026-06-16 10:18:30 +09:00
parent 2670c31d37
commit c7b92dbec5
4 changed files with 105 additions and 5 deletions
@@ -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<typeof cond>) => {
d.onUpdateCondition?.(id, next);
@@ -423,7 +426,7 @@ export const ConditionFlowNode = memo(function ConditionFlowNode({ id, data, sel
return (
<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}
onDragOver={onDragOver}
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-badge">{indLabel}</div>
<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 && (
<button
type="button"
@@ -446,6 +468,7 @@ export const ConditionFlowNode = memo(function ConditionFlowNode({ id, data, sel
onClick={e => {
e.stopPropagation();
setSettingsOpen(v => !v);
setPresetOpen(false);
}}
>
<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 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 && (
<ConditionNodeSettings
condition={cond}
@@ -737,6 +737,26 @@ function StrategyEditorCanvasInner({
}
}, [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 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,
]);