ERP-node/frontend/components/screen/RealtimePreview.tsx

344 lines
11 KiB
TypeScript
Raw Normal View History

"use client";
import React from "react";
2025-09-09 14:29:04 +09:00
import { ComponentData, WebType, WidgetComponent, FileComponent, AreaComponent, AreaLayoutType } from "@/types/screen";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
2025-09-03 15:23:12 +09:00
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
2025-09-05 21:52:19 +09:00
import { FileUpload } from "./widgets/FileUpload";
import { useAuth } from "@/hooks/useAuth";
2025-09-09 14:29:04 +09:00
import { DynamicWebTypeRenderer, WebTypeRegistry } from "@/lib/registry";
2025-09-09 17:42:23 +09:00
import { DataTableTemplate } from "@/components/screen/templates/DataTableTemplate";
2025-09-01 15:22:47 +09:00
import {
Database,
Type,
Hash,
List,
AlignLeft,
CheckSquare,
Radio,
Calendar,
Code,
Building,
File,
Group,
ChevronDown,
ChevronRight,
2025-09-03 15:23:12 +09:00
Search,
RotateCcw,
2025-09-03 16:38:10 +09:00
Plus,
Edit,
Trash2,
2025-09-05 12:04:13 +09:00
Upload,
2025-09-08 13:10:09 +09:00
Square,
CreditCard,
Layout,
Grid3x3,
Columns,
Rows,
SidebarOpen,
Folder,
ChevronUp,
2025-09-01 15:22:47 +09:00
} from "lucide-react";
interface RealtimePreviewProps {
component: ComponentData;
isSelected?: boolean;
2025-09-01 16:40:24 +09:00
onClick?: (e?: React.MouseEvent) => void;
onDragStart?: (e: React.DragEvent) => void;
onDragEnd?: () => void;
2025-09-01 15:22:47 +09:00
onGroupToggle?: (groupId: string) => void; // 그룹 접기/펼치기
children?: React.ReactNode; // 그룹 내 자식 컴포넌트들
}
2025-09-08 13:10:09 +09:00
// 영역 레이아웃에 따른 아이콘 반환
const getAreaIcon = (layoutType: AreaLayoutType) => {
switch (layoutType) {
case "flex-row":
2025-09-09 14:29:04 +09:00
return <Layout className="h-4 w-4 text-blue-600" />;
case "grid":
return <Grid3x3 className="h-4 w-4 text-green-600" />;
2025-09-08 13:10:09 +09:00
case "flex-column":
2025-09-09 14:29:04 +09:00
return <Columns className="h-4 w-4 text-purple-600" />;
case "panel":
return <Rows className="h-4 w-4 text-orange-600" />;
2025-09-08 13:10:09 +09:00
case "sidebar":
2025-09-09 14:29:04 +09:00
return <SidebarOpen className="h-4 w-4 text-indigo-600" />;
2025-09-08 13:10:09 +09:00
case "tabs":
2025-09-09 14:29:04 +09:00
return <Folder className="h-4 w-4 text-pink-600" />;
2025-09-08 13:10:09 +09:00
default:
2025-09-09 14:29:04 +09:00
return <Square className="h-4 w-4 text-gray-500" />;
2025-09-08 13:10:09 +09:00
}
};
2025-09-09 14:29:04 +09:00
// 영역 렌더링
const renderArea = (component: ComponentData, children?: React.ReactNode) => {
const area = component as AreaComponent;
const { layoutType, title } = area;
2025-09-08 13:10:09 +09:00
const renderPlaceholder = () => (
2025-09-09 14:29:04 +09:00
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
<div className="text-center">
2025-09-08 13:10:09 +09:00
{getAreaIcon(layoutType)}
2025-09-09 14:29:04 +09:00
<p className="mt-2 text-sm text-gray-600">{title || `${layoutType} 영역`}</p>
<p className="text-xs text-gray-400"> </p>
2025-09-08 13:10:09 +09:00
</div>
</div>
);
return (
2025-09-09 14:29:04 +09:00
<div className="relative h-full w-full">
<div className="absolute inset-0 h-full w-full">
{children && React.Children.count(children) > 0 ? children : renderPlaceholder()}
</div>
2025-09-08 13:10:09 +09:00
</div>
);
};
2025-09-09 14:29:04 +09:00
// 동적 웹 타입 위젯 렌더링 컴포넌트
const WidgetRenderer: React.FC<{ component: ComponentData }> = ({ component }) => {
2025-09-05 12:04:13 +09:00
// 위젯 컴포넌트가 아닌 경우 빈 div 반환
if (component.type !== "widget") {
return <div className="text-xs text-gray-500"> </div>;
}
const widget = component as WidgetComponent;
const { widgetType, label, placeholder, required, readonly, columnName, style } = widget;
2025-09-01 15:22:47 +09:00
// 디버깅: 실제 widgetType 값 확인
2025-09-09 14:29:04 +09:00
console.log("RealtimePreviewDynamic - widgetType:", widgetType, "columnName:", columnName);
2025-09-01 17:05:36 +09:00
// 사용자가 테두리를 설정했는지 확인
const hasCustomBorder = style && (style.borderWidth || style.borderStyle || style.borderColor || style.border);
// 기본 테두리 제거 여부 결정 - Shadcn UI 기본 border 클래스를 덮어쓰기
const borderClass = hasCustomBorder ? "!border-0" : "";
const commonProps = {
placeholder: placeholder || "입력하세요...",
disabled: readonly,
required: required,
2025-09-01 17:05:36 +09:00
className: `w-full h-full ${borderClass}`,
};
2025-09-09 14:29:04 +09:00
// 동적 웹타입 렌더링 사용
if (widgetType) {
try {
return (
2025-09-09 14:29:04 +09:00
<DynamicWebTypeRenderer
webType={widgetType}
props={{
...commonProps,
component: widget,
value: undefined, // 미리보기이므로 값은 없음
readonly: readonly,
2025-09-03 11:32:09 +09:00
}}
2025-09-09 14:29:04 +09:00
config={widget.webTypeConfig}
2025-09-03 11:32:09 +09:00
/>
);
2025-09-09 14:29:04 +09:00
} catch (error) {
console.error(`웹타입 "${widgetType}" 렌더링 실패:`, error);
// 오류 발생 시 폴백으로 기본 input 렌더링
return <Input type="text" {...commonProps} placeholder={`${widgetType} (렌더링 오류)`} />;
2025-09-03 11:32:09 +09:00
}
2025-09-09 14:29:04 +09:00
}
2025-09-09 14:29:04 +09:00
// 웹타입이 없는 경우 기본 input 렌더링 (하위 호환성)
return <Input type="text" {...commonProps} />;
};
2025-09-09 14:29:04 +09:00
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
const getWidgetIcon = (widgetType: WebType | undefined) => {
if (!widgetType) {
return <Type className="h-4 w-4 text-gray-500" />;
}
2025-09-09 14:29:04 +09:00
// 레지스트리에서 웹타입 정의 조회
const webTypeDefinition = WebTypeRegistry.getWebType(widgetType);
if (webTypeDefinition && webTypeDefinition.icon) {
const IconComponent = webTypeDefinition.icon;
return <IconComponent className="h-4 w-4" />;
}
2025-09-09 14:29:04 +09:00
// 기본 아이콘 매핑 (하위 호환성)
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" />;
}
};
2025-09-09 14:29:04 +09:00
export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
component,
isSelected = false,
onClick,
onDragStart,
onDragEnd,
2025-09-01 15:22:47 +09:00
onGroupToggle,
2025-09-09 14:29:04 +09:00
children,
}) => {
2025-09-09 14:29:04 +09:00
const { user } = useAuth();
const { type, id, position, size, style = {} } = component;
// 컴포넌트 스타일 계산
const componentStyle = {
position: "absolute" as const,
left: position?.x || 0,
top: position?.y || 0,
width: size?.width || 200,
height: size?.height || 40,
zIndex: position?.z || 1,
...style,
2025-09-02 16:46:54 +09:00
};
2025-09-09 14:29:04 +09:00
// 선택된 컴포넌트 스타일
const selectionStyle = isSelected
? {
outline: "2px solid #3b82f6",
outlineOffset: "2px",
}
: {};
2025-09-03 15:23:12 +09:00
2025-09-09 14:29:04 +09:00
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
onClick?.(e);
};
2025-09-03 15:23:12 +09:00
2025-09-09 14:29:04 +09:00
const handleDragStart = (e: React.DragEvent) => {
e.stopPropagation();
onDragStart?.(e);
};
2025-09-03 15:23:12 +09:00
2025-09-09 14:29:04 +09:00
const handleDragEnd = () => {
onDragEnd?.();
};
2025-09-03 15:23:12 +09:00
return (
<div
2025-09-09 14:29:04 +09:00
id={`component-${id}`}
className="absolute cursor-pointer"
style={{ ...componentStyle, ...selectionStyle }}
onClick={handleClick}
draggable
2025-09-09 14:29:04 +09:00
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
2025-09-09 14:29:04 +09:00
{/* 컴포넌트 타입별 렌더링 */}
<div className="h-full w-full">
{/* 영역 타입 */}
{type === "area" && renderArea(component, children)}
2025-09-01 15:22:47 +09:00
2025-09-09 14:29:04 +09:00
{/* 데이터 테이블 타입 */}
{type === "datatable" &&
2025-09-03 15:23:12 +09:00
(() => {
const dataTableComponent = component as any; // DataTableComponent 타입
2025-09-09 14:29:04 +09:00
2025-09-03 15:23:12 +09:00
return (
2025-09-09 17:42:23 +09:00
<DataTableTemplate
title={dataTableComponent.title || dataTableComponent.label}
description={`${dataTableComponent.label}을 표시하는 데이터 테이블`}
columns={dataTableComponent.columns}
filters={dataTableComponent.filters}
pagination={dataTableComponent.pagination}
actions={
dataTableComponent.actions || {
showSearchButton: dataTableComponent.showSearchButton ?? true,
searchButtonText: dataTableComponent.searchButtonText || "검색",
enableExport: dataTableComponent.enableExport ?? true,
enableRefresh: dataTableComponent.enableRefresh ?? true,
enableAdd: dataTableComponent.enableAdd ?? true,
enableEdit: dataTableComponent.enableEdit ?? true,
enableDelete: dataTableComponent.enableDelete ?? true,
addButtonText: dataTableComponent.addButtonText || "추가",
editButtonText: dataTableComponent.editButtonText || "수정",
deleteButtonText: dataTableComponent.deleteButtonText || "삭제",
}
}
style={component.style}
className="h-full w-full"
isPreview={true}
/>
2025-09-03 15:23:12 +09:00
);
})()}
2025-09-09 14:29:04 +09:00
{/* 그룹 타입 */}
2025-09-02 16:46:54 +09:00
{type === "group" && (
<div className="relative h-full w-full">
<div className="absolute inset-0">{children}</div>
</div>
)}
2025-09-09 14:29:04 +09:00
{/* 위젯 타입 - 동적 렌더링 */}
2025-09-02 16:46:54 +09:00
{type === "widget" && (
<div className="flex h-full flex-col">
2025-09-09 14:29:04 +09:00
<div className="pointer-events-none flex-1">
<WidgetRenderer component={component} />
</div>
2025-09-02 16:46:54 +09:00
</div>
)}
2025-09-05 21:52:19 +09:00
2025-09-09 14:29:04 +09:00
{/* 파일 타입 */}
2025-09-05 21:52:19 +09:00
{type === "file" && (
<div className="flex h-full flex-col">
2025-09-09 14:29:04 +09:00
<div className="pointer-events-none flex-1 rounded border-2 border-dashed border-gray-300 bg-gray-50 p-4">
<div className="flex h-full flex-col items-center justify-center text-center">
<File className="mb-2 h-8 w-8 text-gray-400" />
<p className="text-sm text-gray-600"> </p>
<p className="mt-1 text-xs text-gray-400"> </p>
</div>
2025-09-05 21:52:19 +09:00
</div>
</div>
)}
2025-09-02 16:46:54 +09:00
</div>
2025-09-09 14:29:04 +09:00
{/* 선택된 컴포넌트 정보 표시 */}
{isSelected && (
<div className="absolute -top-6 left-0 rounded bg-blue-600 px-2 py-1 text-xs text-white">
{type === "widget" && (
<div className="flex items-center gap-1">
{getWidgetIcon((component as WidgetComponent).widgetType)}
{(component as WidgetComponent).widgetType || "widget"}
</div>
)}
{type !== "widget" && type}
</div>
)}
</div>
);
};
2025-09-09 14:29:04 +09:00
// 기존 RealtimePreview와의 호환성을 위한 export
export { RealtimePreviewDynamic as RealtimePreview };
RealtimePreviewDynamic.displayName = "RealtimePreviewDynamic";