2025-09-10 15:30:14 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
|
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
2025-09-19 15:47:35 +09:00
|
|
|
import {
|
|
|
|
|
AlertDialog,
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
} from "@/components/ui/alert-dialog";
|
2025-09-10 15:30:14 +09:00
|
|
|
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";
|
2025-09-19 15:47:35 +09:00
|
|
|
import { CheckCircle } from "lucide-react";
|
2025-09-10 15:30:14 +09:00
|
|
|
import { JsonRelationship } from "@/lib/api/dataflow";
|
|
|
|
|
|
|
|
|
|
interface SaveDiagramModalProps {
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
2025-09-19 15:47:35 +09:00
|
|
|
onSave: (diagramName: string) => Promise<{ success: boolean; error?: string }>;
|
2025-09-10 15:30:14 +09:00
|
|
|
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("");
|
2025-09-19 15:47:35 +09:00
|
|
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
|
|
|
|
const [savedDiagramName, setSavedDiagramName] = useState("");
|
2025-09-10 15:30:14 +09:00
|
|
|
|
|
|
|
|
// defaultName이 변경될 때마다 diagramName 업데이트
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setDiagramName(defaultName);
|
|
|
|
|
}, [defaultName]);
|
|
|
|
|
|
2025-09-19 15:47:35 +09:00
|
|
|
const handleSave = async () => {
|
2025-09-10 15:30:14 +09:00
|
|
|
const trimmedName = diagramName.trim();
|
|
|
|
|
|
|
|
|
|
if (!trimmedName) {
|
|
|
|
|
setNameError("관계도 이름을 입력해주세요.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trimmedName.length < 2) {
|
|
|
|
|
setNameError("관계도 이름은 2글자 이상이어야 합니다.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trimmedName.length > 100) {
|
|
|
|
|
setNameError("관계도 이름은 100글자를 초과할 수 없습니다.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setNameError("");
|
2025-09-19 15:47:35 +09:00
|
|
|
|
|
|
|
|
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("저장 중 오류가 발생했습니다.");
|
|
|
|
|
}
|
2025-09-10 15:30:14 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleClose = () => {
|
|
|
|
|
if (!isLoading) {
|
|
|
|
|
setDiagramName(defaultName);
|
|
|
|
|
setNameError("");
|
|
|
|
|
onClose();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-19 15:47:35 +09:00
|
|
|
const handleSuccessModalClose = () => {
|
|
|
|
|
setShowSuccessModal(false);
|
|
|
|
|
setSavedDiagramName("");
|
|
|
|
|
handleClose(); // 원래 모달도 닫기
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
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 (
|
2025-09-19 15:47:35 +09:00
|
|
|
<>
|
|
|
|
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
|
|
|
|
<DialogContent className="max-h-[80vh] max-w-2xl overflow-y-auto">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle className="text-lg font-semibold">📊 관계도 저장</DialogTitle>
|
|
|
|
|
</DialogHeader>
|
2025-09-10 15:30:14 +09:00
|
|
|
|
2025-09-19 15:47:35 +09:00
|
|
|
<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-red-500 focus:border-red-500" : ""}
|
|
|
|
|
/>
|
|
|
|
|
{nameError && <p className="text-sm text-red-600">{nameError}</p>}
|
2025-09-10 15:30:14 +09:00
|
|
|
</div>
|
2025-09-19 15:47:35 +09:00
|
|
|
|
|
|
|
|
{/* 관계 요약 정보 */}
|
|
|
|
|
<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-blue-600">{relationships.length}</div>
|
|
|
|
|
<div className="text-sm text-gray-600">관계 수</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<div className="text-2xl font-bold text-green-600">{connectedTables.length}</div>
|
|
|
|
|
<div className="text-sm text-gray-600">연결된 테이블</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-gray-600">연결된 컬럼</div>
|
2025-09-10 15:30:14 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-09-19 15:47:35 +09:00
|
|
|
{/* 연결된 테이블 목록 */}
|
|
|
|
|
{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-gray-600">
|
|
|
|
|
{relationship.fromTable} → {relationship.toTable}
|
|
|
|
|
</div>
|
2025-09-10 15:30:14 +09:00
|
|
|
</div>
|
2025-09-19 15:47:35 +09:00
|
|
|
<Badge variant="outline" className="text-xs">
|
|
|
|
|
{relationship.connectionType}
|
|
|
|
|
</Badge>
|
2025-09-10 15:30:14 +09:00
|
|
|
</div>
|
2025-09-19 15:47:35 +09:00
|
|
|
))}
|
|
|
|
|
</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>
|
2025-09-10 15:30:14 +09:00
|
|
|
</div>
|
|
|
|
|
)}
|
2025-09-19 15:47:35 +09:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<DialogFooter 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>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
|
|
|
|
{/* 저장 성공 알림 모달 */}
|
|
|
|
|
<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>
|
|
|
|
|
</>
|
2025-09-10 15:30:14 +09:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default SaveDiagramModal;
|