From 61aac5c5c3764a6214f2637aa4013da4147f4a14 Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Fri, 19 Sep 2025 15:47:35 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B4=80=EA=B3=84=EB=8F=84=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=8B=9C=20=EB=AA=A8=EB=8B=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=A4=91=EB=B3=B5=20=EC=95=88=EB=82=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/dataflowDiagramController.ts | 47 ++- .../dataflow/ConnectionSetupModal.tsx | 163 +++++---- .../components/dataflow/DataFlowDesigner.tsx | 49 ++- .../components/dataflow/SaveDiagramModal.tsx | 326 +++++++++++------- frontend/lib/api/client.ts | 6 +- frontend/lib/api/dataflow.ts | 20 +- 6 files changed, 411 insertions(+), 200 deletions(-) diff --git a/backend-node/src/controllers/dataflowDiagramController.ts b/backend-node/src/controllers/dataflowDiagramController.ts index 7e955e78..e18ef615 100644 --- a/backend-node/src/controllers/dataflowDiagramController.ts +++ b/backend-node/src/controllers/dataflowDiagramController.ts @@ -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, diff --git a/frontend/components/dataflow/ConnectionSetupModal.tsx b/frontend/components/dataflow/ConnectionSetupModal.tsx index b5d5e89c..cf3c55c0 100644 --- a/frontend/components/dataflow/ConnectionSetupModal.tsx +++ b/frontend/components/dataflow/ConnectionSetupModal.tsx @@ -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 = ({ isOpen, @@ -61,6 +69,9 @@ export const ConnectionSetupModal: React.FC = ({ const [selectedFromColumns, setSelectedFromColumns] = useState([]); const [selectedToColumns, setSelectedToColumns] = useState([]); const [tableColumnsCache, setTableColumnsCache] = useState<{ [tableName: string]: ColumnInfo[] }>({}); + const [showSuccessModal, setShowSuccessModal] = useState(false); + const [createdConnectionName, setCreatedConnectionName] = useState(""); + const [pendingRelationshipData, setPendingRelationshipData] = useState(null); // 조건 관리 훅 사용 const { @@ -465,11 +476,10 @@ export const ConnectionSetupModal: React.FC = ({ }, }; - toast.success("관계가 생성되었습니다!"); - - // 부모 컴포넌트로 관계 데이터 전달 (DB 저장 없이) - onConfirm(relationshipData); - handleCancel(); // 모달 닫기 + // 성공 모달 표시를 위한 상태 설정 + setCreatedConnectionName(config.relationshipName); + setPendingRelationshipData(relationshipData); + setShowSuccessModal(true); }; const handleCancel = () => { @@ -482,6 +492,19 @@ export const ConnectionSetupModal: React.FC = ({ 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 = ({ if (!connection) return null; return ( - - - - - - 필드 연결 설정 - - + <> + + + + + + 필드 연결 설정 + + -
- {/* 기본 연결 설정 */} -
-
- - setConfig({ ...config, relationshipName: e.target.value })} - placeholder="employee_id_department_id_연결" - className="text-sm" - /> +
+ {/* 기본 연결 설정 */} +
+
+ + setConfig({ ...config, relationshipName: e.target.value })} + placeholder="employee_id_department_id_연결" + className="text-sm" + /> +
+ + {/* 연결 종류 선택 */} + + + {/* 조건부 연결을 위한 조건 설정 */} + {isConditionalConnection(config.connectionType) && ( + + )} + + {/* 연결 종류별 상세 설정 */} + {renderConnectionTypeSettings()}
- {/* 연결 종류 선택 */} - - - {/* 조건부 연결을 위한 조건 설정 */} - {isConditionalConnection(config.connectionType) && ( - - )} - - {/* 연결 종류별 상세 설정 */} - {renderConnectionTypeSettings()} -
- - - - - - -
+ + + + +
+
+ + + + + + 연결 생성 완료 + + + {createdConnectionName} 연결이 생성되었습니다. +
+ + 생성된 연결은 데이터플로우 다이어그램에서 확인할 수 있습니다. + +
+
+ + 확인 + +
+
+ ); }; diff --git a/frontend/components/dataflow/DataFlowDesigner.tsx b/frontend/components/dataflow/DataFlowDesigner.tsx index f3a5372f..daf2b454 100644 --- a/frontend/components/dataflow/DataFlowDesigner.tsx +++ b/frontend/components/dataflow/DataFlowDesigner.tsx @@ -606,10 +606,10 @@ export const DataFlowDesigner: React.FC = ({ // 관계도 저장 함수 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 = ({ 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); } diff --git a/frontend/components/dataflow/SaveDiagramModal.tsx b/frontend/components/dataflow/SaveDiagramModal.tsx index a2385efc..02ae7f99 100644 --- a/frontend/components/dataflow/SaveDiagramModal.tsx +++ b/frontend/components/dataflow/SaveDiagramModal.tsx @@ -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 = ({ }) => { 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 = ({ } 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 = ({ } }; + 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 = ({ ).sort(); return ( - - - - 📊 관계도 저장 - + <> + + + + 📊 관계도 저장 + -
- {/* 관계도 이름 입력 */} -
- - { - setDiagramName(e.target.value); - if (nameError) setNameError(""); - }} - onKeyPress={handleKeyPress} - placeholder="예: 사용자-부서 관계도" - disabled={isLoading} - className={nameError ? "border-red-500 focus:border-red-500" : ""} - /> - {nameError &&

{nameError}

} -
+
+ {/* 관계도 이름 입력 */} +
+ + { + setDiagramName(e.target.value); + if (nameError) setNameError(""); + }} + onKeyPress={handleKeyPress} + placeholder="예: 사용자-부서 관계도" + disabled={isLoading} + className={nameError ? "border-red-500 focus:border-red-500" : ""} + /> + {nameError &&

{nameError}

} +
- {/* 관계 요약 정보 */} -
-
-
{relationships.length}
-
관계 수
-
-
-
{connectedTables.length}
-
연결된 테이블
-
-
-
- {relationships.reduce((sum, rel) => sum + rel.fromColumns.length, 0)} + {/* 관계 요약 정보 */} +
+
+
{relationships.length}
+
관계 수
-
연결된 컬럼
-
-
- - {/* 연결된 테이블 목록 */} - {connectedTables.length > 0 && ( - - - 연결된 테이블 - - -
- {connectedTables.map((table) => ( - - {table} - - ))} +
+
{connectedTables.length}
+
연결된 테이블
+
+
+
+ {relationships.reduce((sum, rel) => sum + rel.fromColumns.length, 0)}
- - - )} +
연결된 컬럼
+
+
- {/* 관계 목록 미리보기 */} - {relationships.length > 0 && ( - - - 관계 목록 - - -
- {relationships.map((relationship, index) => ( -
-
-
- - {relationship.connectionType || "simple-key"} - - - {relationship.relationshipName || `${relationship.fromTable} → ${relationship.toTable}`} - -
-
- {relationship.fromTable} → {relationship.toTable} -
-
- - {relationship.connectionType} + {/* 연결된 테이블 목록 */} + {connectedTables.length > 0 && ( + + + 연결된 테이블 + + +
+ {connectedTables.map((table) => ( + + {table} -
- ))} -
- - - )} - - {/* 관계가 없는 경우 안내 */} - {relationships.length === 0 && ( -
-
📭
-
생성된 관계가 없습니다.
-
테이블을 추가하고 컬럼을 연결해서 관계를 생성해보세요.
-
- )} -
- - - -
+ + )} - - - -
+ + {/* 관계 목록 미리보기 */} + {relationships.length > 0 && ( + + + 관계 목록 + + +
+ {relationships.map((relationship, index) => ( +
+
+
+ + {relationship.connectionType || "simple-key"} + + + {relationship.relationshipName || `${relationship.fromTable} → ${relationship.toTable}`} + +
+
+ {relationship.fromTable} → {relationship.toTable} +
+
+ + {relationship.connectionType} + +
+ ))} +
+
+
+ )} + + {/* 관계가 없는 경우 안내 */} + {relationships.length === 0 && ( +
+
📭
+
생성된 관계가 없습니다.
+
테이블을 추가하고 컬럼을 연결해서 관계를 생성해보세요.
+
+ )} + + + + + + +
+
+ + {/* 저장 성공 알림 모달 */} + + + + + + 관계도 저장 완료 + + + {savedDiagramName} 관계도가 성공적으로 저장되었습니다. +
+ + 저장된 관계도는 관리 메뉴에서 확인하고 수정할 수 있습니다. + +
+
+ + 확인 + +
+
+ ); }; diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts index 3b6ff3b6..2660014f 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -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); } diff --git a/frontend/lib/api/dataflow.ts b/frontend/lib/api/dataflow.ts index 48a59529..d2acdf95 100644 --- a/frontend/lib/api/dataflow.ts +++ b/frontend/lib/api/dataflow.ts @@ -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; } }