2025-09-18 18:49:30 +09:00
|
|
|
"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";
|
2025-09-19 02:15:21 +09:00
|
|
|
import { InteractiveScreenViewer } from "./InteractiveScreenViewer";
|
2025-09-18 18:49:30 +09:00
|
|
|
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];
|
2025-09-19 02:15:21 +09:00
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-09-18 18:49:30 +09:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} 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",
|
2025-09-18 21:33:04 +09:00
|
|
|
zIndex: 9999, // 모든 컴포넌트보다 위에 표시
|
2025-09-18 18:49:30 +09:00
|
|
|
}}
|
2025-09-29 19:32:20 +09:00
|
|
|
data-radix-portal="true"
|
2025-09-18 18:49:30 +09:00
|
|
|
>
|
|
|
|
|
<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" }}>
|
2025-09-29 19:32:20 +09:00
|
|
|
{components.map((component, index) => (
|
2025-09-18 18:49:30 +09:00
|
|
|
<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,
|
2025-09-29 19:32:20 +09:00
|
|
|
zIndex: component.position?.z || (1000 + index), // 모달 내부에서 충분히 높은 z-index
|
2025-09-18 18:49:30 +09:00
|
|
|
}}
|
|
|
|
|
>
|
2025-10-01 17:27:24 +09:00
|
|
|
{/* 위젯 컴포넌트는 InteractiveScreenViewer 사용 (라벨 표시) */}
|
2025-09-19 02:15:21 +09:00
|
|
|
{component.type === "widget" ? (
|
|
|
|
|
<InteractiveScreenViewer
|
|
|
|
|
component={component}
|
|
|
|
|
allComponents={components}
|
2025-10-01 17:27:24 +09:00
|
|
|
hideLabel={false} // ✅ 라벨 표시
|
2025-09-19 02:15:21 +09:00
|
|
|
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,
|
2025-10-01 17:27:24 +09:00
|
|
|
labelDisplay: true, // ✅ 라벨 표시
|
2025-09-19 02:15:21 +09:00
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
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}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2025-09-18 18:49:30 +09:00
|
|
|
</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>
|
|
|
|
|
);
|
|
|
|
|
};
|