From 7440453bb30e63509ae59687697e1ca746ede4ea Mon Sep 17 00:00:00 2001 From: Macbook Date: Tue, 16 Jun 2026 10:02:03 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A7=80=ED=91=9C=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=ED=94=84=EB=A6=AC=EC=85=8B=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.css | 106 ++++++ .../strategyEditor/ConditionPresetGraph.tsx | 70 ++++ .../strategyEditor/ConditionPresetPicker.tsx | 95 ++++++ .../strategyEditor/StrategyListEditor.tsx | 33 ++ frontend/src/utils/conditionPresets.ts | 305 ++++++++++++++++++ frontend/src/utils/strategyEditorShared.tsx | 2 +- 6 files changed, 610 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/strategyEditor/ConditionPresetGraph.tsx create mode 100644 frontend/src/components/strategyEditor/ConditionPresetPicker.tsx create mode 100644 frontend/src/utils/conditionPresets.ts diff --git a/frontend/src/App.css b/frontend/src/App.css index c727714..1656a53 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -7594,6 +7594,112 @@ html.desktop-client .tmb-logo-version { .sp-node-settings:hover { background: rgba(230, 194, 0, 0.22); } +.sp-node-preset { + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + padding: 0; + border: none; + border-radius: var(--radius); + background: rgba(167, 139, 250, 0.15); + color: #a78bfa; + cursor: pointer; + flex-shrink: 0; +} +.sp-node-preset:hover { + background: rgba(167, 139, 250, 0.28); +} +.sp-node-preset-pop { + position: absolute; + top: calc(100% + 4px); + right: 8px; + z-index: 55; + 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(--bg2); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); +} +.se-cond-preset-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + font-size: 0.62rem; + font-weight: 700; + color: #a78bfa; + margin-bottom: 4px; + text-transform: uppercase; + letter-spacing: 0.06em; +} +.se-cond-preset-hint { + margin: 0 0 8px; + font-size: 0.65rem; + color: var(--text2); + line-height: 1.35; +} +.se-cond-preset-empty { + margin: 0; + font-size: 0.68rem; + color: var(--text2); +} +.se-cond-preset-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 4px; +} +.se-cond-preset-item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 6px 8px; + border: 1px solid transparent; + border-radius: 6px; + background: color-mix(in srgb, var(--bg3, #1a1a2e) 80%, transparent); + color: var(--text1); + cursor: pointer; + text-align: left; +} +.se-cond-preset-item:hover { + border-color: color-mix(in srgb, #a78bfa 50%, transparent); + background: rgba(167, 139, 250, 0.08); +} +.se-cond-preset-graph-wrap { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 56px; + height: 32px; + border-radius: 4px; + background: rgba(0, 0, 0, 0.2); + color: #a78bfa; +} +.se-cond-preset-text { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} +.se-cond-preset-label { + font-size: 0.72rem; + font-weight: 600; + color: var(--text1); +} +.se-cond-preset-desc { + font-size: 0.62rem; + color: var(--text2); + line-height: 1.3; +} .sp-node-settings-pop { position: absolute; top: calc(100% + 4px); diff --git a/frontend/src/components/strategyEditor/ConditionPresetGraph.tsx b/frontend/src/components/strategyEditor/ConditionPresetGraph.tsx new file mode 100644 index 0000000..168fd1d --- /dev/null +++ b/frontend/src/components/strategyEditor/ConditionPresetGraph.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import type { ConditionPresetGraph } from '../../utils/conditionPresets'; + +interface Props { + type: ConditionPresetGraph; + size?: number; +} + +/** 조건 프리셋 미니 그래프 아이콘 */ +export default function ConditionPresetGraph({ type, size = 48 }: Props) { + const w = size; + const h = Math.round(size * 0.56); + const stroke = 'currentColor'; + const thresh = '#6b7280'; + const line = '#a78bfa'; + const line2 = '#60a5fa'; + + return ( + + {/* 임계/기준 수평선 */} + {(type === 'cross_up' || type === 'cross_down' || type === 'above' || type === 'below' || type === 'between') && ( + + )} + + {type === 'cross_up' && ( + + )} + {type === 'cross_down' && ( + + )} + {type === 'above' && ( + + )} + {type === 'below' && ( + + )} + {type === 'slope_up' && ( + + )} + {type === 'slope_down' && ( + + )} + {type === 'dual_cross_up' && ( + <> + + + + )} + {type === 'dual_cross_down' && ( + <> + + + + )} + {type === 'between' && ( + <> + + + + + )} + + ); +} diff --git a/frontend/src/components/strategyEditor/ConditionPresetPicker.tsx b/frontend/src/components/strategyEditor/ConditionPresetPicker.tsx new file mode 100644 index 0000000..3f9832e --- /dev/null +++ b/frontend/src/components/strategyEditor/ConditionPresetPicker.tsx @@ -0,0 +1,95 @@ +import React, { useEffect, useRef } from 'react'; +import type { ConditionDSL } from '../../utils/strategyTypes'; +import type { DefType } from '../../utils/strategyEditorShared'; +import { + applyConditionPreset, + getConditionPresetsForIndicator, + type ConditionPreset, +} from '../../utils/conditionPresets'; +import ConditionPresetGraph from './ConditionPresetGraph'; + +interface Props { + condition: ConditionDSL; + def: DefType; + signalType: 'buy' | 'sell'; + onChange: (next: ConditionDSL) => void; + onClose: () => void; + popoverClassName?: string; +} + +export default function ConditionPresetPicker({ + condition, + def, + signalType, + onChange, + onClose, + popoverClassName = 'sp-node-preset-pop', +}: Props) { + const popRef = useRef(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; + }); + + useEffect(() => { + const onDoc = (e: MouseEvent) => { + if (popRef.current && !popRef.current.contains(e.target as Node)) { + onClose(); + } + }; + const t = window.setTimeout(() => document.addEventListener('mousedown', onDoc), 0); + return () => { + window.clearTimeout(t); + document.removeEventListener('mousedown', onDoc); + }; + }, [onClose]); + + const handlePick = (preset: ConditionPreset) => { + onChange(applyConditionPreset(condition, preset, def)); + onClose(); + }; + + if (sorted.length === 0) { + return ( +
+
+ 조건 선택 + +
+

이 지표에 등록된 조건 템플릿이 없습니다.

+
+ ); + } + + return ( +
+
+ 조건 선택 + +
+

그래프 패턴을 선택하면 캔들 범위·조건대상·조건이 자동 설정됩니다.

+
    + {sorted.map(preset => ( +
  • + +
  • + ))} +
+
+ ); +} diff --git a/frontend/src/components/strategyEditor/StrategyListEditor.tsx b/frontend/src/components/strategyEditor/StrategyListEditor.tsx index 2d1ea53..629c71f 100644 --- a/frontend/src/components/strategyEditor/StrategyListEditor.tsx +++ b/frontend/src/components/strategyEditor/StrategyListEditor.tsx @@ -19,6 +19,7 @@ import { stochPairDisplayName, } from '../../utils/stochOverboughtPair'; import ConditionNodeSettings from './ConditionNodeSettings'; +import ConditionPresetPicker from './ConditionPresetPicker'; import StochPairNodeSettings from './StochPairNodeSettings'; import { type EditorConditionState, @@ -85,8 +86,10 @@ const TreeNodeComp: React.FC = ({ const color = isLogic ? NODE_COLORS[node.type] : IND_COLOR; const nodeDropKey = `${dropScope}:${node.id}`; const [settingsOpen, setSettingsOpen] = useState(false); + const [presetOpen, setPresetOpen] = useState(false); const isStochPair = isStochOverboughtPairRoot(node); const showSettings = node.type === 'CONDITION' && node.condition && hasNodeSettings(node.condition); + const showPreset = node.type === 'CONDITION' && node.condition && !node.condition.composite; const showStochPairSettings = isStochPair; const label = node.type === 'CONDITION' && node.condition ? nodeToText(node, def) @@ -170,6 +173,25 @@ const TreeNodeComp: React.FC = ({ compact /> )} + {showPreset && ( + + )} {(showSettings || showStochPairSettings) && (