import React, { useEffect, useState } from 'react'; export const COMBO_CUSTOM = '__custom__'; interface Props { value: number; options: number[]; min?: number; max?: number; allowDecimal?: boolean; onChange: (value: number) => void; /** true: 상단 입력창 항상 표시 + 하단 목록 선택 (조건대상 직접입력) */ alwaysShowInput?: 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 ComboNumberInput({ value, options, min = 1, max = 500, allowDecimal = false, onChange, alwaysShowInput = false, disabled = false, }: Props) { 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 (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 commitCustom = () => { const parsed = parseDraft(draft, allowDecimal); if (parsed == null) { setDraft(String(value)); return; } const clamped = clampValue(parsed, min, max, allowDecimal); onChange(clamped); setDraft(String(clamped)); }; 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 ? ( 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 (
{alwaysShowInput ? inputEl : null} {!alwaysShowInput ? inputEl : null}
); }