ui 수정 및 시현할 기능 업데이트
This commit is contained in:
305
frontend/components/screen/SaveModal.tsx
Normal file
305
frontend/components/screen/SaveModal.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
"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, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
||||
import { InteractiveScreenViewer } from "./InteractiveScreenViewer";
|
||||
import { screenApi } from "@/lib/api/screen";
|
||||
import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm";
|
||||
import { ComponentData } from "@/lib/types/screen";
|
||||
|
||||
interface SaveModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
screenId?: number;
|
||||
modalSize?: "sm" | "md" | "lg" | "xl" | "full";
|
||||
initialData?: any; // 수정 모드일 때 기존 데이터
|
||||
onSaveSuccess?: () => void; // 저장 성공 시 콜백 (테이블 새로고침용)
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장 전용 모달 컴포넌트
|
||||
* - 저장 성공 시: 메시지 표시 → 모달 닫기 → 테이블 새로고침
|
||||
* - 저장 실패 시: 에러 메시지 표시, 모달 유지
|
||||
*/
|
||||
export const SaveModal: React.FC<SaveModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
screenId,
|
||||
modalSize = "lg",
|
||||
initialData,
|
||||
onSaveSuccess,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState<Record<string, any>>(initialData || {});
|
||||
const [originalData, setOriginalData] = useState<Record<string, any>>(initialData || {});
|
||||
const [screenData, setScreenData] = useState<any>(null);
|
||||
const [components, setComponents] = useState<ComponentData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// 모달 크기 설정
|
||||
const modalSizeClasses = {
|
||||
sm: "max-w-md",
|
||||
md: "max-w-2xl",
|
||||
lg: "max-w-4xl",
|
||||
xl: "max-w-6xl",
|
||||
full: "max-w-[95vw]",
|
||||
};
|
||||
|
||||
// 화면 데이터 로드
|
||||
useEffect(() => {
|
||||
const loadScreenData = async () => {
|
||||
if (!screenId || !isOpen) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// 화면 정보 로드
|
||||
const screen = await screenApi.getScreen(screenId);
|
||||
setScreenData(screen);
|
||||
|
||||
// 레이아웃 로드
|
||||
const layout = await screenApi.getLayout(screenId);
|
||||
setComponents(layout.components || []);
|
||||
|
||||
// initialData가 있으면 폼에 채우기
|
||||
if (initialData) {
|
||||
setFormData(initialData);
|
||||
setOriginalData(initialData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("화면 로드 실패:", error);
|
||||
toast.error("화면을 불러오는데 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadScreenData();
|
||||
}, [screenId, isOpen, initialData]);
|
||||
|
||||
// closeSaveModal 이벤트 리스너
|
||||
useEffect(() => {
|
||||
const handleCloseSaveModal = () => {
|
||||
console.log("🚪 SaveModal 닫기 이벤트 수신");
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('closeSaveModal', handleCloseSaveModal);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('closeSaveModal', handleCloseSaveModal);
|
||||
}
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
// 저장 핸들러
|
||||
const handleSave = async () => {
|
||||
if (!screenData || !screenId) return;
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
|
||||
// 변경된 데이터만 추출 (수정 모드일 때)
|
||||
const changedData: Record<string, any> = {};
|
||||
if (initialData) {
|
||||
// 수정 모드: 변경된 필드만 전송
|
||||
Object.keys(formData).forEach((key) => {
|
||||
if (formData[key] !== originalData[key]) {
|
||||
changedData[key] = formData[key];
|
||||
}
|
||||
});
|
||||
|
||||
// 변경사항이 없으면 저장하지 않음
|
||||
if (Object.keys(changedData).length === 0) {
|
||||
toast.info("변경된 내용이 없습니다.");
|
||||
setIsSaving(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 저장할 데이터 준비
|
||||
const dataToSave = initialData ? changedData : formData;
|
||||
|
||||
// 테이블명 결정
|
||||
const tableName =
|
||||
screenData.tableName ||
|
||||
components.find((c) => c.columnName)?.tableName ||
|
||||
"dynamic_form_data";
|
||||
|
||||
const saveData: DynamicFormData = {
|
||||
screenId: screenId,
|
||||
tableName: tableName,
|
||||
data: dataToSave,
|
||||
};
|
||||
|
||||
console.log("💾 저장 요청 데이터:", saveData);
|
||||
|
||||
// API 호출
|
||||
const result = await dynamicFormApi.saveFormData(saveData);
|
||||
|
||||
if (result.success) {
|
||||
// ✅ 저장 성공
|
||||
toast.success(initialData ? "수정되었습니다!" : "저장되었습니다!");
|
||||
|
||||
// 모달 닫기
|
||||
onClose();
|
||||
|
||||
// 테이블 새로고침 콜백 호출
|
||||
if (onSaveSuccess) {
|
||||
setTimeout(() => {
|
||||
onSaveSuccess();
|
||||
}, 300); // 모달 닫힘 애니메이션 후 실행
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.message || "저장에 실패했습니다.");
|
||||
}
|
||||
} catch (error: any) {
|
||||
// ❌ 저장 실패 - 모달은 닫히지 않음
|
||||
console.error("저장 실패:", error);
|
||||
toast.error(`저장 중 오류가 발생했습니다: ${error.message || "알 수 없는 오류"}`);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 동적 크기 계산 (컴포넌트들의 위치 기반)
|
||||
const calculateDynamicSize = () => {
|
||||
if (!components.length) return { width: 800, height: 600 };
|
||||
|
||||
const maxX = Math.max(...components.map((c) => (c.position?.x || 0) + (c.size?.width || 200)));
|
||||
const maxY = Math.max(...components.map((c) => (c.position?.y || 0) + (c.size?.height || 40)));
|
||||
|
||||
const padding = 40;
|
||||
return {
|
||||
width: Math.max(maxX + padding, 400),
|
||||
height: Math.max(maxY + padding, 300),
|
||||
};
|
||||
};
|
||||
|
||||
const dynamicSize = calculateDynamicSize();
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !isSaving && !open && onClose()}>
|
||||
<DialogContent className={`${modalSizeClasses[modalSize]} max-h-[90vh] p-0 gap-0`}>
|
||||
<DialogHeader className="px-6 py-4 border-b">
|
||||
<div className="flex items-center justify-between">
|
||||
<DialogTitle className="text-lg font-semibold">
|
||||
{initialData ? "데이터 수정" : "데이터 등록"}
|
||||
</DialogTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
저장 중...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4" />
|
||||
저장
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
disabled={isSaving}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="overflow-auto p-6">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</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,
|
||||
}}
|
||||
>
|
||||
{component.type === "widget" ? (
|
||||
<InteractiveScreenViewer
|
||||
component={component}
|
||||
allComponents={components}
|
||||
formData={formData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
}}
|
||||
hideLabel={false}
|
||||
/>
|
||||
) : (
|
||||
<DynamicComponentRenderer
|
||||
component={{
|
||||
...component,
|
||||
style: {
|
||||
...component.style,
|
||||
labelDisplay: true,
|
||||
},
|
||||
}}
|
||||
screenId={screenId}
|
||||
tableName={screenData.tableName}
|
||||
formData={formData}
|
||||
originalData={originalData}
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[fieldName]: value,
|
||||
}));
|
||||
}}
|
||||
mode={initialData ? "edit" : "create"}
|
||||
isInModal={true}
|
||||
isInteractive={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
화면에 컴포넌트가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user