2025-12-19 15:44:38 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* UnifiedInput
|
2026-01-19 18:21:30 +09:00
|
|
|
*
|
2025-12-19 15:44:38 +09:00
|
|
|
* 통합 입력 컴포넌트
|
|
|
|
|
* - text: 텍스트 입력
|
|
|
|
|
* - number: 숫자 입력
|
|
|
|
|
* - password: 비밀번호 입력
|
|
|
|
|
* - slider: 슬라이더 입력
|
|
|
|
|
* - color: 색상 선택
|
|
|
|
|
* - button: 버튼 (입력이 아닌 액션)
|
|
|
|
|
*/
|
|
|
|
|
|
2026-01-19 18:21:30 +09:00
|
|
|
import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
2025-12-19 15:44:38 +09:00
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Slider } from "@/components/ui/slider";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
2026-01-19 18:21:30 +09:00
|
|
|
import { UnifiedInputProps, UnifiedInputConfig, UnifiedInputFormat } from "@/types/unified-components";
|
|
|
|
|
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
|
|
|
|
|
import { AutoGenerationConfig } from "@/types/screen";
|
2025-12-19 15:44:38 +09:00
|
|
|
|
|
|
|
|
// 형식별 입력 마스크 및 검증 패턴
|
|
|
|
|
const FORMAT_PATTERNS: Record<UnifiedInputFormat, { pattern: RegExp; placeholder: string }> = {
|
|
|
|
|
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)}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 텍스트 입력 컴포넌트
|
|
|
|
|
*/
|
2026-01-19 18:21:30 +09:00
|
|
|
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) => {
|
2025-12-19 15:44:38 +09:00
|
|
|
// 형식에 따른 값 포맷팅
|
2026-01-19 18:21:30 +09:00
|
|
|
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<HTMLInputElement>) => {
|
|
|
|
|
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],
|
|
|
|
|
);
|
2025-12-19 15:44:38 +09:00
|
|
|
|
|
|
|
|
const displayValue = useMemo(() => {
|
|
|
|
|
if (value === undefined || value === null) return "";
|
|
|
|
|
return formatValue(String(value));
|
|
|
|
|
}, [value, formatValue]);
|
|
|
|
|
|
|
|
|
|
const inputPlaceholder = placeholder || FORMAT_PATTERNS[format].placeholder;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Input
|
|
|
|
|
ref={ref}
|
|
|
|
|
type="text"
|
|
|
|
|
value={displayValue}
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
placeholder={inputPlaceholder}
|
|
|
|
|
readOnly={readonly}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
className={cn("h-full w-full", className)}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
TextInput.displayName = "TextInput";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 숫자 입력 컴포넌트
|
|
|
|
|
*/
|
2026-01-19 18:21:30 +09:00
|
|
|
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<HTMLInputElement>) => {
|
|
|
|
|
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],
|
|
|
|
|
);
|
2025-12-19 15:44:38 +09:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Input
|
|
|
|
|
ref={ref}
|
|
|
|
|
type="number"
|
|
|
|
|
value={value ?? ""}
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
min={min}
|
|
|
|
|
max={max}
|
|
|
|
|
step={step}
|
|
|
|
|
placeholder={placeholder || "숫자 입력"}
|
|
|
|
|
readOnly={readonly}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
className={cn("h-full w-full", className)}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
NumberInput.displayName = "NumberInput";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 비밀번호 입력 컴포넌트
|
|
|
|
|
*/
|
2026-01-19 18:21:30 +09:00
|
|
|
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) => {
|
2025-12-19 15:44:38 +09:00
|
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<Input
|
|
|
|
|
ref={ref}
|
|
|
|
|
type={showPassword ? "text" : "password"}
|
|
|
|
|
value={value ?? ""}
|
|
|
|
|
onChange={(e) => onChange?.(e.target.value)}
|
|
|
|
|
placeholder={placeholder || "비밀번호 입력"}
|
|
|
|
|
readOnly={readonly}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
className={cn("h-full w-full pr-10", className)}
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setShowPassword(!showPassword)}
|
2026-01-19 18:21:30 +09:00
|
|
|
className="text-muted-foreground hover:text-foreground absolute top-1/2 right-2 -translate-y-1/2 text-xs"
|
2025-12-19 15:44:38 +09:00
|
|
|
>
|
|
|
|
|
{showPassword ? "숨김" : "보기"}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
PasswordInput.displayName = "PasswordInput";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 슬라이더 입력 컴포넌트
|
|
|
|
|
*/
|
2026-01-19 18:21:30 +09:00
|
|
|
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) => {
|
2025-12-19 15:44:38 +09:00
|
|
|
return (
|
|
|
|
|
<div ref={ref} className={cn("flex items-center gap-4", className)}>
|
|
|
|
|
<Slider
|
|
|
|
|
value={[value ?? min]}
|
|
|
|
|
onValueChange={(values) => onChange?.(values[0])}
|
|
|
|
|
min={min}
|
|
|
|
|
max={max}
|
|
|
|
|
step={step}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
className="flex-1"
|
|
|
|
|
/>
|
2026-01-19 18:21:30 +09:00
|
|
|
<span className="w-12 text-right text-sm font-medium">{value ?? min}</span>
|
2025-12-19 15:44:38 +09:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
SliderInput.displayName = "SliderInput";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 색상 선택 컴포넌트
|
|
|
|
|
*/
|
2026-01-19 18:21:30 +09:00
|
|
|
const ColorInput = forwardRef<
|
|
|
|
|
HTMLInputElement,
|
|
|
|
|
{
|
|
|
|
|
value?: string;
|
|
|
|
|
onChange?: (value: string) => void;
|
|
|
|
|
disabled?: boolean;
|
|
|
|
|
className?: string;
|
|
|
|
|
}
|
|
|
|
|
>(({ value, onChange, disabled, className }, ref) => {
|
2025-12-19 15:44:38 +09:00
|
|
|
return (
|
|
|
|
|
<div className={cn("flex items-center gap-2", className)}>
|
|
|
|
|
<Input
|
|
|
|
|
ref={ref}
|
|
|
|
|
type="color"
|
|
|
|
|
value={value || "#000000"}
|
|
|
|
|
onChange={(e) => onChange?.(e.target.value)}
|
|
|
|
|
disabled={disabled}
|
2026-01-19 18:21:30 +09:00
|
|
|
className="h-full w-12 cursor-pointer p-1"
|
2025-12-19 15:44:38 +09:00
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
type="text"
|
|
|
|
|
value={value || "#000000"}
|
|
|
|
|
onChange={(e) => onChange?.(e.target.value)}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
className="h-full flex-1 uppercase"
|
|
|
|
|
maxLength={7}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
ColorInput.displayName = "ColorInput";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 여러 줄 텍스트 입력 컴포넌트
|
|
|
|
|
*/
|
2026-01-19 18:21:30 +09:00
|
|
|
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) => {
|
2025-12-19 15:44:38 +09:00
|
|
|
return (
|
|
|
|
|
<textarea
|
|
|
|
|
ref={ref}
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={(e) => onChange?.(e.target.value)}
|
|
|
|
|
placeholder={placeholder}
|
|
|
|
|
rows={rows}
|
|
|
|
|
readOnly={readonly}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
className={cn(
|
2026-01-19 18:21:30 +09:00
|
|
|
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[60px] w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
|
|
|
className,
|
2025-12-19 15:44:38 +09:00
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
TextareaInput.displayName = "TextareaInput";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 메인 UnifiedInput 컴포넌트
|
|
|
|
|
*/
|
2026-01-19 18:21:30 +09:00
|
|
|
export const UnifiedInput = forwardRef<HTMLDivElement, UnifiedInputProps>((props, ref) => {
|
|
|
|
|
const { id, label, required, readonly, disabled, style, size, config: configProp, value, onChange } = props;
|
|
|
|
|
|
|
|
|
|
// formData 추출 (채번규칙 날짜 컬럼 기준 생성 시 사용)
|
|
|
|
|
const formData = (props as any).formData || {};
|
|
|
|
|
const columnName = (props as any).columnName;
|
|
|
|
|
|
|
|
|
|
// config가 없으면 기본값 사용
|
|
|
|
|
const config = (configProp || { type: "text" }) as UnifiedInputConfig & {
|
|
|
|
|
inputType?: string;
|
|
|
|
|
rows?: number;
|
|
|
|
|
autoGeneration?: AutoGenerationConfig;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 자동생성 설정 추출
|
|
|
|
|
const autoGeneration: AutoGenerationConfig = (props as any).autoGeneration ||
|
|
|
|
|
(config as any).autoGeneration || {
|
|
|
|
|
type: "none",
|
|
|
|
|
enabled: false,
|
|
|
|
|
};
|
2025-12-19 15:44:38 +09:00
|
|
|
|
2026-01-19 18:21:30 +09:00
|
|
|
// 자동생성 상태 관리
|
|
|
|
|
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string | null>(null);
|
|
|
|
|
const isGeneratingRef = useRef(false);
|
|
|
|
|
const hasGeneratedRef = useRef(false);
|
|
|
|
|
const lastFormDataRef = useRef<string>(""); // 마지막 formData 추적 (채번 규칙용)
|
|
|
|
|
|
|
|
|
|
// 수정 모드 여부 확인
|
|
|
|
|
const originalData = (props as any).originalData || (props as any)._originalData;
|
|
|
|
|
const isEditMode = originalData && Object.keys(originalData).length > 0;
|
|
|
|
|
|
|
|
|
|
// 채번 규칙인 경우 formData 변경 감지 (자기 자신 필드 제외)
|
|
|
|
|
const formDataForNumbering = useMemo(() => {
|
|
|
|
|
if (autoGeneration.type !== "numbering_rule") return "";
|
|
|
|
|
// 자기 자신의 값은 제외 (무한 루프 방지)
|
|
|
|
|
const { [columnName]: _, ...rest } = formData;
|
|
|
|
|
return JSON.stringify(rest);
|
|
|
|
|
}, [autoGeneration.type, formData, columnName]);
|
|
|
|
|
|
|
|
|
|
// 자동생성 로직
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const generateValue = async () => {
|
|
|
|
|
// 자동생성 비활성화 또는 생성 중
|
|
|
|
|
if (!autoGeneration.enabled || isGeneratingRef.current) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 수정 모드에서는 자동생성 안함
|
|
|
|
|
if (isEditMode) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 채번 규칙인 경우: formData가 변경되었는지 확인
|
|
|
|
|
const isNumberingRule = autoGeneration.type === "numbering_rule";
|
|
|
|
|
const formDataChanged =
|
|
|
|
|
isNumberingRule && formDataForNumbering !== lastFormDataRef.current && lastFormDataRef.current !== "";
|
|
|
|
|
|
|
|
|
|
// 이미 생성되었고, formData 변경이 아닌 경우 스킵
|
|
|
|
|
if (hasGeneratedRef.current && !formDataChanged) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 첫 생성 시: 값이 이미 있으면 스킵 (formData 변경 시에는 강제 재생성)
|
|
|
|
|
if (!formDataChanged && value !== undefined && value !== null && value !== "") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isGeneratingRef.current = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// formData를 전달하여 날짜 컬럼 기준 생성 지원
|
|
|
|
|
const generatedValue = await AutoGenerationUtils.generateValue(autoGeneration, columnName, formData);
|
|
|
|
|
|
|
|
|
|
if (generatedValue !== null && generatedValue !== undefined) {
|
|
|
|
|
setAutoGeneratedValue(generatedValue);
|
|
|
|
|
onChange?.(generatedValue);
|
|
|
|
|
hasGeneratedRef.current = true;
|
|
|
|
|
|
|
|
|
|
// formData 기록
|
|
|
|
|
if (isNumberingRule) {
|
|
|
|
|
lastFormDataRef.current = formDataForNumbering;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("자동생성 실패:", error);
|
|
|
|
|
} finally {
|
|
|
|
|
isGeneratingRef.current = false;
|
2025-12-19 15:44:38 +09:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-19 18:21:30 +09:00
|
|
|
generateValue();
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [autoGeneration.enabled, autoGeneration.type, isEditMode, formDataForNumbering]);
|
|
|
|
|
|
|
|
|
|
// 실제 표시할 값 (자동생성 값 또는 props value)
|
|
|
|
|
const displayValue = autoGeneratedValue ?? value;
|
|
|
|
|
|
|
|
|
|
// 조건부 렌더링 체크
|
|
|
|
|
// TODO: conditional 처리 로직 추가
|
|
|
|
|
|
|
|
|
|
// 타입별 입력 컴포넌트 렌더링
|
|
|
|
|
const renderInput = () => {
|
|
|
|
|
const inputType = config.inputType || config.type || "text";
|
|
|
|
|
switch (inputType) {
|
|
|
|
|
case "text":
|
|
|
|
|
return (
|
|
|
|
|
<TextInput
|
|
|
|
|
value={displayValue}
|
|
|
|
|
onChange={(v) => {
|
|
|
|
|
setAutoGeneratedValue(null); // 사용자 입력 시 자동생성 값 초기화
|
|
|
|
|
onChange?.(v);
|
|
|
|
|
}}
|
|
|
|
|
format={config.format}
|
|
|
|
|
mask={config.mask}
|
|
|
|
|
placeholder={config.placeholder}
|
|
|
|
|
readonly={readonly || (autoGeneration.enabled && hasGeneratedRef.current)}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
case "number":
|
|
|
|
|
return (
|
|
|
|
|
<NumberInput
|
|
|
|
|
value={typeof displayValue === "number" ? displayValue : undefined}
|
|
|
|
|
onChange={(v) => {
|
|
|
|
|
setAutoGeneratedValue(null);
|
|
|
|
|
onChange?.(v ?? 0);
|
|
|
|
|
}}
|
|
|
|
|
min={config.min}
|
|
|
|
|
max={config.max}
|
|
|
|
|
step={config.step}
|
|
|
|
|
placeholder={config.placeholder}
|
|
|
|
|
readonly={readonly}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
case "password":
|
|
|
|
|
return (
|
|
|
|
|
<PasswordInput
|
|
|
|
|
value={typeof displayValue === "string" ? displayValue : ""}
|
|
|
|
|
onChange={(v) => {
|
|
|
|
|
setAutoGeneratedValue(null);
|
|
|
|
|
onChange?.(v);
|
|
|
|
|
}}
|
|
|
|
|
placeholder={config.placeholder}
|
|
|
|
|
readonly={readonly}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
case "slider":
|
|
|
|
|
return (
|
|
|
|
|
<SliderInput
|
|
|
|
|
value={typeof displayValue === "number" ? displayValue : (config.min ?? 0)}
|
|
|
|
|
onChange={(v) => {
|
|
|
|
|
setAutoGeneratedValue(null);
|
|
|
|
|
onChange?.(v);
|
|
|
|
|
}}
|
|
|
|
|
min={config.min}
|
|
|
|
|
max={config.max}
|
|
|
|
|
step={config.step}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
case "color":
|
|
|
|
|
return (
|
|
|
|
|
<ColorInput
|
|
|
|
|
value={typeof displayValue === "string" ? displayValue : "#000000"}
|
|
|
|
|
onChange={(v) => {
|
|
|
|
|
setAutoGeneratedValue(null);
|
|
|
|
|
onChange?.(v);
|
|
|
|
|
}}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
case "textarea":
|
|
|
|
|
return (
|
|
|
|
|
<TextareaInput
|
|
|
|
|
value={displayValue as string}
|
|
|
|
|
onChange={(v) => {
|
|
|
|
|
setAutoGeneratedValue(null);
|
|
|
|
|
onChange?.(v);
|
|
|
|
|
}}
|
|
|
|
|
placeholder={config.placeholder}
|
|
|
|
|
rows={config.rows}
|
|
|
|
|
readonly={readonly}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2025-12-19 15:44:38 +09:00
|
|
|
|
2026-01-19 18:21:30 +09:00
|
|
|
default:
|
|
|
|
|
return (
|
|
|
|
|
<TextInput
|
|
|
|
|
value={displayValue}
|
|
|
|
|
onChange={(v) => {
|
|
|
|
|
setAutoGeneratedValue(null);
|
|
|
|
|
onChange?.(v);
|
2025-12-19 15:44:38 +09:00
|
|
|
}}
|
2026-01-19 18:21:30 +09:00
|
|
|
placeholder={config.placeholder}
|
|
|
|
|
readonly={readonly}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 라벨이 표시될 때 입력 필드가 차지할 높이 계산
|
|
|
|
|
const showLabel = label && style?.labelDisplay !== false;
|
|
|
|
|
// size에서 우선 가져오고, 없으면 style에서 가져옴
|
|
|
|
|
const componentWidth = size?.width || style?.width;
|
|
|
|
|
const componentHeight = size?.height || style?.height;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={ref}
|
|
|
|
|
id={id}
|
|
|
|
|
className="flex flex-col"
|
|
|
|
|
style={{
|
|
|
|
|
width: componentWidth,
|
|
|
|
|
height: componentHeight,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{showLabel && (
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor={id}
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: style?.labelFontSize,
|
|
|
|
|
color: style?.labelColor,
|
|
|
|
|
fontWeight: style?.labelFontWeight,
|
|
|
|
|
marginBottom: style?.labelMarginBottom,
|
|
|
|
|
}}
|
|
|
|
|
className="flex-shrink-0 text-sm font-medium"
|
|
|
|
|
>
|
|
|
|
|
{label}
|
|
|
|
|
{required && <span className="ml-0.5 text-orange-500">*</span>}
|
|
|
|
|
</Label>
|
|
|
|
|
)}
|
|
|
|
|
<div className="min-h-0 flex-1">{renderInput()}</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
});
|
2025-12-19 15:44:38 +09:00
|
|
|
|
|
|
|
|
UnifiedInput.displayName = "UnifiedInput";
|
|
|
|
|
|
|
|
|
|
export default UnifiedInput;
|