전략편집기 직접입력 수정

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,
.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;
@@ -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 (
<div className="se-combo-field">
<div className={`se-combo-field${mode === 'custom' ? ' se-combo-field--custom-only' : ''}`}>
<select
className="se-combo-field-select sp-cond-sel"
value={selectValue}
@@ -86,15 +133,26 @@ export default function ComboFieldSelect({
)}
</select>
{mode === 'custom' && canCustom && (
<ComboNumberInput
value={customNumber}
options={numberPresets}
min={min}
max={max}
allowDecimal={allowDecimal}
alwaysShowInput
<input
type="text"
className="se-combo-field-custom-input sp-cond-sel"
value={draft}
placeholder="값 입력"
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>
+18 -1
View File
@@ -1097,8 +1097,25 @@
flex-direction: column;
gap: 6px;
}
.se-combo-field--custom-only {
gap: 6px;
}
.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 {
outline: none;
border-color: var(--se-input-focus);