ERP-node/frontend/components/screen/EditModal.tsx

359 lines
13 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } 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 { screenApi } from "@/lib/api/screen";
import { ComponentData } from "@/lib/types/screen";
interface EditModalProps {
isOpen: boolean;
onClose: () => void;
screenId?: number;
modalSize?: "sm" | "md" | "lg" | "xl" | "full";
editData?: any;
onSave?: () => void;
onDataChange?: (formData: Record<string, any>) => void; // 폼 데이터 변경 콜백 추가
}
/**
* 편집 모달 컴포넌트
* 선택된 데이터를 폼 화면에 로드하여 편집할 수 있게 해주는 모달
*/
export const EditModal: React.FC<EditModalProps> = ({
isOpen,
onClose,
screenId,
modalSize = "lg",
editData,
onSave,
onDataChange,
}) => {
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[]>([]);
// 컴포넌트 기반 동적 크기 계산
const calculateModalSize = () => {
if (components.length === 0) {
return { width: 600, height: 400 }; // 기본 크기
}
const maxWidth = Math.max(...components.map((c) => (c.position?.x || 0) + (c.size?.width || 200)), 500) + 100; // 더 넉넉한 여백
const maxHeight = Math.max(...components.map((c) => (c.position?.y || 0) + (c.size?.height || 40)), 400) + 20; // 최소한의 여백만 추가
console.log(`🎯 계산된 모달 크기: ${maxWidth}px x ${maxHeight}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 dynamicSize = calculateModalSize();
// 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 지연으로 렌더링 완료 후 실행
}
}, [isOpen, dynamicSize]);
// 편집 데이터가 변경되면 폼 데이터 및 원본 데이터 초기화
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 {
console.log("⚠️ editData가 없습니다.");
setOriginalData({});
setFormData({});
}
}, [editData]);
// formData 변경 시 로그
useEffect(() => {
console.log("🔄 EditModal formData 상태 변경:", formData);
console.log("🔄 formData 키들:", Object.keys(formData || {}));
}, [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 () => {
try {
setLoading(true);
console.log("💾 편집 데이터 저장:", formData);
// TODO: 실제 저장 API 호출
// const result = await DynamicFormApi.updateFormData({
// screenId,
// data: formData,
// });
// 임시: 저장 성공 시뮬레이션
await new Promise((resolve) => setTimeout(resolve, 1000));
toast.success("수정이 완료되었습니다.");
onSave?.();
onClose();
} catch (error) {
console.error("❌ 저장 실패:", error);
toast.error("저장 중 오류가 발생했습니다.");
} finally {
setLoading(false);
}
};
// 초기화 처리
const handleReset = () => {
if (editData) {
setFormData({ ...editData });
toast.info("초기값으로 되돌렸습니다.");
}
};
// 모달 크기 클래스 매핑
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";
}
};
if (!screenId) {
return null;
}
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent
className="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"
>
<DialogHeader className="sr-only">
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-hidden">
{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-4 border-gray-300 border-t-blue-500"></div>
<p className="text-gray-600"> ...</p>
</div>
</div>
) : screenData && components.length > 0 ? (
// 원본 화면과 동일한 레이아웃으로 렌더링
<div
className="relative bg-white"
style={{
// 실제 컨텐츠 크기 그대로 적용 (여백 제거)
width: dynamicSize.width,
height: dynamicSize.height,
overflow: "hidden",
}}
>
{/* 화면 컴포넌트들 원본 레이아웃 유지하여 렌더링 */}
<div className="relative" style={{ minHeight: "300px" }}>
{components.map((component, index) => (
<div
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
}}
>
{/* 위젯 컴포넌트는 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>
</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>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
};