"use client"; import React from "react"; import { ComponentData, WebType, WidgetComponent } from "@/types/screen"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; import { Database, Type, Hash, List, AlignLeft, CheckSquare, Radio, Calendar, Code, Building, File, } from "lucide-react"; // 컴포넌트 렌더러들 자동 등록 import "@/lib/registry/components"; interface RealtimePreviewProps { component: ComponentData; isSelected?: boolean; isDesignMode?: boolean; // 편집 모드 여부 onClick?: (e?: React.MouseEvent) => void; onDoubleClick?: (e?: React.MouseEvent) => void; // 더블클릭 핸들러 추가 onDragStart?: (e: React.DragEvent) => void; onDragEnd?: () => void; onGroupToggle?: (groupId: string) => void; // 그룹 접기/펼치기 children?: React.ReactNode; // 그룹 내 자식 컴포넌트들 selectedScreen?: any; onZoneComponentDrop?: (e: React.DragEvent, zoneId: string, layoutId: string) => void; // 존별 드롭 핸들러 onZoneClick?: (zoneId: string) => void; // 존 클릭 핸들러 onConfigChange?: (config: any) => void; // 설정 변경 핸들러 // 버튼 액션을 위한 props screenId?: number; tableName?: string; userId?: string; // 🆕 현재 사용자 ID userName?: string; // 🆕 현재 사용자 이름 companyCode?: string; // 🆕 현재 사용자의 회사 코드 selectedRowsData?: any[]; onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void; flowSelectedData?: any[]; flowSelectedStepId?: number | null; onFlowSelectedDataChange?: (selectedData: any[], stepId: number | null) => void; refreshKey?: number; onRefresh?: () => void; flowRefreshKey?: number; onFlowRefresh?: () => void; // 폼 데이터 관련 props formData?: Record; onFormDataChange?: (fieldName: string, value: any) => void; } // 동적 위젯 타입 아이콘 (레지스트리에서 조회) const getWidgetIcon = (widgetType: WebType | undefined): React.ReactNode => { if (!widgetType) return ; const iconMap: Record = { text: Aa, number: , decimal: , date: , datetime: , select: , dropdown: , textarea: , boolean: , checkbox: , radio: , code: , entity: , file: , email: @, tel: , button: BTN, }; return iconMap[widgetType] || ; }; export const RealtimePreviewDynamic: React.FC = ({ component, isSelected = false, isDesignMode = true, // 기본값은 편집 모드 onClick, onDoubleClick, onDragStart, onDragEnd, onGroupToggle, children, selectedScreen, onZoneComponentDrop, onZoneClick, onConfigChange, screenId, tableName, userId, // 🆕 사용자 ID userName, // 🆕 사용자 이름 companyCode, // 🆕 회사 코드 selectedRowsData, onSelectedRowsChange, flowSelectedData, flowSelectedStepId, onFlowSelectedDataChange, refreshKey, onRefresh, flowRefreshKey, onFlowRefresh, formData, onFormDataChange, }) => { const [actualHeight, setActualHeight] = React.useState(null); const contentRef = React.useRef(null); const lastUpdatedHeight = React.useRef(null); // 플로우 위젯의 실제 높이 측정 React.useEffect(() => { const isFlowWidget = component.type === "component" && (component as any).componentType === "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); // 컴포넌트의 실제 size.height도 업데이트 (중복 업데이트 방지) if (onConfigChange && finalHeight !== lastUpdatedHeight.current && finalHeight !== component.size?.height) { lastUpdatedHeight.current = finalHeight; // size는 별도 속성이므로 직접 업데이트 const event = new CustomEvent("updateComponentSize", { detail: { componentId: component.id, height: finalHeight, }, }); window.dispatchEvent(event); } } } }; // 초기 측정 (렌더링 완료 후) 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(); }; } }, [component.type, component.id, actualHeight, component.size?.height, onConfigChange]); const { id, type, position, size, style: componentStyle } = component; // 선택 상태에 따른 스타일 (z-index 낮춤 - 패널과 모달보다 아래) const selectionStyle = isSelected ? { outline: "2px solid rgb(59, 130, 246)", outlineOffset: "0px", // 스크롤 방지를 위해 0으로 설정 zIndex: 20, } : {}; // 컴포넌트 기본 스타일 - 레이아웃은 항상 맨 아래 // 너비 우선순위: style.width > 조건부 100% > size.width (픽셀값) const getWidth = () => { // 1순위: style.width가 있으면 우선 사용 (퍼센트 값) if (componentStyle?.width) { console.log("✅ [getWidth] style.width 사용:", { componentId: id, label: component.label, styleWidth: componentStyle.width, gridColumns: (component as any).gridColumns, componentStyle: componentStyle, baseStyle: { left: `${position.x}px`, top: `${position.y}px`, width: componentStyle.width, height: getHeight(), }, }); return componentStyle.width; } // 2순위: x=0인 컴포넌트는 전체 너비 사용 (버튼 제외) const isButtonComponent = (component.type === "widget" && (component as WidgetComponent).widgetType === "button") || (component.type === "component" && (component as any).componentType?.includes("button")); if (position.x === 0 && !isButtonComponent) { console.log("⚠️ [getWidth] 100% 사용 (x=0):", { componentId: id, label: component.label, }); return "100%"; } // 3순위: size.width (픽셀) if (component.componentConfig?.type === "table-list") { const width = `${Math.max(size?.width || 120, 120)}px`; console.log("📏 [getWidth] 픽셀 사용 (table-list):", { componentId: id, label: component.label, width, }); return width; } const width = `${size?.width || 100}px`; console.log("📏 [getWidth] 픽셀 사용 (기본):", { componentId: id, label: component.label, width, sizeWidth: size?.width, }); return width; }; const getHeight = () => { // 플로우 위젯의 경우 측정된 높이 사용 const isFlowWidget = component.type === "component" && (component as any).componentType === "flow-widget"; if (isFlowWidget && actualHeight) { return `${actualHeight}px`; } // 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`; }; const baseStyle = { left: `${position.x}px`, top: `${position.y}px`, width: getWidth(), // getWidth()가 모든 우선순위를 처리 height: getHeight(), zIndex: component.type === "layout" ? 1 : position.z || 2, ...componentStyle, right: undefined, }; // 🔍 DOM 렌더링 후 실제 크기 측정 const innerDivRef = React.useRef(null); const outerDivRef = React.useRef(null); React.useEffect(() => { if (outerDivRef.current && innerDivRef.current) { const outerRect = outerDivRef.current.getBoundingClientRect(); const innerRect = innerDivRef.current.getBoundingClientRect(); const computedOuter = window.getComputedStyle(outerDivRef.current); const computedInner = window.getComputedStyle(innerDivRef.current); console.log("📐 [DOM 실제 크기 상세]:", { componentId: id, label: component.label, gridColumns: (component as any).gridColumns, "1. baseStyle.width": baseStyle.width, "2. 외부 div (파란 테두리)": { width: `${outerRect.width}px`, height: `${outerRect.height}px`, computedWidth: computedOuter.width, computedHeight: computedOuter.height, }, "3. 내부 div (컨텐츠 래퍼)": { width: `${innerRect.width}px`, height: `${innerRect.height}px`, computedWidth: computedInner.width, computedHeight: computedInner.height, className: innerDivRef.current.className, inlineStyle: innerDivRef.current.getAttribute("style"), }, "4. 너비 비교": { "외부 / 내부": `${outerRect.width}px / ${innerRect.width}px`, "비율": `${((innerRect.width / outerRect.width) * 100).toFixed(2)}%`, }, }); } }, [id, component.label, (component as any).gridColumns, baseStyle.width]); const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); onClick?.(e); }; const handleDoubleClick = (e: React.MouseEvent) => { e.stopPropagation(); onDoubleClick?.(e); }; const handleDragStart = (e: React.DragEvent) => { e.stopPropagation(); onDragStart?.(e); }; const handleDragEnd = () => { onDragEnd?.(); }; return (
{/* 동적 컴포넌트 렌더링 */}
{ // 멀티 ref 처리 innerDivRef.current = node; if (component.type === "component" && (component as any).componentType === "flow-widget") { (contentRef as any).current = node; } }} className={`${component.type === "component" && (component as any).componentType === "flow-widget" ? "h-auto" : "h-full"} overflow-visible`} style={{ width: "100%", maxWidth: "100%" }} >
{/* 선택된 컴포넌트 정보 표시 */} {isSelected && (
{type === "widget" && (
{getWidgetIcon((component as WidgetComponent).widgetType)} {(component as WidgetComponent).widgetType || "widget"}
)} {type !== "widget" && (
{component.componentConfig?.type || type}
)}
)}
); }; // 기존 RealtimePreview와의 호환성을 위한 export export { RealtimePreviewDynamic as RealtimePreview }; export default RealtimePreviewDynamic;