"use client"; import React, { useState, useCallback, useEffect } 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 { useSplitPanelContext } from "@/contexts/SplitPanelContext"; // 조건부 표시 평가 함수 function evaluateConditional( conditional: ComponentData["conditional"], formData: Record, allComponents: ComponentData[], ): { visible: boolean; disabled: boolean } { if (!conditional || !conditional.enabled) { return { visible: true, disabled: false }; } const { field, operator, value, action } = conditional; // 참조 필드의 현재 값 가져오기 // 필드 ID로 컴포넌트를 찾아 columnName 또는 id로 formData에서 값 조회 const refComponent = allComponents.find((c) => c.id === field); const fieldName = (refComponent as any)?.columnName || field; const fieldValue = formData[fieldName]; // 조건 평가 let conditionMet = false; switch (operator) { case "=": conditionMet = fieldValue === value || String(fieldValue) === String(value); break; case "!=": conditionMet = fieldValue !== value && String(fieldValue) !== String(value); break; case ">": conditionMet = Number(fieldValue) > Number(value); break; case "<": conditionMet = Number(fieldValue) < Number(value); break; case "in": if (Array.isArray(value)) { conditionMet = value.includes(fieldValue) || value.map(String).includes(String(fieldValue)); } break; case "notIn": if (Array.isArray(value)) { conditionMet = !value.includes(fieldValue) && !value.map(String).includes(String(fieldValue)); } else { conditionMet = true; } break; case "isEmpty": conditionMet = fieldValue === null || fieldValue === undefined || fieldValue === "" || (Array.isArray(fieldValue) && fieldValue.length === 0); break; case "isNotEmpty": conditionMet = fieldValue !== null && fieldValue !== undefined && fieldValue !== "" && !(Array.isArray(fieldValue) && fieldValue.length === 0); break; default: conditionMet = true; } // 액션에 따른 결과 반환 switch (action) { case "show": return { visible: conditionMet, disabled: false }; case "hide": return { visible: !conditionMet, disabled: false }; case "enable": return { visible: true, disabled: !conditionMet }; case "disable": return { visible: true, disabled: conditionMet }; default: return { visible: true, disabled: false }; } } // 컴포넌트 렌더러들을 강제로 로드하여 레지스트리에 등록 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; }; menuObjid?: number; // 메뉴 OBJID (코드 스코프용) onSave?: () => Promise; onRefresh?: () => void; onFlowRefresh?: () => void; // 외부에서 전달받는 사용자 정보 (ScreenModal 등에서 사용) userId?: string; userName?: string; companyCode?: string; // 그룹 데이터 (EditModal에서 전달) groupedData?: Record[]; // 비활성화할 필드 목록 (EditModal에서 전달) disabledFields?: string[]; // EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록) isInModal?: boolean; // 원본 데이터 (수정 모드에서 UPDATE 판단용) originalData?: Record | null; // 탭 관련 정보 (탭 내부의 컴포넌트에서 사용) parentTabId?: string; // 부모 탭 ID parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID } export const InteractiveScreenViewerDynamic: React.FC = ({ component, allComponents, formData: externalFormData, onFormDataChange, hideLabel = false, screenInfo, menuObjid, onSave, onRefresh, onFlowRefresh, userId: externalUserId, userName: externalUserName, companyCode: externalCompanyCode, groupedData, disabledFields = [], isInModal = false, originalData, parentTabId, parentTabsComponentId, }) => { const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { userName: authUserName, user: authUser } = useAuth(); const splitPanelContext = useSplitPanelContext(); // 분할 패널 컨텍스트 // 외부에서 전달받은 사용자 정보가 있으면 우선 사용 (ScreenModal 등에서) const userName = externalUserName || authUserName; const user = externalUserId && externalUserId !== authUser?.userId ? { userId: externalUserId, userName: externalUserName || authUserName || "", companyCode: externalCompanyCode || authUser?.companyCode || "", isAdmin: authUser?.isAdmin || false, } : authUser; 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>({}); // 🆕 분할 패널에서 매핑된 부모 데이터 가져오기 // disableAutoDataTransfer가 true이면 자동 전달 비활성화 (버튼 클릭으로만 전달) const splitPanelMappedData = React.useMemo(() => { if (splitPanelContext && !splitPanelContext.disableAutoDataTransfer) { return splitPanelContext.getMappedParentData(); } return {}; }, [splitPanelContext, splitPanelContext?.selectedLeftData, splitPanelContext?.disableAutoDataTransfer]); // formData 결정 (외부에서 전달받은 것이 있으면 우선 사용, 분할 패널 데이터도 병합) const formData = React.useMemo(() => { const baseData = externalFormData || localFormData; // 분할 패널 매핑 데이터가 있으면 병합 (기존 값이 없는 경우에만) // disableAutoDataTransfer가 true이면 자동 병합 안함 if (Object.keys(splitPanelMappedData).length > 0) { const merged = { ...baseData }; for (const [key, value] of Object.entries(splitPanelMappedData)) { // 기존 값이 없거나 빈 값인 경우에만 매핑 데이터 적용 if (merged[key] === undefined || merged[key] === null || merged[key] === "") { merged[key] = value; } } return merged; } return baseData; }, [externalFormData, localFormData, splitPanelMappedData]); // 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], ); // 🆕 Enter 키로 다음 필드 이동 useEffect(() => { const handleEnterKey = (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { const target = e.target as HTMLElement; // 한글 조합 중이면 무시 (한글 입력 문제 방지) if ((e as any).isComposing || e.keyCode === 229) { return; } // textarea는 제외 (여러 줄 입력) if (target.tagName === "TEXTAREA") { return; } // input, select 등의 폼 요소에서만 작동 if (target.tagName === "INPUT" || target.tagName === "SELECT" || target.getAttribute("role") === "combobox") { e.preventDefault(); // 모든 포커스 가능한 요소 찾기 const focusableElements = document.querySelectorAll( 'input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [role="combobox"]:not([disabled])', ); // 화면에 보이는 순서(Y 좌표 → X 좌표)대로 정렬 const focusableArray = Array.from(focusableElements).sort((a, b) => { const rectA = a.getBoundingClientRect(); const rectB = b.getBoundingClientRect(); // Y 좌표 차이가 10px 이상이면 Y 좌표로 정렬 (위에서 아래로) if (Math.abs(rectA.top - rectB.top) > 10) { return rectA.top - rectB.top; } // 같은 줄이면 X 좌표로 정렬 (왼쪽에서 오른쪽으로) return rectA.left - rectB.left; }); const currentIndex = focusableArray.indexOf(target); if (currentIndex !== -1 && currentIndex < focusableArray.length - 1) { // 다음 요소로 포커스 이동 const nextElement = focusableArray[currentIndex + 1]; nextElement.focus(); // select() 제거: 한글 입력 시 이전 필드의 마지막 글자가 복사되는 버그 방지 } } } }; document.addEventListener("keydown", handleEnterKey); return () => { document.removeEventListener("keydown", handleEnterKey); }; }, []); // 🆕 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) => { // 조건부 표시 평가 const conditionalResult = evaluateConditional(comp.conditional, formData, allComponents); // 조건에 따라 숨김 처리 if (!conditionalResult.visible) { return null; } // 데이터 테이블 컴포넌트 처리 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); }} groupedData={groupedData} disabledFields={disabledFields} flowSelectedData={flowSelectedData} flowSelectedStepId={flowSelectedStepId} onFlowSelectedDataChange={(selectedData, stepId) => { console.log("플로우에서 선택된 데이터:", { selectedData, stepId }); setFlowSelectedData(selectedData); setFlowSelectedStepId(stepId); }} onRefresh={ onRefresh || (() => { console.log("InteractiveScreenViewerDynamic onRefresh 호출"); }) } onFlowRefresh={onFlowRefresh} onClose={() => { // buttonActions.ts가 이미 처리함 }} // 탭 관련 정보 전달 parentTabId={parentTabId} parentTabsComponentId={parentTabsComponentId} /> ); } 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; // ✅ 격자 시스템 잔재 제거: style.width, style.height는 무시 // size.width, size.height가 부모 컨테이너에서 적용되므로 const { width, height, ...styleWithoutSize } = comp.style; return React.cloneElement(element, { style: { ...element.props.style, ...styleWithoutSize, // width/height 제외한 스타일만 적용 width: "100%", height: "100%", minHeight: "100%", maxHeight: "100%", boxSizing: "border-box", }, }); }; // 조건부 비활성화 적용 const isConditionallyDisabled = conditionalResult.disabled; // 동적 웹타입 렌더링 사용 if (widgetType) { try { const dynamicElement = ( handleFormDataChange(fieldName, value), onFormDataChange: handleFormDataChange, formData: formData, // 🆕 전체 formData 전달 isInteractive: true, readonly: readonly || isConditionallyDisabled, // 조건부 비활성화 적용 disabled: isConditionallyDisabled, // 조건부 비활성화 전달 required: required, placeholder: placeholder, className: "w-full h-full", isInModal: isInModal, // 🆕 EditModal 내부 여부 전달 onSave: onSave, // 🆕 EditModal의 handleSave 콜백 전달 groupedData: groupedData, // 🆕 그룹 데이터 전달 (RepeatScreenModal용) }} 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 || isConditionallyDisabled} required={required} className="h-full w-full" /> ); return applyStyles(fallbackElement); } } // 웹타입이 없는 경우 기본 input 렌더링 (하위 호환성) const defaultElement = ( handleFormDataChange(fieldName, e.target.value)} placeholder={placeholder || "입력하세요"} disabled={readonly || isConditionallyDisabled} 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}`); } } }; // 🆕 즉시 저장(quickInsert) 액션 핸들러 const handleQuickInsertAction = async () => { // componentConfig에서 quickInsertConfig 가져오기 const quickInsertConfig = (comp as any).componentConfig?.action?.quickInsertConfig; if (!quickInsertConfig?.targetTable) { toast.error("대상 테이블이 설정되지 않았습니다."); return; } // 1. 대상 테이블의 컬럼 목록 조회 (자동 매핑용) let targetTableColumns: string[] = []; try { const { default: apiClient } = await import("@/lib/api/client"); const columnsResponse = await apiClient.get( `/table-management/tables/${quickInsertConfig.targetTable}/columns`, ); if (columnsResponse.data?.success && columnsResponse.data?.data) { const columnsData = columnsResponse.data.data.columns || columnsResponse.data.data; targetTableColumns = columnsData.map((col: any) => col.columnName || col.column_name || col.name); console.log("📍 대상 테이블 컬럼 목록:", targetTableColumns); } } catch (error) { console.error("대상 테이블 컬럼 조회 실패:", error); } // 2. 컬럼 매핑에서 값 수집 const insertData: Record = {}; const columnMappings = quickInsertConfig.columnMappings || []; for (const mapping of columnMappings) { let value: any; switch (mapping.sourceType) { case "component": // 같은 화면의 컴포넌트에서 값 가져오기 // 방법1: sourceColumnName 사용 if (mapping.sourceColumnName && formData[mapping.sourceColumnName] !== undefined) { value = formData[mapping.sourceColumnName]; console.log(`📍 컴포넌트 값 (sourceColumnName): ${mapping.sourceColumnName} = ${value}`); } // 방법2: sourceComponentId로 컴포넌트 찾아서 columnName 사용 else if (mapping.sourceComponentId) { const sourceComp = allComponents.find((c: any) => c.id === mapping.sourceComponentId); if (sourceComp) { const fieldName = (sourceComp as any).columnName || sourceComp.id; value = formData[fieldName]; console.log(`📍 컴포넌트 값 (컴포넌트 조회): ${fieldName} = ${value}`); } } break; case "leftPanel": // 분할 패널 좌측 선택 데이터에서 값 가져오기 if (mapping.sourceColumn && splitPanelContext?.selectedLeftData) { value = splitPanelContext.selectedLeftData[mapping.sourceColumn]; } break; case "fixed": value = mapping.fixedValue; break; case "currentUser": if (mapping.userField) { switch (mapping.userField) { case "userId": value = user?.userId; break; case "userName": value = userName; break; case "companyCode": value = user?.companyCode; break; case "deptCode": value = authUser?.deptCode; break; } } break; } if (value !== undefined && value !== null && value !== "") { insertData[mapping.targetColumn] = value; } } // 3. 좌측 패널 선택 데이터에서 자동 매핑 (컬럼명이 같고 대상 테이블에 있는 경우) if (splitPanelContext?.selectedLeftData && targetTableColumns.length > 0) { const leftData = splitPanelContext.selectedLeftData; console.log("📍 좌측 패널 자동 매핑 시작:", leftData); for (const [key, val] of Object.entries(leftData)) { // 이미 매핑된 컬럼은 스킵 if (insertData[key] !== undefined) { continue; } // 대상 테이블에 해당 컬럼이 없으면 스킵 if (!targetTableColumns.includes(key)) { continue; } // 시스템 컬럼 제외 const systemColumns = ["id", "created_date", "updated_date", "writer", "writer_name"]; if (systemColumns.includes(key)) { continue; } // _label, _name 으로 끝나는 표시용 컬럼 제외 if (key.endsWith("_label") || key.endsWith("_name")) { continue; } // 값이 있으면 자동 추가 if (val !== undefined && val !== null && val !== "") { insertData[key] = val; console.log(`📍 자동 매핑 추가: ${key} = ${val}`); } } } console.log("🚀 quickInsert 최종 데이터:", insertData); // 4. 필수값 검증 if (Object.keys(insertData).length === 0) { toast.error("저장할 데이터가 없습니다. 값을 선택해주세요."); return; } // 5. 중복 체크 (설정된 경우) if (quickInsertConfig.duplicateCheck?.enabled && quickInsertConfig.duplicateCheck?.columns?.length > 0) { try { const { default: apiClient } = await import("@/lib/api/client"); // 중복 체크를 위한 검색 조건 구성 const searchConditions: Record = {}; for (const col of quickInsertConfig.duplicateCheck.columns) { if (insertData[col] !== undefined) { searchConditions[col] = { value: insertData[col], operator: "equals" }; } } console.log("📍 중복 체크 조건:", searchConditions); // 기존 데이터 조회 const checkResponse = await apiClient.post(`/table-management/tables/${quickInsertConfig.targetTable}/data`, { page: 1, pageSize: 1, search: searchConditions, }); console.log("📍 중복 체크 응답:", checkResponse.data); // data 배열이 있고 길이가 0보다 크면 중복 const existingData = checkResponse.data?.data?.data || checkResponse.data?.data || []; if (Array.isArray(existingData) && existingData.length > 0) { toast.error(quickInsertConfig.duplicateCheck.errorMessage || "이미 존재하는 데이터입니다."); return; } } catch (error) { console.error("중복 체크 오류:", error); // 중복 체크 실패 시 계속 진행 } } // 6. API 호출 try { const { default: apiClient } = await import("@/lib/api/client"); const response = await apiClient.post( `/table-management/tables/${quickInsertConfig.targetTable}/add`, insertData, ); if (response.data?.success) { // 7. 성공 후 동작 if (quickInsertConfig.afterInsert?.showSuccessMessage !== false) { toast.success(quickInsertConfig.afterInsert?.successMessage || "저장되었습니다."); } // 데이터 새로고침 (테이블리스트, 카드 디스플레이) if (quickInsertConfig.afterInsert?.refreshData !== false) { console.log("📍 데이터 새로고침 이벤트 발송"); if (typeof window !== "undefined") { window.dispatchEvent(new CustomEvent("refreshTable")); window.dispatchEvent(new CustomEvent("refreshCardDisplay")); } } // 지정된 컴포넌트 초기화 if (quickInsertConfig.afterInsert?.clearComponents?.length > 0) { for (const componentId of quickInsertConfig.afterInsert.clearComponents) { const targetComp = allComponents.find((c: any) => c.id === componentId); if (targetComp) { const fieldName = (targetComp as any).columnName || targetComp.id; onFormDataChange?.(fieldName, ""); } } } } else { toast.error(response.data?.message || "저장에 실패했습니다."); } } catch (error: any) { console.error("quickInsert 오류:", error); toast.error(error.response?.data?.message || 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; case "quickInsert": await handleQuickInsertAction(); 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; // ✅ 격자 시스템 잔재 제거: style.width, style.height 무시 const { width: styleWidth, height: styleHeight, ...styleWithoutSize } = style; // TableSearchWidget의 경우 높이를 자동으로 설정 const isTableSearchWidget = (component as any).componentId === "table-search-widget"; const componentStyle = { position: "absolute" as const, left: position?.x || 0, top: position?.y || 0, zIndex: position?.z || 1, ...styleWithoutSize, // width/height 제외한 스타일만 먼저 적용 width: size?.width || 200, // size의 픽셀 값이 최종 우선순위 height: isTableSearchWidget ? "auto" : size?.height || 10, minHeight: isTableSearchWidget ? "48px" : undefined, }; 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";