diff --git a/backend-node/src/controllers/dataflowController.ts b/backend-node/src/controllers/dataflowController.ts index ff69ce5c..e516b5ce 100644 --- a/backend-node/src/controllers/dataflowController.ts +++ b/backend-node/src/controllers/dataflowController.ts @@ -718,3 +718,111 @@ export async function getDiagramRelationships( res.status(500).json(response); } } + +/** + * 관계도 복사 + */ +export async function copyDiagram( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { diagramName } = req.params; + const companyCode = (req.user as any)?.company_code || "*"; + + if (!diagramName) { + const response: ApiResponse = { + success: false, + message: "관계도 이름이 필요합니다.", + error: { + code: "MISSING_DIAGRAM_NAME", + details: "diagramName 파라미터가 필요합니다.", + }, + }; + res.status(400).json(response); + return; + } + + const dataflowService = new DataflowService(); + const newDiagramName = await dataflowService.copyDiagram( + companyCode, + decodeURIComponent(diagramName) + ); + + const response: ApiResponse<{ newDiagramName: string }> = { + success: true, + message: "관계도가 성공적으로 복사되었습니다.", + data: { newDiagramName }, + }; + + res.status(200).json(response); + } catch (error) { + logger.error("관계도 복사 실패:", error); + const response: ApiResponse = { + success: false, + message: "관계도 복사에 실패했습니다.", + error: { + code: "DIAGRAM_COPY_FAILED", + details: + error instanceof Error + ? error.message + : "알 수 없는 오류가 발생했습니다.", + }, + }; + res.status(500).json(response); + } +} + +/** + * 관계도 삭제 + */ +export async function deleteDiagram( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { diagramName } = req.params; + const companyCode = (req.user as any)?.company_code || "*"; + + if (!diagramName) { + const response: ApiResponse = { + success: false, + message: "관계도 이름이 필요합니다.", + error: { + code: "MISSING_DIAGRAM_NAME", + details: "diagramName 파라미터가 필요합니다.", + }, + }; + res.status(400).json(response); + return; + } + + const dataflowService = new DataflowService(); + const deletedCount = await dataflowService.deleteDiagram( + companyCode, + decodeURIComponent(diagramName) + ); + + const response: ApiResponse<{ deletedCount: number }> = { + success: true, + message: "관계도가 성공적으로 삭제되었습니다.", + data: { deletedCount }, + }; + + res.status(200).json(response); + } catch (error) { + logger.error("관계도 삭제 실패:", error); + const response: ApiResponse = { + success: false, + message: "관계도 삭제에 실패했습니다.", + error: { + code: "DIAGRAM_DELETE_FAILED", + details: + error instanceof Error + ? error.message + : "알 수 없는 오류가 발생했습니다.", + }, + }; + res.status(500).json(response); + } +} diff --git a/backend-node/src/routes/dataflowRoutes.ts b/backend-node/src/routes/dataflowRoutes.ts index e84e1c0d..21c9bc3f 100644 --- a/backend-node/src/routes/dataflowRoutes.ts +++ b/backend-node/src/routes/dataflowRoutes.ts @@ -12,6 +12,8 @@ import { getTableData, getDataFlowDiagrams, getDiagramRelationships, + copyDiagram, + deleteDiagram, } from "../controllers/dataflowController"; const router = express.Router(); @@ -94,4 +96,16 @@ router.get("/diagrams", getDataFlowDiagrams); */ router.get("/diagrams/:diagramName/relationships", getDiagramRelationships); +/** + * 관계도 복사 + * POST /api/dataflow/diagrams/:diagramName/copy + */ +router.post("/diagrams/:diagramName/copy", copyDiagram); + +/** + * 관계도 삭제 + * DELETE /api/dataflow/diagrams/:diagramName + */ +router.delete("/diagrams/:diagramName", deleteDiagram); + export default router; diff --git a/backend-node/src/services/dataflowService.ts b/backend-node/src/services/dataflowService.ts index b30e2fa8..3958b109 100644 --- a/backend-node/src/services/dataflowService.ts +++ b/backend-node/src/services/dataflowService.ts @@ -889,4 +889,113 @@ export class DataflowService { throw error; } } + + /** + * 관계도 복사 + */ + async copyDiagram( + companyCode: string, + originalDiagramName: string + ): Promise { + try { + logger.info(`DataflowService: 관계도 복사 시작 - ${originalDiagramName}`); + + // 원본 관계도의 모든 관계 조회 + const originalRelationships = await prisma.table_relationships.findMany({ + where: { + company_code: companyCode, + relationship_name: originalDiagramName, + is_active: "Y", + }, + }); + + if (originalRelationships.length === 0) { + throw new Error("복사할 관계도를 찾을 수 없습니다."); + } + + // 새로운 관계도 이름 생성 (중복 검사) + let newDiagramName = `${originalDiagramName} (1)`; + let counter = 1; + + while (true) { + const existingDiagram = await prisma.table_relationships.findFirst({ + where: { + company_code: companyCode, + relationship_name: newDiagramName, + is_active: "Y", + }, + }); + + if (!existingDiagram) { + break; + } + + counter++; + newDiagramName = `${originalDiagramName} (${counter})`; + } + + // 트랜잭션으로 모든 관계 복사 + const copiedRelationships = await prisma.$transaction( + originalRelationships.map((rel) => + prisma.table_relationships.create({ + data: { + relationship_name: newDiagramName, + from_table_name: rel.from_table_name, + from_column_name: rel.from_column_name, + to_table_name: rel.to_table_name, + to_column_name: rel.to_column_name, + relationship_type: rel.relationship_type, + connection_type: rel.connection_type, + settings: rel.settings as any, + company_code: rel.company_code, + is_active: "Y", + created_by: rel.created_by, + updated_by: rel.updated_by, + }, + }) + ) + ); + + logger.info( + `DataflowService: 관계도 복사 완료 - ${originalDiagramName} → ${newDiagramName}, ${copiedRelationships.length}개 관계 복사` + ); + + return newDiagramName; + } catch (error) { + logger.error( + `DataflowService: 관계도 복사 실패 - ${originalDiagramName}`, + error + ); + throw error; + } + } + + /** + * 관계도 삭제 + */ + async deleteDiagram( + companyCode: string, + diagramName: string + ): Promise { + try { + logger.info(`DataflowService: 관계도 삭제 시작 - ${diagramName}`); + + // 관계도의 모든 관계 삭제 (하드 삭제) + const deleteResult = await prisma.table_relationships.deleteMany({ + where: { + company_code: companyCode, + relationship_name: diagramName, + }, + }); + + logger.info( + `DataflowService: 관계도 삭제 완료 - ${diagramName}, ${deleteResult.count}개 관계 삭제` + ); + + return deleteResult.count; + } catch (error) { + logger.error(`DataflowService: 관계도 삭제 실패 - ${diagramName}`, error); + throw error; + } + } } diff --git a/frontend/components/dataflow/DataFlowList.tsx b/frontend/components/dataflow/DataFlowList.tsx index ba2afdb2..5aa522fd 100644 --- a/frontend/components/dataflow/DataFlowList.tsx +++ b/frontend/components/dataflow/DataFlowList.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; @@ -12,6 +12,14 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { MoreHorizontal, Trash2, Copy, Plus, Search, Network, Database, Calendar, User } from "lucide-react"; import { DataFlowAPI, DataFlowDiagram } from "@/lib/api/dataflow"; import { toast } from "sonner"; @@ -30,49 +38,86 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig const [totalPages, setTotalPages] = useState(1); const [total, setTotal] = useState(0); + // 모달 상태 + const [showCopyModal, setShowCopyModal] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [selectedDiagramForAction, setSelectedDiagramForAction] = useState(null); + + // 목록 로드 함수 분리 + const loadDiagrams = useCallback(async () => { + try { + setLoading(true); + const response = await DataFlowAPI.getDataFlowDiagrams(currentPage, 20, searchTerm); + setDiagrams(response.diagrams || []); + setTotal(response.total || 0); + setTotalPages(Math.max(1, Math.ceil((response.total || 0) / 20))); + } catch (error) { + console.error("관계도 목록 조회 실패", error); + toast.error("관계도 목록을 불러오는데 실패했습니다."); + } finally { + setLoading(false); + } + }, [currentPage, searchTerm]); + // 관계도 목록 로드 useEffect(() => { - let abort = false; - const load = async () => { - try { - setLoading(true); - const response = await DataFlowAPI.getDataFlowDiagrams(currentPage, 20, searchTerm); - if (abort) return; - - setDiagrams(response.diagrams || []); - setTotal(response.total || 0); - setTotalPages(Math.max(1, Math.ceil((response.total || 0) / 20))); - } catch (error) { - console.error("관계도 목록 조회 실패", error); - setDiagrams([]); - setTotalPages(1); - setTotal(0); - toast.error("관계도 목록을 불러오는데 실패했습니다."); - } finally { - if (!abort) setLoading(false); - } - }; - load(); - return () => { - abort = true; - }; - }, [currentPage, searchTerm]); + loadDiagrams(); + }, [loadDiagrams]); const handleDiagramSelect = (diagram: DataFlowDiagram) => { onDiagramSelect(diagram); }; const handleDelete = (diagram: DataFlowDiagram) => { - if (confirm(`"${diagram.diagramName}" 관계도를 삭제하시겠습니까?`)) { - // 삭제 API 호출 - console.log("삭제:", diagram); - toast.info("삭제 기능은 아직 구현되지 않았습니다."); - } + setSelectedDiagramForAction(diagram); + setShowDeleteModal(true); }; const handleCopy = (diagram: DataFlowDiagram) => { - console.log("복사:", diagram); - toast.info("복사 기능은 아직 구현되지 않았습니다."); + setSelectedDiagramForAction(diagram); + setShowCopyModal(true); + }; + + // 복사 확인 + const handleConfirmCopy = async () => { + if (!selectedDiagramForAction) return; + + try { + setLoading(true); + const newDiagramName = await DataFlowAPI.copyDiagram(selectedDiagramForAction.diagramName); + toast.success(`관계도가 성공적으로 복사되었습니다: ${newDiagramName}`); + + // 목록 새로고침 + await loadDiagrams(); + } catch (error) { + console.error("관계도 복사 실패:", error); + toast.error("관계도 복사에 실패했습니다."); + } finally { + setLoading(false); + setShowCopyModal(false); + setSelectedDiagramForAction(null); + } + }; + + // 삭제 확인 + const handleConfirmDelete = async () => { + if (!selectedDiagramForAction) return; + + try { + setLoading(true); + const deletedCount = await DataFlowAPI.deleteDiagram(selectedDiagramForAction.diagramName); + toast.success(`관계도가 삭제되었습니다 (${deletedCount}개 관계 삭제)`); + + // 목록 새로고침 + await loadDiagrams(); + } catch (error) { + console.error("관계도 삭제 실패:", error); + toast.error("관계도 삭제에 실패했습니다."); + } finally { + setLoading(false); + setShowDeleteModal(false); + setSelectedDiagramForAction(null); + } }; // 연결 타입에 따른 배지 색상 @@ -291,6 +336,52 @@ export default function DataFlowList({ onDiagramSelect, selectedDiagram, onDesig )} + + {/* 복사 확인 모달 */} + + + + 관계도 복사 + + “{selectedDiagramForAction?.diagramName}” 관계도를 복사하시겠습니까? +
+ 새로운 관계도는 원본 이름 뒤에 (1), (2), (3)... 형태로 생성됩니다. +
+
+ + + + +
+
+ + {/* 삭제 확인 모달 */} + + + + 관계도 삭제 + + “{selectedDiagramForAction?.diagramName}” 관계도를 완전히 삭제하시겠습니까? +
+ + 이 작업은 되돌릴 수 없으며, 모든 관계 정보가 영구적으로 삭제됩니다. + +
+
+ + + + +
+
); } diff --git a/frontend/lib/api/dataflow.ts b/frontend/lib/api/dataflow.ts index 4b2cf695..2eee2398 100644 --- a/frontend/lib/api/dataflow.ts +++ b/frontend/lib/api/dataflow.ts @@ -395,4 +395,42 @@ export class DataFlowAPI { throw error; } } + + // 관계도 복사 + static async copyDiagram(diagramName: string): Promise { + try { + const encodedDiagramName = encodeURIComponent(diagramName); + const response = await apiClient.post>( + `/dataflow/diagrams/${encodedDiagramName}/copy`, + ); + + if (!response.data.success) { + throw new Error(response.data.message || "관계도 복사에 실패했습니다."); + } + + return response.data.data?.newDiagramName || ""; + } catch (error) { + console.error("관계도 복사 오류:", error); + throw error; + } + } + + // 관계도 삭제 + static async deleteDiagram(diagramName: string): Promise { + try { + const encodedDiagramName = encodeURIComponent(diagramName); + const response = await apiClient.delete>( + `/dataflow/diagrams/${encodedDiagramName}`, + ); + + if (!response.data.success) { + throw new Error(response.data.message || "관계도 삭제에 실패했습니다."); + } + + return response.data.data?.deletedCount || 0; + } catch (error) { + console.error("관계도 삭제 오류:", error); + throw error; + } + } }