185 lines
4.5 KiB
TypeScript
185 lines
4.5 KiB
TypeScript
|
|
/**
|
||
|
|
* FormField - 통합 폼 필드 컴포넌트
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { ReactNode } from "react";
|
||
|
|
import { Label } from "../ui/label";
|
||
|
|
import { Input } from "../ui/input";
|
||
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
|
||
|
|
import { Textarea } from "../ui/textarea";
|
||
|
|
import { AlertCircle } from "lucide-react";
|
||
|
|
|
||
|
|
export type FormFieldType = 'text' | 'number' | 'date' | 'select' | 'textarea' | 'custom' | 'password';
|
||
|
|
|
||
|
|
export interface SelectOption {
|
||
|
|
value: string;
|
||
|
|
label: string;
|
||
|
|
disabled?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface FormFieldProps {
|
||
|
|
label: string;
|
||
|
|
required?: boolean;
|
||
|
|
type?: FormFieldType;
|
||
|
|
value?: string | number;
|
||
|
|
onChange?: (value: string) => void;
|
||
|
|
placeholder?: string;
|
||
|
|
disabled?: boolean;
|
||
|
|
error?: string;
|
||
|
|
helpText?: string;
|
||
|
|
options?: SelectOption[];
|
||
|
|
selectPlaceholder?: string;
|
||
|
|
children?: ReactNode;
|
||
|
|
className?: string;
|
||
|
|
inputClassName?: string;
|
||
|
|
rows?: number;
|
||
|
|
min?: number;
|
||
|
|
max?: number;
|
||
|
|
step?: number;
|
||
|
|
htmlFor?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function FormField({
|
||
|
|
label,
|
||
|
|
required = false,
|
||
|
|
type = 'text',
|
||
|
|
value,
|
||
|
|
onChange,
|
||
|
|
placeholder,
|
||
|
|
disabled = false,
|
||
|
|
error,
|
||
|
|
helpText,
|
||
|
|
options = [],
|
||
|
|
selectPlaceholder = "선택하세요",
|
||
|
|
children,
|
||
|
|
className = "",
|
||
|
|
inputClassName = "",
|
||
|
|
rows = 3,
|
||
|
|
min,
|
||
|
|
max,
|
||
|
|
step,
|
||
|
|
htmlFor,
|
||
|
|
}: FormFieldProps) {
|
||
|
|
|
||
|
|
const renderInput = () => {
|
||
|
|
switch (type) {
|
||
|
|
case 'select':
|
||
|
|
return (
|
||
|
|
<Select
|
||
|
|
value={value as string}
|
||
|
|
onValueChange={onChange}
|
||
|
|
disabled={disabled}
|
||
|
|
>
|
||
|
|
<SelectTrigger className={`${error ? 'border-red-500' : ''} ${inputClassName}`}>
|
||
|
|
<SelectValue placeholder={selectPlaceholder} />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{options.map((option) => (
|
||
|
|
<SelectItem
|
||
|
|
key={option.value}
|
||
|
|
value={option.value}
|
||
|
|
disabled={option.disabled}
|
||
|
|
>
|
||
|
|
{option.label}
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
);
|
||
|
|
|
||
|
|
case 'textarea':
|
||
|
|
return (
|
||
|
|
<Textarea
|
||
|
|
id={htmlFor}
|
||
|
|
value={value as string}
|
||
|
|
onChange={(e) => onChange?.(e.target.value)}
|
||
|
|
placeholder={placeholder}
|
||
|
|
disabled={disabled}
|
||
|
|
rows={rows}
|
||
|
|
className={`${error ? 'border-red-500' : ''} ${inputClassName}`}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
|
||
|
|
case 'custom':
|
||
|
|
return children;
|
||
|
|
|
||
|
|
case 'number':
|
||
|
|
return (
|
||
|
|
<Input
|
||
|
|
id={htmlFor}
|
||
|
|
type="number"
|
||
|
|
value={value}
|
||
|
|
onChange={(e) => onChange?.(e.target.value)}
|
||
|
|
placeholder={placeholder}
|
||
|
|
disabled={disabled}
|
||
|
|
min={min}
|
||
|
|
max={max}
|
||
|
|
step={step}
|
||
|
|
className={`${error ? 'border-red-500' : ''} ${inputClassName}`}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
|
||
|
|
case 'date':
|
||
|
|
return (
|
||
|
|
<Input
|
||
|
|
id={htmlFor}
|
||
|
|
type="date"
|
||
|
|
value={value as string}
|
||
|
|
onChange={(e) => onChange?.(e.target.value)}
|
||
|
|
disabled={disabled}
|
||
|
|
className={`${error ? 'border-red-500' : ''} ${inputClassName}`}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
|
||
|
|
case 'password':
|
||
|
|
return (
|
||
|
|
<Input
|
||
|
|
id={htmlFor}
|
||
|
|
type="password"
|
||
|
|
value={value as string}
|
||
|
|
onChange={(e) => onChange?.(e.target.value)}
|
||
|
|
placeholder={placeholder}
|
||
|
|
disabled={disabled}
|
||
|
|
className={`${error ? 'border-red-500' : ''} ${inputClassName}`}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
|
||
|
|
case 'text':
|
||
|
|
default:
|
||
|
|
return (
|
||
|
|
<Input
|
||
|
|
id={htmlFor}
|
||
|
|
type="text"
|
||
|
|
value={value as string}
|
||
|
|
onChange={(e) => onChange?.(e.target.value)}
|
||
|
|
placeholder={placeholder}
|
||
|
|
disabled={disabled}
|
||
|
|
className={`${error ? 'border-red-500' : ''} ${inputClassName}`}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={className}>
|
||
|
|
<Label htmlFor={htmlFor}>
|
||
|
|
{label} {required && <span className="text-red-500">*</span>}
|
||
|
|
</Label>
|
||
|
|
|
||
|
|
<div className="mt-1">
|
||
|
|
{renderInput()}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{error && (
|
||
|
|
<div className="flex items-center gap-1 mt-1 text-sm text-red-500">
|
||
|
|
<AlertCircle className="h-3 w-3" />
|
||
|
|
<span>{error}</span>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{helpText && !error && (
|
||
|
|
<p className="text-xs text-muted-foreground mt-1">{helpText}</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|