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