"use client";
import React, { useState, useEffect } from "react";
import { ComponentData, WebType, isWidgetComponent, isContainerComponent } from "@/types";
import { isFileComponent } from "@/lib/utils/componentTypeUtils";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
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";
import { FileUpload } from "./widgets/FileUpload";
import { useAuth } from "@/hooks/useAuth";
import { DynamicWebTypeRenderer, WebTypeRegistry } from "@/lib/registry";
import { DataTableTemplate } from "@/components/screen/templates/DataTableTemplate";
import {
Database,
Type,
Hash,
List,
AlignLeft,
CheckSquare,
Radio,
Calendar,
Code,
Building,
File,
Group,
ChevronDown,
ChevronRight,
Search,
RotateCcw,
Plus,
Edit,
Trash2,
Upload,
Square,
CreditCard,
Layout,
Grid3x3,
Columns,
Rows,
SidebarOpen,
Folder,
ChevronUp,
Image as ImageIcon,
FileText,
Video,
Music,
Archive,
Presentation,
} from "lucide-react";
interface RealtimePreviewProps {
component: ComponentData;
isSelected?: boolean;
onClick?: (e?: React.MouseEvent) => void;
onDragStart?: (e: React.DragEvent) => void;
onDragEnd?: () => void;
onGroupToggle?: (groupId: string) => void; // 그룹 접기/펼치기
children?: React.ReactNode; // 그룹 내 자식 컴포넌트들
}
// 영역 레이아웃에 따른 아이콘 반환
const getAreaIcon = (layoutDirection?: "horizontal" | "vertical") => {
switch (layoutDirection) {
case "horizontal":
return ;
case "vertical":
return ;
default:
return ;
}
};
// 영역 렌더링
const renderArea = (component: ComponentData, children?: React.ReactNode) => {
if (!isContainerComponent(component) || component.type !== "area") {
return null;
}
const area = component;
const { layoutDirection, label } = area;
const renderPlaceholder = () => (
{getAreaIcon(layoutDirection)}
{label || `${layoutDirection || "기본"} 영역`}
컴포넌트를 드래그해서 추가하세요
);
return (
{children && React.Children.count(children) > 0 ? children : renderPlaceholder()}
);
};
// 동적 웹 타입 위젯 렌더링 컴포넌트
const WidgetRenderer: React.FC<{ component: ComponentData }> = ({ component }) => {
// 위젯 컴포넌트가 아닌 경우 빈 div 반환
if (!isWidgetComponent(component)) {
return 위젯이 아닙니다
;
}
const widget = component;
const { widgetType, label, placeholder, required, readonly, columnName, style } = widget;
// 디버깅: 실제 widgetType 값 확인
// console.log("RealtimePreviewDynamic - widgetType:", widgetType, "columnName:", columnName);
// 사용자가 테두리를 설정했는지 확인
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,
className: `w-full h-full ${borderClass}`,
};
// 파일 컴포넌트는 별도 로직에서 처리하므로 여기서는 제외
if (isFileComponent(widget)) {
// console.log("🎯 RealtimePreview - 파일 컴포넌트 감지 (별도 처리):", {
// componentId: widget.id,
// widgetType: widgetType,
// isFileComponent: true
// });
return 파일 컴포넌트 (별도 렌더링)
;
}
// 동적 웹타입 렌더링 사용
if (widgetType) {
try {
return (
);
} catch (error) {
// console.error(`웹타입 "${widgetType}" 렌더링 실패:`, error);
// 오류 발생 시 폴백으로 기본 input 렌더링
return ;
}
}
// 웹타입이 없는 경우 기본 input 렌더링 (하위 호환성)
return ;
};
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
const getWidgetIcon = (widgetType: WebType | undefined) => {
if (!widgetType) {
return ;
}
// 레지스트리에서 웹타입 정의 조회
const webTypeDefinition = WebTypeRegistry.getWebType(widgetType);
if (webTypeDefinition && webTypeDefinition.icon) {
const IconComponent = webTypeDefinition.icon;
return ;
}
// 기본 아이콘 매핑 (하위 호환성)
switch (widgetType) {
case "text":
case "email":
case "tel":
return ;
case "number":
case "decimal":
return ;
case "date":
case "datetime":
return ;
case "select":
case "dropdown":
return
;
case "textarea":
return ;
case "boolean":
case "checkbox":
return ;
case "radio":
return ;
case "code":
return ;
case "entity":
return ;
case "file":
return ;
default:
return ;
}
};
export const RealtimePreviewDynamic: React.FC = ({
component,
isSelected = false,
onClick,
onDragStart,
onDragEnd,
onGroupToggle,
children,
}) => {
const { user } = useAuth();
const { type, id, position, size, style = {} } = component;
const [fileUpdateTrigger, setFileUpdateTrigger] = useState(0);
// 전역 파일 상태 변경 감지 (해당 컴포넌트만)
useEffect(() => {
const handleGlobalFileStateChange = (event: CustomEvent) => {
// console.log("🎯🎯🎯 RealtimePreview 이벤트 수신:", {
// eventComponentId: event.detail.componentId,
// currentComponentId: component.id,
// isMatch: event.detail.componentId === component.id,
// filesCount: event.detail.files?.length || 0,
// action: event.detail.action,
// delayed: event.detail.delayed || false,
// attempt: event.detail.attempt || 1,
// eventDetail: event.detail
// });
if (event.detail.componentId === component.id) {
// console.log("✅✅✅ RealtimePreview 파일 상태 변경 감지 - 리렌더링 시작:", {
// componentId: component.id,
// filesCount: event.detail.files?.length || 0,
// action: event.detail.action,
// oldTrigger: fileUpdateTrigger,
// delayed: event.detail.delayed || false,
// attempt: event.detail.attempt || 1
// });
setFileUpdateTrigger(prev => {
const newTrigger = prev + 1;
// console.log("🔄🔄🔄 fileUpdateTrigger 업데이트:", {
// old: prev,
// new: newTrigger,
// componentId: component.id,
// attempt: event.detail.attempt || 1
// });
return newTrigger;
});
} else {
// console.log("❌ 컴포넌트 ID 불일치:", {
// eventComponentId: event.detail.componentId,
// currentComponentId: component.id
// });
}
};
// 강제 업데이트 함수 등록
const forceUpdate = (componentId: string, files: any[]) => {
// console.log("🔥🔥🔥 RealtimePreview 강제 업데이트 호출:", {
// targetComponentId: componentId,
// currentComponentId: component.id,
// isMatch: componentId === component.id,
// filesCount: files.length
// });
if (componentId === component.id) {
// console.log("✅✅✅ RealtimePreview 강제 업데이트 적용:", {
// componentId: component.id,
// filesCount: files.length,
// oldTrigger: fileUpdateTrigger
// });
setFileUpdateTrigger(prev => {
const newTrigger = prev + 1;
// console.log("🔄🔄🔄 강제 fileUpdateTrigger 업데이트:", {
// old: prev,
// new: newTrigger,
// componentId: component.id
// });
return newTrigger;
});
}
};
if (typeof window !== 'undefined') {
try {
window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
// 전역 강제 업데이트 함수 등록
if (!(window as any).forceRealtimePreviewUpdate) {
(window as any).forceRealtimePreviewUpdate = forceUpdate;
}
} catch (error) {
// console.warn("RealtimePreview 이벤트 리스너 등록 실패:", error);
}
return () => {
try {
window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
} catch (error) {
// console.warn("RealtimePreview 이벤트 리스너 제거 실패:", error);
}
};
}
}, [component.id, fileUpdateTrigger]);
// 컴포넌트 스타일 계산
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,
};
// 선택된 컴포넌트 스타일
const selectionStyle = isSelected
? {
outline: "2px solid #3b82f6",
outlineOffset: "2px",
}
: {};
const handleClick = (e: React.MouseEvent) => {
// 컴포넌트 영역 내에서만 클릭 이벤트 처리
e.stopPropagation();
onClick?.(e);
};
const handleDragStart = (e: React.DragEvent) => {
e.stopPropagation();
onDragStart?.(e);
};
const handleDragEnd = () => {
onDragEnd?.();
};
return (
{/* 컴포넌트 타입별 렌더링 */}
{/* 영역 타입 */}
{type === "area" && renderArea(component, children)}
{/* 데이터 테이블 타입 */}
{type === "datatable" &&
(() => {
const dataTableComponent = component as any; // DataTableComponent 타입
return (
);
})()}
{/* 그룹 타입 */}
{type === "group" && (
)}
{/* 위젯 타입 - 동적 렌더링 (파일 컴포넌트 제외) */}
{type === "widget" && !isFileComponent(component) && (
)}
{/* 파일 타입 - 레거시 및 신규 타입 지원 */}
{isFileComponent(component) && (() => {
const fileComponent = component as any;
const uploadedFiles = fileComponent.uploadedFiles || [];
// 전역 상태에서 최신 파일 정보 가져오기
const globalFileState = typeof window !== 'undefined' ? (window as any).globalFileState || {} : {};
const globalFiles = globalFileState[component.id] || [];
// 최신 파일 정보 사용 (전역 상태 > 컴포넌트 속성)
const currentFiles = globalFiles.length > 0 ? globalFiles : uploadedFiles;
// console.log("🔍 RealtimePreview 파일 컴포넌트 렌더링:", {
// componentId: component.id,
// uploadedFilesCount: uploadedFiles.length,
// globalFilesCount: globalFiles.length,
// currentFilesCount: currentFiles.length,
// currentFiles: currentFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName || f.name })),
// componentType: component.type,
// fileUpdateTrigger: fileUpdateTrigger,
// timestamp: new Date().toISOString()
// });
return (
{currentFiles.length > 0 ? (
업로드된 파일 ({currentFiles.length})
{currentFiles.map((file: any, index: number) => {
// 파일 확장자에 따른 아이콘 선택
const getFileIcon = (fileName: string) => {
const ext = fileName.split('.').pop()?.toLowerCase() || '';
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) {
return
;
}
if (['pdf', 'doc', 'docx', 'txt', 'rtf', 'hwp', 'hwpx', 'hwpml', 'pages'].includes(ext)) {
return
;
}
if (['ppt', 'pptx', 'hpt', 'keynote'].includes(ext)) {
return
;
}
if (['xls', 'xlsx', 'hcdt', 'numbers'].includes(ext)) {
return
;
}
if (['mp4', 'avi', 'mov', 'wmv', 'webm', 'ogg'].includes(ext)) {
return
;
}
if (['mp3', 'wav', 'flac', 'aac'].includes(ext)) {
return
;
}
if (['zip', 'rar', '7z', 'tar'].includes(ext)) {
return
;
}
return
;
};
return (
{getFileIcon(file.realFileName || file.name || '')}
{file.realFileName || file.name || `파일 ${index + 1}`}
{file.fileSize ? `${Math.round(file.fileSize / 1024)} KB` : ''}
);
})}
) : (
업로드된 파일 (0)
파일 업로드 영역
상세설정에서 파일을 업로드하세요
)}
);
})()}
{/* 선택된 컴포넌트 정보 표시 */}
{isSelected && (
{type === "widget" && (
{getWidgetIcon(isWidgetComponent(component) ? (component.widgetType as WebType) : undefined)}
{isWidgetComponent(component) ? component.widgetType || "widget" : component.type}
)}
{type !== "widget" && type}
)}
);
};
// 기존 RealtimePreview와의 호환성을 위한 export
export { RealtimePreviewDynamic as RealtimePreview };
RealtimePreviewDynamic.displayName = "RealtimePreviewDynamic";