ERP-node/frontend/components/dataflow/SaveDiagramModal.tsx

293 lines
11 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import {
ResizableDialog,
ResizableDialogContent,
ResizableDialogHeader,
ResizableDialogTitle,
ResizableDialogDescription,
ResizableDialogFooter,
} from "@/components/ui/resizable-dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CheckCircle } from "lucide-react";
import { JsonRelationship } from "@/lib/api/dataflow";
interface SaveDiagramModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (diagramName: string) => Promise<{ success: boolean; error?: string }>;
relationships: JsonRelationship[];
defaultName?: string;
isLoading?: boolean;
}
const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
isOpen,
onClose,
onSave,
relationships,
defaultName = "",
isLoading = false,
}) => {
const [diagramName, setDiagramName] = useState(defaultName);
const [nameError, setNameError] = useState("");
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [savedDiagramName, setSavedDiagramName] = useState("");
// defaultName이 변경될 때마다 diagramName 업데이트
useEffect(() => {
setDiagramName(defaultName);
}, [defaultName]);
const handleSave = async () => {
const trimmedName = diagramName.trim();
if (!trimmedName) {
setNameError("관계도 이름을 입력해주세요.");
return;
}
if (trimmedName.length < 2) {
setNameError("관계도 이름은 2글자 이상이어야 합니다.");
return;
}
if (trimmedName.length > 100) {
setNameError("관계도 이름은 100글자를 초과할 수 없습니다.");
return;
}
setNameError("");
try {
// 부모에게 저장 요청하고 결과 받기
const result = await onSave(trimmedName);
if (result.success) {
// 성공 시 성공 모달 표시
setSavedDiagramName(trimmedName);
setShowSuccessModal(true);
} else {
// 실패 시 에러 메시지 표시
if (
result.error?.includes("중복된 이름입니다") ||
result.error?.includes("중복") ||
result.error?.includes("duplicate") ||
result.error?.includes("already exists")
) {
setNameError("중복된 이름입니다.");
} else {
setNameError(result.error || "저장 중 오류가 발생했습니다.");
}
}
} catch (error) {
// 중복 에러가 아닌 경우만 콘솔에 로그 출력
const isDuplicateError =
error && typeof error === "object" && "response" in error && (error as any).response?.status === 409;
if (!isDuplicateError) {
console.error("저장 오류:", error);
}
setNameError("저장 중 오류가 발생했습니다.");
}
};
const handleClose = () => {
if (!isLoading) {
setDiagramName(defaultName);
setNameError("");
onClose();
}
};
const handleSuccessModalClose = () => {
setShowSuccessModal(false);
setSavedDiagramName("");
handleClose(); // 원래 모달도 닫기
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !isLoading) {
handleSave();
}
};
// 관련된 테이블 목록 추출
const connectedTables = Array.from(
new Set([...relationships.map((rel) => rel.fromTable), ...relationships.map((rel) => rel.toTable)]),
).sort();
return (
<>
<ResizableDialog open={isOpen} onOpenChange={handleClose}>
<ResizableDialogContent className="max-h-[80vh] max-w-2xl overflow-y-auto">
<ResizableDialogHeader>
<ResizableDialogTitle className="text-lg font-semibold">📊 </ResizableDialogTitle>
</ResizableDialogHeader>
<div className="space-y-6">
{/* 관계도 이름 입력 */}
<div className="space-y-2">
<Label htmlFor="diagram-name" className="text-sm font-medium">
*
</Label>
<Input
id="diagram-name"
value={diagramName}
onChange={(e) => {
setDiagramName(e.target.value);
if (nameError) setNameError("");
}}
onKeyPress={handleKeyPress}
placeholder="예: 사용자-부서 관계도"
disabled={isLoading}
className={nameError ? "border-destructive focus:border-destructive" : ""}
/>
{nameError && <p className="text-sm text-destructive">{nameError}</p>}
</div>
{/* 관계 요약 정보 */}
<div className="grid grid-cols-3 gap-4 rounded-lg bg-gray-50 p-4">
<div className="text-center">
<div className="text-2xl font-bold text-primary">{relationships.length}</div>
<div className="text-sm text-muted-foreground"> </div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">{connectedTables.length}</div>
<div className="text-sm text-muted-foreground"> </div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-purple-600">
{relationships.reduce((sum, rel) => sum + rel.fromColumns.length, 0)}
</div>
<div className="text-sm text-muted-foreground"> </div>
</div>
</div>
{/* 연결된 테이블 목록 */}
{connectedTables.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-sm"> </CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{connectedTables.map((table) => (
<Badge key={table} variant="outline" className="text-xs">
{table}
</Badge>
))}
</div>
</CardContent>
</Card>
)}
{/* 관계 목록 미리보기 */}
{relationships.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-sm"> </CardTitle>
</CardHeader>
<CardContent>
<div className="max-h-60 space-y-3 overflow-y-auto">
{relationships.map((relationship, index) => (
<div
key={relationship.id || index}
className="flex items-center justify-between rounded-lg border bg-white p-3 hover:bg-gray-50"
>
<div className="flex-1">
<div className="flex items-center gap-2 text-sm">
<Badge variant="secondary" className="text-xs">
{relationship.connectionType || "simple-key"}
</Badge>
<span className="font-medium">
{relationship.relationshipName || `${relationship.fromTable}${relationship.toTable}`}
</span>
</div>
<div className="mt-1 text-xs text-muted-foreground">
{relationship.fromTable} {relationship.toTable}
</div>
</div>
<Badge variant="outline" className="text-xs">
{relationship.connectionType}
</Badge>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* 관계가 없는 경우 안내 */}
{relationships.length === 0 && (
<div className="py-8 text-center text-gray-500">
<div className="mb-2 text-4xl">📭</div>
<div className="text-sm"> .</div>
<div className="mt-1 text-xs text-gray-400"> .</div>
</div>
)}
</div>
<ResizableDialogFooter className="flex gap-2">
<Button variant="outline" onClick={handleClose} disabled={isLoading}>
</Button>
<Button
onClick={handleSave}
disabled={isLoading || relationships.length === 0}
className="bg-blue-600 hover:bg-blue-700"
>
{isLoading ? (
<div className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent"></div>
...
</div>
) : (
"저장하기"
)}
</Button>
</ResizableDialogFooter>
</ResizableDialogContent>
</ResizableDialog>
{/* 저장 성공 알림 모달 */}
<AlertDialog open={showSuccessModal} onOpenChange={setShowSuccessModal}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<CheckCircle className="h-5 w-5 text-green-500" />
</AlertDialogTitle>
<AlertDialogDescription className="text-base">
<span className="font-medium text-green-600">{savedDiagramName}</span> .
<br />
<span className="mt-2 block text-sm text-gray-500">
.
</span>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction onClick={handleSuccessModalClose}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};
export default SaveDiagramModal;