전략편집기 직접입력 수정

This commit is contained in:
Macbook
2026-05-27 10:12:05 +09:00
parent cd5502d302
commit ca1fe7b563
3 changed files with 101 additions and 21 deletions
+6 -1
View File
@@ -6859,7 +6859,8 @@ html.theme-blue {
} }
.sp-node-settings-pop .se-flow-settings-field input, .sp-node-settings-pop .se-flow-settings-field input,
.se-flow-settings-field .se-combo-num-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%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
padding: 4px 6px; padding: 4px 6px;
@@ -6869,6 +6870,10 @@ html.theme-blue {
color: var(--text); color: var(--text);
font-size: 0.72rem; font-size: 0.72rem;
} }
.se-combo-field-custom-input:focus {
outline: none;
border-color: var(--se-input-focus, var(--accent));
}
.se-combo-num { .se-combo-num {
width: 100%; width: 100%;
display: flex; display: flex;
@@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import ComboNumberInput from './ComboNumberInput';
export const COMBO_FIELD_CUSTOM = '__field_custom__'; export const COMBO_FIELD_CUSTOM = '__field_custom__';
@@ -9,12 +8,9 @@ export type FieldCustomKind = 'period' | 'threshold' | 'none';
interface Props { interface Props {
options: FieldOption[]; options: FieldOption[];
/** 현재 DSL 필드값 (예: CCI_VALUE_9, K_80) */
fieldValue: string; fieldValue: string;
/** 프리셋 목록에 있는 값인지 */
isPresetField: boolean; isPresetField: boolean;
customKind: FieldCustomKind; customKind: FieldCustomKind;
/** 직접입력 시 표시·편집할 숫자 (기간 또는 임계값) */
customNumber: number; customNumber: number;
numberPresets?: number[]; numberPresets?: number[];
min?: number; min?: number;
@@ -22,18 +18,49 @@ interface Props {
allowDecimal?: boolean; allowDecimal?: boolean;
onFieldChange: (field: string) => void; onFieldChange: (field: string) => void;
onCustomNumberChange: (n: number) => void; onCustomNumberChange: (n: number) => void;
/** 직접입력 선택 시 상단 select에 표시할 라벨 (예: CCI 1 라인(37일)) */
customOptionLabel?: string; customOptionLabel?: string;
disabled?: boolean; 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({ export default function ComboFieldSelect({
options, options,
fieldValue, fieldValue,
isPresetField, isPresetField,
customKind, customKind,
customNumber, customNumber,
numberPresets = [], numberPresets: _numberPresets,
min = 1, min = 1,
max = 500, max = 500,
allowDecimal = false, allowDecimal = false,
@@ -46,29 +73,49 @@ export default function ComboFieldSelect({
const [mode, setMode] = useState<'preset' | 'custom'>(() => const [mode, setMode] = useState<'preset' | 'custom'>(() =>
(canCustom && !isPresetField) ? 'custom' : 'preset', (canCustom && !isPresetField) ? 'custom' : 'preset',
); );
const [draft, setDraft] = useState(String(customNumber));
const [focused, setFocused] = useState(false);
useEffect(() => { useEffect(() => {
if (!canCustom) { if (!canCustom) {
setMode('preset'); setMode('preset');
return; return;
} }
/* 프리셋 필드여도 직접입력 모드는 유지 — isPresetField만으로 preset으로 되돌리지 않음 */
if (!isPresetField) setMode('custom'); 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 selectValue = mode === 'custom' ? COMBO_FIELD_CUSTOM : fieldValue;
const handleSelect = (raw: string) => { const handleSelect = (raw: string) => {
if (raw === COMBO_FIELD_CUSTOM) { if (raw === COMBO_FIELD_CUSTOM) {
if (canCustom) setMode('custom'); if (canCustom) {
setMode('custom');
onCustomNumberChange(customNumber);
setDraft(String(customNumber));
}
return; return;
} }
setMode('preset'); setMode('preset');
onFieldChange(raw); 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 ( return (
<div className="se-combo-field"> <div className={`se-combo-field${mode === 'custom' ? ' se-combo-field--custom-only' : ''}`}>
<select <select
className="se-combo-field-select sp-cond-sel" className="se-combo-field-select sp-cond-sel"
value={selectValue} value={selectValue}
@@ -86,15 +133,26 @@ export default function ComboFieldSelect({
)} )}
</select> </select>
{mode === 'custom' && canCustom && ( {mode === 'custom' && canCustom && (
<ComboNumberInput <input
value={customNumber} type="text"
options={numberPresets} className="se-combo-field-custom-input sp-cond-sel"
min={min} value={draft}
max={max} placeholder="값 입력"
allowDecimal={allowDecimal}
alwaysShowInput
disabled={disabled} disabled={disabled}
onChange={onCustomNumberChange} inputMode={allowDecimal ? 'decimal' : 'numeric'}
aria-label="직접 입력"
onChange={e => setDraft(sanitizeDraft(e.target.value, allowDecimal))}
onFocus={() => setFocused(true)}
onBlur={() => {
setFocused(false);
commitCustom();
}}
onKeyDown={e => {
if (e.key === 'Enter') {
e.preventDefault();
(e.target as HTMLInputElement).blur();
}
}}
/> />
)} )}
</div> </div>
+18 -1
View File
@@ -1097,8 +1097,25 @@
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
} }
.se-combo-field--custom-only {
gap: 6px;
}
.se-combo-field-select { width: 100%; } .se-combo-field-select { width: 100%; }
.sp-cond-field .se-combo-field .se-combo-num { margin-top: 0; } .se-combo-field-custom-input {
width: 100%;
box-sizing: border-box;
padding: 4px 6px;
border-radius: 4px;
border: 1px solid var(--se-input-border);
background: var(--se-input-bg);
color: var(--se-text);
font-size: 0.72rem;
}
.se-combo-field-custom-input:focus {
outline: none;
border-color: var(--se-input-focus);
}
.sp-cond-field .se-combo-field { margin-top: 0; }
.se-combo-num-input:focus { .se-combo-num-input:focus {
outline: none; outline: none;
border-color: var(--se-input-focus); border-color: var(--se-input-focus);