관계도 저장 시 모달 및 이름 중복 안내 구현

This commit is contained in:
hyeonsu 2025-09-19 15:47:35 +09:00
parent 11b1743f6b
commit 61aac5c5c3
6 changed files with 411 additions and 200 deletions

View File

@ -143,16 +143,36 @@ export const createDataflowDiagram = async (req: Request, res: Response) => {
message: "관계도가 성공적으로 생성되었습니다.", message: "관계도가 성공적으로 생성되었습니다.",
}); });
} catch (error) { } catch (error) {
logger.error("관계도 생성 실패:", error); // 디버깅을 위한 에러 정보 출력
logger.error("에러 디버깅:", {
errorType: typeof error,
errorCode: (error as any)?.code,
errorMessage: error instanceof Error ? error.message : "Unknown error",
errorName: (error as any)?.name,
errorMeta: (error as any)?.meta,
});
// 중복 이름 에러 처리 // 중복 이름 에러인지 먼저 확인 (로그 출력 전에)
if (error instanceof Error && error.message.includes("unique constraint")) { const isDuplicateError =
(error && typeof error === "object" && (error as any).code === "P2002") || // Prisma unique constraint error code
(error instanceof Error &&
(error.message.includes("unique constraint") ||
error.message.includes("Unique constraint") ||
error.message.includes("duplicate key") ||
error.message.includes("UNIQUE constraint failed") ||
error.message.includes("unique_diagram_name_per_company")));
if (isDuplicateError) {
// 중복 에러는 콘솔에 로그 출력하지 않음
return res.status(409).json({ return res.status(409).json({
success: false, success: false,
message: "이미 존재하는 관계도 이름입니다.", message: "중복된 이름입니다.",
}); });
} }
// 다른 에러만 로그 출력
logger.error("관계도 생성 실패:", error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,
message: "관계도 생성 중 오류가 발생했습니다.", message: "관계도 생성 중 오류가 발생했습니다.",
@ -214,6 +234,25 @@ export const updateDataflowDiagram = async (req: Request, res: Response) => {
message: "관계도가 성공적으로 수정되었습니다.", message: "관계도가 성공적으로 수정되었습니다.",
}); });
} catch (error) { } catch (error) {
// 중복 이름 에러인지 먼저 확인 (로그 출력 전에)
const isDuplicateError =
(error && typeof error === "object" && (error as any).code === "P2002") || // Prisma unique constraint error code
(error instanceof Error &&
(error.message.includes("unique constraint") ||
error.message.includes("Unique constraint") ||
error.message.includes("duplicate key") ||
error.message.includes("UNIQUE constraint failed") ||
error.message.includes("unique_diagram_name_per_company")));
if (isDuplicateError) {
// 중복 에러는 콘솔에 로그 출력하지 않음
return res.status(409).json({
success: false,
message: "중복된 이름입니다.",
});
}
// 다른 에러만 로그 출력
logger.error("관계도 수정 실패:", error); logger.error("관계도 수정 실패:", error);
return res.status(500).json({ return res.status(500).json({
success: false, success: false,

View File

@ -2,16 +2,24 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Link } from "lucide-react"; import { Link, CheckCircle } from "lucide-react";
import { DataFlowAPI, TableRelationship, TableInfo, ColumnInfo, ConditionNode } from "@/lib/api/dataflow"; import { DataFlowAPI, TableRelationship, TableInfo, ColumnInfo, ConditionNode } from "@/lib/api/dataflow";
import { import {
ConnectionConfig, ConnectionConfig,
SimpleKeySettings, SimpleKeySettings,
DataSaveSettings, DataSaveSettings,
ExternalCallSettings,
SimpleExternalCallSettings, SimpleExternalCallSettings,
ConnectionSetupModalProps, ConnectionSetupModalProps,
} from "@/types/connectionTypes"; } from "@/types/connectionTypes";
@ -22,7 +30,7 @@ import { ConnectionTypeSelector } from "./connection/ConnectionTypeSelector";
import { SimpleKeySettings as SimpleKeySettingsComponent } from "./connection/SimpleKeySettings"; import { SimpleKeySettings as SimpleKeySettingsComponent } from "./connection/SimpleKeySettings";
import { DataSaveSettings as DataSaveSettingsComponent } from "./connection/DataSaveSettings"; import { DataSaveSettings as DataSaveSettingsComponent } from "./connection/DataSaveSettings";
import { SimpleExternalCallSettings as ExternalCallSettingsComponent } from "./connection/SimpleExternalCallSettings"; import { SimpleExternalCallSettings as ExternalCallSettingsComponent } from "./connection/SimpleExternalCallSettings";
import toast from "react-hot-toast"; import { toast } from "sonner";
export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
isOpen, isOpen,
@ -61,6 +69,9 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
const [selectedFromColumns, setSelectedFromColumns] = useState<string[]>([]); const [selectedFromColumns, setSelectedFromColumns] = useState<string[]>([]);
const [selectedToColumns, setSelectedToColumns] = useState<string[]>([]); const [selectedToColumns, setSelectedToColumns] = useState<string[]>([]);
const [tableColumnsCache, setTableColumnsCache] = useState<{ [tableName: string]: ColumnInfo[] }>({}); const [tableColumnsCache, setTableColumnsCache] = useState<{ [tableName: string]: ColumnInfo[] }>({});
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [createdConnectionName, setCreatedConnectionName] = useState("");
const [pendingRelationshipData, setPendingRelationshipData] = useState<TableRelationship | null>(null);
// 조건 관리 훅 사용 // 조건 관리 훅 사용
const { const {
@ -465,11 +476,10 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
}, },
}; };
toast.success("관계가 생성되었습니다!"); // 성공 모달 표시를 위한 상태 설정
setCreatedConnectionName(config.relationshipName);
// 부모 컴포넌트로 관계 데이터 전달 (DB 저장 없이) setPendingRelationshipData(relationshipData);
onConfirm(relationshipData); setShowSuccessModal(true);
handleCancel(); // 모달 닫기
}; };
const handleCancel = () => { const handleCancel = () => {
@ -482,6 +492,19 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
onCancel(); onCancel();
}; };
const handleSuccessModalClose = () => {
setShowSuccessModal(false);
setCreatedConnectionName("");
// 저장된 관계 데이터를 부모에게 전달
if (pendingRelationshipData) {
onConfirm(pendingRelationshipData);
setPendingRelationshipData(null);
}
handleCancel(); // 원래 모달도 닫기
};
// 연결 종류별 설정 패널 렌더링 // 연결 종류별 설정 패널 렌더링
const renderConnectionTypeSettings = () => { const renderConnectionTypeSettings = () => {
switch (config.connectionType) { switch (config.connectionType) {
@ -619,60 +642,82 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
if (!connection) return null; if (!connection) return null;
return ( return (
<Dialog open={isOpen} onOpenChange={handleCancel}> <>
<DialogContent className="max-h-[80vh] max-w-3xl overflow-y-auto"> <Dialog open={isOpen} onOpenChange={handleCancel}>
<DialogHeader> <DialogContent className="max-h-[80vh] max-w-3xl overflow-y-auto">
<DialogTitle className="flex items-center gap-2 text-lg"> <DialogHeader>
<Link className="h-4 w-4" /> <DialogTitle className="flex items-center gap-2 text-lg">
<Link className="h-4 w-4" />
</DialogTitle>
</DialogHeader> </DialogTitle>
</DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
{/* 기본 연결 설정 */} {/* 기본 연결 설정 */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<Label htmlFor="relationshipName"> </Label> <Label htmlFor="relationshipName"> </Label>
<Input <Input
id="relationshipName" id="relationshipName"
value={config.relationshipName} value={config.relationshipName}
onChange={(e) => setConfig({ ...config, relationshipName: e.target.value })} onChange={(e) => setConfig({ ...config, relationshipName: e.target.value })}
placeholder="employee_id_department_id_연결" placeholder="employee_id_department_id_연결"
className="text-sm" className="text-sm"
/> />
</div>
</div> </div>
{/* 연결 종류 선택 */}
<ConnectionTypeSelector config={config} onConfigChange={setConfig} />
{/* 조건부 연결을 위한 조건 설정 */}
{isConditionalConnection(config.connectionType) && (
<ConditionalSettings
conditions={conditions}
fromTableColumns={fromTableColumns}
onAddCondition={addCondition}
onAddGroupStart={addGroupStart}
onAddGroupEnd={addGroupEnd}
onUpdateCondition={updateCondition}
onRemoveCondition={removeCondition}
getCurrentGroupLevel={getCurrentGroupLevel}
/>
)}
{/* 연결 종류별 상세 설정 */}
{renderConnectionTypeSettings()}
</div> </div>
{/* 연결 종류 선택 */} <DialogFooter>
<ConnectionTypeSelector config={config} onConfigChange={setConfig} /> <Button variant="outline" onClick={handleCancel}>
{/* 조건부 연결을 위한 조건 설정 */} </Button>
{isConditionalConnection(config.connectionType) && ( <Button onClick={handleConfirm} disabled={isButtonDisabled()}>
<ConditionalSettings
conditions={conditions} </Button>
fromTableColumns={fromTableColumns} </DialogFooter>
onAddCondition={addCondition} </DialogContent>
onAddGroupStart={addGroupStart} </Dialog>
onAddGroupEnd={addGroupEnd} <AlertDialog open={showSuccessModal} onOpenChange={setShowSuccessModal}>
onUpdateCondition={updateCondition} <AlertDialogContent>
onRemoveCondition={removeCondition} <AlertDialogHeader>
getCurrentGroupLevel={getCurrentGroupLevel} <AlertDialogTitle className="flex items-center gap-2">
/> <CheckCircle className="h-5 w-5 text-green-500" />
)}
</AlertDialogTitle>
{/* 연결 종류별 상세 설정 */} <AlertDialogDescription className="text-base">
{renderConnectionTypeSettings()} <span className="font-medium text-green-600">{createdConnectionName}</span> .
</div> <br />
<span className="mt-2 block text-sm text-gray-500">
<DialogFooter> .
<Button variant="outline" onClick={handleCancel}> </span>
</AlertDialogDescription>
</Button> </AlertDialogHeader>
<Button onClick={handleConfirm} disabled={isButtonDisabled()}> <AlertDialogFooter>
<AlertDialogAction onClick={handleSuccessModalClose}></AlertDialogAction>
</Button> </AlertDialogFooter>
</DialogFooter> </AlertDialogContent>
</DialogContent> </AlertDialog>
</Dialog> </>
); );
}; };

View File

@ -606,10 +606,10 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
// 관계도 저장 함수 // 관계도 저장 함수
const handleSaveDiagram = useCallback( const handleSaveDiagram = useCallback(
async (diagramName: string) => { async (diagramName: string): Promise<{ success: boolean; error?: string }> => {
if (nodes.length === 0) { if (nodes.length === 0) {
toast.error("저장할 테이블이 없습니다."); toast.error("저장할 테이블이 없습니다.");
return; return { success: false, error: "저장할 테이블이 없습니다." };
} }
setIsSaving(true); setIsSaving(true);
@ -704,12 +704,49 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
setCurrentDiagramName(newDiagram.diagram_name); setCurrentDiagramName(newDiagram.diagram_name);
} }
toast.success(`관계도 "${diagramName}"가 성공적으로 저장되었습니다.`);
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
setShowSaveModal(false); // 성공 모달은 SaveDiagramModal에서 처리하므로 여기서는 toast 제거
return { success: true };
} catch (error) { } catch (error) {
console.error("관계도 저장 실패:", error); // 에러 메시지 분석
toast.error("관계도 저장 중 오류가 발생했습니다."); let errorMessage = "관계도 저장 중 오류가 발생했습니다.";
let isDuplicateError = false;
// Axios 에러 처리
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as any;
if (axiosError.response?.status === 409) {
// 중복 이름 에러 (409 Conflict)
errorMessage = "중복된 이름입니다.";
isDuplicateError = true;
} else if (axiosError.response?.data?.message) {
// 백엔드에서 제공한 에러 메시지 사용
if (axiosError.response.data.message.includes("중복된 이름입니다")) {
errorMessage = "중복된 이름입니다.";
isDuplicateError = true;
} else {
errorMessage = axiosError.response.data.message;
}
}
} else if (error instanceof Error) {
if (
error.message.includes("중복") ||
error.message.includes("duplicate") ||
error.message.includes("already exists")
) {
errorMessage = "중복된 이름입니다.";
isDuplicateError = true;
} else {
errorMessage = error.message;
}
}
// 중복 에러가 아닌 경우만 콘솔에 로그 출력
if (!isDuplicateError) {
console.error("관계도 저장 실패:", error);
}
return { success: false, error: errorMessage };
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }

View File

@ -2,17 +2,27 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CheckCircle } from "lucide-react";
import { JsonRelationship } from "@/lib/api/dataflow"; import { JsonRelationship } from "@/lib/api/dataflow";
interface SaveDiagramModalProps { interface SaveDiagramModalProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
onSave: (diagramName: string) => void; onSave: (diagramName: string) => Promise<{ success: boolean; error?: string }>;
relationships: JsonRelationship[]; relationships: JsonRelationship[];
defaultName?: string; defaultName?: string;
isLoading?: boolean; isLoading?: boolean;
@ -28,13 +38,15 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
}) => { }) => {
const [diagramName, setDiagramName] = useState(defaultName); const [diagramName, setDiagramName] = useState(defaultName);
const [nameError, setNameError] = useState(""); const [nameError, setNameError] = useState("");
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [savedDiagramName, setSavedDiagramName] = useState("");
// defaultName이 변경될 때마다 diagramName 업데이트 // defaultName이 변경될 때마다 diagramName 업데이트
useEffect(() => { useEffect(() => {
setDiagramName(defaultName); setDiagramName(defaultName);
}, [defaultName]); }, [defaultName]);
const handleSave = () => { const handleSave = async () => {
const trimmedName = diagramName.trim(); const trimmedName = diagramName.trim();
if (!trimmedName) { if (!trimmedName) {
@ -53,7 +65,39 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
} }
setNameError(""); setNameError("");
onSave(trimmedName);
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 = () => { const handleClose = () => {
@ -64,6 +108,12 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
} }
}; };
const handleSuccessModalClose = () => {
setShowSuccessModal(false);
setSavedDiagramName("");
handleClose(); // 원래 모달도 닫기
};
const handleKeyPress = (e: React.KeyboardEvent) => { const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !isLoading) { if (e.key === "Enter" && !isLoading) {
handleSave(); handleSave();
@ -76,136 +126,160 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
).sort(); ).sort();
return ( return (
<Dialog open={isOpen} onOpenChange={handleClose}> <>
<DialogContent className="max-h-[80vh] max-w-2xl overflow-y-auto"> <Dialog open={isOpen} onOpenChange={handleClose}>
<DialogHeader> <DialogContent className="max-h-[80vh] max-w-2xl overflow-y-auto">
<DialogTitle className="text-lg font-semibold">📊 </DialogTitle> <DialogHeader>
</DialogHeader> <DialogTitle className="text-lg font-semibold">📊 </DialogTitle>
</DialogHeader>
<div className="space-y-6"> <div className="space-y-6">
{/* 관계도 이름 입력 */} {/* 관계도 이름 입력 */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="diagram-name" className="text-sm font-medium"> <Label htmlFor="diagram-name" className="text-sm font-medium">
* *
</Label> </Label>
<Input <Input
id="diagram-name" id="diagram-name"
value={diagramName} value={diagramName}
onChange={(e) => { onChange={(e) => {
setDiagramName(e.target.value); setDiagramName(e.target.value);
if (nameError) setNameError(""); if (nameError) setNameError("");
}} }}
onKeyPress={handleKeyPress} onKeyPress={handleKeyPress}
placeholder="예: 사용자-부서 관계도" placeholder="예: 사용자-부서 관계도"
disabled={isLoading} disabled={isLoading}
className={nameError ? "border-red-500 focus:border-red-500" : ""} className={nameError ? "border-red-500 focus:border-red-500" : ""}
/> />
{nameError && <p className="text-sm text-red-600">{nameError}</p>} {nameError && <p className="text-sm text-red-600">{nameError}</p>}
</div> </div>
{/* 관계 요약 정보 */} {/* 관계 요약 정보 */}
<div className="grid grid-cols-3 gap-4 rounded-lg bg-gray-50 p-4"> <div className="grid grid-cols-3 gap-4 rounded-lg bg-gray-50 p-4">
<div className="text-center"> <div className="text-center">
<div className="text-2xl font-bold text-blue-600">{relationships.length}</div> <div className="text-2xl font-bold text-blue-600">{relationships.length}</div>
<div className="text-sm text-gray-600"> </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>
<div className="text-sm text-gray-600"> </div> <div className="text-center">
</div> <div className="text-2xl font-bold text-green-600">{connectedTables.length}</div>
</div> <div className="text-sm text-gray-600"> </div>
</div>
{/* 연결된 테이블 목록 */} <div className="text-center">
{connectedTables.length > 0 && ( <div className="text-2xl font-bold text-purple-600">
<Card> {relationships.reduce((sum, rel) => sum + rel.fromColumns.length, 0)}
<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> </div>
</CardContent> <div className="text-sm text-gray-600"> </div>
</Card> </div>
)} </div>
{/* 관계 목록 미리보기 */} {/* 연결된 테이블 목록 */}
{relationships.length > 0 && ( {connectedTables.length > 0 && (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-sm"> </CardTitle> <CardTitle className="text-sm"> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="max-h-60 space-y-3 overflow-y-auto"> <div className="flex flex-wrap gap-2">
{relationships.map((relationship, index) => ( {connectedTables.map((table) => (
<div <Badge key={table} variant="outline" className="text-xs">
key={relationship.id || index} {table}
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>
</div>
<Badge variant="outline" className="text-xs">
{relationship.connectionType}
</Badge> </Badge>
</div> ))}
))} </div>
</div> </CardContent>
</CardContent> </Card>
</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>
<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> {relationships.length > 0 && (
</Dialog> <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>
</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>
<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>
</>
); );
}; };

View File

@ -126,9 +126,9 @@ apiClient.interceptors.response.use(
// 409 에러 (중복 데이터)는 조용하게 처리 // 409 에러 (중복 데이터)는 조용하게 처리
if (status === 409) { if (status === 409) {
// 중복 검사 API 완전히 조용하게 처리 // 중복 검사 API와 관계도 저장은 완전히 조용하게 처리
if (url?.includes("/check-duplicate")) { if (url?.includes("/check-duplicate") || url?.includes("/dataflow-diagrams")) {
// 중복 검사 정상적인 비즈니스 로직이므로 콘솔 출력 없음 // 중복 검사와 관계도 중복 이름은 정상적인 비즈니스 로직이므로 콘솔 출력 없음
return Promise.reject(error); return Promise.reject(error);
} }

View File

@ -824,7 +824,15 @@ export class DataFlowAPI {
return response.data.data as JsonDataFlowDiagram; return response.data.data as JsonDataFlowDiagram;
} catch (error) { } catch (error) {
console.error("JSON 관계도 생성 오류:", error); // 409 에러(중복 이름)는 콘솔 로그 출력하지 않음
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as any;
if (axiosError.response?.status !== 409) {
console.error("JSON 관계도 생성 오류:", error);
}
} else {
console.error("JSON 관계도 생성 오류:", error);
}
throw error; throw error;
} }
} }
@ -859,7 +867,15 @@ export class DataFlowAPI {
return response.data.data as JsonDataFlowDiagram; return response.data.data as JsonDataFlowDiagram;
} catch (error) { } catch (error) {
console.error("JSON 관계도 수정 오류:", error); // 409 에러(중복 이름)는 콘솔 로그 출력하지 않음
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as any;
if (axiosError.response?.status !== 409) {
console.error("JSON 관계도 수정 오류:", error);
}
} else {
console.error("JSON 관계도 수정 오류:", error);
}
throw error; throw error;
} }
} }