관계도 저장 시 모달 및 이름 중복 안내 구현
This commit is contained in:
parent
11b1743f6b
commit
61aac5c5c3
|
|
@ -143,16 +143,36 @@ export const createDataflowDiagram = async (req: Request, res: Response) => {
|
|||
message: "관계도가 성공적으로 생성되었습니다.",
|
||||
});
|
||||
} 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({
|
||||
success: false,
|
||||
message: "이미 존재하는 관계도 이름입니다.",
|
||||
message: "중복된 이름입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 다른 에러만 로그 출력
|
||||
logger.error("관계도 생성 실패:", error);
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "관계도 생성 중 오류가 발생했습니다.",
|
||||
|
|
@ -214,6 +234,25 @@ export const updateDataflowDiagram = async (req: Request, res: Response) => {
|
|||
message: "관계도가 성공적으로 수정되었습니다.",
|
||||
});
|
||||
} 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);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
|
|
|
|||
|
|
@ -2,16 +2,24 @@
|
|||
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
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 { Input } from "@/components/ui/input";
|
||||
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 {
|
||||
ConnectionConfig,
|
||||
SimpleKeySettings,
|
||||
DataSaveSettings,
|
||||
ExternalCallSettings,
|
||||
SimpleExternalCallSettings,
|
||||
ConnectionSetupModalProps,
|
||||
} from "@/types/connectionTypes";
|
||||
|
|
@ -22,7 +30,7 @@ import { ConnectionTypeSelector } from "./connection/ConnectionTypeSelector";
|
|||
import { SimpleKeySettings as SimpleKeySettingsComponent } from "./connection/SimpleKeySettings";
|
||||
import { DataSaveSettings as DataSaveSettingsComponent } from "./connection/DataSaveSettings";
|
||||
import { SimpleExternalCallSettings as ExternalCallSettingsComponent } from "./connection/SimpleExternalCallSettings";
|
||||
import toast from "react-hot-toast";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
||||
isOpen,
|
||||
|
|
@ -61,6 +69,9 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
const [selectedFromColumns, setSelectedFromColumns] = useState<string[]>([]);
|
||||
const [selectedToColumns, setSelectedToColumns] = useState<string[]>([]);
|
||||
const [tableColumnsCache, setTableColumnsCache] = useState<{ [tableName: string]: ColumnInfo[] }>({});
|
||||
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||
const [createdConnectionName, setCreatedConnectionName] = useState("");
|
||||
const [pendingRelationshipData, setPendingRelationshipData] = useState<TableRelationship | null>(null);
|
||||
|
||||
// 조건 관리 훅 사용
|
||||
const {
|
||||
|
|
@ -465,11 +476,10 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
},
|
||||
};
|
||||
|
||||
toast.success("관계가 생성되었습니다!");
|
||||
|
||||
// 부모 컴포넌트로 관계 데이터 전달 (DB 저장 없이)
|
||||
onConfirm(relationshipData);
|
||||
handleCancel(); // 모달 닫기
|
||||
// 성공 모달 표시를 위한 상태 설정
|
||||
setCreatedConnectionName(config.relationshipName);
|
||||
setPendingRelationshipData(relationshipData);
|
||||
setShowSuccessModal(true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
|
|
@ -482,6 +492,19 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
onCancel();
|
||||
};
|
||||
|
||||
const handleSuccessModalClose = () => {
|
||||
setShowSuccessModal(false);
|
||||
setCreatedConnectionName("");
|
||||
|
||||
// 저장된 관계 데이터를 부모에게 전달
|
||||
if (pendingRelationshipData) {
|
||||
onConfirm(pendingRelationshipData);
|
||||
setPendingRelationshipData(null);
|
||||
}
|
||||
|
||||
handleCancel(); // 원래 모달도 닫기
|
||||
};
|
||||
|
||||
// 연결 종류별 설정 패널 렌더링
|
||||
const renderConnectionTypeSettings = () => {
|
||||
switch (config.connectionType) {
|
||||
|
|
@ -619,60 +642,82 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|||
if (!connection) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleCancel}>
|
||||
<DialogContent className="max-h-[80vh] max-w-3xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-lg">
|
||||
<Link className="h-4 w-4" />
|
||||
필드 연결 설정
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<>
|
||||
<Dialog open={isOpen} onOpenChange={handleCancel}>
|
||||
<DialogContent className="max-h-[80vh] max-w-3xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-lg">
|
||||
<Link className="h-4 w-4" />
|
||||
필드 연결 설정
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 기본 연결 설정 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="relationshipName">연결 이름</Label>
|
||||
<Input
|
||||
id="relationshipName"
|
||||
value={config.relationshipName}
|
||||
onChange={(e) => setConfig({ ...config, relationshipName: e.target.value })}
|
||||
placeholder="employee_id_department_id_연결"
|
||||
className="text-sm"
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
{/* 기본 연결 설정 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="relationshipName">연결 이름</Label>
|
||||
<Input
|
||||
id="relationshipName"
|
||||
value={config.relationshipName}
|
||||
onChange={(e) => setConfig({ ...config, relationshipName: e.target.value })}
|
||||
placeholder="employee_id_department_id_연결"
|
||||
className="text-sm"
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* 연결 종류 선택 */}
|
||||
<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>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} disabled={isButtonDisabled()}>
|
||||
연결 생성
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} disabled={isButtonDisabled()}>
|
||||
연결 생성
|
||||
</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">{createdConnectionName}</span> 연결이 생성되었습니다.
|
||||
<br />
|
||||
<span className="mt-2 block text-sm text-gray-500">
|
||||
생성된 연결은 데이터플로우 다이어그램에서 확인할 수 있습니다.
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction onClick={handleSuccessModalClose}>확인</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -606,10 +606,10 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
|
||||
// 관계도 저장 함수
|
||||
const handleSaveDiagram = useCallback(
|
||||
async (diagramName: string) => {
|
||||
async (diagramName: string): Promise<{ success: boolean; error?: string }> => {
|
||||
if (nodes.length === 0) {
|
||||
toast.error("저장할 테이블이 없습니다.");
|
||||
return;
|
||||
return { success: false, error: "저장할 테이블이 없습니다." };
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
|
|
@ -704,12 +704,49 @@ export const DataFlowDesigner: React.FC<DataFlowDesignerProps> = ({
|
|||
setCurrentDiagramName(newDiagram.diagram_name);
|
||||
}
|
||||
|
||||
toast.success(`관계도 "${diagramName}"가 성공적으로 저장되었습니다.`);
|
||||
setHasUnsavedChanges(false);
|
||||
setShowSaveModal(false);
|
||||
// 성공 모달은 SaveDiagramModal에서 처리하므로 여기서는 toast 제거
|
||||
return { success: true };
|
||||
} 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 {
|
||||
setIsSaving(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,27 @@
|
|||
|
||||
import React, { useState, useEffect } from "react";
|
||||
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 { 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) => void;
|
||||
onSave: (diagramName: string) => Promise<{ success: boolean; error?: string }>;
|
||||
relationships: JsonRelationship[];
|
||||
defaultName?: string;
|
||||
isLoading?: boolean;
|
||||
|
|
@ -28,13 +38,15 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
|||
}) => {
|
||||
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 = () => {
|
||||
const handleSave = async () => {
|
||||
const trimmedName = diagramName.trim();
|
||||
|
||||
if (!trimmedName) {
|
||||
|
|
@ -53,7 +65,39 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
|||
}
|
||||
|
||||
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 = () => {
|
||||
|
|
@ -64,6 +108,12 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleSuccessModalClose = () => {
|
||||
setShowSuccessModal(false);
|
||||
setSavedDiagramName("");
|
||||
handleClose(); // 원래 모달도 닫기
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !isLoading) {
|
||||
handleSave();
|
||||
|
|
@ -76,136 +126,160 @@ const SaveDiagramModal: React.FC<SaveDiagramModalProps> = ({
|
|||
).sort();
|
||||
|
||||
return (
|
||||
<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>
|
||||
<>
|
||||
<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>
|
||||
|
||||
<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>}
|
||||
</div>
|
||||
<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>}
|
||||
</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-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 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-sm text-gray-600">연결된 컬럼</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 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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
<div className="text-sm text-gray-600">연결된 컬럼</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 관계 목록 미리보기 */}
|
||||
{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>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{relationship.connectionType}
|
||||
{/* 연결된 테이블 목록 */}
|
||||
{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>
|
||||
))}
|
||||
</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>
|
||||
) : (
|
||||
"💾 저장하기"
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 관계 목록 미리보기 */}
|
||||
{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>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -126,9 +126,9 @@ apiClient.interceptors.response.use(
|
|||
|
||||
// 409 에러 (중복 데이터)는 조용하게 처리
|
||||
if (status === 409) {
|
||||
// 중복 검사 API는 완전히 조용하게 처리
|
||||
if (url?.includes("/check-duplicate")) {
|
||||
// 중복 검사는 정상적인 비즈니스 로직이므로 콘솔 출력 없음
|
||||
// 중복 검사 API와 관계도 저장은 완전히 조용하게 처리
|
||||
if (url?.includes("/check-duplicate") || url?.includes("/dataflow-diagrams")) {
|
||||
// 중복 검사와 관계도 중복 이름은 정상적인 비즈니스 로직이므로 콘솔 출력 없음
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -824,7 +824,15 @@ export class DataFlowAPI {
|
|||
|
||||
return response.data.data as JsonDataFlowDiagram;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -859,7 +867,15 @@ export class DataFlowAPI {
|
|||
|
||||
return response.data.data as JsonDataFlowDiagram;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue