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

205 lines
6.4 KiB
TypeScript
Raw Normal View History

2025-09-09 14:29:04 +09:00
"use client";
import React from "react";
2025-09-10 14:09:32 +09:00
import { ComponentData, WebType, WidgetComponent } from "@/types/screen";
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
2025-09-09 14:29:04 +09:00
import {
Database,
Type,
Hash,
List,
AlignLeft,
CheckSquare,
Radio,
Calendar,
Code,
Building,
File,
} from "lucide-react";
2025-09-10 14:09:32 +09:00
// 컴포넌트 렌더러들 자동 등록
import "@/lib/registry/components";
2025-09-09 14:29:04 +09:00
interface RealtimePreviewProps {
component: ComponentData;
isSelected?: boolean;
2025-09-12 14:24:25 +09:00
isDesignMode?: boolean; // 편집 모드 여부
2025-09-09 14:29:04 +09:00
onClick?: (e?: React.MouseEvent) => void;
onDoubleClick?: (e?: React.MouseEvent) => void; // 더블클릭 핸들러 추가
2025-09-09 14:29:04 +09:00
onDragStart?: (e: React.DragEvent) => void;
onDragEnd?: () => void;
onGroupToggle?: (groupId: string) => void; // 그룹 접기/펼치기
children?: React.ReactNode; // 그룹 내 자식 컴포넌트들
2025-09-11 16:21:00 +09:00
selectedScreen?: any;
onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void; // 존별 드롭 핸들러
onZoneClick?: (zoneId: string) => void; // 존 클릭 핸들러
onConfigChange?: (config: any) => void; // 설정 변경 핸들러
2025-09-09 14:29:04 +09:00
}
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
2025-09-10 14:09:32 +09:00
const getWidgetIcon = (widgetType: WebType | undefined): React.ReactNode => {
if (!widgetType) return <Type className="h-3 w-3" />;
const iconMap: Record<string, React.ReactNode> = {
text: <span className="text-xs">Aa</span>,
number: <Hash className="h-3 w-3" />,
decimal: <Hash className="h-3 w-3" />,
date: <Calendar className="h-3 w-3" />,
datetime: <Calendar className="h-3 w-3" />,
select: <List className="h-3 w-3" />,
dropdown: <List className="h-3 w-3" />,
textarea: <AlignLeft className="h-3 w-3" />,
boolean: <CheckSquare className="h-3 w-3" />,
checkbox: <CheckSquare className="h-3 w-3" />,
radio: <Radio className="h-3 w-3" />,
code: <Code className="h-3 w-3" />,
entity: <Building className="h-3 w-3" />,
file: <File className="h-3 w-3" />,
email: <span className="text-xs">@</span>,
tel: <span className="text-xs"></span>,
button: <span className="text-xs">BTN</span>,
};
2025-09-09 14:29:04 +09:00
2025-09-10 14:09:32 +09:00
return iconMap[widgetType] || <Type className="h-3 w-3" />;
2025-09-09 14:29:04 +09:00
};
export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
component,
isSelected = false,
2025-09-12 14:24:25 +09:00
isDesignMode = true, // 기본값은 편집 모드
2025-09-09 14:29:04 +09:00
onClick,
onDoubleClick,
2025-09-09 14:29:04 +09:00
onDragStart,
onDragEnd,
onGroupToggle,
children,
2025-09-11 12:22:39 +09:00
selectedScreen,
2025-09-11 16:21:00 +09:00
onZoneComponentDrop,
onZoneClick,
onConfigChange,
2025-09-09 14:29:04 +09:00
}) => {
2025-09-10 14:09:32 +09:00
const { id, type, position, size, style: componentStyle } = component;
2025-09-09 14:29:04 +09:00
// 선택 상태에 따른 스타일 (z-index 낮춤 - 패널과 모달보다 아래)
2025-09-09 14:29:04 +09:00
const selectionStyle = isSelected
? {
outline: "2px solid #3b82f6",
outlineOffset: "2px",
zIndex: 20, // 패널과 모달보다 낮게 설정
2025-09-09 14:29:04 +09:00
}
: {};
2025-09-11 16:21:00 +09:00
// 컴포넌트 기본 스타일 - 레이아웃은 항상 맨 아래
2025-10-14 13:27:02 +09:00
// 너비 우선순위: style.width > size.width (픽셀값)
const getWidth = () => {
// 1순위: style.width가 있으면 우선 사용
if (componentStyle?.width) {
return componentStyle.width;
}
// 2순위: size.width (픽셀)
if (component.componentConfig?.type === "table-list") {
return `${Math.max(size?.width || 120, 120)}px`;
}
return `${size?.width || 100}px`;
};
const getHeight = () => {
// 1순위: style.height가 있으면 우선 사용
if (componentStyle?.height) {
return componentStyle.height;
}
// 2순위: size.height (픽셀)
if (component.componentConfig?.type === "table-list") {
return `${Math.max(size?.height || 200, 200)}px`;
}
return `${size?.height || 40}px`;
};
2025-09-10 14:09:32 +09:00
const baseStyle = {
left: `${position.x}px`,
top: `${position.y}px`,
2025-10-14 13:27:02 +09:00
width: getWidth(),
height: getHeight(),
2025-09-11 16:21:00 +09:00
zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상
2025-09-10 14:09:32 +09:00
...componentStyle,
2025-10-14 13:27:02 +09:00
// style.width와 style.height는 이미 getWidth/getHeight에서 처리했으므로 중복 적용됨
2025-09-10 14:09:32 +09:00
};
2025-09-09 14:29:04 +09:00
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
onClick?.(e);
};
const handleDoubleClick = (e: React.MouseEvent) => {
e.stopPropagation();
onDoubleClick?.(e);
};
2025-09-09 14:29:04 +09:00
const handleDragStart = (e: React.DragEvent) => {
e.stopPropagation();
onDragStart?.(e);
};
const handleDragEnd = () => {
onDragEnd?.();
};
return (
<div
id={`component-${id}`}
className="absolute cursor-pointer transition-all duration-200 ease-out"
2025-09-10 14:09:32 +09:00
style={{ ...baseStyle, ...selectionStyle }}
2025-09-09 14:29:04 +09:00
onClick={handleClick}
onDoubleClick={handleDoubleClick}
2025-09-09 14:29:04 +09:00
draggable
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
2025-09-10 14:09:32 +09:00
{/* 동적 컴포넌트 렌더링 */}
2025-10-14 16:45:30 +09:00
<div
className={`h-full w-full max-w-full ${component.componentConfig?.type === "table-list" ? "overflow-hidden" : "overflow-hidden"}`}
>
2025-09-10 14:09:32 +09:00
<DynamicComponentRenderer
component={component}
isSelected={isSelected}
2025-09-12 14:24:25 +09:00
isDesignMode={isDesignMode}
isInteractive={!isDesignMode} // 편집 모드가 아닐 때만 인터랙티브
2025-09-10 14:09:32 +09:00
onClick={onClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
children={children}
2025-09-11 12:22:39 +09:00
selectedScreen={selectedScreen}
2025-09-11 16:21:00 +09:00
onZoneComponentDrop={onZoneComponentDrop}
onZoneClick={onZoneClick}
onConfigChange={onConfigChange}
2025-09-10 14:09:32 +09:00
/>
2025-09-09 14:29:04 +09:00
</div>
{/* 선택된 컴포넌트 정보 표시 */}
{isSelected && (
2025-10-14 13:27:02 +09:00
<div className="absolute -top-8 left-0 rounded-lg bg-gray-800/90 px-3 py-2 text-xs text-white shadow-lg backdrop-blur-sm">
2025-09-09 14:29:04 +09:00
{type === "widget" && (
<div className="flex items-center gap-2">
2025-09-09 14:29:04 +09:00
{getWidgetIcon((component as WidgetComponent).widgetType)}
<span className="font-medium">{(component as WidgetComponent).widgetType || "widget"}</span>
2025-09-09 14:29:04 +09:00
</div>
)}
2025-09-10 14:09:32 +09:00
{type !== "widget" && (
<div className="flex items-center gap-2">
<span className="font-medium">{component.componentConfig?.type || type}</span>
2025-09-10 14:09:32 +09:00
</div>
)}
2025-09-09 14:29:04 +09:00
</div>
)}
</div>
);
};
// 기존 RealtimePreview와의 호환성을 위한 export
export { RealtimePreviewDynamic as RealtimePreview };
2025-09-10 14:09:32 +09:00
export default RealtimePreviewDynamic;