diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 28dded7b..2d3fb513 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -2,276 +2,293 @@ import React, { useState, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { X, Save, RotateCcw } from "lucide-react"; -import { toast } from "sonner"; -import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; -import { InteractiveScreenViewer } from "./InteractiveScreenViewer"; +import { InteractiveScreenViewerDynamic } from "@/components/screen/InteractiveScreenViewerDynamic"; import { screenApi } from "@/lib/api/screen"; -import { ComponentData } from "@/lib/types/screen"; +import { ComponentData } from "@/types/screen"; +import { toast } from "sonner"; +import { dynamicFormApi } from "@/lib/api/dynamicForm"; -interface EditModalProps { +interface EditModalState { isOpen: boolean; - onClose: () => void; - screenId?: number; - modalSize?: "sm" | "md" | "lg" | "xl" | "full"; - editData?: any; + screenId: number | null; + title: string; + description?: string; + modalSize: "sm" | "md" | "lg" | "xl"; + editData: Record; onSave?: () => void; - onDataChange?: (formData: Record) => void; // 폼 데이터 변경 콜백 추가 - modalTitle?: string; // 모달 제목 - modalDescription?: string; // 모달 설명 } -/** - * 편집 모달 컴포넌트 - * 선택된 데이터를 폼 화면에 로드하여 편집할 수 있게 해주는 모달 - */ -export const EditModal: React.FC = ({ - isOpen, - onClose, - screenId, - modalSize = "lg", - editData, - onSave, - onDataChange, - modalTitle, - modalDescription, -}) => { - const [loading, setLoading] = useState(false); - const [formData, setFormData] = useState({}); - const [originalData, setOriginalData] = useState({}); // 부분 업데이트용 원본 데이터 - const [screenData, setScreenData] = useState(null); - const [components, setComponents] = useState([]); +interface EditModalProps { + className?: string; +} - // 컴포넌트 기반 동적 크기 계산 - const calculateModalSize = () => { +export const EditModal: React.FC = ({ className }) => { + const [modalState, setModalState] = useState({ + isOpen: false, + screenId: null, + title: "", + description: "", + modalSize: "md", + editData: {}, + onSave: undefined, + }); + + 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>({}); + const [originalData, setOriginalData] = useState>({}); + + // 화면의 실제 크기 계산 함수 (ScreenModal과 동일) + const calculateScreenDimensions = (components: ComponentData[]) => { if (components.length === 0) { - return { width: 600, height: 400 }; // 기본 크기 + return { + width: 400, + height: 300, + offsetX: 0, + offsetY: 0, + }; } - const maxWidth = Math.max(...components.map((c) => (c.position?.x || 0) + (c.size?.width || 200)), 500) + 100; // 더 넉넉한 여백 + // 모든 컴포넌트의 경계 찾기 + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; - const contentHeight = Math.max(...components.map((c) => (c.position?.y || 0) + (c.size?.height || 40)), 400) + 20; // 컨텐츠 높이 + 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"); - // 헤더 높이 추가 (ScreenModal과 동일) - const headerHeight = 60; // DialogHeader 높이 (타이틀 + 패딩) - const maxHeight = contentHeight + headerHeight; + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x + width); + maxY = Math.max(maxY, y + height); + }); - console.log( - `🎯 계산된 모달 크기: ${maxWidth}px x ${maxHeight}px (컨텐츠: ${contentHeight}px + 헤더: ${headerHeight}px)`, - ); - console.log( - "📍 컴포넌트 위치들:", - components.map((c) => ({ x: c.position?.x, y: c.position?.y, w: c.size?.width, h: c.size?.height })), - ); - return { width: maxWidth, height: maxHeight }; + // 실제 컨텐츠 크기 계산 + 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 dynamicSize = calculateModalSize(); - - // EditModal 전용 닫기 이벤트 리스너 + // 전역 모달 이벤트 리스너 useEffect(() => { - const handleCloseEditModal = () => { - console.log("🚪 EditModal: closeEditModal 이벤트 수신"); - onClose(); + const handleOpenEditModal = (event: CustomEvent) => { + const { screenId, title, description, modalSize, editData, onSave } = event.detail; + console.log("🚀 EditModal 열기 이벤트 수신:", { + screenId, + title, + description, + modalSize, + editData, + }); + + setModalState({ + isOpen: true, + screenId, + title, + description: description || "", + modalSize: modalSize || "lg", + editData: editData || {}, + onSave, + }); + + // 편집 데이터로 폼 데이터 초기화 + setFormData(editData || {}); + setOriginalData(editData || {}); }; + const handleCloseEditModal = () => { + console.log("🚪 EditModal 닫기 이벤트 수신"); + handleClose(); + }; + + window.addEventListener("openEditModal", handleOpenEditModal as EventListener); window.addEventListener("closeEditModal", handleCloseEditModal); return () => { + window.removeEventListener("openEditModal", handleOpenEditModal as EventListener); window.removeEventListener("closeEditModal", handleCloseEditModal); }; - }, [onClose]); + }, []); - // DialogContent 크기 강제 적용 + // 화면 데이터 로딩 useEffect(() => { - if (isOpen && dynamicSize) { - // 모달이 렌더링된 후 DOM 직접 조작으로 크기 강제 적용 - setTimeout(() => { - const dialogContent = document.querySelector('[role="dialog"] > div'); - const modalContent = document.querySelector('[role="dialog"] [class*="overflow-auto"]'); - - if (dialogContent) { - const targetWidth = dynamicSize.width; - const targetHeight = dynamicSize.height; - - console.log(`🔧 DialogContent 크기 강제 적용: ${targetWidth}px x ${targetHeight}px`); - - dialogContent.style.width = `${targetWidth}px`; - dialogContent.style.height = `${targetHeight}px`; - dialogContent.style.minWidth = `${targetWidth}px`; - dialogContent.style.minHeight = `${targetHeight}px`; - dialogContent.style.maxWidth = "95vw"; - dialogContent.style.maxHeight = "95vh"; - dialogContent.style.padding = "0"; - } - - // 스크롤 완전 제거 - if (modalContent) { - modalContent.style.overflow = "hidden"; - console.log("🚫 스크롤 완전 비활성화"); - } - }, 100); // 100ms 지연으로 렌더링 완료 후 실행 + if (modalState.isOpen && modalState.screenId) { + loadScreenData(modalState.screenId); } - }, [isOpen, dynamicSize]); + }, [modalState.isOpen, modalState.screenId]); - // 편집 데이터가 변경되면 폼 데이터 및 원본 데이터 초기화 - useEffect(() => { - if (editData) { - console.log("📋 편집 데이터 로드:", editData); - console.log("📋 편집 데이터 키들:", Object.keys(editData)); - - // 원본 데이터와 현재 폼 데이터 모두 설정 - const dataClone = { ...editData }; - setOriginalData(dataClone); // 원본 데이터 저장 (부분 업데이트용) - setFormData(dataClone); // 편집용 폼 데이터 설정 - - console.log("📋 originalData 설정 완료:", dataClone); - console.log("📋 formData 설정 완료:", dataClone); - } else { - setOriginalData({}); - setFormData({}); - } - }, [editData]); - - // formData 변경 시 로그 - useEffect(() => {}, [formData]); - - // 화면 데이터 로드 - useEffect(() => { - const fetchScreenData = async () => { - if (!screenId || !isOpen) return; - - try { - setLoading(true); - console.log("🔄 화면 데이터 로드 시작:", screenId); - - // 화면 정보와 레이아웃 데이터를 동시에 로드 - const [screenInfo, layoutData] = await Promise.all([ - screenApi.getScreen(screenId), - screenApi.getLayout(screenId), - ]); - - console.log("📋 화면 정보:", screenInfo); - console.log("🎨 레이아웃 데이터:", layoutData); - - setScreenData(screenInfo); - - if (layoutData && layoutData.components) { - setComponents(layoutData.components); - console.log("✅ 화면 컴포넌트 로드 완료:", layoutData.components); - - // 컴포넌트와 formData 매칭 정보 출력 - console.log("🔍 컴포넌트-formData 매칭 분석:"); - layoutData.components.forEach((comp) => { - if (comp.columnName) { - const formValue = formData[comp.columnName]; - console.log( - ` - ${comp.columnName}: "${formValue}" (타입: ${comp.type}, 웹타입: ${(comp as any).widgetType})`, - ); - - // 코드 타입인 경우 특별히 로깅 - if ((comp as any).widgetType === "code") { - console.log(" 🔍 코드 타입 세부정보:", { - columnName: comp.columnName, - componentId: comp.id, - formValue, - webTypeConfig: (comp as any).webTypeConfig, - }); - } - } - }); - } else { - console.log("⚠️ 레이아웃 데이터가 없습니다:", layoutData); - } - } catch (error) { - console.error("❌ 화면 데이터 로드 실패:", error); - toast.error("화면을 불러오는데 실패했습니다."); - } finally { - setLoading(false); - } - }; - - fetchScreenData(); - }, [screenId, isOpen]); - - // 저장 처리 - const handleSave = async () => { + const loadScreenData = async (screenId: number) => { try { setLoading(true); - console.log("💾 편집 데이터 저장:", formData); - // TODO: 실제 저장 API 호출 - // const result = await DynamicFormApi.updateFormData({ - // screenId, - // data: formData, - // }); + console.log("화면 데이터 로딩 시작:", screenId); - // 임시: 저장 성공 시뮬레이션 - await new Promise((resolve) => setTimeout(resolve, 1000)); + // 화면 정보와 레이아웃 데이터 로딩 + const [screenInfo, layoutData] = await Promise.all([ + screenApi.getScreen(screenId), + screenApi.getLayout(screenId), + ]); - toast.success("수정이 완료되었습니다."); - onSave?.(); - onClose(); + console.log("API 응답:", { screenInfo, layoutData }); + + if (screenInfo && layoutData) { + const components = layoutData.components || []; + + // 화면의 실제 크기 계산 + const dimensions = calculateScreenDimensions(components); + setScreenDimensions(dimensions); + + setScreenData({ + components, + screenInfo: screenInfo, + }); + console.log("화면 데이터 설정 완료:", { + componentsCount: components.length, + dimensions, + screenInfo, + }); + } else { + throw new Error("화면 데이터가 없습니다"); + } } catch (error) { - console.error("❌ 저장 실패:", error); - toast.error("저장 중 오류가 발생했습니다."); + console.error("화면 데이터 로딩 오류:", error); + toast.error("화면을 불러오는 중 오류가 발생했습니다."); + handleClose(); } finally { setLoading(false); } }; - // 초기화 처리 - const handleReset = () => { - if (editData) { - setFormData({ ...editData }); - toast.info("초기값으로 되돌렸습니다."); + const handleClose = () => { + setModalState({ + isOpen: false, + screenId: null, + title: "", + description: "", + modalSize: "md", + editData: {}, + onSave: undefined, + }); + setScreenData(null); + setFormData({}); + setOriginalData({}); + }; + + // 저장 버튼 클릭 시 - UPDATE 액션 실행 + const handleSave = async () => { + if (!screenData?.screenInfo?.tableName) { + toast.error("테이블 정보가 없습니다."); + return; + } + + try { + console.log("💾 수정 저장 시작:", { + tableName: screenData.screenInfo.tableName, + formData, + originalData, + }); + + // 변경된 필드만 추출 + const changedData: Record = {}; + Object.keys(formData).forEach((key) => { + if (formData[key] !== originalData[key]) { + changedData[key] = formData[key]; + } + }); + + console.log("📝 변경된 필드:", changedData); + + if (Object.keys(changedData).length === 0) { + toast.info("변경된 내용이 없습니다."); + handleClose(); + return; + } + + // UPDATE 액션 실행 + const response = await dynamicFormApi.updateData(screenData.screenInfo.tableName, { + ...originalData, // 원본 데이터 (WHERE 조건용) + ...changedData, // 변경된 데이터만 + }); + + if (response.success) { + toast.success("데이터가 수정되었습니다."); + + // 부모 컴포넌트의 onSave 콜백 실행 + if (modalState.onSave) { + modalState.onSave(); + } + + handleClose(); + } else { + throw new Error(response.message || "수정에 실패했습니다."); + } + } catch (error: any) { + console.error("❌ 수정 실패:", error); + toast.error(error.message || "데이터 수정 중 오류가 발생했습니다."); } }; - // 모달 크기 클래스 매핑 - const getModalSizeClass = () => { - switch (modalSize) { - case "sm": - return "max-w-md"; - case "md": - return "max-w-lg"; - case "lg": - return "max-w-4xl"; - case "xl": - return "max-w-6xl"; - case "full": - return "max-w-[95vw] max-h-[95vh]"; - default: - return "max-w-4xl"; + // 모달 크기 설정 - ScreenModal과 동일 + 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; + 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", + }, + }; }; - if (!screenId) { - return null; - } + const modalStyle = getModalStyle(); return ( - - - {/* 모달 헤더 - 항상 표시 (ScreenModal과 동일 구조) */} + + - {modalTitle || "데이터 수정"} - {modalDescription && !loading && ( - {modalDescription} + {modalState.title || "데이터 수정"} + {modalState.description && !loading && ( + {modalState.description} )} {loading && ( {loading ? "화면을 불러오는 중입니다..." : ""} @@ -286,96 +303,61 @@ export const EditModal: React.FC = ({

화면을 불러오는 중...

- ) : screenData && components.length > 0 ? ( - // 원본 화면과 동일한 레이아웃으로 렌더링 + ) : screenData ? (
- {/* 화면 컴포넌트들 원본 레이아웃 유지하여 렌더링 */} -
- {components.map((component, index) => ( -
{ + // 컴포넌트 위치를 offset만큼 조정 + const offsetX = screenDimensions?.offsetX || 0; + const offsetY = screenDimensions?.offsetY || 0; + + const adjustedComponent = { + ...component, + position: { + ...component.position, + x: parseFloat(component.position?.x?.toString() || "0") - offsetX, + y: parseFloat(component.position?.y?.toString() || "0") - offsetY, + }, + }; + + return ( + { + console.log(`🎯 EditModal onFormDataChange 호출: ${fieldName} = "${value}"`); + console.log("📋 현재 formData:", formData); + setFormData((prev) => { + const newFormData = { + ...prev, + [fieldName]: value, + }; + console.log("📝 EditModal 업데이트된 formData:", newFormData); + return newFormData; + }); }} - > - {/* 위젯 컴포넌트는 InteractiveScreenViewer 사용 (라벨 표시) */} - {component.type === "widget" ? ( - { - console.log("📝 폼 데이터 변경:", fieldName, value); - const newFormData = { ...formData, [fieldName]: value }; - setFormData(newFormData); - - // 변경된 데이터를 즉시 부모로 전달 - if (onDataChange) { - console.log("📤 EditModal -> 부모로 데이터 전달:", newFormData); - onDataChange(newFormData); - } - }} - screenInfo={{ - id: screenId || 0, - tableName: screenData.tableName, - }} - /> - ) : ( - { - console.log("📝 폼 데이터 변경:", fieldName, value); - const newFormData = { ...formData, [fieldName]: value }; - setFormData(newFormData); - - // 변경된 데이터를 즉시 부모로 전달 - if (onDataChange) { - console.log("📤 EditModal -> 부모로 데이터 전달:", newFormData); - onDataChange(newFormData); - } - }} - // 편집 모드로 설정 - mode="edit" - // 모달 내에서 렌더링되고 있음을 표시 - isInModal={true} - // 인터랙티브 모드 활성화 (formData 사용을 위해 필수) - isInteractive={true} - /> - )} -
- ))} -
+ screenInfo={{ + id: modalState.screenId!, + tableName: screenData.screenInfo?.tableName, + }} + onSave={handleSave} + /> + ); + })}
) : (
-
-

화면을 불러올 수 없습니다.

-

화면 ID: {screenId}

-
+

화면 데이터가 없습니다.

)} @@ -383,3 +365,5 @@ export const EditModal: React.FC = ({
); }; + +export default EditModal;