"use client"; import React, { useState, useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { useAuth } from "@/hooks/useAuth"; import { uploadFilesAndCreateData } from "@/lib/api/file"; import { toast } from "sonner"; import { ComponentData, WidgetComponent, DataTableComponent, FileComponent, ButtonTypeConfig } from "@/types/screen"; import { FileUploadComponent } from "@/lib/registry/components/file-upload/FileUploadComponent"; import { InteractiveDataTable } from "./InteractiveDataTable"; import { DynamicWebTypeRenderer } from "@/lib/registry"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; import { filterDOMProps } from "@/lib/utils/domPropsFilter"; import { isFileComponent, isDataTableComponent, isButtonComponent } from "@/lib/utils/componentTypeUtils"; // 컴포넌트 렌더러들을 강제로 로드하여 레지스트리에 등록 import "@/lib/registry/components/ButtonRenderer"; import "@/lib/registry/components/CardRenderer"; import "@/lib/registry/components/DashboardRenderer"; import "@/lib/registry/components/WidgetRenderer"; import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm"; import { useParams } from "next/navigation"; import { screenApi } from "@/lib/api/screen"; interface InteractiveScreenViewerProps { component: ComponentData; allComponents: ComponentData[]; formData?: Record; onFormDataChange?: (fieldName: string, value: any) => void; hideLabel?: boolean; screenInfo?: { id: number; tableName?: string; }; } export const InteractiveScreenViewerDynamic: React.FC = ({ component, allComponents, formData: externalFormData, onFormDataChange, hideLabel = false, screenInfo, }) => { const { userName, user } = useAuth(); const [localFormData, setLocalFormData] = useState>({}); const [dateValues, setDateValues] = useState>({}); // 팝업 화면 상태 const [popupScreen, setPopupScreen] = useState<{ screenId: number; title: string; size: string; } | null>(null); // 팝업 화면 레이아웃 상태 const [popupLayout, setPopupLayout] = useState([]); const [popupLoading, setPopupLoading] = useState(false); const [popupScreenResolution, setPopupScreenResolution] = useState<{ width: number; height: number } | null>(null); const [popupScreenInfo, setPopupScreenInfo] = useState<{ id: number; tableName?: string } | null>(null); // 팝업 전용 formData 상태 const [popupFormData, setPopupFormData] = useState>({}); // formData 결정 (외부에서 전달받은 것이 있으면 우선 사용) const formData = externalFormData || localFormData; // 자동값 생성 함수 const generateAutoValue = useCallback( (autoValueType: string): string => { const now = new Date(); switch (autoValueType) { case "current_datetime": return now.toISOString().slice(0, 19).replace("T", " "); case "current_date": return now.toISOString().slice(0, 10); case "current_time": return now.toTimeString().slice(0, 8); case "current_user": return userName || "사용자"; case "uuid": return crypto.randomUUID(); case "sequence": return `SEQ_${Date.now()}`; default: return ""; } }, [userName], ); // 팝업 화면 레이아웃 로드 React.useEffect(() => { if (popupScreen?.screenId) { loadPopupScreen(popupScreen.screenId); } }, [popupScreen?.screenId]); const loadPopupScreen = async (screenId: number) => { try { setPopupLoading(true); const response = await screenApi.getScreenLayout(screenId); if (response.success && response.data) { const screenData = response.data; setPopupLayout(screenData.components || []); setPopupScreenResolution({ width: screenData.screenResolution?.width || 1200, height: screenData.screenResolution?.height || 800, }); setPopupScreenInfo({ id: screenData.id, tableName: screenData.tableName, }); } else { toast.error("팝업 화면을 불러올 수 없습니다."); setPopupScreen(null); } } catch (error) { console.error("팝업 화면 로드 오류:", error); toast.error("팝업 화면 로드 중 오류가 발생했습니다."); setPopupScreen(null); } finally { setPopupLoading(false); } }; // 폼 데이터 변경 핸들러 const handleFormDataChange = (fieldName: string, value: any) => { console.log(`🎯 InteractiveScreenViewerDynamic handleFormDataChange 호출: ${fieldName} = "${value}"`); console.log(`📋 onFormDataChange 존재 여부:`, !!onFormDataChange); if (onFormDataChange) { console.log(`📤 InteractiveScreenViewerDynamic -> onFormDataChange 호출: ${fieldName} = "${value}"`); onFormDataChange(fieldName, value); } else { console.log(`💾 InteractiveScreenViewerDynamic 로컬 상태 업데이트: ${fieldName} = "${value}"`); setLocalFormData((prev) => ({ ...prev, [fieldName]: value })); } }; // 동적 대화형 위젯 렌더링 const renderInteractiveWidget = (comp: ComponentData) => { // 데이터 테이블 컴포넌트 처리 if (isDataTableComponent(comp)) { return ( ); } // 버튼 컴포넌트 처리 if (isButtonComponent(comp)) { return renderButton(comp); } // 파일 컴포넌트 처리 if (isFileComponent(comp)) { return renderFileComponent(comp as FileComponent); } // 위젯 컴포넌트가 아닌 경우 DynamicComponentRenderer 사용 if (comp.type !== "widget") { console.log("🎯 InteractiveScreenViewer - DynamicComponentRenderer 사용:", { componentId: comp.id, componentType: comp.type, componentConfig: comp.componentConfig, style: comp.style, size: comp.size, position: comp.position, }); return ( { // 화면 새로고침 로직 (필요시 구현) // console.log("화면 새로고침 요청"); }} onClose={() => { // 화면 닫기 로직 (필요시 구현) // console.log("화면 닫기 요청"); }} /> ); } const widget = comp as WidgetComponent; const { widgetType, label, placeholder, required, readonly, columnName } = widget; const fieldName = columnName || comp.id; const currentValue = formData[fieldName] || ""; // 스타일 적용 const applyStyles = (element: React.ReactElement) => { if (!comp.style) return element; return React.cloneElement(element, { style: { ...element.props.style, ...comp.style, width: "100%", height: "100%", minHeight: "100%", maxHeight: "100%", boxSizing: "border-box", }, }); }; // 동적 웹타입 렌더링 사용 if (widgetType) { try { const dynamicElement = ( handleFormDataChange(fieldName, value), onFormDataChange: handleFormDataChange, isInteractive: true, readonly: readonly, required: required, placeholder: placeholder, className: "w-full h-full", }} config={widget.webTypeConfig} onEvent={(event: string, data: any) => { // 이벤트 처리 console.log(`Widget event: ${event}`, data); }} /> ); return applyStyles(dynamicElement); } catch (error) { console.error(`웹타입 "${widgetType}" 대화형 렌더링 실패:`, error); // 오류 발생 시 폴백으로 기본 input 렌더링 const fallbackElement = ( handleFormDataChange(fieldName, e.target.value)} placeholder={`${widgetType} (렌더링 오류)`} disabled={readonly} required={required} className="h-full w-full" /> ); return applyStyles(fallbackElement); } } // 웹타입이 없는 경우 기본 input 렌더링 (하위 호환성) const defaultElement = ( handleFormDataChange(fieldName, e.target.value)} placeholder={placeholder || "입력하세요"} disabled={readonly} required={required} className="h-full w-full" /> ); return applyStyles(defaultElement); }; // 버튼 렌더링 const renderButton = (comp: ComponentData) => { const config = (comp as any).webTypeConfig as ButtonTypeConfig | undefined; const { label } = comp; // 버튼 액션 핸들러들 const handleSaveAction = async () => { if (!screenInfo?.tableName) { toast.error("테이블명이 설정되지 않았습니다."); return; } try { const saveData: DynamicFormData = { tableName: screenInfo.tableName, data: formData, }; console.log("💾 저장 액션 실행:", saveData); const response = await dynamicFormApi.saveData(saveData); if (response.success) { toast.success("데이터가 성공적으로 저장되었습니다."); } else { toast.error(response.message || "저장에 실패했습니다."); } } catch (error) { console.error("저장 오류:", error); toast.error("저장 중 오류가 발생했습니다."); } }; const handleDeleteAction = async () => { if (!config?.confirmationEnabled || window.confirm(config.confirmationMessage || "정말 삭제하시겠습니까?")) { console.log("🗑️ 삭제 액션 실행"); toast.success("삭제가 완료되었습니다."); } }; const handlePopupAction = () => { if (config?.popupScreenId) { setPopupScreen({ screenId: config.popupScreenId, title: config.popupTitle || "팝업 화면", size: config.popupSize || "medium", }); } }; const handleNavigateAction = () => { const navigateType = config?.navigateType || "url"; if (navigateType === "screen" && config?.navigateScreenId) { const screenPath = `/screens/${config.navigateScreenId}`; if (config.navigateTarget === "_blank") { window.open(screenPath, "_blank"); } else { window.location.href = screenPath; } } else if (navigateType === "url" && config?.navigateUrl) { if (config.navigateTarget === "_blank") { window.open(config.navigateUrl, "_blank"); } else { window.location.href = config.navigateUrl; } } }; const handleCustomAction = async () => { if (config?.customAction) { try { const result = eval(config.customAction); if (result instanceof Promise) { await result; } console.log("⚡ 커스텀 액션 실행 완료"); } catch (error) { throw new Error(`커스텀 액션 실행 실패: ${error.message}`); } } }; const handleClick = async () => { try { const actionType = config?.actionType || "save"; switch (actionType) { case "save": await handleSaveAction(); break; case "delete": await handleDeleteAction(); break; case "popup": handlePopupAction(); break; case "navigate": handleNavigateAction(); break; case "custom": await handleCustomAction(); break; default: console.log("🔘 기본 버튼 클릭"); } } catch (error) { console.error("버튼 액션 오류:", error); toast.error(error.message || "액션 실행 중 오류가 발생했습니다."); } }; return ( ); }; // 파일 컴포넌트 렌더링 const renderFileComponent = (comp: FileComponent) => { const { label, readonly } = comp; const fieldName = comp.columnName || comp.id; // 화면 ID 추출 (URL에서) const screenId = screenInfo?.screenId || (typeof window !== 'undefined' && window.location.pathname.includes('/screens/') ? parseInt(window.location.pathname.split('/screens/')[1]) : null); return (
{/* 실제 FileUploadComponent 사용 */} { console.log("📝 실제 화면 파일 업로드 완료:", data); if (onFormDataChange) { Object.entries(data).forEach(([key, value]) => { onFormDataChange(key, value); }); } }} onUpdate={(updates) => { console.log("🔄🔄🔄 실제 화면 파일 컴포넌트 업데이트:", { componentId: comp.id, hasUploadedFiles: !!updates.uploadedFiles, filesCount: updates.uploadedFiles?.length || 0, hasLastFileUpdate: !!updates.lastFileUpdate, updates }); // 파일 업로드/삭제 완료 시 formData 업데이터 if (updates.uploadedFiles && onFormDataChange) { onFormDataChange(fieldName, updates.uploadedFiles); } // 🎯 화면설계 모드와 동기화를 위한 전역 이벤트 발생 (업로드/삭제 모두) if (updates.uploadedFiles !== undefined && typeof window !== 'undefined') { // 업로드인지 삭제인지 판단 (lastFileUpdate가 있으면 변경사항 있음) const action = updates.lastFileUpdate ? 'update' : 'sync'; const eventDetail = { componentId: comp.id, files: updates.uploadedFiles, fileCount: updates.uploadedFiles.length, action: action, timestamp: updates.lastFileUpdate || Date.now(), source: 'realScreen' // 실제 화면에서 온 이벤트임을 표시 }; console.log("🚀🚀🚀 실제 화면 파일 변경 이벤트 발생:", eventDetail); const event = new CustomEvent('globalFileStateChanged', { detail: eventDetail }); window.dispatchEvent(event); console.log("✅✅✅ 실제 화면 → 화면설계 모드 동기화 이벤트 발생 완료"); // 추가 지연 이벤트들 (화면설계 모드가 열려있을 때를 대비) setTimeout(() => { console.log("🔄 실제 화면 추가 이벤트 발생 (지연 100ms)"); window.dispatchEvent(new CustomEvent('globalFileStateChanged', { detail: { ...eventDetail, delayed: true } })); }, 100); setTimeout(() => { console.log("🔄 실제 화면 추가 이벤트 발생 (지연 500ms)"); window.dispatchEvent(new CustomEvent('globalFileStateChanged', { detail: { ...eventDetail, delayed: true, attempt: 2 } })); }, 500); } }} />
); }; // 메인 렌더링 const { type, position, size, style = {} } = component; 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, }; return ( <>
{/* 라벨 숨김 - 모달에서는 라벨을 표시하지 않음 */} {/* 위젯 렌더링 */} {renderInteractiveWidget(component)}
{/* 팝업 화면 렌더링 */} {popupScreen && ( setPopupScreen(null)}> {popupScreen.title} {popupLoading ? (
로딩 중...
) : (
{popupLayout.map((popupComponent) => ( { setPopupFormData((prev) => ({ ...prev, [fieldName]: value })); }} screenInfo={popupScreenInfo} /> ))}
)}
)} ); }; // 기존 InteractiveScreenViewer와의 호환성을 위한 export export { InteractiveScreenViewerDynamic as InteractiveScreenViewer }; InteractiveScreenViewerDynamic.displayName = "InteractiveScreenViewerDynamic";