전략편집기 직접입력 수정
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user