"use client"; import * as React from "react"; import { cn } from "@/lib/utils"; import { removeLeadingZeros } from "@/lib/formatters"; import { Minus, Plus } from "lucide-react"; export interface QuantityInputProps extends Omit, "onChange" | "value" | "type"> { value: number | string | undefined; onChange: (value: number | undefined) => void; error?: boolean; /** 최소값 (기본: 0) */ min?: number; /** 최대값 */ max?: number; /** 증감 단위 (기본: 1) */ step?: number; /** +/- 버튼 표시 (기본: false) */ showButtons?: boolean; /** 단위 접미사 (개, EA, 박스 등) */ suffix?: string; /** 빈 값 허용 (기본: true) */ allowEmpty?: boolean; } /** * QuantityInput - 수량 입력 전용 컴포넌트 * * 특징: * - 정수만 허용 * - 기본 최소값 0 (음수 불가) * - 선택적 +/- 버튼 * - 단위 접미사 지원 * - Leading zero 자동 제거 * * @example * // 기본 * * * // +/- 버튼 포함 * * * // 단위 표시 * * * // 범위 제한 * */ const QuantityInput = React.forwardRef( ( { className, value, onChange, error, min = 0, max, step = 1, showButtons = false, suffix, allowEmpty = true, disabled, onFocus, onBlur, // eslint-disable-next-line @typescript-eslint/no-unused-vars defaultValue: _defaultValue, // controlled component이므로 defaultValue 무시 ...props }, ref ) => { const [isFocused, setIsFocused] = React.useState(false); const [displayValue, setDisplayValue] = React.useState(""); // 외부 value 변경 시 displayValue 동기화 React.useEffect(() => { if (!isFocused) { if (value === undefined || value === null || value === "") { setDisplayValue(""); } else { const numValue = typeof value === "string" ? parseInt(value, 10) : Math.floor(value); if (!isNaN(numValue)) { setDisplayValue(String(numValue)); } else { setDisplayValue(""); } } } }, [value, isFocused]); // 값 클램핑 const clampValue = (num: number): number => { let result = num; if (min !== undefined && result < min) result = min; if (max !== undefined && result > max) result = max; return result; }; // 입력값 정제 const sanitizeInput = (input: string): string => { // 숫자만 허용 let result = input.replace(/\D/g, ""); // Leading zero 제거 if (result) { result = removeLeadingZeros(result); } return result; }; // 값을 숫자로 변환 const parseValue = (input: string): number | undefined => { if (!input) { return allowEmpty ? undefined : min; } const num = parseInt(input, 10); if (isNaN(num)) { return allowEmpty ? undefined : min; } return clampValue(num); }; const handleChange = (e: React.ChangeEvent) => { const rawValue = e.target.value; const sanitized = sanitizeInput(rawValue); setDisplayValue(sanitized); const numValue = parseValue(sanitized); onChange(numValue); }; const handleFocus = (e: React.FocusEvent) => { setIsFocused(true); onFocus?.(e); }; const handleBlur = (e: React.FocusEvent) => { setIsFocused(false); const sanitized = sanitizeInput(displayValue); const numValue = parseValue(sanitized); onChange(numValue); if (numValue !== undefined) { setDisplayValue(String(numValue)); } else { setDisplayValue(""); } onBlur?.(e); }; const handleKeyDown = (e: React.KeyboardEvent) => { const allowedKeys = [ "Backspace", "Delete", "Tab", "Escape", "Enter", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End" ]; if (allowedKeys.includes(e.key)) return; // Ctrl/Cmd + A, C, V, X 허용 if ((e.ctrlKey || e.metaKey) && ["a", "c", "v", "x"].includes(e.key.toLowerCase())) { return; } // 숫자만 허용 if (/^\d$/.test(e.key)) return; e.preventDefault(); }; // +/- 버튼 핸들러 const handleIncrement = () => { if (disabled) return; const currentValue = value !== undefined && value !== null && value !== "" ? (typeof value === "string" ? parseInt(value, 10) : Math.floor(value)) : min; const newValue = clampValue((isNaN(currentValue) ? min : currentValue) + step); onChange(newValue); }; const handleDecrement = () => { if (disabled) return; const currentValue = value !== undefined && value !== null && value !== "" ? (typeof value === "string" ? parseInt(value, 10) : Math.floor(value)) : min; const newValue = clampValue((isNaN(currentValue) ? min : currentValue) - step); onChange(newValue); }; const isAtMin = value !== undefined && typeof value === "number" && value <= min; const isAtMax = max !== undefined && value !== undefined && typeof value === "number" && value >= max; return (
{/* 감소 버튼 */} {showButtons && ( )}
{/* 접미사 (버튼 없을 때만) */} {suffix && !showButtons && !isFocused && displayValue && ( {suffix} )}
{/* 증가 버튼 */} {showButtons && ( )}
); } ); QuantityInput.displayName = "QuantityInput"; export { QuantityInput };