import { PrismaClient } from "@prisma/client"; import { logger } from "../utils/logger"; const prisma = new PrismaClient(); // 테이블 관계 생성 데이터 타입 interface CreateTableRelationshipData { diagramId?: number; // 기존 관계도에 추가하는 경우 relationshipName: string; fromTableName: string; fromColumnName: string; toTableName: string; toColumnName: string; relationshipType: string; connectionType: string; companyCode: string; settings: any; createdBy: string; } // 테이블 관계 수정 데이터 타입 interface UpdateTableRelationshipData { relationshipName?: string; fromTableName?: string; fromColumnName?: string; toTableName?: string; toColumnName?: string; relationshipType?: string; connectionType?: string; settings?: any; updatedBy: string; } export class DataflowService { /** * 테이블 관계 생성 */ async createTableRelationship(data: CreateTableRelationshipData) { try { logger.info("DataflowService: 테이블 관계 생성 시작", data); // diagram_id 결정 로직 let diagramId = data.diagramId; if (!diagramId) { // 새로운 관계도인 경우, 새로운 diagram_id 생성 // 현재 최대 diagram_id + 1 const maxDiagramId = await prisma.table_relationships.findFirst({ where: { company_code: data.companyCode, }, orderBy: { diagram_id: "desc", }, select: { diagram_id: true, }, }); diagramId = (maxDiagramId?.diagram_id || 0) + 1; } // 중복 관계 확인 (같은 diagram_id 내에서) const existingRelationship = await prisma.table_relationships.findFirst({ where: { diagram_id: diagramId, from_table_name: data.fromTableName, from_column_name: data.fromColumnName, to_table_name: data.toTableName, to_column_name: data.toColumnName, company_code: data.companyCode, is_active: "Y", }, }); if (existingRelationship) { throw new Error( `이미 존재하는 관계입니다: ${data.fromTableName}.${data.fromColumnName} → ${data.toTableName}.${data.toColumnName}` ); } // 새 관계 생성 const relationship = await prisma.table_relationships.create({ data: { diagram_id: diagramId, relationship_name: data.relationshipName, from_table_name: data.fromTableName, from_column_name: data.fromColumnName, to_table_name: data.toTableName, to_column_name: data.toColumnName, relationship_type: data.relationshipType, connection_type: data.connectionType, company_code: data.companyCode, settings: data.settings, created_by: data.createdBy, updated_by: data.createdBy, }, }); logger.info( `DataflowService: 테이블 관계 생성 완료 - ID: ${relationship.relationship_id}, Diagram ID: ${relationship.diagram_id}` ); return relationship; } catch (error) { logger.error("DataflowService: 테이블 관계 생성 실패", error); throw error; } } /** * 회사별 테이블 관계 목록 조회 */ async getTableRelationships(companyCode: string) { try { logger.info( `DataflowService: 테이블 관계 목록 조회 시작 - 회사코드: ${companyCode}` ); // 관리자는 모든 회사의 관계를 볼 수 있음 const whereCondition: any = { is_active: "Y", }; if (companyCode !== "*") { whereCondition.company_code = companyCode; } const relationships = await prisma.table_relationships.findMany({ where: whereCondition, orderBy: { created_date: "desc", }, }); logger.info( `DataflowService: 테이블 관계 목록 조회 완료 - ${relationships.length}개` ); return relationships; } catch (error) { logger.error("DataflowService: 테이블 관계 목록 조회 실패", error); throw error; } } /** * 특정 테이블 관계 조회 */ async getTableRelationship(relationshipId: number, companyCode: string) { try { logger.info( `DataflowService: 테이블 관계 조회 시작 - ID: ${relationshipId}, 회사코드: ${companyCode}` ); const whereCondition: any = { relationship_id: relationshipId, is_active: "Y", }; // 관리자가 아닌 경우 회사코드 제한 if (companyCode !== "*") { whereCondition.company_code = companyCode; } const relationship = await prisma.table_relationships.findFirst({ where: whereCondition, }); if (relationship) { logger.info( `DataflowService: 테이블 관계 조회 완료 - ID: ${relationshipId}` ); } else { logger.warn( `DataflowService: 테이블 관계를 찾을 수 없음 - ID: ${relationshipId}` ); } return relationship; } catch (error) { logger.error("DataflowService: 테이블 관계 조회 실패", error); throw error; } } /** * 테이블 관계 수정 */ async updateTableRelationship( relationshipId: number, updateData: UpdateTableRelationshipData, companyCode: string ) { try { logger.info( `DataflowService: 테이블 관계 수정 시작 - ID: ${relationshipId}`, updateData ); // 기존 관계 확인 const existingRelationship = await this.getTableRelationship( relationshipId, companyCode ); if (!existingRelationship) { return null; } // 관계 수정 const relationship = await prisma.table_relationships.update({ where: { relationship_id: relationshipId, }, data: { ...updateData, updated_date: new Date(), }, }); logger.info( `DataflowService: 테이블 관계 수정 완료 - ID: ${relationshipId}` ); return relationship; } catch (error) { logger.error("DataflowService: 테이블 관계 수정 실패", error); throw error; } } /** * 테이블 관계 삭제 (소프트 삭제) */ async deleteTableRelationship(relationshipId: number, companyCode: string) { try { logger.info( `DataflowService: 테이블 관계 삭제 시작 - ID: ${relationshipId}, 회사코드: ${companyCode}` ); // 기존 관계 확인 const existingRelationship = await this.getTableRelationship( relationshipId, companyCode ); if (!existingRelationship) { return false; } // 소프트 삭제 (is_active = 'N') await prisma.table_relationships.update({ where: { relationship_id: relationshipId, }, data: { is_active: "N", updated_date: new Date(), }, }); logger.info( `DataflowService: 테이블 관계 삭제 완료 - ID: ${relationshipId}` ); return true; } catch (error) { logger.error("DataflowService: 테이블 관계 삭제 실패", error); throw error; } } /** * 특정 테이블과 관련된 모든 관계 조회 */ async getRelationshipsByTable(tableName: string, companyCode: string) { try { logger.info( `DataflowService: 테이블별 관계 조회 시작 - 테이블: ${tableName}, 회사코드: ${companyCode}` ); const whereCondition: any = { OR: [{ from_table_name: tableName }, { to_table_name: tableName }], is_active: "Y", }; // 관리자가 아닌 경우 회사코드 제한 if (companyCode !== "*") { whereCondition.company_code = companyCode; } const relationships = await prisma.table_relationships.findMany({ where: whereCondition, orderBy: { created_date: "desc", }, }); logger.info( `DataflowService: 테이블별 관계 조회 완료 - ${relationships.length}개` ); return relationships; } catch (error) { logger.error("DataflowService: 테이블별 관계 조회 실패", error); throw error; } } /** * 연결 타입별 관계 조회 */ async getRelationshipsByConnectionType( connectionType: string, companyCode: string ) { try { logger.info( `DataflowService: 연결타입별 관계 조회 시작 - 타입: ${connectionType}, 회사코드: ${companyCode}` ); const whereCondition: any = { connection_type: connectionType, is_active: "Y", }; // 관리자가 아닌 경우 회사코드 제한 if (companyCode !== "*") { whereCondition.company_code = companyCode; } const relationships = await prisma.table_relationships.findMany({ where: whereCondition, orderBy: { created_date: "desc", }, }); logger.info( `DataflowService: 연결타입별 관계 조회 완료 - ${relationships.length}개` ); return relationships; } catch (error) { logger.error("DataflowService: 연결타입별 관계 조회 실패", error); throw error; } } /** * 관계 통계 조회 */ async getRelationshipStats(companyCode: string) { try { logger.info( `DataflowService: 관계 통계 조회 시작 - 회사코드: ${companyCode}` ); const whereCondition: any = { is_active: "Y", }; // 관리자가 아닌 경우 회사코드 제한 if (companyCode !== "*") { whereCondition.company_code = companyCode; } // 전체 관계 수 const totalCount = await prisma.table_relationships.count({ where: whereCondition, }); // 관계 타입별 통계 const relationshipTypeStats = await prisma.table_relationships.groupBy({ by: ["relationship_type"], where: whereCondition, _count: { relationship_id: true, }, }); // 연결 타입별 통계 const connectionTypeStats = await prisma.table_relationships.groupBy({ by: ["connection_type"], where: whereCondition, _count: { relationship_id: true, }, }); const stats = { totalCount, relationshipTypeStats: relationshipTypeStats.map((stat) => ({ type: stat.relationship_type, count: stat._count.relationship_id, })), connectionTypeStats: connectionTypeStats.map((stat) => ({ type: stat.connection_type, count: stat._count.relationship_id, })), }; logger.info(`DataflowService: 관계 통계 조회 완료`, stats); return stats; } catch (error) { logger.error("DataflowService: 관계 통계 조회 실패", error); throw error; } } // ==================== 데이터 중계 관리 ==================== /** * 데이터 관계 연결 생성 */ async createDataLink(linkData: { relationshipId: number; fromTableName: string; fromColumnName: string; toTableName: string; toColumnName: string; connectionType: string; companyCode: string; bridgeData?: any; createdBy: string; }) { try { logger.info( `DataflowService: 데이터 연결 생성 시작 - 관계ID: ${linkData.relationshipId}` ); const bridge = await prisma.data_relationship_bridge.create({ data: { relationship_id: linkData.relationshipId, from_table_name: linkData.fromTableName, from_column_name: linkData.fromColumnName, to_table_name: linkData.toTableName, to_column_name: linkData.toColumnName, connection_type: linkData.connectionType, company_code: linkData.companyCode, bridge_data: linkData.bridgeData || {}, created_by: linkData.createdBy, }, }); logger.info( `DataflowService: 데이터 연결 생성 완료 - Bridge ID: ${bridge.bridge_id}` ); return bridge; } catch (error) { logger.error("DataflowService: 데이터 연결 생성 실패", error); throw error; } } /** * 관계별 연결된 데이터 조회 */ async getLinkedDataByRelationship( relationshipId: number, companyCode: string ) { try { logger.info( `DataflowService: 관계별 연결 데이터 조회 시작 - 관계ID: ${relationshipId}` ); const whereCondition: any = { relationship_id: relationshipId, is_active: "Y", }; // 관리자가 아닌 경우 회사코드 제한 if (companyCode !== "*") { whereCondition.company_code = companyCode; } const linkedData = await prisma.data_relationship_bridge.findMany({ where: whereCondition, orderBy: { created_at: "desc" }, include: { relationship: { select: { relationship_name: true, relationship_type: true, connection_type: true, }, }, }, }); logger.info( `DataflowService: 관계별 연결 데이터 조회 완료 - ${linkedData.length}건` ); return linkedData; } catch (error) { logger.error("DataflowService: 관계별 연결 데이터 조회 실패", error); throw error; } } /** * 특정 테이블의 연결된 데이터 조회 */ async getLinkedDataByTable( tableName: string, keyValue?: string, companyCode?: string ) { try { logger.info( `DataflowService: 테이블별 연결 데이터 조회 시작 - 테이블: ${tableName}` ); const whereCondition: any = { OR: [{ from_table_name: tableName }, { to_table_name: tableName }], is_active: "Y", }; // keyValue 파라미터는 더 이상 사용하지 않음 (key_value 필드 제거됨) // 회사코드 필터링 if (companyCode && companyCode !== "*") { whereCondition.company_code = companyCode; } const linkedData = await prisma.data_relationship_bridge.findMany({ where: whereCondition, orderBy: { created_at: "desc" }, include: { relationship: { select: { relationship_name: true, relationship_type: true, connection_type: true, }, }, }, }); logger.info( `DataflowService: 테이블별 연결 데이터 조회 완료 - ${linkedData.length}건` ); return linkedData; } catch (error) { logger.error("DataflowService: 테이블별 연결 데이터 조회 실패", error); throw error; } } /** * 데이터 연결 수정 */ async updateDataLink( bridgeId: number, updateData: { bridgeData?: any; updatedBy: string; }, companyCode: string ) { try { logger.info( `DataflowService: 데이터 연결 수정 시작 - Bridge ID: ${bridgeId}` ); const whereCondition: any = { bridge_id: bridgeId, is_active: "Y", }; // 관리자가 아닌 경우 회사코드 제한 if (companyCode !== "*") { whereCondition.company_code = companyCode; } const updatedBridge = await prisma.data_relationship_bridge.update({ where: whereCondition, data: { ...updateData, updated_at: new Date(), }, }); logger.info( `DataflowService: 데이터 연결 수정 완료 - Bridge ID: ${bridgeId}` ); return updatedBridge; } catch (error) { logger.error("DataflowService: 데이터 연결 수정 실패", error); throw error; } } /** * 데이터 연결 삭제 (소프트 삭제) */ async deleteDataLink( bridgeId: number, companyCode: string, deletedBy: string ) { try { logger.info( `DataflowService: 데이터 연결 삭제 시작 - Bridge ID: ${bridgeId}` ); const whereCondition: any = { bridge_id: bridgeId, is_active: "Y", }; // 관리자가 아닌 경우 회사코드 제한 if (companyCode !== "*") { whereCondition.company_code = companyCode; } await prisma.data_relationship_bridge.update({ where: whereCondition, data: { is_active: "N", updated_at: new Date(), updated_by: deletedBy, }, }); logger.info( `DataflowService: 데이터 연결 삭제 완료 - Bridge ID: ${bridgeId}` ); return true; } catch (error) { logger.error("DataflowService: 데이터 연결 삭제 실패", error); throw error; } } /** * 관계 삭제 시 연결된 모든 데이터도 삭제 */ async deleteAllLinkedDataByRelationship( relationshipId: number, companyCode: string, deletedBy: string ) { try { logger.info( `DataflowService: 관계별 모든 데이터 연결 삭제 시작 - 관계ID: ${relationshipId}` ); const whereCondition: any = { relationship_id: relationshipId, is_active: "Y", }; // 관리자가 아닌 경우 회사코드 제한 if (companyCode !== "*") { whereCondition.company_code = companyCode; } const result = await prisma.data_relationship_bridge.updateMany({ where: whereCondition, data: { is_active: "N", updated_at: new Date(), updated_by: deletedBy, }, }); logger.info( `DataflowService: 관계별 모든 데이터 연결 삭제 완료 - ${result.count}건` ); return result.count; } catch (error) { logger.error("DataflowService: 관계별 모든 데이터 연결 삭제 실패", error); throw error; } } // ==================== 테이블 데이터 조회 ==================== /** * 테이블 실제 데이터 조회 (페이징) */ async getTableData( tableName: string, page: number = 1, limit: number = 10, search: string = "", searchColumn: string = "", companyCode: string = "*" ) { try { logger.info(`DataflowService: 테이블 데이터 조회 시작 - ${tableName}`); // 테이블 존재 여부 확인 (정보 스키마 사용) const tableExists = await prisma.$queryRaw` SELECT table_name FROM information_schema.tables WHERE table_name = ${tableName.toLowerCase()} AND table_schema = 'public' `; if ( !tableExists || (Array.isArray(tableExists) && tableExists.length === 0) ) { throw new Error(`테이블 '${tableName}'이 존재하지 않습니다.`); } // 전체 데이터 개수 조회 let totalCountQuery = `SELECT COUNT(*) as total FROM "${tableName}"`; let dataQuery = `SELECT * FROM "${tableName}"`; // 검색 조건 추가 if (search && searchColumn) { const whereCondition = `WHERE "${searchColumn}" ILIKE '%${search}%'`; totalCountQuery += ` ${whereCondition}`; dataQuery += ` ${whereCondition}`; } // 페이징 처리 const offset = (page - 1) * limit; dataQuery += ` ORDER BY 1 LIMIT ${limit} OFFSET ${offset}`; // 실제 쿼리 실행 const [totalResult, dataResult] = await Promise.all([ prisma.$queryRawUnsafe(totalCountQuery), prisma.$queryRawUnsafe(dataQuery), ]); const total = Array.isArray(totalResult) && totalResult.length > 0 ? Number((totalResult[0] as any).total) : 0; const data = Array.isArray(dataResult) ? dataResult : []; const result = { data, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), hasNext: page < Math.ceil(total / limit), hasPrev: page > 1, }, }; logger.info( `DataflowService: 테이블 데이터 조회 완료 - ${tableName}, 총 ${total}건 중 ${data.length}건 조회` ); return result; } catch (error) { logger.error( `DataflowService: 테이블 데이터 조회 실패 - ${tableName}`, error ); throw error; } } /** * 관계도 그룹 목록 조회 (diagram_id별로 그룹화) */ async getDataFlowDiagrams( companyCode: string, page: number = 1, size: number = 20, searchTerm: string = "" ) { try { logger.info( `DataflowService: 관계도 목록 조회 시작 - ${companyCode}, page: ${page}, size: ${size}, search: ${searchTerm}` ); // diagram_id별로 그룹화하여 조회 const whereCondition = { company_code: companyCode, is_active: "Y", ...(searchTerm && { OR: [ { relationship_name: { contains: searchTerm, mode: "insensitive" as any, }, }, { from_table_name: { contains: searchTerm, mode: "insensitive" as any, }, }, { to_table_name: { contains: searchTerm, mode: "insensitive" as any, }, }, ], }), }; // diagram_id별로 그룹화된 데이터 조회 const relationships = await prisma.table_relationships.findMany({ where: whereCondition, select: { relationship_id: true, diagram_id: true, relationship_name: true, from_table_name: true, to_table_name: true, connection_type: true, relationship_type: true, created_date: true, created_by: true, updated_date: true, updated_by: true, }, orderBy: [{ diagram_id: "asc" }, { created_date: "desc" }], }); // diagram_id별로 그룹화 const diagramMap = new Map(); relationships.forEach((rel) => { const diagramId = rel.diagram_id; if (!diagramMap.has(diagramId)) { diagramMap.set(diagramId, { diagramId: diagramId, diagramName: rel.relationship_name, // 첫 번째 관계의 이름을 사용 connectionType: rel.connection_type, relationshipType: rel.relationship_type, tableCount: new Set(), relationshipCount: 0, createdAt: rel.created_date, createdBy: rel.created_by, updatedAt: rel.updated_date, updatedBy: rel.updated_by, tables: [], }); } const diagram = diagramMap.get(diagramId); diagram.tableCount.add(rel.from_table_name); diagram.tableCount.add(rel.to_table_name); diagram.relationshipCount++; // 최신 업데이트 시간 유지 if (rel.updated_date && rel.updated_date > diagram.updatedAt) { diagram.updatedAt = rel.updated_date; diagram.updatedBy = rel.updated_by; } }); // Set을 배열로 변환하고 테이블 개수 계산 const diagrams = Array.from(diagramMap.values()).map((diagram) => ({ ...diagram, tableCount: diagram.tableCount.size, tables: Array.from(diagram.tableCount), })); // 페이징 처리 const total = diagrams.length; const startIndex = (page - 1) * size; const endIndex = startIndex + size; const paginatedDiagrams = diagrams.slice(startIndex, endIndex); const result = { diagrams: paginatedDiagrams, total, page, size, totalPages: Math.ceil(total / size), hasNext: page < Math.ceil(total / size), hasPrev: page > 1, }; logger.info( `DataflowService: 관계도 목록 조회 완료 - 총 ${total}개 관계도 중 ${paginatedDiagrams.length}개 조회` ); return result; } catch (error) { logger.error("DataflowService: 관계도 목록 조회 실패", error); throw error; } } /** * 특정 관계도의 모든 관계 조회 */ async getDiagramRelationships(companyCode: string, diagramName: string) { try { logger.info( `DataflowService: 관계도 관계 조회 시작 - ${companyCode}, diagram: ${diagramName}` ); const relationships = await prisma.table_relationships.findMany({ where: { company_code: companyCode, relationship_name: diagramName, is_active: "Y", }, orderBy: { created_date: "asc", }, }); logger.info( `DataflowService: 관계도 관계 조회 완료 - ${diagramName}, ${relationships.length}개 관계` ); return relationships; } catch (error) { logger.error( `DataflowService: 관계도 관계 조회 실패 - ${diagramName}`, error ); throw error; } } /** * 관계도 복사 (diagram_id 기반) */ 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})`; } // 새로운 diagram_id 생성 const maxDiagramId = await prisma.table_relationships.findFirst({ where: { company_code: companyCode, }, orderBy: { diagram_id: "desc", }, select: { diagram_id: true, }, }); const newDiagramId = (maxDiagramId?.diagram_id || 0) + 1; // 트랜잭션으로 모든 관계 복사 const copiedRelationships = await prisma.$transaction( originalRelationships.map((rel) => prisma.table_relationships.create({ data: { diagram_id: newDiagramId, 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} (diagram_id: ${newDiagramId}), ${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; } } /** * diagram_id로 해당 관계도의 모든 관계 조회 */ async getDiagramRelationshipsByDiagramId( companyCode: string, diagramId: number ) { try { logger.info( `DataflowService: diagram_id로 관계도 관계 조회 - ${diagramId}` ); // diagram_id로 모든 관계 조회 const relationships = await prisma.table_relationships.findMany({ where: { diagram_id: diagramId, company_code: companyCode, is_active: "Y", }, orderBy: [{ relationship_id: "asc" }], }); logger.info( `DataflowService: diagram_id로 관계도 관계 조회 완료 - ${relationships.length}개 관계` ); return relationships.map((rel) => ({ ...rel, settings: rel.settings as any, })); } catch (error) { logger.error( `DataflowService: diagram_id로 관계도 관계 조회 실패 - ${diagramId}`, error ); throw error; } } /** * relationship_id로 해당 관계도의 모든 관계 조회 (하위 호환성 유지) */ async getDiagramRelationshipsByRelationshipId( companyCode: string, relationshipId: number ) { try { logger.info( `DataflowService: relationship_id로 관계도 관계 조회 - ${relationshipId}` ); // 먼저 해당 relationship_id의 diagram_id를 찾음 const targetRelationship = await prisma.table_relationships.findFirst({ where: { relationship_id: relationshipId, company_code: companyCode, is_active: "Y", }, select: { diagram_id: true, }, }); if (!targetRelationship) { throw new Error("해당 관계 ID를 찾을 수 없습니다."); } // diagram_id로 모든 관계 조회 return this.getDiagramRelationshipsByDiagramId( companyCode, targetRelationship.diagram_id ); } catch (error) { logger.error( `DataflowService: relationship_id로 관계도 관계 조회 실패 - ${relationshipId}`, error ); throw error; } } }