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;
|
2025-09-26 13:11:34 +09:00
|
|
|
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; // 존 클릭 핸들러
|
2025-09-24 18:07:36 +09:00
|
|
|
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,
|
2025-09-26 13:11:34 +09:00
|
|
|
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,
|
2025-09-24 18:07:36 +09:00
|
|
|
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
|
|
|
|
2025-09-18 21:33:04 +09:00
|
|
|
// 선택 상태에 따른 스타일 (z-index 낮춤 - 패널과 모달보다 아래)
|
2025-09-09 14:29:04 +09:00
|
|
|
const selectionStyle = isSelected
|
|
|
|
|
? {
|
2025-10-20 10:55:33 +09:00
|
|
|
outline: "2px solid rgb(59, 130, 246)",
|
2025-09-09 14:29:04 +09:00
|
|
|
outlineOffset: "2px",
|
2025-10-17 16:21:08 +09:00
|
|
|
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(),
|
2025-10-22 17:19:47 +09:00
|
|
|
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);
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-26 13:11:34 +09:00
|
|
|
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}`}
|
2025-09-30 10:30:05 +09:00
|
|
|
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}
|
2025-09-26 13:11:34 +09:00
|
|
|
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
|
2025-10-14 17:40:51 +09:00
|
|
|
className={`h-full w-full max-w-full ${
|
2025-10-22 17:19:47 +09:00
|
|
|
component.componentConfig?.type === "table-list" ? "overflow-hidden" : "overflow-visible"
|
2025-10-14 17:40:51 +09:00
|
|
|
}`}
|
2025-10-14 16:45:30 +09:00
|
|
|
>
|
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}
|
2025-09-24 18:07:36 +09:00
|
|
|
onConfigChange={onConfigChange}
|
2025-09-10 14:09:32 +09:00
|
|
|
/>
|
2025-09-09 14:29:04 +09:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 선택된 컴포넌트 정보 표시 */}
|
|
|
|
|
{isSelected && (
|
2025-10-17 16:21:08 +09:00
|
|
|
<div className="bg-primary text-primary-foreground absolute -top-7 left-0 rounded-md px-2.5 py-1 text-xs font-medium shadow-sm">
|
2025-09-09 14:29:04 +09:00
|
|
|
{type === "widget" && (
|
2025-10-17 16:21:08 +09:00
|
|
|
<div className="flex items-center gap-1.5">
|
2025-09-09 14:29:04 +09:00
|
|
|
{getWidgetIcon((component as WidgetComponent).widgetType)}
|
2025-10-17 16:21:08 +09:00
|
|
|
<span>{(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" && (
|
2025-10-17 16:21:08 +09:00
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
<span>{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;
|