"use client"; import React, { useState, useEffect, useRef } from "react"; import { ResizableDialog, ResizableDialogContent, ResizableDialogHeader, ResizableDialogTitle, ResizableDialogDescription, } from "@/components/ui/resizable-dialog"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic"; import { screenApi } from "@/lib/api/screen"; import { ComponentData } from "@/types/screen"; import { toast } from "sonner"; import { useAuth } from "@/hooks/useAuth"; import { TableOptionsProvider } from "@/contexts/TableOptionsContext"; import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext"; interface ScreenModalState { isOpen: boolean; screenId: number | null; title: string; description?: string; size: "sm" | "md" | "lg" | "xl"; } interface ScreenModalProps { className?: string; } export const ScreenModal: React.FC = ({ className }) => { const { userId, userName, user } = useAuth(); const [modalState, setModalState] = useState({ isOpen: false, screenId: null, title: "", description: "", size: "md", }); const [screenData, setScreenData] = useState<{ components: ComponentData[]; screenInfo: any; } | null>(null); const [loading, setLoading] = useState(false); const [screenDimensions, setScreenDimensions] = useState<{ width: number; height: number; offsetX?: number; offsetY?: number; } | null>(null); // 폼 데이터 상태 추가 const [formData, setFormData] = useState>({}); // 연속 등록 모드 상태 (localStorage에 저장하여 리렌더링에 영향받지 않도록) const continuousModeRef = useRef(false); const [, setForceUpdate] = useState(0); // 강제 리렌더링용 (값은 사용하지 않음) // localStorage에서 연속 모드 상태 복원 useEffect(() => { const savedMode = localStorage.getItem("screenModal_continuousMode"); if (savedMode === "true") { continuousModeRef.current = true; // console.log("🔄 연속 모드 복원: true"); } }, []); // 화면의 실제 크기 계산 함수 const calculateScreenDimensions = (components: ComponentData[]) => { if (components.length === 0) { return { width: 400, height: 300, offsetX: 0, offsetY: 0, }; } // 모든 컴포넌트의 경계 찾기 let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; components.forEach((component) => { const x = parseFloat(component.position?.x?.toString() || "0"); const y = parseFloat(component.position?.y?.toString() || "0"); const width = parseFloat(component.size?.width?.toString() || "100"); const height = parseFloat(component.size?.height?.toString() || "40"); minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x + width); maxY = Math.max(maxY, y + height); }); // 실제 컨텐츠 크기 계산 const contentWidth = maxX - minX; const contentHeight = maxY - minY; // 적절한 여백 추가 const paddingX = 40; const paddingY = 40; const finalWidth = Math.max(contentWidth + paddingX, 400); const finalHeight = Math.max(contentHeight + paddingY, 300); return { width: Math.min(finalWidth, window.innerWidth * 0.95), height: Math.min(finalHeight, window.innerHeight * 0.9), offsetX: Math.max(0, minX - paddingX / 2), // 좌측 여백 고려 offsetY: Math.max(0, minY - paddingY / 2), // 상단 여백 고려 }; }; // 전역 모달 이벤트 리스너 useEffect(() => { const handleOpenModal = (event: CustomEvent) => { const { screenId, title, description, size, urlParams } = event.detail; // 🆕 URL 파라미터가 있으면 현재 URL에 추가 if (urlParams && typeof window !== "undefined") { const currentUrl = new URL(window.location.href); Object.entries(urlParams).forEach(([key, value]) => { currentUrl.searchParams.set(key, String(value)); }); // pushState로 URL 변경 (페이지 새로고침 없이) window.history.pushState({}, "", currentUrl.toString()); console.log("✅ URL 파라미터 추가:", urlParams); } setModalState({ isOpen: true, screenId, title, description: description || "", size, }); }; const handleCloseModal = () => { // 🆕 URL 파라미터 제거 if (typeof window !== "undefined") { const currentUrl = new URL(window.location.href); // dataSourceId 파라미터 제거 currentUrl.searchParams.delete("dataSourceId"); window.history.pushState({}, "", currentUrl.toString()); console.log("🧹 URL 파라미터 제거"); } setModalState({ isOpen: false, screenId: null, title: "", description: "", size: "md", }); setScreenData(null); setFormData({}); continuousModeRef.current = false; localStorage.setItem("screenModal_continuousMode", "false"); // localStorage에 저장 // console.log("🔄 연속 모드 초기화: false"); }; // 저장 성공 이벤트 처리 (연속 등록 모드 지원) const handleSaveSuccess = () => { const isContinuousMode = continuousModeRef.current; // console.log("💾 저장 성공 이벤트 수신"); // console.log("📌 현재 연속 모드 상태 (ref):", isContinuousMode); // console.log("📌 localStorage:", localStorage.getItem("screenModal_continuousMode")); if (isContinuousMode) { // 연속 모드: 폼만 초기화하고 모달은 유지 // console.log("✅ 연속 모드 활성화 - 폼만 초기화"); // 폼만 초기화 (연속 모드 상태는 localStorage에 저장되어 있으므로 유지됨) setFormData({}); toast.success("저장되었습니다. 계속 입력하세요."); } else { // 일반 모드: 모달 닫기 // console.log("❌ 일반 모드 - 모달 닫기"); handleCloseModal(); } }; window.addEventListener("openScreenModal", handleOpenModal as EventListener); window.addEventListener("closeSaveModal", handleCloseModal); window.addEventListener("saveSuccessInModal", handleSaveSuccess); return () => { window.removeEventListener("openScreenModal", handleOpenModal as EventListener); window.removeEventListener("closeSaveModal", handleCloseModal); window.removeEventListener("saveSuccessInModal", handleSaveSuccess); }; }, []); // 의존성 제거 (ref 사용으로 최신 상태 참조) // 화면 데이터 로딩 useEffect(() => { if (modalState.isOpen && modalState.screenId) { loadScreenData(modalState.screenId); } }, [modalState.isOpen, modalState.screenId]); const loadScreenData = async (screenId: number) => { try { setLoading(true); console.log("화면 데이터 로딩 시작:", screenId); // 화면 정보와 레이아웃 데이터 로딩 const [screenInfo, layoutData] = await Promise.all([ screenApi.getScreen(screenId), screenApi.getLayout(screenId), ]); console.log("API 응답:", { screenInfo, layoutData }); // 🆕 URL 파라미터 확인 (수정 모드) if (typeof window !== "undefined") { const urlParams = new URLSearchParams(window.location.search); const mode = urlParams.get("mode"); const editId = urlParams.get("editId"); const tableName = urlParams.get("tableName") || screenInfo.tableName; const groupByColumnsParam = urlParams.get("groupByColumns"); console.log("📋 URL 파라미터 확인:", { mode, editId, tableName, groupByColumnsParam }); // 수정 모드이고 editId가 있으면 해당 레코드 조회 if (mode === "edit" && editId && tableName) { try { console.log("🔍 수정 데이터 조회 시작:", { tableName, editId, groupByColumnsParam }); const { dataApi } = await import("@/lib/api/data"); // groupByColumns 파싱 let groupByColumns: string[] = []; if (groupByColumnsParam) { try { groupByColumns = JSON.parse(groupByColumnsParam); console.log("✅ [ScreenModal] groupByColumns 파싱 성공:", groupByColumns); } catch (e) { console.warn("groupByColumns 파싱 실패:", e); } } else { console.warn("⚠️ [ScreenModal] groupByColumnsParam이 없습니다!"); } console.log("🚀 [ScreenModal] API 호출 직전:", { tableName, editId, enableEntityJoin: true, groupByColumns, groupByColumnsLength: groupByColumns.length, }); // 🆕 apiClient를 named import로 가져오기 const { apiClient } = await import("@/lib/api/client"); const params: any = { enableEntityJoin: true, // 엔티티 조인 활성화 (모든 엔티티 컬럼 자동 포함) }; if (groupByColumns.length > 0) { params.groupByColumns = JSON.stringify(groupByColumns); console.log("✅ [ScreenModal] groupByColumns를 params에 추가:", params.groupByColumns); } console.log("📡 [ScreenModal] 실제 API 요청:", { url: `/data/${tableName}/${editId}`, params, }); const apiResponse = await apiClient.get(`/data/${tableName}/${editId}`, { params }); const response = apiResponse.data; console.log("📩 [ScreenModal] API 응답 받음:", { success: response.success, hasData: !!response.data, dataType: response.data ? (Array.isArray(response.data) ? "배열" : "객체") : "없음", dataLength: Array.isArray(response.data) ? response.data.length : 1, }); if (response.success && response.data) { // 배열인 경우 (그룹핑) vs 단일 객체 const isArray = Array.isArray(response.data); if (isArray) { console.log(`✅ 수정 데이터 로드 완료 (그룹 레코드: ${response.data.length}개)`); console.log("📦 전체 데이터 (JSON):", JSON.stringify(response.data, null, 2)); } else { console.log("✅ 수정 데이터 로드 완료 (필드 수:", Object.keys(response.data).length, ")"); console.log("📊 모든 필드 키:", Object.keys(response.data)); console.log("📦 전체 데이터 (JSON):", JSON.stringify(response.data, null, 2)); } // 🔧 날짜 필드 정규화 (타임존 제거) const normalizeDates = (data: any): any => { if (Array.isArray(data)) { return data.map(normalizeDates); } if (typeof data !== 'object' || data === null) { return data; } const normalized: any = {}; for (const [key, value] of Object.entries(data)) { if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value)) { // ISO 날짜 형식 감지: YYYY-MM-DD만 추출 const before = value; const after = value.split('T')[0]; console.log(`🔧 [날짜 정규화] ${key}: ${before} → ${after}`); normalized[key] = after; } else { normalized[key] = value; } } return normalized; }; console.log("📥 [ScreenModal] API 응답 원본:", JSON.stringify(response.data, null, 2)); const normalizedData = normalizeDates(response.data); console.log("📥 [ScreenModal] 정규화 후:", JSON.stringify(normalizedData, null, 2)); // 🔧 배열 데이터는 formData로 설정하지 않음 (SelectedItemsDetailInput만 사용) if (Array.isArray(normalizedData)) { console.log("⚠️ [ScreenModal] 그룹 레코드(배열)는 formData로 설정하지 않음. SelectedItemsDetailInput만 사용합니다."); setFormData(normalizedData); // SelectedItemsDetailInput이 직접 사용 } else { setFormData(normalizedData); } // setFormData 직후 확인 console.log("🔄 setFormData 호출 완료 (날짜 정규화됨)"); } else { console.error("❌ 수정 데이터 로드 실패:", response.error); toast.error("데이터를 불러올 수 없습니다."); } } catch (error) { console.error("❌ 수정 데이터 조회 오류:", error); toast.error("데이터를 불러오는 중 오류가 발생했습니다."); } } } // screenApi는 직접 데이터를 반환하므로 .success 체크 불필요 if (screenInfo && layoutData) { const components = layoutData.components || []; // 화면 관리에서 설정한 해상도 사용 (우선순위) const screenResolution = (layoutData as any).screenResolution || (screenInfo as any).screenResolution; let dimensions; if (screenResolution && screenResolution.width && screenResolution.height) { // 화면 관리에서 설정한 해상도 사용 dimensions = { width: screenResolution.width, height: screenResolution.height, offsetX: 0, offsetY: 0, }; console.log("✅ 화면 관리 해상도 사용:", dimensions); } else { // 해상도 정보가 없으면 자동 계산 dimensions = calculateScreenDimensions(components); console.log("⚠️ 자동 계산된 크기 사용:", dimensions); } setScreenDimensions(dimensions); setScreenData({ components, screenInfo: screenInfo, }); console.log("화면 데이터 설정 완료:", { componentsCount: components.length, dimensions, screenInfo, }); } else { throw new Error("화면 데이터가 없습니다"); } } catch (error) { console.error("화면 데이터 로딩 오류:", error); toast.error("화면을 불러오는 중 오류가 발생했습니다."); handleClose(); } finally { setLoading(false); } }; const handleClose = () => { // 🔧 URL 파라미터 제거 (mode, editId, tableName 등) if (typeof window !== "undefined") { const currentUrl = new URL(window.location.href); currentUrl.searchParams.delete("mode"); currentUrl.searchParams.delete("editId"); currentUrl.searchParams.delete("tableName"); currentUrl.searchParams.delete("groupByColumns"); window.history.pushState({}, "", currentUrl.toString()); console.log("🧹 [ScreenModal] URL 파라미터 제거 (모달 닫힘)"); } setModalState({ isOpen: false, screenId: null, title: "", size: "md", }); setScreenData(null); setFormData({}); // 폼 데이터 초기화 }; // 모달 크기 설정 - 화면 내용에 맞게 동적 조정 const getModalStyle = () => { if (!screenDimensions) { return { className: "w-fit min-w-[400px] max-w-4xl max-h-[90vh] overflow-hidden p-0", style: {}, }; } // 헤더 높이를 최소화 (제목 영역만) const headerHeight = 60; // DialogHeader 최소 높이 (타이틀 + 최소 패딩) const totalHeight = screenDimensions.height + headerHeight; return { className: "overflow-hidden p-0", style: { width: `${Math.min(screenDimensions.width, window.innerWidth * 0.98)}px`, height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`, maxWidth: "98vw", maxHeight: "95vh", }, }; }; const modalStyle = getModalStyle(); // 안정적인 modalId를 상태로 저장 (모달이 닫혀도 유지) const [persistedModalId, setPersistedModalId] = useState(undefined); // modalId 생성 및 업데이트 useEffect(() => { // 모달이 열려있고 screenId가 있을 때만 업데이트 if (!modalState.isOpen) return; let newModalId: string | undefined; // 1순위: screenId (가장 안정적) if (modalState.screenId) { newModalId = `screen-modal-${modalState.screenId}`; // console.log("🔑 ScreenModal modalId 생성:", { // method: "screenId", // screenId: modalState.screenId, // result: newModalId, // }); } // 2순위: 테이블명 else if (screenData?.screenInfo?.tableName) { newModalId = `screen-modal-table-${screenData.screenInfo.tableName}`; // console.log("🔑 ScreenModal modalId 생성:", { // method: "tableName", // tableName: screenData.screenInfo.tableName, // result: newModalId, // }); } // 3순위: 화면명 else if (screenData?.screenInfo?.screenName) { newModalId = `screen-modal-name-${screenData.screenInfo.screenName}`; // console.log("🔑 ScreenModal modalId 생성:", { // method: "screenName", // screenName: screenData.screenInfo.screenName, // result: newModalId, // }); } // 4순위: 제목 else if (modalState.title) { const titleId = modalState.title.replace(/\s+/g, "-"); newModalId = `screen-modal-title-${titleId}`; // console.log("🔑 ScreenModal modalId 생성:", { // method: "title", // title: modalState.title, // result: newModalId, // }); } if (newModalId) { setPersistedModalId(newModalId); } }, [ modalState.isOpen, modalState.screenId, modalState.title, screenData?.screenInfo?.tableName, screenData?.screenInfo?.screenName, ]); return (
{modalState.title} {modalState.description && !loading && ( {modalState.description} )} {loading && ( {loading ? "화면을 불러오는 중입니다..." : ""} )}
{loading ? (

화면을 불러오는 중...

) : screenData ? (
{screenData.components.map((component) => { // 화면 관리 해상도를 사용하는 경우 offset 조정 불필요 const offsetX = screenDimensions?.offsetX || 0; const offsetY = screenDimensions?.offsetY || 0; // offset이 0이면 원본 위치 사용 (화면 관리 해상도 사용 시) const adjustedComponent = offsetX === 0 && offsetY === 0 ? component : { ...component, position: { ...component.position, x: parseFloat(component.position?.x?.toString() || "0") - offsetX, y: parseFloat(component.position?.y?.toString() || "0") - offsetY, }, }; return ( { setFormData((prev) => ({ ...prev, [fieldName]: value, })); }} onRefresh={() => { // 부모 화면의 테이블 새로고침 이벤트 발송 console.log("🔄 모달에서 부모 화면 테이블 새로고침 이벤트 발송"); window.dispatchEvent(new CustomEvent("refreshTable")); }} screenInfo={{ id: modalState.screenId!, tableName: screenData.screenInfo?.tableName, }} userId={userId} userName={userName} companyCode={user?.companyCode} /> ); })}
) : (

화면 데이터가 없습니다.

)}
{/* 연속 등록 모드 체크박스 */}
{ const isChecked = checked === true; continuousModeRef.current = isChecked; localStorage.setItem("screenModal_continuousMode", String(isChecked)); setForceUpdate((prev) => prev + 1); // 체크박스 UI 업데이트를 위한 강제 리렌더링 // console.log("🔄 연속 모드 변경:", isChecked); }} />
); }; export default ScreenModal;