수정 모달

This commit is contained in:
kjs 2025-10-30 12:08:58 +09:00
parent 556354219a
commit 4d9e783c57
1 changed files with 287 additions and 303 deletions

View File

@ -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<string, any>;
onSave?: () => void;
onDataChange?: (formData: Record<string, any>) => void; // 폼 데이터 변경 콜백 추가
modalTitle?: string; // 모달 제목
modalDescription?: string; // 모달 설명
}
/**
*
*
*/
export const EditModal: React.FC<EditModalProps> = ({
isOpen,
onClose,
screenId,
modalSize = "lg",
editData,
onSave,
onDataChange,
modalTitle,
modalDescription,
}) => {
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState<any>({});
const [originalData, setOriginalData] = useState<any>({}); // 부분 업데이트용 원본 데이터
const [screenData, setScreenData] = useState<any>(null);
const [components, setComponents] = useState<ComponentData[]>([]);
interface EditModalProps {
className?: string;
}
// 컴포넌트 기반 동적 크기 계산
const calculateModalSize = () => {
export const EditModal: React.FC<EditModalProps> = ({ className }) => {
const [modalState, setModalState] = useState<EditModalState>({
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<Record<string, any>>({});
const [originalData, setOriginalData] = useState<Record<string, any>>({});
// 화면의 실제 크기 계산 함수 (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<string, any> = {};
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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent
className="overflow-hidden p-0"
style={{
// 실제 컨텐츠 크기 그대로 적용 (패딩/여백 제거)
width: dynamicSize.width,
height: dynamicSize.height,
minWidth: dynamicSize.width,
minHeight: dynamicSize.height,
maxWidth: "95vw",
maxHeight: "95vh",
zIndex: 9999, // 모든 컴포넌트보다 위에 표시
}}
data-radix-portal="true"
>
{/* 모달 헤더 - 항상 표시 (ScreenModal과 동일 구조) */}
<Dialog open={modalState.isOpen} onOpenChange={handleClose}>
<DialogContent className={`${modalStyle.className} ${className || ""}`} style={modalStyle.style}>
<DialogHeader className="shrink-0 border-b px-4 py-3">
<DialogTitle className="text-base">{modalTitle || "데이터 수정"}</DialogTitle>
{modalDescription && !loading && (
<DialogDescription className="text-muted-foreground text-xs">{modalDescription}</DialogDescription>
<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>
@ -286,96 +303,61 @@ export const EditModal: React.FC<EditModalProps> = ({
<p className="text-muted-foreground"> ...</p>
</div>
</div>
) : screenData && components.length > 0 ? (
// 원본 화면과 동일한 레이아웃으로 렌더링
) : screenData ? (
<div
className="relative bg-white"
style={{
// 실제 컨텐츠 크기 그대로 적용 (여백 제거)
width: dynamicSize.width,
height: dynamicSize.height,
overflow: "hidden",
width: screenDimensions?.width || 800,
height: screenDimensions?.height || 600,
transformOrigin: "center center",
maxWidth: "100%",
maxHeight: "100%",
}}
>
{/* 화면 컴포넌트들 원본 레이아웃 유지하여 렌더링 */}
<div className="relative" style={{ minHeight: "300px" }}>
{components.map((component, index) => (
<div
{screenData.components.map((component) => {
// 컴포넌트 위치를 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 (
<InteractiveScreenViewerDynamic
key={component.id}
style={{
position: "absolute",
top: component.position?.y || 0,
left: component.position?.x || 0,
width: component.size?.width || 200,
height: component.size?.height || 40,
zIndex: component.position?.z || 1000 + index, // 모달 내부에서 충분히 높은 z-index
component={adjustedComponent}
allComponents={screenData.components}
formData={formData}
onFormDataChange={(fieldName, value) => {
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" ? (
<InteractiveScreenViewer
component={component}
allComponents={components}
hideLabel={false} // ✅ 라벨 표시
formData={formData}
onFormDataChange={(fieldName, value) => {
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,
}}
/>
) : (
<DynamicComponentRenderer
component={{
...component,
style: {
...component.style,
labelDisplay: true, // ✅ 라벨 표시
},
}}
screenId={screenId}
tableName={screenData.tableName}
formData={formData}
originalData={originalData} // 부분 업데이트용 원본 데이터 전달
onFormDataChange={(fieldName, value) => {
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}
/>
)}
</div>
))}
</div>
screenInfo={{
id: modalState.screenId!,
tableName: screenData.screenInfo?.tableName,
}}
onSave={handleSave}
/>
);
})}
</div>
) : (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<p className="text-gray-500"> .</p>
<p className="mt-1 text-sm text-gray-400"> ID: {screenId}</p>
</div>
<p className="text-muted-foreground"> .</p>
</div>
)}
</div>
@ -383,3 +365,5 @@ export const EditModal: React.FC<EditModalProps> = ({
</Dialog>
);
};
export default EditModal;