"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; isDesignMode?: boolean; onClick?: (e?: React.MouseEvent) => void; onDragStart?: (e: React.DragEvent) => void; onDragEnd?: () => void; onGroupToggle?: (groupId: string) => void; // 그룹 접기/펼치기 children?: React.ReactNode; // 그룹 내 자식 컴포넌트들 // 플로우 선택 데이터 전달용 onFlowSelectedDataChange?: (selectedData: any[], stepId: number | null) => void; } // 영역 레이아웃에 따른 아이콘 반환 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; isDesignMode?: boolean }> = ({ component, isDesignMode = false }) => { // 위젯 컴포넌트가 아닌 경우 빈 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, isDesignMode = false, onClick, onDragStart, onDragEnd, onGroupToggle, children, onFlowSelectedDataChange, }) => { const { user } = useAuth(); const { type, id, position, size, style = {} } = component; const [fileUpdateTrigger, setFileUpdateTrigger] = useState(0); const [actualHeight, setActualHeight] = useState(null); const contentRef = React.useRef(null); // 플로우 위젯의 실제 높이 측정 useEffect(() => { const isFlowWidget = type === "flow" || (type === "component" && (component as any).componentConfig?.type === "flow-widget"); if (isFlowWidget && contentRef.current) { const measureHeight = () => { if (contentRef.current) { // getBoundingClientRect()로 실제 렌더링된 높이 측정 const rect = contentRef.current.getBoundingClientRect(); const measured = rect.height; // scrollHeight도 함께 확인하여 더 큰 값 사용 const scrollHeight = contentRef.current.scrollHeight; const rawHeight = Math.max(measured, scrollHeight); // 40px 단위로 올림 const finalHeight = Math.ceil(rawHeight / 40) * 40; if (finalHeight > 0 && Math.abs(finalHeight - (actualHeight || 0)) > 10) { setActualHeight(finalHeight); } } }; // 초기 측정 (렌더링 완료 후) const initialTimer = setTimeout(() => { measureHeight(); }, 100); // 추가 측정 (데이터 로딩 완료 대기) const delayedTimer = setTimeout(() => { measureHeight(); }, 500); // 스텝 클릭 등으로 높이가 변경될 때를 위한 추가 측정 const extendedTimer = setTimeout(() => { measureHeight(); }, 1000); // ResizeObserver로 크기 변화 감지 (스텝 클릭 시 데이터 테이블 펼쳐짐) const resizeObserver = new ResizeObserver(() => { // 약간의 지연을 두고 측정 (DOM 업데이트 완료 대기) setTimeout(() => { measureHeight(); }, 100); }); resizeObserver.observe(contentRef.current); return () => { clearTimeout(initialTimer); clearTimeout(delayedTimer); clearTimeout(extendedTimer); resizeObserver.disconnect(); }; } }, [type, id]); // 전역 파일 상태 변경 감지 (해당 컴포넌트만) 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 isFlowWidget = type === "flow" || (type === "component" && (component as any).componentConfig?.type === "flow-widget"); // 높이 결정 로직 let finalHeight = size?.height || 40; if (isFlowWidget && actualHeight) { finalHeight = actualHeight; } // 🔍 디버깅: position.x 값 확인 const positionX = position?.x || 0; console.log("🔍 RealtimePreview componentStyle 설정:", { componentId: id, positionX, sizeWidth: size?.width, styleWidth: style?.width, willUse100Percent: positionX === 0, }); // 너비 결정 로직: style.width (퍼센트) > 조건부 100% > size.width (픽셀) const getWidth = () => { // 1순위: style.width가 있으면 우선 사용 (퍼센트 값) if (style?.width) { return style.width; } // 2순위: left가 0이면 100% if (positionX === 0) { return "100%"; } // 3순위: size.width 픽셀 값 return size?.width || 200; }; const componentStyle = { position: "absolute" as const, ...style, // 먼저 적용하고 left: positionX, top: position?.y || 0, width: getWidth(), // 우선순위에 따른 너비 height: finalHeight, zIndex: position?.z || 1, // right 속성 강제 제거 right: undefined, }; // 선택된 컴포넌트 스타일 const selectionStyle = isSelected ? { outline: "2px solid rgb(59, 130, 246)", 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 === "flow" || (type === "component" && (component as any).componentConfig?.type === "flow-widget")) && (() => { const FlowWidget = require("@/components/screen/widgets/FlowWidget").FlowWidget; // componentConfig에서 flowId 추출 const flowConfig = (component as any).componentConfig || {}; console.log("🔍 RealtimePreview 플로우 위젯 변환:", { compType: component.type, hasComponentConfig: !!(component as any).componentConfig, flowConfig, flowConfigFlowId: flowConfig.flowId, }); const flowComponent = { ...component, type: "flow" as const, flowId: flowConfig.flowId, flowName: flowConfig.flowName, showStepCount: flowConfig.showStepCount !== false, allowDataMove: flowConfig.allowDataMove || false, displayMode: flowConfig.displayMode || "horizontal", }; console.log("🔍 RealtimePreview 최종 flowComponent:", flowComponent); return (
); })()} {/* 그룹 타입 */} {type === "group" && (
{children}
)} {/* 위젯 타입 - 동적 렌더링 (파일 컴포넌트 제외) */} {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
) : (

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