676 lines
25 KiB
TypeScript
676 lines
25 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useRef } from "react";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/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";
|
|
|
|
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<ScreenModalProps> = ({ className }) => {
|
|
const { userId, userName, user } = useAuth();
|
|
const splitPanelContext = useSplitPanelContext();
|
|
|
|
const [modalState, setModalState] = useState<ScreenModalState>({
|
|
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<Record<string, any>>({});
|
|
|
|
// 🆕 원본 데이터 상태 (수정 모드에서 UPDATE 판단용)
|
|
const [originalData, setOriginalData] = useState<Record<string, any> | null>(null);
|
|
|
|
// 🆕 선택된 데이터 상태 (RepeatScreenModal 등에서 사용)
|
|
const [selectedData, setSelectedData] = useState<Record<string, any>[]>([]);
|
|
|
|
// 연속 등록 모드 상태 (state로 변경 - 체크박스 UI 업데이트를 위해)
|
|
const [continuousMode, setContinuousMode] = useState(false);
|
|
|
|
// 화면 리셋 키 (컴포넌트 강제 리마운트용)
|
|
const [resetKey, setResetKey] = useState(0);
|
|
|
|
// 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<number>(0);
|
|
|
|
// 전역 모달 이벤트 리스너
|
|
useEffect(() => {
|
|
const handleOpenModal = (event: CustomEvent) => {
|
|
const {
|
|
screenId,
|
|
title,
|
|
description,
|
|
size,
|
|
urlParams,
|
|
editData,
|
|
splitPanelParentData,
|
|
selectedData: eventSelectedData,
|
|
selectedIds,
|
|
} = 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로 설정 (수정 모드)
|
|
if (editData) {
|
|
// 🆕 배열인 경우 두 가지 데이터를 설정:
|
|
// 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 {
|
|
// 🆕 신규 등록 모드: 분할 패널 부모 데이터가 있으면 미리 설정
|
|
// 🔧 중요: 신규 등록 시에는 연결 필드(equipment_code 등)만 전달해야 함
|
|
// 모든 필드를 전달하면 동일한 컬럼명이 있을 때 부모 값이 들어가는 문제 발생
|
|
// 예: 설비의 manufacturer가 소모품의 manufacturer로 들어감
|
|
|
|
// parentDataMapping에서 명시된 필드만 추출
|
|
const parentDataMapping = splitPanelContext?.parentDataMapping || [];
|
|
|
|
// 부모 데이터 소스
|
|
const rawParentData =
|
|
splitPanelParentData && Object.keys(splitPanelParentData).length > 0
|
|
? splitPanelParentData
|
|
: splitPanelContext?.selectedLeftData || {};
|
|
|
|
// 🔧 신규 등록 모드에서는 연결에 필요한 필드만 전달
|
|
const parentData: Record<string, any> = {};
|
|
|
|
// 필수 연결 필드: 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);
|
|
|
|
// 화면 정보와 레이아웃 데이터 로딩
|
|
const [screenInfo, layoutData] = await Promise.all([
|
|
screenApi.getScreen(screenId),
|
|
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");
|
|
|
|
// 수정 모드이고 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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
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());
|
|
}
|
|
|
|
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: undefined, // undefined로 변경 - defaultWidth/defaultHeight 사용
|
|
};
|
|
}
|
|
|
|
// 화면관리에서 설정한 크기 = 컨텐츠 영역 크기
|
|
// 실제 모달 크기 = 컨텐츠 + 헤더 + 연속등록 체크박스 + gap + padding
|
|
const headerHeight = 52; // DialogHeader (타이틀 + border-b + py-3)
|
|
const footerHeight = 52; // 연속 등록 모드 체크박스 영역
|
|
const dialogGap = 16; // DialogContent gap-4
|
|
const extraPadding = 24; // 추가 여백 (안전 마진)
|
|
|
|
const totalHeight = screenDimensions.height + headerHeight + footerHeight + dialogGap + extraPadding;
|
|
|
|
return {
|
|
className: "overflow-hidden p-0",
|
|
style: {
|
|
width: `${Math.min(screenDimensions.width + 48, 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<string | undefined>(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 (
|
|
<Dialog open={modalState.isOpen} onOpenChange={handleClose}>
|
|
<DialogContent
|
|
className={`${modalStyle.className} ${className || ""} max-w-none`}
|
|
{...(modalStyle.style && { style: modalStyle.style })}
|
|
>
|
|
<DialogHeader className="shrink-0 border-b px-4 py-3">
|
|
<div className="flex items-center gap-2">
|
|
<DialogTitle className="text-base">{modalState.title}</DialogTitle>
|
|
{modalState.description && !loading && (
|
|
<DialogDescription className="text-muted-foreground text-xs">{modalState.description}</DialogDescription>
|
|
)}
|
|
{loading && (
|
|
<DialogDescription className="text-xs">{loading ? "화면을 불러오는 중입니다..." : ""}</DialogDescription>
|
|
)}
|
|
</div>
|
|
</DialogHeader>
|
|
|
|
<div className="flex-1 overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent">
|
|
{loading ? (
|
|
<div className="flex h-full items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-b-2 border-blue-600"></div>
|
|
<p className="text-muted-foreground">화면을 불러오는 중...</p>
|
|
</div>
|
|
</div>
|
|
) : screenData ? (
|
|
<ActiveTabProvider>
|
|
<TableOptionsProvider>
|
|
<div
|
|
className="relative mx-auto bg-white"
|
|
style={{
|
|
width: `${screenDimensions?.width || 800}px`,
|
|
height: `${screenDimensions?.height || 600}px`,
|
|
transformOrigin: "center center",
|
|
}}
|
|
>
|
|
{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 (
|
|
<InteractiveScreenViewerDynamic
|
|
key={`${component.id}-${resetKey}`}
|
|
component={adjustedComponent}
|
|
allComponents={screenData.components}
|
|
formData={formData}
|
|
originalData={originalData} // 🆕 원본 데이터 전달 (UPDATE 판단용)
|
|
onFormDataChange={(fieldName, value) => {
|
|
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}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
</TableOptionsProvider>
|
|
</ActiveTabProvider>
|
|
) : (
|
|
<div className="flex h-full items-center justify-center">
|
|
<p className="text-muted-foreground">화면 데이터가 없습니다.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 연속 등록 모드 체크박스 */}
|
|
<div className="border-t px-4 py-3">
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
id="continuous-mode"
|
|
checked={continuousMode}
|
|
onCheckedChange={(checked) => {
|
|
const isChecked = checked === true;
|
|
setContinuousMode(isChecked);
|
|
localStorage.setItem("screenModal_continuousMode", String(isChecked));
|
|
}}
|
|
/>
|
|
<Label htmlFor="continuous-mode" className="cursor-pointer text-sm font-normal select-none">
|
|
저장 후 계속 입력 (연속 등록 모드)
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default ScreenModal;
|