287 lines
9.1 KiB
TypeScript
287 lines
9.1 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { ComponentData, WebType } from "@/types/screen";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
// import { Checkbox } from "@/components/ui/checkbox";
|
|
// import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import {
|
|
Database,
|
|
Type,
|
|
Hash,
|
|
List,
|
|
AlignLeft,
|
|
CheckSquare,
|
|
Radio,
|
|
Calendar,
|
|
Code,
|
|
Building,
|
|
File,
|
|
Group,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
} from "lucide-react";
|
|
|
|
interface RealtimePreviewProps {
|
|
component: ComponentData;
|
|
isSelected?: boolean;
|
|
onClick?: () => void;
|
|
onDragStart?: (e: React.DragEvent) => void;
|
|
onDragEnd?: () => void;
|
|
onGroupToggle?: (groupId: string) => void; // 그룹 접기/펼치기
|
|
children?: React.ReactNode; // 그룹 내 자식 컴포넌트들
|
|
}
|
|
|
|
// 웹 타입에 따른 위젯 렌더링
|
|
const renderWidget = (component: ComponentData) => {
|
|
const { widgetType, label, placeholder, required, readonly, columnName } = component;
|
|
|
|
// 디버깅: 실제 widgetType 값 확인
|
|
console.log("RealtimePreview - widgetType:", widgetType, "columnName:", columnName);
|
|
|
|
const commonProps = {
|
|
placeholder: placeholder || `입력하세요...`,
|
|
disabled: readonly,
|
|
required: required,
|
|
className: "w-full h-full",
|
|
};
|
|
|
|
switch (widgetType) {
|
|
case "text":
|
|
case "email":
|
|
case "tel":
|
|
return <Input type={widgetType === "email" ? "email" : widgetType === "tel" ? "tel" : "text"} {...commonProps} />;
|
|
|
|
case "number":
|
|
case "decimal":
|
|
return <Input type="number" step={widgetType === "decimal" ? "0.01" : "1"} {...commonProps} />;
|
|
|
|
case "date":
|
|
case "datetime":
|
|
return <Input type={widgetType === "datetime" ? "datetime-local" : "date"} {...commonProps} />;
|
|
|
|
case "select":
|
|
case "dropdown":
|
|
return (
|
|
<select
|
|
disabled={readonly}
|
|
required={required}
|
|
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100"
|
|
>
|
|
<option value="">{placeholder || "선택하세요..."}</option>
|
|
<option value="option1">옵션 1</option>
|
|
<option value="option2">옵션 2</option>
|
|
<option value="option3">옵션 3</option>
|
|
</select>
|
|
);
|
|
|
|
case "textarea":
|
|
case "text_area":
|
|
return <Textarea {...commonProps} rows={3} />;
|
|
|
|
case "boolean":
|
|
case "checkbox":
|
|
return (
|
|
<div className="flex items-center space-x-2">
|
|
<input
|
|
type="checkbox"
|
|
id={`checkbox-${component.id}`}
|
|
disabled={readonly}
|
|
required={required}
|
|
className="h-4 w-4"
|
|
/>
|
|
<Label htmlFor={`checkbox-${component.id}`} className="text-sm">
|
|
{label || columnName}
|
|
</Label>
|
|
</div>
|
|
);
|
|
|
|
case "radio":
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center space-x-2">
|
|
<input
|
|
type="radio"
|
|
id={`radio1-${component.id}`}
|
|
name={`radio-${component.id}`}
|
|
disabled={readonly}
|
|
className="h-4 w-4"
|
|
/>
|
|
<Label htmlFor={`radio1-${component.id}`} className="text-sm">
|
|
옵션 1
|
|
</Label>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<input
|
|
type="radio"
|
|
id={`radio2-${component.id}`}
|
|
name={`radio-${component.id}`}
|
|
disabled={readonly}
|
|
className="h-4 w-4"
|
|
/>
|
|
<Label htmlFor={`radio2-${component.id}`} className="text-sm">
|
|
옵션 2
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case "code":
|
|
return (
|
|
<Textarea {...commonProps} rows={4} className="w-full font-mono text-sm" placeholder="코드를 입력하세요..." />
|
|
);
|
|
|
|
case "entity":
|
|
return (
|
|
<select
|
|
disabled={readonly}
|
|
required={required}
|
|
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100"
|
|
>
|
|
<option value="">엔티티를 선택하세요...</option>
|
|
<option value="user">사용자</option>
|
|
<option value="product">제품</option>
|
|
<option value="order">주문</option>
|
|
</select>
|
|
);
|
|
|
|
case "file":
|
|
return (
|
|
<input
|
|
type="file"
|
|
disabled={readonly}
|
|
required={required}
|
|
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100"
|
|
/>
|
|
);
|
|
|
|
default:
|
|
return <Input type="text" {...commonProps} />;
|
|
}
|
|
};
|
|
|
|
// 위젯 타입 아이콘
|
|
const getWidgetIcon = (widgetType: WebType | undefined) => {
|
|
switch (widgetType) {
|
|
case "text":
|
|
case "email":
|
|
case "tel":
|
|
return <Type className="h-4 w-4 text-blue-600" />;
|
|
case "number":
|
|
case "decimal":
|
|
return <Hash className="h-4 w-4 text-green-600" />;
|
|
case "date":
|
|
case "datetime":
|
|
return <Calendar className="h-4 w-4 text-purple-600" />;
|
|
case "select":
|
|
case "dropdown":
|
|
return <List className="h-4 w-4 text-orange-600" />;
|
|
case "textarea":
|
|
case "text_area":
|
|
return <AlignLeft className="h-4 w-4 text-indigo-600" />;
|
|
case "boolean":
|
|
case "checkbox":
|
|
return <CheckSquare className="h-4 w-4 text-blue-600" />;
|
|
case "radio":
|
|
return <Radio className="h-4 w-4 text-blue-600" />;
|
|
case "code":
|
|
return <Code className="h-4 w-4 text-gray-600" />;
|
|
case "entity":
|
|
return <Building className="h-4 w-4 text-cyan-600" />;
|
|
case "file":
|
|
return <File className="h-4 w-4 text-yellow-600" />;
|
|
default:
|
|
return <Type className="h-4 w-4 text-gray-500" />;
|
|
}
|
|
};
|
|
|
|
export const RealtimePreview: React.FC<RealtimePreviewProps> = ({
|
|
component,
|
|
isSelected = false,
|
|
onClick,
|
|
onDragStart,
|
|
onDragEnd,
|
|
children,
|
|
onGroupToggle,
|
|
}) => {
|
|
const { type, label, tableName, columnName, widgetType, size, style } = component;
|
|
|
|
return (
|
|
<div
|
|
className={`absolute cursor-move transition-all ${
|
|
isSelected ? "ring-opacity-50 ring-2 ring-blue-500" : "hover:ring-opacity-50 hover:ring-1 hover:ring-gray-300"
|
|
}`}
|
|
style={{
|
|
left: `${component.position.x}px`,
|
|
top: `${component.position.y}px`,
|
|
width: `${size.width * 80}px`,
|
|
height: `${size.height}px`,
|
|
...style,
|
|
}}
|
|
onClick={onClick}
|
|
draggable
|
|
onDragStart={onDragStart}
|
|
onDragEnd={onDragEnd}
|
|
onMouseDown={(e) => {
|
|
// 드래그 시작을 위한 마우스 다운 이벤트
|
|
e.stopPropagation();
|
|
}}
|
|
>
|
|
{type === "container" && (
|
|
<div className="pointer-events-none flex h-full flex-col items-center justify-center p-2">
|
|
<div className="flex flex-col items-center space-y-1">
|
|
<Database className="h-6 w-6 text-blue-600" />
|
|
<div className="text-center">
|
|
<div className="text-xs font-medium">{label}</div>
|
|
<div className="text-xs text-gray-500">{tableName}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{type === "group" && (
|
|
<div className="flex h-full flex-col rounded-lg border border-gray-200 bg-gray-50">
|
|
{/* 그룹 헤더 */}
|
|
<div
|
|
className="pointer-events-auto flex cursor-pointer items-center justify-between rounded-t-lg border-b bg-white px-2 py-1"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onGroupToggle?.(component.id);
|
|
}}
|
|
>
|
|
<div className="flex items-center space-x-1">
|
|
<Group className="h-3 w-3 text-blue-600" />
|
|
<span className="text-xs font-medium">{label || "그룹"}</span>
|
|
<span className="text-xs text-gray-500">({children ? children.length : 0}개)</span>
|
|
</div>
|
|
{component.collapsible &&
|
|
(component.collapsed ? (
|
|
<ChevronRight className="h-3 w-3 text-gray-500" />
|
|
) : (
|
|
<ChevronDown className="h-3 w-3 text-gray-500" />
|
|
))}
|
|
</div>
|
|
|
|
{/* 그룹 내용 */}
|
|
{!component.collapsed && (
|
|
<div className="pointer-events-none flex-1 space-y-1 overflow-auto p-1">
|
|
{children ? children : <div className="py-2 text-center text-xs text-gray-400">그룹이 비어있습니다</div>}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{type === "widget" && (
|
|
<div className="flex h-full flex-col">
|
|
{/* 위젯 본체 - 실제 웹 위젯처럼 보이도록 */}
|
|
<div className="pointer-events-none flex-1">{renderWidget(component)}</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RealtimePreview;
|