From ca1fe7b563db3464700e5bbfa2cd06e1b1c39464 Mon Sep 17 00:00:00 2001 From: Macbook Date: Wed, 27 May 2026 10:12:05 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EB=9E=B5=ED=8E=B8=EC=A7=91=EA=B8=B0?= =?UTF-8?q?=20=EC=A7=81=EC=A0=91=EC=9E=85=EB=A0=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.css | 7 +- .../strategyEditor/ComboFieldSelect.tsx | 96 +++++++++++++++---- frontend/src/styles/strategyEditor.css | 19 +++- 3 files changed, 101 insertions(+), 21 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index 5780a6e..e6fe0bf 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -6859,7 +6859,8 @@ html.theme-blue { } .sp-node-settings-pop .se-flow-settings-field input, .se-flow-settings-field .se-combo-num-input, -.sp-node-settings-pop .se-combo-num-input { +.sp-node-settings-pop .se-combo-num-input, +.se-combo-field-custom-input { width: 100%; box-sizing: border-box; padding: 4px 6px; @@ -6869,6 +6870,10 @@ html.theme-blue { color: var(--text); font-size: 0.72rem; } +.se-combo-field-custom-input:focus { + outline: none; + border-color: var(--se-input-focus, var(--accent)); +} .se-combo-num { width: 100%; display: flex; diff --git a/frontend/src/components/strategyEditor/ComboFieldSelect.tsx b/frontend/src/components/strategyEditor/ComboFieldSelect.tsx index 237d595..18716ba 100644 --- a/frontend/src/components/strategyEditor/ComboFieldSelect.tsx +++ b/frontend/src/components/strategyEditor/ComboFieldSelect.tsx @@ -1,5 +1,4 @@ -import React, { useEffect, useState } from 'react'; -import ComboNumberInput from './ComboNumberInput'; +import React, { useCallback, useEffect, useState } from 'react'; export const COMBO_FIELD_CUSTOM = '__field_custom__'; @@ -9,12 +8,9 @@ export type FieldCustomKind = 'period' | 'threshold' | 'none'; interface Props { options: FieldOption[]; - /** 현재 DSL 필드값 (예: CCI_VALUE_9, K_80) */ fieldValue: string; - /** 프리셋 목록에 있는 값인지 */ isPresetField: boolean; customKind: FieldCustomKind; - /** 직접입력 시 표시·편집할 숫자 (기간 또는 임계값) */ customNumber: number; numberPresets?: number[]; min?: number; @@ -22,18 +18,49 @@ interface Props { allowDecimal?: boolean; onFieldChange: (field: string) => void; onCustomNumberChange: (n: number) => void; - /** 직접입력 선택 시 상단 select에 표시할 라벨 (예: CCI 1 라인(37일)) */ customOptionLabel?: string; disabled?: boolean; } +function sanitizeDraft(raw: string, allowDecimal: boolean): string { + if (allowDecimal) { + let s = raw.replace(/[^\d.-]/g, ''); + const minus = s.startsWith('-') ? '-' : ''; + s = s.replace(/-/g, ''); + const dot = s.indexOf('.'); + if (dot !== -1) { + s = s.slice(0, dot + 1) + s.slice(dot + 1).replace(/\./g, ''); + } + return minus + s; + } + return raw.replace(/\D/g, ''); +} + +function parseDraft(raw: string, allowDecimal: boolean): number | null { + const trimmed = raw.trim(); + if (!trimmed || trimmed === '-' || trimmed === '.') return null; + const n = allowDecimal ? parseFloat(trimmed) : parseInt(trimmed, 10); + return Number.isFinite(n) ? n : null; +} + +function clampValue( + parsed: number, + min: number, + max: number, + allowDecimal: boolean, +): number { + return allowDecimal + ? Math.max(min, Math.min(max, parsed)) + : Math.max(min, Math.min(max, Math.round(parsed))); +} + export default function ComboFieldSelect({ options, fieldValue, isPresetField, customKind, customNumber, - numberPresets = [], + numberPresets: _numberPresets, min = 1, max = 500, allowDecimal = false, @@ -46,29 +73,49 @@ export default function ComboFieldSelect({ const [mode, setMode] = useState<'preset' | 'custom'>(() => (canCustom && !isPresetField) ? 'custom' : 'preset', ); + const [draft, setDraft] = useState(String(customNumber)); + const [focused, setFocused] = useState(false); useEffect(() => { if (!canCustom) { setMode('preset'); return; } - /* 프리셋 필드여도 직접입력 모드는 유지 — isPresetField만으로 preset으로 되돌리지 않음 */ if (!isPresetField) setMode('custom'); - }, [isPresetField, canCustom, fieldValue]); + }, [isPresetField, canCustom]); + + useEffect(() => { + if (!focused) setDraft(String(customNumber)); + }, [customNumber, focused]); const selectValue = mode === 'custom' ? COMBO_FIELD_CUSTOM : fieldValue; const handleSelect = (raw: string) => { if (raw === COMBO_FIELD_CUSTOM) { - if (canCustom) setMode('custom'); + if (canCustom) { + setMode('custom'); + onCustomNumberChange(customNumber); + setDraft(String(customNumber)); + } return; } setMode('preset'); onFieldChange(raw); }; + const commitCustom = useCallback(() => { + const parsed = parseDraft(draft, allowDecimal); + if (parsed == null) { + setDraft(String(customNumber)); + return; + } + const clamped = clampValue(parsed, min, max, allowDecimal); + onCustomNumberChange(clamped); + setDraft(String(clamped)); + }, [draft, allowDecimal, min, max, onCustomNumberChange, customNumber]); + return ( -
+