전략편집기 수정
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import React, { useEffect, useId, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
export const COMBO_CUSTOM = '__custom__';
|
||||
|
||||
interface Props {
|
||||
value: number;
|
||||
@@ -7,6 +9,9 @@ interface Props {
|
||||
max?: number;
|
||||
allowDecimal?: boolean;
|
||||
onChange: (value: number) => void;
|
||||
/** true: 상단 입력창 항상 표시 + 하단 목록 선택 (조건대상 직접입력) */
|
||||
alwaysShowInput?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function sanitizeDraft(raw: string, allowDecimal: boolean): string {
|
||||
@@ -30,6 +35,17 @@ function parseDraft(raw: string, allowDecimal: boolean): number | null {
|
||||
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 ComboNumberInput({
|
||||
value,
|
||||
options,
|
||||
@@ -37,56 +53,104 @@ export default function ComboNumberInput({
|
||||
max = 500,
|
||||
allowDecimal = false,
|
||||
onChange,
|
||||
alwaysShowInput = false,
|
||||
disabled = false,
|
||||
}: Props) {
|
||||
const listId = useId().replace(/:/g, '');
|
||||
const uniqueOptions = [...new Set(options)].sort((a, b) => a - b);
|
||||
const isPreset = uniqueOptions.includes(value);
|
||||
const [mode, setMode] = useState<'preset' | 'custom'>(() =>
|
||||
(alwaysShowInput || !isPreset) ? 'custom' : 'preset',
|
||||
);
|
||||
const [draft, setDraft] = useState(String(value));
|
||||
const [focused, setFocused] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!focused) setDraft(String(value));
|
||||
}, [value, focused]);
|
||||
if (alwaysShowInput) {
|
||||
if (!focused) setDraft(String(value));
|
||||
return;
|
||||
}
|
||||
if (isPreset) {
|
||||
setMode('preset');
|
||||
if (!focused) setDraft(String(value));
|
||||
} else {
|
||||
setMode('custom');
|
||||
if (!focused) setDraft(String(value));
|
||||
}
|
||||
}, [value, isPreset, focused, alwaysShowInput]);
|
||||
|
||||
const commit = () => {
|
||||
const commitCustom = () => {
|
||||
const parsed = parseDraft(draft, allowDecimal);
|
||||
if (parsed == null) {
|
||||
setDraft(String(value));
|
||||
return;
|
||||
}
|
||||
const clamped = allowDecimal
|
||||
? Math.max(min, Math.min(max, parsed))
|
||||
: Math.max(min, Math.min(max, Math.round(parsed)));
|
||||
const clamped = clampValue(parsed, min, max, allowDecimal);
|
||||
onChange(clamped);
|
||||
setDraft(String(clamped));
|
||||
};
|
||||
|
||||
const uniqueOptions = [...new Set([...options, value])].sort((a, b) => a - b);
|
||||
const handleSelect = (raw: string) => {
|
||||
if (raw === COMBO_CUSTOM) {
|
||||
if (!alwaysShowInput) setMode('custom');
|
||||
setDraft(String(value));
|
||||
return;
|
||||
}
|
||||
const n = allowDecimal ? parseFloat(raw) : parseInt(raw, 10);
|
||||
if (!Number.isFinite(n)) return;
|
||||
const clamped = clampValue(n, min, max, allowDecimal);
|
||||
if (!alwaysShowInput) setMode('preset');
|
||||
onChange(clamped);
|
||||
setDraft(String(clamped));
|
||||
};
|
||||
|
||||
const showInput = alwaysShowInput || mode === 'custom';
|
||||
const selectValue = alwaysShowInput
|
||||
? (isPreset ? String(value) : '')
|
||||
: (mode === 'custom' || !isPreset ? COMBO_CUSTOM : String(value));
|
||||
|
||||
const inputEl = showInput ? (
|
||||
<input
|
||||
type="text"
|
||||
className="se-combo-num-input"
|
||||
inputMode={allowDecimal ? 'decimal' : 'numeric'}
|
||||
value={draft}
|
||||
placeholder="값 입력"
|
||||
disabled={disabled}
|
||||
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();
|
||||
}
|
||||
}}
|
||||
aria-label="직접 입력"
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="se-combo-num">
|
||||
<input
|
||||
type="text"
|
||||
className="se-combo-num-input"
|
||||
inputMode={allowDecimal ? 'decimal' : 'numeric'}
|
||||
list={listId}
|
||||
value={draft}
|
||||
onChange={e => setDraft(sanitizeDraft(e.target.value, allowDecimal))}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => {
|
||||
setFocused(false);
|
||||
commit();
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<datalist id={listId}>
|
||||
<div className={`se-combo-num${alwaysShowInput ? ' se-combo-num--always-input' : ''}`}>
|
||||
{alwaysShowInput ? inputEl : null}
|
||||
<select
|
||||
className="se-combo-num-select sp-cond-sel"
|
||||
value={selectValue}
|
||||
disabled={disabled}
|
||||
onChange={e => handleSelect(e.target.value)}
|
||||
aria-label="목록에서 선택"
|
||||
>
|
||||
{alwaysShowInput && !isPreset && (
|
||||
<option value="" disabled>목록에서 선택</option>
|
||||
)}
|
||||
{uniqueOptions.map(opt => (
|
||||
<option key={opt} value={String(opt)} />
|
||||
<option key={opt} value={String(opt)}>{opt}</option>
|
||||
))}
|
||||
</datalist>
|
||||
{!alwaysShowInput && <option value={COMBO_CUSTOM}>직접입력</option>}
|
||||
</select>
|
||||
{!alwaysShowInput ? inputEl : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user