210 lines
8.0 KiB
TypeScript
210 lines
8.0 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { WidgetComponent } from "@/types/screen";
|
||
|
|
import InputWidget from "./widgets/InputWidget";
|
||
|
|
import SelectWidget from "./widgets/SelectWidget";
|
||
|
|
import TextareaWidget from "./widgets/TextareaWidget";
|
||
|
|
import { Calendar, CheckSquare, Radio, FileText, Hash, Database } from "lucide-react";
|
||
|
|
|
||
|
|
interface WidgetFactoryProps {
|
||
|
|
widget: WidgetComponent;
|
||
|
|
value?: string;
|
||
|
|
onChange?: (value: string) => void;
|
||
|
|
className?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function WidgetFactory({ widget, value, onChange, className }: WidgetFactoryProps) {
|
||
|
|
// 웹 타입에 따라 적절한 컴포넌트 렌더링
|
||
|
|
switch (widget.widgetType) {
|
||
|
|
case "text":
|
||
|
|
return <InputWidget widget={widget} value={value} onChange={onChange} className={className} />;
|
||
|
|
|
||
|
|
case "number":
|
||
|
|
return <InputWidget widget={widget} value={value} onChange={onChange} className={className} />;
|
||
|
|
|
||
|
|
case "date":
|
||
|
|
return (
|
||
|
|
<div className={`space-y-2 ${className}`}>
|
||
|
|
{widget.label && (
|
||
|
|
<label htmlFor={widget.id} className="text-sm font-medium">
|
||
|
|
{widget.label}
|
||
|
|
{widget.required && <span className="ml-1 text-red-500">*</span>}
|
||
|
|
</label>
|
||
|
|
)}
|
||
|
|
<div className="relative">
|
||
|
|
<Calendar className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||
|
|
<input
|
||
|
|
id={widget.id}
|
||
|
|
type="date"
|
||
|
|
placeholder={widget.placeholder || "날짜를 선택하세요"}
|
||
|
|
value={value || ""}
|
||
|
|
onChange={(e) => onChange?.(e.target.value)}
|
||
|
|
required={widget.required}
|
||
|
|
readOnly={widget.readonly}
|
||
|
|
className="w-full rounded-md border border-gray-300 py-2 pr-3 pl-10 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "code":
|
||
|
|
return (
|
||
|
|
<div className={`space-y-2 ${className}`}>
|
||
|
|
{widget.label && (
|
||
|
|
<label htmlFor={widget.id} className="text-sm font-medium">
|
||
|
|
{widget.label}
|
||
|
|
{widget.required && <span className="ml-1 text-red-500">*</span>}
|
||
|
|
</label>
|
||
|
|
)}
|
||
|
|
<div className="relative">
|
||
|
|
<Hash className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||
|
|
<input
|
||
|
|
id={widget.id}
|
||
|
|
type="text"
|
||
|
|
placeholder={widget.placeholder || "코드를 입력하세요"}
|
||
|
|
value={value || ""}
|
||
|
|
onChange={(e) => onChange?.(e.target.value)}
|
||
|
|
required={widget.required}
|
||
|
|
readOnly={widget.readonly}
|
||
|
|
className="w-full rounded-md border border-gray-300 py-2 pr-3 pl-10 font-mono text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "entity":
|
||
|
|
return (
|
||
|
|
<div className={`space-y-2 ${className}`}>
|
||
|
|
{widget.label && (
|
||
|
|
<label htmlFor={widget.id} className="text-sm font-medium">
|
||
|
|
{widget.label}
|
||
|
|
{widget.required && <span className="ml-1 text-red-500">*</span>}
|
||
|
|
</label>
|
||
|
|
)}
|
||
|
|
<div className="relative">
|
||
|
|
<Database className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||
|
|
<input
|
||
|
|
id={widget.id}
|
||
|
|
type="text"
|
||
|
|
placeholder={widget.placeholder || "엔티티를 선택하세요"}
|
||
|
|
value={value || ""}
|
||
|
|
onChange={(e) => onChange?.(e.target.value)}
|
||
|
|
required={widget.required}
|
||
|
|
readOnly={widget.readonly}
|
||
|
|
className="w-full rounded-md border border-gray-300 py-2 pr-3 pl-10 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "file":
|
||
|
|
return (
|
||
|
|
<div className={`space-y-2 ${className}`}>
|
||
|
|
{widget.label && (
|
||
|
|
<label htmlFor={widget.id} className="text-sm font-medium">
|
||
|
|
{widget.label}
|
||
|
|
{widget.required && <span className="ml-1 text-red-500">*</span>}
|
||
|
|
</label>
|
||
|
|
)}
|
||
|
|
<div className="relative">
|
||
|
|
<FileText className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||
|
|
<input
|
||
|
|
id={widget.id}
|
||
|
|
type="file"
|
||
|
|
onChange={(e) => onChange?.(e.target.files?.[0]?.name || "")}
|
||
|
|
required={widget.required}
|
||
|
|
disabled={widget.readonly}
|
||
|
|
className="w-full rounded-md border border-gray-300 py-2 pr-3 pl-10 text-sm file:mr-4 file:rounded-md file:border-0 file:bg-blue-50 file:px-4 file:py-1 file:text-sm file:font-medium file:text-blue-700 hover:file:bg-blue-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "select":
|
||
|
|
return <SelectWidget widget={widget} value={value} onChange={onChange} className={className} />;
|
||
|
|
|
||
|
|
case "checkbox":
|
||
|
|
return (
|
||
|
|
<div className={`space-y-2 ${className}`}>
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<CheckSquare className="h-4 w-4 text-gray-400" />
|
||
|
|
<input
|
||
|
|
id={widget.id}
|
||
|
|
type="checkbox"
|
||
|
|
checked={value === "true"}
|
||
|
|
onChange={(e) => onChange?.(e.target.checked ? "true" : "false")}
|
||
|
|
required={widget.required}
|
||
|
|
disabled={widget.readonly}
|
||
|
|
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||
|
|
/>
|
||
|
|
{widget.label && (
|
||
|
|
<label htmlFor={widget.id} className="text-sm font-medium">
|
||
|
|
{widget.label}
|
||
|
|
{widget.required && <span className="ml-1 text-red-500">*</span>}
|
||
|
|
</label>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "radio":
|
||
|
|
return (
|
||
|
|
<div className={`space-y-2 ${className}`}>
|
||
|
|
{widget.label && (
|
||
|
|
<label className="text-sm font-medium">
|
||
|
|
{widget.label}
|
||
|
|
{widget.required && <span className="ml-1 text-red-500">*</span>}
|
||
|
|
</label>
|
||
|
|
)}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Radio className="h-4 w-4 text-gray-400" />
|
||
|
|
<input
|
||
|
|
id={`${widget.id}-yes`}
|
||
|
|
name={widget.id}
|
||
|
|
type="radio"
|
||
|
|
value="yes"
|
||
|
|
checked={value === "yes"}
|
||
|
|
onChange={(e) => onChange?.(e.target.value)}
|
||
|
|
required={widget.required}
|
||
|
|
disabled={widget.readonly}
|
||
|
|
className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
|
||
|
|
/>
|
||
|
|
<label htmlFor={`${widget.id}-yes`} className="text-sm">
|
||
|
|
예
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center space-x-2">
|
||
|
|
<Radio className="h-4 w-4 text-gray-400" />
|
||
|
|
<input
|
||
|
|
id={`${widget.id}-no`}
|
||
|
|
name={widget.id}
|
||
|
|
type="radio"
|
||
|
|
value="no"
|
||
|
|
checked={value === "no"}
|
||
|
|
onChange={(e) => onChange?.(e.target.value)}
|
||
|
|
required={widget.required}
|
||
|
|
disabled={widget.readonly}
|
||
|
|
className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
|
||
|
|
/>
|
||
|
|
<label htmlFor={`${widget.id}-no`} className="text-sm">
|
||
|
|
아니오
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
|
||
|
|
case "textarea":
|
||
|
|
return <TextareaWidget widget={widget} value={value} onChange={onChange} className={className} />;
|
||
|
|
|
||
|
|
default:
|
||
|
|
return (
|
||
|
|
<div className={`rounded border border-red-300 bg-red-50 p-4 text-red-600 ${className}`}>
|
||
|
|
<p className="text-sm font-medium">지원하지 않는 위젯 타입</p>
|
||
|
|
<p className="text-xs text-red-500">타입: {widget.widgetType}</p>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|