2025-09-12 14:24:25 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from "react";
|
2025-09-19 02:15:21 +09:00
|
|
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
2025-09-12 14:24:25 +09:00
|
|
|
|
import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic";
|
|
|
|
|
|
import { screenApi } from "@/lib/api/screen";
|
|
|
|
|
|
import { ComponentData } from "@/types/screen";
|
|
|
|
|
|
import { toast } from "sonner";
|
|
|
|
|
|
|
|
|
|
|
|
interface ScreenModalState {
|
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
|
screenId: number | null;
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
size: "sm" | "md" | "lg" | "xl";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ScreenModalProps {
|
|
|
|
|
|
className?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|
|
|
|
|
const [modalState, setModalState] = useState<ScreenModalState>({
|
|
|
|
|
|
isOpen: false,
|
|
|
|
|
|
screenId: null,
|
|
|
|
|
|
title: "",
|
|
|
|
|
|
size: "md",
|
|
|
|
|
|
});
|
2025-09-18 10:05:50 +09:00
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
const [screenData, setScreenData] = useState<{
|
|
|
|
|
|
components: ComponentData[];
|
|
|
|
|
|
screenInfo: any;
|
|
|
|
|
|
} | null>(null);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [screenDimensions, setScreenDimensions] = useState<{
|
|
|
|
|
|
width: number;
|
|
|
|
|
|
height: number;
|
|
|
|
|
|
} | null>(null);
|
|
|
|
|
|
|
2025-09-18 10:05:50 +09:00
|
|
|
|
// 폼 데이터 상태 추가
|
|
|
|
|
|
const [formData, setFormData] = useState<Record<string, any>>({});
|
|
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
// 화면의 실제 크기 계산 함수
|
|
|
|
|
|
const calculateScreenDimensions = (components: ComponentData[]) => {
|
|
|
|
|
|
let maxWidth = 800; // 최소 너비
|
|
|
|
|
|
let maxHeight = 600; // 최소 높이
|
|
|
|
|
|
|
2025-09-19 02:15:21 +09:00
|
|
|
|
console.log("🔍 화면 크기 계산 시작:", { componentsCount: components.length });
|
|
|
|
|
|
|
|
|
|
|
|
components.forEach((component, index) => {
|
|
|
|
|
|
// position과 size는 BaseComponent에서 별도 속성으로 관리
|
|
|
|
|
|
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");
|
2025-09-12 14:24:25 +09:00
|
|
|
|
|
|
|
|
|
|
// 컴포넌트의 오른쪽 끝과 아래쪽 끝 계산
|
|
|
|
|
|
const rightEdge = x + width;
|
|
|
|
|
|
const bottomEdge = y + height;
|
|
|
|
|
|
|
2025-09-19 02:15:21 +09:00
|
|
|
|
console.log(
|
|
|
|
|
|
`📏 컴포넌트 ${index + 1} (${component.id}): x=${x}, y=${y}, w=${width}, h=${height}, rightEdge=${rightEdge}, bottomEdge=${bottomEdge}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const newMaxWidth = Math.max(maxWidth, rightEdge + 100); // 여백 증가
|
|
|
|
|
|
const newMaxHeight = Math.max(maxHeight, bottomEdge + 100); // 여백 증가
|
|
|
|
|
|
|
|
|
|
|
|
if (newMaxWidth > maxWidth || newMaxHeight > maxHeight) {
|
|
|
|
|
|
console.log(`🔄 크기 업데이트: ${maxWidth}×${maxHeight} → ${newMaxWidth}×${newMaxHeight}`);
|
|
|
|
|
|
maxWidth = newMaxWidth;
|
|
|
|
|
|
maxHeight = newMaxHeight;
|
|
|
|
|
|
}
|
2025-09-12 14:24:25 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-19 02:15:21 +09:00
|
|
|
|
console.log("📊 컴포넌트 기반 계산 결과:", { maxWidth, maxHeight });
|
|
|
|
|
|
|
|
|
|
|
|
// 브라우저 크기 제한 확인 (더욱 관대하게 설정)
|
|
|
|
|
|
const maxAllowedWidth = window.innerWidth * 0.98; // 95% -> 98%
|
|
|
|
|
|
const maxAllowedHeight = window.innerHeight * 0.95; // 90% -> 95%
|
|
|
|
|
|
|
|
|
|
|
|
console.log("📐 크기 제한 정보:", {
|
|
|
|
|
|
계산된크기: { maxWidth, maxHeight },
|
|
|
|
|
|
브라우저제한: { maxAllowedWidth, maxAllowedHeight },
|
|
|
|
|
|
브라우저크기: { width: window.innerWidth, height: window.innerHeight },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 기반 크기를 우선 적용하되, 브라우저 제한을 고려
|
|
|
|
|
|
const finalDimensions = {
|
|
|
|
|
|
width: Math.min(maxWidth, maxAllowedWidth),
|
|
|
|
|
|
height: Math.min(maxHeight, maxAllowedHeight),
|
2025-09-12 14:24:25 +09:00
|
|
|
|
};
|
2025-09-19 02:15:21 +09:00
|
|
|
|
|
|
|
|
|
|
console.log("✅ 최종 화면 크기:", finalDimensions);
|
|
|
|
|
|
console.log("🔧 크기 적용 분석:", {
|
|
|
|
|
|
width적용: maxWidth <= maxAllowedWidth ? "컴포넌트기준" : "브라우저제한",
|
|
|
|
|
|
height적용: maxHeight <= maxAllowedHeight ? "컴포넌트기준" : "브라우저제한",
|
|
|
|
|
|
컴포넌트크기: { maxWidth, maxHeight },
|
|
|
|
|
|
최종크기: finalDimensions,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return finalDimensions;
|
2025-09-12 14:24:25 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 전역 모달 이벤트 리스너
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleOpenModal = (event: CustomEvent) => {
|
|
|
|
|
|
const { screenId, title, size } = event.detail;
|
|
|
|
|
|
setModalState({
|
|
|
|
|
|
isOpen: true,
|
|
|
|
|
|
screenId,
|
|
|
|
|
|
title,
|
|
|
|
|
|
size,
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener("openScreenModal", handleOpenModal as EventListener);
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
window.removeEventListener("openScreenModal", handleOpenModal as EventListener);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 화면 데이터 로딩
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (modalState.isOpen && modalState.screenId) {
|
|
|
|
|
|
loadScreenData(modalState.screenId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [modalState.isOpen, modalState.screenId]);
|
|
|
|
|
|
|
|
|
|
|
|
const loadScreenData = async (screenId: number) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
console.log("화면 데이터 로딩 시작:", screenId);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
// 화면 정보와 레이아웃 데이터 로딩
|
|
|
|
|
|
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 || [];
|
2025-09-18 10:05:50 +09:00
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
// 화면의 실제 크기 계산
|
|
|
|
|
|
const dimensions = calculateScreenDimensions(components);
|
|
|
|
|
|
setScreenDimensions(dimensions);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
|
2025-09-12 14:24:25 +09:00
|
|
|
|
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);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
setFormData({}); // 폼 데이터 초기화
|
2025-09-12 14:24:25 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 모달 크기 설정 - 화면 내용에 맞게 동적 조정
|
|
|
|
|
|
const getModalStyle = () => {
|
|
|
|
|
|
if (!screenDimensions) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
className: "w-fit min-w-[400px] max-w-4xl max-h-[80vh] overflow-hidden",
|
2025-09-18 10:05:50 +09:00
|
|
|
|
style: {},
|
2025-09-12 14:24:25 +09:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-19 02:15:21 +09:00
|
|
|
|
// 헤더 높이와 패딩을 고려한 전체 높이 계산 (실제 측정값 기반)
|
|
|
|
|
|
const headerHeight = 80; // DialogHeader + 패딩 (더 정확한 값)
|
2025-09-12 14:24:25 +09:00
|
|
|
|
const totalHeight = screenDimensions.height + headerHeight;
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
className: "overflow-hidden p-0",
|
|
|
|
|
|
style: {
|
2025-09-19 02:15:21 +09:00
|
|
|
|
width: `${Math.min(screenDimensions.width + 48, window.innerWidth * 0.98)}px`, // 브라우저 제한 적용
|
|
|
|
|
|
height: `${Math.min(totalHeight, window.innerHeight * 0.95)}px`, // 브라우저 제한 적용
|
|
|
|
|
|
maxWidth: "98vw", // 안전장치
|
|
|
|
|
|
maxHeight: "95vh", // 안전장치
|
2025-09-18 10:05:50 +09:00
|
|
|
|
},
|
2025-09-12 14:24:25 +09:00
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const modalStyle = getModalStyle();
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Dialog open={modalState.isOpen} onOpenChange={handleClose}>
|
2025-09-18 10:05:50 +09:00
|
|
|
|
<DialogContent className={`${modalStyle.className} ${className || ""}`} style={modalStyle.style}>
|
|
|
|
|
|
<DialogHeader className="border-b px-6 py-4">
|
2025-09-12 14:24:25 +09:00
|
|
|
|
<DialogTitle>{modalState.title}</DialogTitle>
|
2025-09-19 02:15:21 +09:00
|
|
|
|
<DialogDescription>{loading ? "화면을 불러오는 중입니다..." : "화면 내용을 표시합니다."}</DialogDescription>
|
2025-09-12 14:24:25 +09:00
|
|
|
|
</DialogHeader>
|
2025-09-18 10:05:50 +09:00
|
|
|
|
|
2025-09-19 02:15:21 +09:00
|
|
|
|
<div className="flex-1 p-4">
|
2025-09-12 14:24:25 +09:00
|
|
|
|
{loading ? (
|
2025-09-18 10:05:50 +09:00
|
|
|
|
<div className="flex h-full items-center justify-center">
|
2025-09-12 14:24:25 +09:00
|
|
|
|
<div className="text-center">
|
2025-09-18 10:05:50 +09:00
|
|
|
|
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-b-2 border-blue-600"></div>
|
2025-09-12 14:24:25 +09:00
|
|
|
|
<p className="text-gray-600">화면을 불러오는 중...</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : screenData ? (
|
2025-09-18 10:05:50 +09:00
|
|
|
|
<div
|
2025-09-19 02:15:21 +09:00
|
|
|
|
className="relative bg-white"
|
2025-09-12 14:24:25 +09:00
|
|
|
|
style={{
|
2025-09-18 10:05:50 +09:00
|
|
|
|
width: screenDimensions?.width || 800,
|
|
|
|
|
|
height: screenDimensions?.height || 600,
|
2025-09-12 14:24:25 +09:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{screenData.components.map((component) => (
|
|
|
|
|
|
<InteractiveScreenViewerDynamic
|
|
|
|
|
|
key={component.id}
|
|
|
|
|
|
component={component}
|
|
|
|
|
|
allComponents={screenData.components}
|
2025-09-18 10:05:50 +09:00
|
|
|
|
formData={formData}
|
|
|
|
|
|
onFormDataChange={(fieldName, value) => {
|
|
|
|
|
|
console.log(`🎯 ScreenModal onFormDataChange 호출: ${fieldName} = "${value}"`);
|
2025-09-19 02:15:21 +09:00
|
|
|
|
console.log("📋 현재 formData:", formData);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
setFormData((prev) => {
|
|
|
|
|
|
const newFormData = {
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[fieldName]: value,
|
|
|
|
|
|
};
|
2025-09-19 02:15:21 +09:00
|
|
|
|
console.log("📝 ScreenModal 업데이트된 formData:", newFormData);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
return newFormData;
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
2025-09-12 14:24:25 +09:00
|
|
|
|
screenInfo={{
|
|
|
|
|
|
id: modalState.screenId!,
|
|
|
|
|
|
tableName: screenData.screenInfo?.tableName,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
2025-09-18 10:05:50 +09:00
|
|
|
|
<div className="flex h-full items-center justify-center">
|
2025-09-12 14:24:25 +09:00
|
|
|
|
<p className="text-gray-600">화면 데이터가 없습니다.</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</DialogContent>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default ScreenModal;
|