"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 }); try { return ( ); } catch (error) { console.error(`파일 웹타입 렌더링 실패:`, error); 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) => { if (event.detail.componentId === component.id) { console.log("🔄 RealtimePreview 파일 상태 변경 감지:", { componentId: component.id, filesCount: event.detail.files?.length || 0, action: event.detail.action }); setFileUpdateTrigger(prev => prev + 1); } }; if (typeof window !== 'undefined') { window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener); return () => { window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener); }; } }, [component.id]); // 컴포넌트 스타일 계산 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" && (
{children}
)} {/* 위젯 타입 - 동적 렌더링 */} {type === "widget" && (
)} {/* 파일 타입 - 레거시 및 신규 타입 지원 */} {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
) : (

업로드된 파일 (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";