"use client"; /** * UnifiedInput * * 통합 입력 컴포넌트 * - text: 텍스트 입력 * - number: 숫자 입력 * - password: 비밀번호 입력 * - slider: 슬라이더 입력 * - color: 색상 선택 * - button: 버튼 (입력이 아닌 액션) */ import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Input } from "@/components/ui/input"; import { Slider } from "@/components/ui/slider"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { UnifiedInputProps, UnifiedInputConfig, UnifiedInputFormat } from "@/types/unified-components"; import { AutoGenerationUtils } from "@/lib/utils/autoGeneration"; import { AutoGenerationConfig } from "@/types/screen"; // 형식별 입력 마스크 및 검증 패턴 const FORMAT_PATTERNS: Record = { none: { pattern: /.*/, placeholder: "" }, email: { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, placeholder: "example@email.com" }, tel: { pattern: /^\d{2,3}-\d{3,4}-\d{4}$/, placeholder: "010-1234-5678" }, url: { pattern: /^https?:\/\/.+/, placeholder: "https://example.com" }, currency: { pattern: /^[\d,]+$/, placeholder: "1,000,000" }, biz_no: { pattern: /^\d{3}-\d{2}-\d{5}$/, placeholder: "123-45-67890" }, }; // 통화 형식 변환 function formatCurrency(value: string | number): string { const num = typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; if (isNaN(num)) return ""; return num.toLocaleString("ko-KR"); } // 사업자번호 형식 변환 function formatBizNo(value: string): string { const digits = value.replace(/\D/g, ""); if (digits.length <= 3) return digits; if (digits.length <= 5) return `${digits.slice(0, 3)}-${digits.slice(3)}`; return `${digits.slice(0, 3)}-${digits.slice(3, 5)}-${digits.slice(5, 10)}`; } // 전화번호 형식 변환 function formatTel(value: string): string { const digits = value.replace(/\D/g, ""); if (digits.length <= 3) return digits; if (digits.length <= 7) return `${digits.slice(0, 3)}-${digits.slice(3)}`; if (digits.length <= 11) return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`; return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7, 11)}`; } /** * 텍스트 입력 컴포넌트 */ const TextInput = forwardRef< HTMLInputElement, { value?: string | number; onChange?: (value: string) => void; format?: UnifiedInputFormat; mask?: string; placeholder?: string; readonly?: boolean; disabled?: boolean; className?: string; } >(({ value, onChange, format = "none", placeholder, readonly, disabled, className }, ref) => { // 형식에 따른 값 포맷팅 const formatValue = useCallback( (val: string): string => { switch (format) { case "currency": return formatCurrency(val); case "biz_no": return formatBizNo(val); case "tel": return formatTel(val); default: return val; } }, [format], ); const handleChange = useCallback( (e: React.ChangeEvent) => { let newValue = e.target.value; // 형식에 따른 자동 포맷팅 if (format === "currency") { // 숫자와 쉼표만 허용 newValue = newValue.replace(/[^\d,]/g, ""); newValue = formatCurrency(newValue); } else if (format === "biz_no") { newValue = formatBizNo(newValue); } else if (format === "tel") { newValue = formatTel(newValue); } onChange?.(newValue); }, [format, onChange], ); const displayValue = useMemo(() => { if (value === undefined || value === null) return ""; return formatValue(String(value)); }, [value, formatValue]); const inputPlaceholder = placeholder || FORMAT_PATTERNS[format].placeholder; return ( ); }); TextInput.displayName = "TextInput"; /** * 숫자 입력 컴포넌트 */ const NumberInput = forwardRef< HTMLInputElement, { value?: number; onChange?: (value: number | undefined) => void; min?: number; max?: number; step?: number; placeholder?: string; readonly?: boolean; disabled?: boolean; className?: string; } >(({ value, onChange, min, max, step = 1, placeholder, readonly, disabled, className }, ref) => { const handleChange = useCallback( (e: React.ChangeEvent) => { const val = e.target.value; if (val === "") { onChange?.(undefined); return; } let num = parseFloat(val); // 범위 제한 if (min !== undefined && num < min) num = min; if (max !== undefined && num > max) num = max; onChange?.(num); }, [min, max, onChange], ); return ( ); }); NumberInput.displayName = "NumberInput"; /** * 비밀번호 입력 컴포넌트 */ const PasswordInput = forwardRef< HTMLInputElement, { value?: string; onChange?: (value: string) => void; placeholder?: string; readonly?: boolean; disabled?: boolean; className?: string; } >(({ value, onChange, placeholder, readonly, disabled, className }, ref) => { const [showPassword, setShowPassword] = useState(false); return (
onChange?.(e.target.value)} placeholder={placeholder || "비밀번호 입력"} readOnly={readonly} disabled={disabled} className={cn("h-full w-full pr-10", className)} />
); }); PasswordInput.displayName = "PasswordInput"; /** * 슬라이더 입력 컴포넌트 */ const SliderInput = forwardRef< HTMLDivElement, { value?: number; onChange?: (value: number) => void; min?: number; max?: number; step?: number; disabled?: boolean; className?: string; } >(({ value, onChange, min = 0, max = 100, step = 1, disabled, className }, ref) => { return (
onChange?.(values[0])} min={min} max={max} step={step} disabled={disabled} className="flex-1" /> {value ?? min}
); }); SliderInput.displayName = "SliderInput"; /** * 색상 선택 컴포넌트 */ const ColorInput = forwardRef< HTMLInputElement, { value?: string; onChange?: (value: string) => void; disabled?: boolean; className?: string; } >(({ value, onChange, disabled, className }, ref) => { return (
onChange?.(e.target.value)} disabled={disabled} className="h-full w-12 cursor-pointer p-1" /> onChange?.(e.target.value)} disabled={disabled} className="h-full flex-1 uppercase" maxLength={7} />
); }); ColorInput.displayName = "ColorInput"; /** * 여러 줄 텍스트 입력 컴포넌트 */ const TextareaInput = forwardRef< HTMLTextAreaElement, { value?: string; onChange?: (value: string) => void; placeholder?: string; rows?: number; readonly?: boolean; disabled?: boolean; className?: string; } >(({ value = "", onChange, placeholder, rows = 3, readonly, disabled, className }, ref) => { return (