지표선택 목록 추가

This commit is contained in:
Macbook
2026-06-16 10:14:05 +09:00
parent 7440453bb3
commit 2670c31d37
7 changed files with 490 additions and 37 deletions
@@ -65,6 +65,21 @@ export default function ConditionPresetGraph({ type, size = 48 }: Props) {
<path d="M4 18 L44 16" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" />
</>
)}
{(type === 'path_first_cross' || type === 'path_pullback_recross' || type === 'path_deep_recovery') && (
<>
<line x1="4" y1="8" x2="44" y2="8" stroke={thresh} strokeWidth="1.2" strokeDasharray="3 2" />
<line x1="4" y1="20" x2="44" y2="20" stroke={thresh} strokeWidth="1.2" strokeDasharray="3 2" />
</>
)}
{type === 'path_first_cross' && (
<path d="M4 16 Q14 16 20 12 T34 6" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" />
)}
{type === 'path_pullback_recross' && (
<path d="M4 10 L14 8 L22 12 L30 10 L38 14 L44 6" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
)}
{type === 'path_deep_recovery' && (
<path d="M4 12 L12 10 L18 22 L26 18 L34 10 L44 6" fill="none" stroke={line} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
)}
</svg>
);
}
@@ -1,38 +1,84 @@
import React, { useEffect, useRef } from 'react';
import type { ConditionDSL } from '../../utils/strategyTypes';
import type { ConditionDSL, LogicNode } from '../../utils/strategyTypes';
import type { DefType } from '../../utils/strategyEditorShared';
import {
applyConditionPreset,
applyCompoundConditionPreset,
getConditionPresetsForIndicator,
type ConditionPreset,
} from '../../utils/conditionPresets';
import { isCompoundConditionPreset } from '../../utils/conditionPathPresets';
import ConditionPresetGraph from './ConditionPresetGraph';
interface Props {
condition: ConditionDSL;
def: DefType;
signalType: 'buy' | 'sell';
nodeId: string;
onChange: (next: ConditionDSL) => void;
onReplaceNode: (node: LogicNode) => void;
onClose: () => void;
popoverClassName?: string;
}
function PresetSection({
title,
presets,
onPick,
}: {
title: string;
presets: ConditionPreset[];
onPick: (preset: ConditionPreset) => void;
}) {
if (presets.length === 0) return null;
return (
<>
<p className="se-cond-preset-section">{title}</p>
<ul className="se-cond-preset-list">
{presets.map(preset => (
<li key={preset.id}>
<button
type="button"
className="se-cond-preset-item"
onClick={() => onPick(preset)}
>
<span className="se-cond-preset-graph-wrap">
<ConditionPresetGraph type={preset.graph} size={52} />
</span>
<span className="se-cond-preset-text">
<span className="se-cond-preset-label">{preset.label}</span>
<span className="se-cond-preset-desc">{preset.description}</span>
</span>
</button>
</li>
))}
</ul>
</>
);
}
export default function ConditionPresetPicker({
condition,
def,
signalType,
nodeId,
onChange,
onReplaceNode,
onClose,
popoverClassName = 'sp-node-preset-pop',
}: Props) {
const popRef = useRef<HTMLDivElement>(null);
const presets = getConditionPresetsForIndicator(condition.indicatorType, def, signalType);
const sorted = [...presets].sort((a, b) => {
const aMatch = a.tags?.includes(signalType) ? 0 : 1;
const bMatch = b.tags?.includes(signalType) ? 0 : 1;
return aMatch - bMatch;
});
const sortBySignal = (list: ConditionPreset[]) =>
[...list].sort((a, b) => {
const aMatch = a.tags?.includes(signalType) ? 0 : 1;
const bMatch = b.tags?.includes(signalType) ? 0 : 1;
return aMatch - bMatch;
});
const basicPresets = sortBySignal(presets.filter(p => p.category !== 'path'));
const pathPresets = sortBySignal(presets.filter(p => p.category === 'path'));
useEffect(() => {
const onDoc = (e: MouseEvent) => {
@@ -48,11 +94,15 @@ export default function ConditionPresetPicker({
}, [onClose]);
const handlePick = (preset: ConditionPreset) => {
onChange(applyConditionPreset(condition, preset, def));
if (isCompoundConditionPreset(preset)) {
onReplaceNode(applyCompoundConditionPreset(nodeId, condition, preset, def));
} else {
onChange(applyConditionPreset(condition, preset, def));
}
onClose();
};
if (sorted.length === 0) {
if (presets.length === 0) {
return (
<div ref={popRef} className={popoverClassName} role="dialog" aria-label="조건 선택">
<div className="se-cond-preset-head">
@@ -71,25 +121,8 @@ export default function ConditionPresetPicker({
<button type="button" className="se-flow-settings-close" onClick={onClose} aria-label="닫기">×</button>
</div>
<p className="se-cond-preset-hint"> ·· .</p>
<ul className="se-cond-preset-list">
{sorted.map(preset => (
<li key={preset.id}>
<button
type="button"
className="se-cond-preset-item"
onClick={() => handlePick(preset)}
>
<span className="se-cond-preset-graph-wrap">
<ConditionPresetGraph type={preset.graph} size={52} />
</span>
<span className="se-cond-preset-text">
<span className="se-cond-preset-label">{preset.label}</span>
<span className="se-cond-preset-desc">{preset.description}</span>
</span>
</button>
</li>
))}
</ul>
<PresetSection title="기본 조건" presets={basicPresets} onPick={handlePick} />
<PresetSection title="상향 돌파 경로 (복합 · AND)" presets={pathPresets} onPick={handlePick} />
</div>
);
}
@@ -218,7 +218,9 @@ const TreeNodeComp: React.FC<TreeNodeProps> = ({
condition={node.condition}
def={def}
signalType={signalType}
nodeId={node.id}
onChange={handleCondChange}
onReplaceNode={onUpdate}
onClose={() => setPresetOpen(false)}
popoverClassName="sp-node-preset-pop"
/>