465 lines
16 KiB
TypeScript
465 lines
16 KiB
TypeScript
"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";
|
|
|
|
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 } = useAuth();
|
|
|
|
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>>({});
|
|
|
|
// 연속 등록 모드 상태 (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 } = event.detail;
|
|
setModalState({
|
|
isOpen: true,
|
|
screenId,
|
|
title,
|
|
description: description || "",
|
|
size,
|
|
});
|
|
};
|
|
|
|
const handleCloseModal = () => {
|
|
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 });
|
|
|
|
// 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 = () => {
|
|
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<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 (
|
|
<ResizableDialog open={modalState.isOpen} onOpenChange={handleClose}>
|
|
<ResizableDialogContent
|
|
className={`${modalStyle.className} ${className || ""}`}
|
|
style={modalStyle.style}
|
|
defaultWidth={600}
|
|
defaultHeight={800}
|
|
minWidth={500}
|
|
minHeight={400}
|
|
maxWidth={1600}
|
|
maxHeight={1200}
|
|
modalId={persistedModalId}
|
|
userId={userId || "guest"}
|
|
>
|
|
<ResizableDialogHeader className="shrink-0 border-b px-4 py-3">
|
|
<div className="flex items-center gap-2">
|
|
<ResizableDialogTitle className="text-base">{modalState.title}</ResizableDialogTitle>
|
|
{modalState.description && !loading && (
|
|
<ResizableDialogDescription className="text-muted-foreground text-xs">
|
|
{modalState.description}
|
|
</ResizableDialogDescription>
|
|
)}
|
|
{loading && (
|
|
<ResizableDialogDescription className="text-xs">
|
|
{loading ? "화면을 불러오는 중입니다..." : ""}
|
|
</ResizableDialogDescription>
|
|
)}
|
|
</div>
|
|
</ResizableDialogHeader>
|
|
|
|
<div className="flex-1 overflow-auto p-6">
|
|
{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 ? (
|
|
<div
|
|
className="relative bg-white mx-auto"
|
|
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}
|
|
component={adjustedComponent}
|
|
allComponents={screenData.components}
|
|
formData={formData}
|
|
onFormDataChange={(fieldName, value) => {
|
|
// console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`);
|
|
// console.log("📋 현재 formData:", formData);
|
|
setFormData((prev) => {
|
|
const newFormData = {
|
|
...prev,
|
|
[fieldName]: value,
|
|
};
|
|
// console.log("📝 ScreenModal 업데이트된 formData:", newFormData);
|
|
return newFormData;
|
|
});
|
|
}}
|
|
onRefresh={() => {
|
|
// 부모 화면의 테이블 새로고침 이벤트 발송
|
|
console.log("🔄 모달에서 부모 화면 테이블 새로고침 이벤트 발송");
|
|
window.dispatchEvent(new CustomEvent("refreshTable"));
|
|
}}
|
|
screenInfo={{
|
|
id: modalState.screenId!,
|
|
tableName: screenData.screenInfo?.tableName,
|
|
}}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<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={continuousModeRef.current}
|
|
onCheckedChange={(checked) => {
|
|
const isChecked = checked === true;
|
|
continuousModeRef.current = isChecked;
|
|
localStorage.setItem("screenModal_continuousMode", String(isChecked));
|
|
setForceUpdate((prev) => prev + 1); // 체크박스 UI 업데이트를 위한 강제 리렌더링
|
|
// console.log("🔄 연속 모드 변경:", isChecked);
|
|
}}
|
|
/>
|
|
<Label
|
|
htmlFor="continuous-mode"
|
|
className="text-sm font-normal cursor-pointer select-none"
|
|
>
|
|
저장 후 계속 입력 (연속 등록 모드)
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
</ResizableDialogContent>
|
|
</ResizableDialog>
|
|
);
|
|
};
|
|
|
|
export default ScreenModal;
|