"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 { FlowButtonGroup } from "./widgets/FlowButtonGroup"; import { FlowVisibilityConfig } from "@/types/control-management"; import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils"; import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; // 컴포넌트 렌더러들을 강제로 로드하여 레지스트리에 등록 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; }; onSave?: () => Promise; } export const InteractiveScreenViewerDynamic: React.FC = ({ component, allComponents, formData: externalFormData, onFormDataChange, hideLabel = false, screenInfo, onSave, }) => { const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { userName, user } = useAuth(); const [localFormData, setLocalFormData] = useState>({}); const [dateValues, setDateValues] = useState>({}); // 테이블에서 선택된 행 데이터 (버튼 액션에 전달) const [selectedRowsData, setSelectedRowsData] = useState([]); // 플로우에서 선택된 데이터 (버튼 액션에 전달) const [flowSelectedData, setFlowSelectedData] = useState([]); const [flowSelectedStepId, setFlowSelectedStepId] = useState(null); // 팝업 화면 상태 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; // formData 업데이트 함수 const updateFormData = useCallback( (fieldName: string, value: any) => { if (onFormDataChange) { onFormDataChange(fieldName, value); } else { setLocalFormData((prev) => ({ ...prev, [fieldName]: value, })); } }, [onFormDataChange], ); // 자동값 생성 함수 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], ); // 🆕 autoFill 자동 입력 초기화 React.useEffect(() => { const initAutoInputFields = async () => { for (const comp of allComponents) { // type: "component" 또는 type: "widget" 모두 처리 if (comp.type === 'widget' || comp.type === 'component') { const widget = comp as any; const fieldName = widget.columnName || widget.id; // autoFill 처리 (테이블 조회 기반 자동 입력) if (widget.autoFill?.enabled || (comp as any).autoFill?.enabled) { const autoFillConfig = widget.autoFill || (comp as any).autoFill; const currentValue = formData[fieldName]; if (currentValue === undefined || currentValue === '') { const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig; // 사용자 정보에서 필터 값 가져오기 const userValue = user?.[userField]; if (userValue && sourceTable && filterColumn && displayColumn) { try { const { tableTypeApi } = await import("@/lib/api/screen"); const result = await tableTypeApi.getTableRecord( sourceTable, filterColumn, userValue, displayColumn ); updateFormData(fieldName, result.value); } catch (error) { console.error(`autoFill 조회 실패: ${fieldName}`, error); } } } } } } }; initAutoInputFields(); }, [allComponents, user]); // 팝업 화면 레이아웃 로드 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 | any, value?: any) => { // 일반 필드 변경 if (onFormDataChange) { onFormDataChange(fieldName, value); } else { setLocalFormData((prev) => ({ ...prev, [fieldName]: value })); } }; // 동적 대화형 위젯 렌더링 const renderInteractiveWidget = (comp: ComponentData) => { // 데이터 테이블 컴포넌트 처리 if (isDataTableComponent(comp)) { return ( { // 테이블 자체에서 loadData를 호출하므로 여기서는 빈 함수 console.log("🔄 InteractiveDataTable 새로고침 트리거됨 (Dynamic)"); }} /> ); } // 파일 컴포넌트 처리 if (isFileComponent(comp)) { return renderFileComponent(comp as FileComponent); } // 버튼 컴포넌트 또는 위젯이 아닌 경우 DynamicComponentRenderer 사용 if (comp.type !== "widget") { return ( { console.log("🔍 테이블에서 선택된 행 데이터:", selectedData); setSelectedRowsData(selectedData); }} flowSelectedData={flowSelectedData} flowSelectedStepId={flowSelectedStepId} onFlowSelectedDataChange={(selectedData, stepId) => { console.log("🔍 플로우에서 선택된 데이터:", { selectedData, stepId }); setFlowSelectedData(selectedData); setFlowSelectedStepId(stepId); }} onRefresh={() => { // 테이블 컴포넌트는 자체적으로 loadData 호출 }} onClose={() => { // buttonActions.ts가 이미 처리함 }} /> ); } 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 () => { // EditModal에서 전달된 onSave가 있으면 우선 사용 (수정 모달) if (onSave) { try { await onSave(); } catch (error) { console.error("저장 오류:", error); toast.error("저장 중 오류가 발생했습니다."); } return; } // 일반 저장 액션 (신규 생성) 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";