"use client"; import React, { useState, useEffect, useRef, useCallback } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, } from "@/components/ui/alert-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"; import { useSplitPanelContext } from "@/contexts/SplitPanelContext"; import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; import { convertV2ToLegacy, isValidV2Layout } from "@/lib/utils/layoutV2Converter"; 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 splitPanelContext = useSplitPanelContext(); 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>({}); // 🆕 원본 데이터 상태 (수정 모드에서 UPDATE 판단용) const [originalData, setOriginalData] = useState | null>(null); // 🆕 선택된 데이터 상태 (RepeatScreenModal 등에서 사용) const [selectedData, setSelectedData] = useState[]>([]); // 연속 등록 모드 상태 (state로 변경 - 체크박스 UI 업데이트를 위해) const [continuousMode, setContinuousMode] = useState(false); // 화면 리셋 키 (컴포넌트 강제 리마운트용) const [resetKey, setResetKey] = useState(0); // 모달 닫기 확인 다이얼로그 표시 상태 const [showCloseConfirm, setShowCloseConfirm] = useState(false); // localStorage에서 연속 모드 상태 복원 useEffect(() => { const savedMode = localStorage.getItem("screenModal_continuousMode"); if (savedMode === "true") { setContinuousMode(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), // 상단 여백 고려 }; }; // 모달이 열린 시간 추적 (저장 성공 이벤트 무시용) const modalOpenedAtRef = React.useRef(0); // 🆕 채번 필드 수동 입력 값 변경 이벤트 리스너 useEffect(() => { const handleNumberingValueChanged = (event: CustomEvent) => { const { columnName, value } = event.detail; if (columnName && modalState.isOpen) { setFormData((prev) => ({ ...prev, [columnName]: value, })); } }; window.addEventListener("numberingValueChanged", handleNumberingValueChanged as EventListener); return () => { window.removeEventListener("numberingValueChanged", handleNumberingValueChanged as EventListener); }; }, [modalState.isOpen]); // 전역 모달 이벤트 리스너 useEffect(() => { const handleOpenModal = (event: CustomEvent) => { const { screenId, title, description, size, urlParams, editData, splitPanelParentData, selectedData: eventSelectedData, selectedIds, isCreateMode, // 🆕 복사 모드 플래그 (true면 editData가 있어도 originalData 설정 안 함) } = event.detail; // 🆕 모달 열린 시간 기록 modalOpenedAtRef.current = Date.now(); // 🆕 선택된 데이터 저장 (RepeatScreenModal 등에서 사용) if (eventSelectedData && Array.isArray(eventSelectedData)) { setSelectedData(eventSelectedData); } else { setSelectedData([]); } // 🆕 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()); } // 🆕 editData가 있으면 formData와 originalData로 설정 (수정 모드) // 🔧 단, isCreateMode가 true이면 (복사 모드) originalData를 설정하지 않음 → 채번 생성 가능 if (editData && !isCreateMode) { // 🆕 배열인 경우 두 가지 데이터를 설정: // 1. formData: 첫 번째 요소(객체) - 일반 입력 필드용 (TextInput 등) // 2. selectedData: 전체 배열 - 다중 항목 컴포넌트용 (SelectedItemsDetailInput 등) if (Array.isArray(editData)) { const firstRecord = editData[0] || {}; setFormData(firstRecord); // 🔧 일반 입력 필드용 (객체) setSelectedData(editData); // 🔧 다중 항목 컴포넌트용 (배열) - groupedData로 전달됨 setOriginalData(firstRecord); // 첫 번째 레코드를 원본으로 저장 } else { setFormData(editData); setSelectedData([editData]); // 🔧 단일 객체도 배열로 변환하여 저장 setOriginalData(editData); // 🆕 원본 데이터 저장 (UPDATE 판단용) } } else if (editData && isCreateMode) { // 🆕 복사 모드: formData만 설정하고 originalData는 null로 유지 (채번 생성 가능) if (Array.isArray(editData)) { const firstRecord = editData[0] || {}; setFormData(firstRecord); setSelectedData(editData); } else { setFormData(editData); setSelectedData([editData]); } setOriginalData(null); // 🔧 복사 모드에서는 originalData를 null로 설정 } else { // 🆕 신규 등록 모드: 분할 패널 부모 데이터가 있으면 미리 설정 // 🔧 중요: 신규 등록 시에는 연결 필드(equipment_code 등)만 전달해야 함 // 모든 필드를 전달하면 동일한 컬럼명이 있을 때 부모 값이 들어가는 문제 발생 // 예: 설비의 manufacturer가 소모품의 manufacturer로 들어감 // parentDataMapping에서 명시된 필드만 추출 const parentDataMapping = splitPanelContext?.parentDataMapping || []; // 부모 데이터 소스 // 🔧 수정: 여러 소스를 병합 (우선순위: splitPanelParentData > selectedLeftData > 기존 formData의 링크 필드) // 예: screen 150→226→227 전환 시: // - splitPanelParentData: item_info 데이터 (screen 226에서 전달) // - selectedLeftData: customer_mng 데이터 (SplitPanel 좌측 선택) // - 기존 formData: 이전 모달에서 설정된 link 필드 (customer_code 등) const contextData = splitPanelContext?.selectedLeftData || {}; const eventData = splitPanelParentData && Object.keys(splitPanelParentData).length > 0 ? splitPanelParentData : {}; // 🆕 기존 formData에서 link 필드(_code, _id)를 가져와 base로 사용 // 모달 체인(226→227)에서 이전 모달의 연결 필드가 유지됨 const previousLinkFields: Record = {}; if (formData && typeof formData === "object" && !Array.isArray(formData)) { const linkFieldPatterns = ["_code", "_id"]; const excludeFields = ["id", "created_date", "updated_date", "created_at", "updated_at", "writer"]; for (const [key, value] of Object.entries(formData)) { if (excludeFields.includes(key)) continue; if (value === undefined || value === null) continue; const isLinkField = linkFieldPatterns.some((pattern) => key.endsWith(pattern)); if (isLinkField) { previousLinkFields[key] = value; } } } const rawParentData = { ...previousLinkFields, ...contextData, ...eventData }; // 🔧 신규 등록 모드에서는 연결에 필요한 필드만 전달 const parentData: Record = {}; // 필수 연결 필드: company_code (멀티테넌시) if (rawParentData.company_code) { parentData.company_code = rawParentData.company_code; } // parentDataMapping에 정의된 필드만 전달 for (const mapping of parentDataMapping) { const sourceValue = rawParentData[mapping.sourceColumn]; if (sourceValue !== undefined && sourceValue !== null) { parentData[mapping.targetColumn] = sourceValue; } } // parentDataMapping이 비어있으면 연결 필드 자동 감지 (equipment_code, xxx_code, xxx_id 패턴) if (parentDataMapping.length === 0) { const linkFieldPatterns = ["_code", "_id"]; const excludeFields = [ "id", "company_code", "created_date", "updated_date", "created_at", "updated_at", "writer", ]; for (const [key, value] of Object.entries(rawParentData)) { if (excludeFields.includes(key)) continue; if (value === undefined || value === null) continue; // 연결 필드 패턴 확인 const isLinkField = linkFieldPatterns.some((pattern) => key.endsWith(pattern)); if (isLinkField) { parentData[key] = value; } } } if (Object.keys(parentData).length > 0) { setFormData(parentData); } else { setFormData({}); } setOriginalData(null); // 신규 등록 모드 } 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()); } setModalState({ isOpen: false, screenId: null, title: "", description: "", size: "md", }); setScreenData(null); setFormData({}); setOriginalData(null); // 🆕 원본 데이터 초기화 setSelectedData([]); // 🆕 선택된 데이터 초기화 setContinuousMode(false); localStorage.setItem("screenModal_continuousMode", "false"); }; // 저장 성공 이벤트 처리 (연속 등록 모드 지원) const handleSaveSuccess = () => { // 🆕 모달이 열린 후 500ms 이내의 저장 성공 이벤트는 무시 (이전 이벤트 방지) const timeSinceOpen = Date.now() - modalOpenedAtRef.current; if (timeSinceOpen < 500) { return; } const isContinuousMode = continuousMode; if (isContinuousMode) { // 연속 모드: 폼만 초기화하고 모달은 유지 setFormData({}); setResetKey((prev) => prev + 1); // 화면 데이터 다시 로드 (채번 규칙 새로 생성) if (modalState.screenId) { loadScreenData(modalState.screenId); } toast.success("저장되었습니다. 계속 입력하세요."); } else { // 일반 모드: 모달 닫기 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); }; }, [continuousMode]); // continuousMode 의존성 추가 // 화면 데이터 로딩 useEffect(() => { if (modalState.isOpen && modalState.screenId) { loadScreenData(modalState.screenId); } }, [modalState.isOpen, modalState.screenId]); const loadScreenData = async (screenId: number) => { try { setLoading(true); // 화면 정보와 레이아웃 데이터 로딩 (V2 API 사용으로 기본값 병합) const [screenInfo, v2LayoutData] = await Promise.all([ screenApi.getScreen(screenId), screenApi.getLayoutV2(screenId), ]); // V2 → Legacy 변환 (기본값 병합 포함) let layoutData: any = null; if (v2LayoutData && isValidV2Layout(v2LayoutData)) { layoutData = convertV2ToLegacy(v2LayoutData); if (layoutData) { // screenResolution은 V2 레이아웃에서 직접 가져오기 layoutData.screenResolution = v2LayoutData.screenResolution || layoutData.screenResolution; } } // V2 레이아웃이 없으면 기존 API로 fallback if (!layoutData) { layoutData = await screenApi.getLayout(screenId); } // 🆕 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"); const primaryKeyColumn = urlParams.get("primaryKeyColumn"); // 🆕 Primary Key 컬럼명 // 수정 모드이고 editId가 있으면 해당 레코드 조회 if (mode === "edit" && editId && tableName) { try { // groupByColumns 파싱 let groupByColumns: string[] = []; if (groupByColumnsParam) { try { groupByColumns = JSON.parse(groupByColumnsParam); } catch { // groupByColumns 파싱 실패 시 무시 } } // 🆕 apiClient를 named import로 가져오기 const { apiClient } = await import("@/lib/api/client"); const params: any = { enableEntityJoin: true, // 엔티티 조인 활성화 (모든 엔티티 컬럼 자동 포함) }; if (groupByColumns.length > 0) { params.groupByColumns = JSON.stringify(groupByColumns); } // 🆕 Primary Key 컬럼명 전달 (백엔드 자동 감지 실패 시 사용) if (primaryKeyColumn) { params.primaryKeyColumn = primaryKeyColumn; } const apiResponse = await apiClient.get(`/data/${tableName}/${editId}`, { params }); const response = apiResponse.data; if (response.success && response.data) { // 🔧 날짜 필드 정규화 (타임존 제거) 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만 추출 normalized[key] = value.split("T")[0]; } else { normalized[key] = value; } } return normalized; }; const normalizedData = normalizeDates(response.data); // 🔧 배열 데이터는 formData로 설정하지 않음 (SelectedItemsDetailInput만 사용) if (Array.isArray(normalizedData)) { setFormData(normalizedData); // SelectedItemsDetailInput이 직접 사용 setOriginalData(normalizedData[0] || null); // 🆕 첫 번째 레코드를 원본으로 저장 } else { setFormData(normalizedData); setOriginalData(normalizedData); // 🆕 원본 데이터 저장 (UPDATE 판단용) } } else { 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, }; } else { // 해상도 정보가 없으면 자동 계산 dimensions = calculateScreenDimensions(components); } setScreenDimensions(dimensions); setScreenData({ components, screenInfo: screenInfo, }); } else { throw new Error("화면 데이터가 없습니다"); } } catch (error) { console.error("화면 데이터 로딩 오류:", error); toast.error("화면을 불러오는 중 오류가 발생했습니다."); handleClose(); } finally { setLoading(false); } }; // 사용자가 바깥 클릭/ESC/X 버튼으로 닫으려 할 때 확인 다이얼로그 표시 const handleCloseAttempt = useCallback(() => { setShowCloseConfirm(true); }, []); // 확인 후 실제로 모달을 닫는 함수 const handleConfirmClose = useCallback(() => { setShowCloseConfirm(false); handleCloseInternal(); }, []); // 닫기 취소 (계속 작업) const handleCancelClose = useCallback(() => { setShowCloseConfirm(false); }, []); const handleCloseInternal = () => { // 🔧 URL 파라미터 제거 (mode, editId, tableName, groupByColumns, dataSourceId 등) 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"); currentUrl.searchParams.delete("dataSourceId"); window.history.pushState({}, "", currentUrl.toString()); } setModalState({ isOpen: false, screenId: null, title: "", size: "md", }); setScreenData(null); setFormData({}); // 폼 데이터 초기화 setOriginalData(null); // 원본 데이터 초기화 setSelectedData([]); // 선택된 데이터 초기화 setContinuousMode(false); localStorage.setItem("screenModal_continuousMode", "false"); }; // 기존 handleClose를 유지 (이벤트 핸들러 등에서 사용) const handleClose = handleCloseInternal; // 모달 크기 설정 - 화면관리 설정 크기 + 헤더/푸터 const getModalStyle = () => { if (!screenDimensions) { return { className: "w-fit min-w-[400px] max-w-4xl max-h-[90vh] overflow-hidden p-0", style: undefined, // undefined로 변경 - defaultWidth/defaultHeight 사용 needsScroll: false, }; } // 화면관리에서 설정한 크기 = 컨텐츠 영역 크기 // 실제 모달 크기 = 컨텐츠 + 헤더 + 연속등록 체크박스 + gap + 패딩 // 🔧 DialogContent의 gap-4 (16px × 2) + 컨텐츠 pt-6 (24px) 포함 const headerHeight = 48; // DialogHeader (타이틀 + border-b + py-3) const footerHeight = 44; // 연속 등록 모드 체크박스 영역 const dialogGap = 32; // gap-4 × 2 (header-content, content-footer 사이) const contentTopPadding = 24; // pt-6 (컨텐츠 영역 상단 패딩) const horizontalPadding = 16; // 좌우 패딩 최소화 const totalHeight = screenDimensions.height + headerHeight + footerHeight + dialogGap + contentTopPadding; const maxAvailableHeight = window.innerHeight * 0.95; // 콘텐츠가 화면 높이를 초과할 때만 스크롤 필요 const needsScroll = totalHeight > maxAvailableHeight; return { className: "overflow-hidden p-0", style: { width: `${Math.min(screenDimensions.width + horizontalPadding, window.innerWidth * 0.98)}px`, // 🔧 height 대신 max-height만 설정 - 콘텐츠가 작으면 자동으로 줄어듦 maxHeight: `${maxAvailableHeight}px`, maxWidth: "98vw", }, needsScroll, }; }; 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 ( { // X 버튼 클릭 시에도 확인 다이얼로그 표시 if (!open) { handleCloseAttempt(); } }} > { e.preventDefault(); handleCloseAttempt(); }} // ESC 키 누를 때도 바로 닫히지 않도록 방지 onEscapeKeyDown={(e) => { e.preventDefault(); handleCloseAttempt(); }} >
{modalState.title} {modalState.description && !loading && ( {modalState.description} )} {loading && ( {loading ? "화면을 불러오는 중입니다..." : ""} )}
{loading ? (

화면을 불러오는 중...

) : screenData ? (
{(() => { // 🆕 동적 y 좌표 조정을 위해 먼저 숨겨지는 컴포넌트들 파악 const isComponentHidden = (comp: any) => { const cc = comp.componentConfig?.conditionalConfig || comp.conditionalConfig; if (!cc?.enabled || !formData) return false; const { field, operator, value, action } = cc; const fieldValue = formData[field]; let conditionMet = false; switch (operator) { case "=": case "==": case "===": conditionMet = fieldValue === value; break; case "!=": case "!==": conditionMet = fieldValue !== value; break; default: conditionMet = fieldValue === value; } return (action === "show" && !conditionMet) || (action === "hide" && conditionMet); }; // 표시되는 컴포넌트들의 y 범위 수집 const visibleRanges: { y: number; bottom: number }[] = []; screenData.components.forEach((comp: any) => { if (!isComponentHidden(comp)) { const y = parseFloat(comp.position?.y?.toString() || "0"); const height = parseFloat(comp.size?.height?.toString() || "0"); visibleRanges.push({ y, bottom: y + height }); } }); // 숨겨지는 컴포넌트의 "실제 빈 공간" 계산 (표시되는 컴포넌트와 겹치지 않는 영역) const getActualGap = (hiddenY: number, hiddenBottom: number): number => { // 숨겨지는 영역 중 표시되는 컴포넌트와 겹치는 부분을 제외 let gapStart = hiddenY; let gapEnd = hiddenBottom; for (const visible of visibleRanges) { // 겹치는 영역 확인 if (visible.y < gapEnd && visible.bottom > gapStart) { // 겹치는 부분을 제외 if (visible.y <= gapStart && visible.bottom >= gapEnd) { // 완전히 덮힘 - 빈 공간 없음 return 0; } else if (visible.y <= gapStart) { // 위쪽이 덮힘 gapStart = visible.bottom; } else if (visible.bottom >= gapEnd) { // 아래쪽이 덮힘 gapEnd = visible.y; } } } return Math.max(0, gapEnd - gapStart); }; // 숨겨지는 컴포넌트들의 실제 빈 공간 수집 const hiddenGaps: { bottom: number; gap: number }[] = []; screenData.components.forEach((comp: any) => { if (isComponentHidden(comp)) { const y = parseFloat(comp.position?.y?.toString() || "0"); const height = parseFloat(comp.size?.height?.toString() || "0"); const bottom = y + height; const gap = getActualGap(y, bottom); if (gap > 0) { hiddenGaps.push({ bottom, gap }); } } }); // bottom 기준으로 정렬 및 중복 제거 (같은 bottom은 가장 큰 gap만 유지) const mergedGaps = new Map(); hiddenGaps.forEach(({ bottom, gap }) => { const existing = mergedGaps.get(bottom) || 0; mergedGaps.set(bottom, Math.max(existing, gap)); }); const sortedGaps = Array.from(mergedGaps.entries()) .map(([bottom, gap]) => ({ bottom, gap })) .sort((a, b) => a.bottom - b.bottom); // 각 컴포넌트의 y 조정값 계산 함수 const getYOffset = (compY: number, compId?: string) => { let offset = 0; for (const { bottom, gap } of sortedGaps) { // 컴포넌트가 숨겨진 영역 아래에 있으면 그 빈 공간만큼 위로 이동 if (compY > bottom) { offset += gap; } } return offset; }; return screenData.components.map((component: any) => { // 숨겨지는 컴포넌트는 렌더링 안함 if (isComponentHidden(component)) { return null; } // 화면 관리 해상도를 사용하는 경우 offset 조정 불필요 const offsetX = screenDimensions?.offsetX || 0; const offsetY = screenDimensions?.offsetY || 0; // 🆕 동적 y 좌표 조정 (숨겨진 컴포넌트 높이만큼 위로 이동) const compY = parseFloat(component.position?.y?.toString() || "0"); const yAdjustment = getYOffset(compY, component.id); // offset이 0이면 원본 위치 사용 (화면 관리 해상도 사용 시) const adjustedComponent = { ...component, position: { ...component.position, x: parseFloat(component.position?.x?.toString() || "0") - offsetX, y: compY - offsetY - yAdjustment, // 🆕 동적 조정 적용 }, }; return ( { setFormData((prev) => { const newFormData = { ...prev, [fieldName]: value, }; return newFormData; }); }} onRefresh={() => { // 부모 화면의 테이블 새로고침 이벤트 발송 window.dispatchEvent(new CustomEvent("refreshTable")); }} screenInfo={{ id: modalState.screenId!, tableName: screenData.screenInfo?.tableName, }} groupedData={selectedData} userId={userId} userName={userName} companyCode={user?.companyCode} /> ); }); })()}
) : (

화면 데이터가 없습니다.

)}
{/* 연속 등록 모드 체크박스 */}
{ const isChecked = checked === true; setContinuousMode(isChecked); localStorage.setItem("screenModal_continuousMode", String(isChecked)); }} />
{/* 모달 닫기 확인 다이얼로그 */} 화면을 닫으시겠습니까? 지금 나가시면 진행 중인 데이터가 저장되지 않습니다.
계속 작업하시려면 '계속 작업' 버튼을 눌러주세요.
계속 작업 나가기
); }; export default ScreenModal;