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