// 배치관리 서비스 // 작성일: 2024-12-24 import { PrismaClient } from "@prisma/client"; import { BatchConfig, BatchMapping, BatchConfigFilter, BatchMappingRequest, BatchValidationResult, ApiResponse, ConnectionInfo, TableInfo, ColumnInfo, } from "../types/batchTypes"; import { ExternalDbConnectionService } from "./externalDbConnectionService"; import { DbConnectionManager } from "./dbConnectionManager"; const prisma = new PrismaClient(); export class BatchService { /** * 배치 설정 목록 조회 */ static async getBatchConfigs( filter: BatchConfigFilter ): Promise> { try { const where: any = {}; // 필터 조건 적용 if (filter.is_active) { where.is_active = filter.is_active; } if (filter.company_code) { where.company_code = filter.company_code; } // 검색 조건 적용 if (filter.search && filter.search.trim()) { where.OR = [ { batch_name: { contains: filter.search.trim(), mode: "insensitive", }, }, { description: { contains: filter.search.trim(), mode: "insensitive", }, }, ]; } const batchConfigs = await prisma.batch_configs.findMany({ where, include: { batch_mappings: true, }, orderBy: [{ is_active: "desc" }, { batch_name: "asc" }], }); return { success: true, data: batchConfigs as BatchConfig[], }; } catch (error) { console.error("배치 설정 목록 조회 오류:", error); return { success: false, message: "배치 설정 목록 조회에 실패했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 특정 배치 설정 조회 */ static async getBatchConfigById( id: number ): Promise> { try { const batchConfig = await prisma.batch_configs.findUnique({ where: { id }, include: { batch_mappings: { orderBy: [ { from_table_name: "asc" }, { from_column_name: "asc" }, { mapping_order: "asc" }, ], }, }, }); if (!batchConfig) { return { success: false, message: "배치 설정을 찾을 수 없습니다.", }; } return { success: true, data: batchConfig as BatchConfig, }; } catch (error) { console.error("배치 설정 조회 오류:", error); return { success: false, message: "배치 설정 조회에 실패했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 배치 설정 생성 */ static async createBatchConfig( data: BatchMappingRequest, userId?: string ): Promise> { try { // 매핑 유효성 검사 const validation = await this.validateBatchMappings(data.mappings); if (!validation.isValid) { return { success: false, message: "매핑 유효성 검사 실패", error: validation.errors.join(", "), }; } // 트랜잭션으로 배치 설정과 매핑 생성 const result = await prisma.$transaction(async (tx) => { // 배치 설정 생성 const batchConfig = await tx.batch_configs.create({ data: { batch_name: data.batch_name, description: data.description, cron_schedule: data.cron_schedule, created_by: userId, updated_by: userId, }, }); // 배치 매핑 생성 const mappings = await Promise.all( data.mappings.map((mapping, index) => tx.batch_mappings.create({ data: { batch_config_id: batchConfig.id, from_connection_type: mapping.from_connection_type, from_connection_id: mapping.from_connection_id, from_table_name: mapping.from_table_name, from_column_name: mapping.from_column_name, from_column_type: mapping.from_column_type, to_connection_type: mapping.to_connection_type, to_connection_id: mapping.to_connection_id, to_table_name: mapping.to_table_name, to_column_name: mapping.to_column_name, to_column_type: mapping.to_column_type, mapping_order: mapping.mapping_order || index + 1, created_by: userId, }, }) ) ); return { ...batchConfig, batch_mappings: mappings, }; }); return { success: true, data: result as BatchConfig, message: "배치 설정이 성공적으로 생성되었습니다.", }; } catch (error) { console.error("배치 설정 생성 오류:", error); return { success: false, message: "배치 설정 생성에 실패했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 배치 설정 수정 */ static async updateBatchConfig( id: number, data: Partial, userId?: string ): Promise> { try { // 기존 배치 설정 확인 const existingConfig = await prisma.batch_configs.findUnique({ where: { id }, include: { batch_mappings: true }, }); if (!existingConfig) { return { success: false, message: "배치 설정을 찾을 수 없습니다.", }; } // 매핑이 제공된 경우 유효성 검사 if (data.mappings) { const validation = await this.validateBatchMappings(data.mappings); if (!validation.isValid) { return { success: false, message: "매핑 유효성 검사 실패", error: validation.errors.join(", "), }; } } // 트랜잭션으로 업데이트 const result = await prisma.$transaction(async (tx) => { // 배치 설정 업데이트 const updateData: any = { updated_by: userId, }; if (data.batch_name) updateData.batch_name = data.batch_name; if (data.description !== undefined) updateData.description = data.description; if (data.cron_schedule) updateData.cron_schedule = data.cron_schedule; const batchConfig = await tx.batch_configs.update({ where: { id }, data: updateData, }); // 매핑이 제공된 경우 기존 매핑 삭제 후 새로 생성 if (data.mappings) { await tx.batch_mappings.deleteMany({ where: { batch_config_id: id }, }); const mappings = await Promise.all( data.mappings.map((mapping, index) => tx.batch_mappings.create({ data: { batch_config_id: id, from_connection_type: mapping.from_connection_type, from_connection_id: mapping.from_connection_id, from_table_name: mapping.from_table_name, from_column_name: mapping.from_column_name, from_column_type: mapping.from_column_type, to_connection_type: mapping.to_connection_type, to_connection_id: mapping.to_connection_id, to_table_name: mapping.to_table_name, to_column_name: mapping.to_column_name, to_column_type: mapping.to_column_type, mapping_order: mapping.mapping_order || index + 1, created_by: userId, }, }) ) ); return { ...batchConfig, batch_mappings: mappings, }; } else { return { ...batchConfig, batch_mappings: existingConfig.batch_mappings, }; } }); return { success: true, data: result as BatchConfig, message: "배치 설정이 성공적으로 수정되었습니다.", }; } catch (error) { console.error("배치 설정 수정 오류:", error); return { success: false, message: "배치 설정 수정에 실패했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 배치 설정 삭제 (논리 삭제) */ static async deleteBatchConfig( id: number, userId?: string ): Promise> { try { const existingConfig = await prisma.batch_configs.findUnique({ where: { id }, }); if (!existingConfig) { return { success: false, message: "배치 설정을 찾을 수 없습니다.", }; } await prisma.batch_configs.update({ where: { id }, data: { is_active: "N", updated_by: userId, }, }); return { success: true, message: "배치 설정이 성공적으로 삭제되었습니다.", }; } catch (error) { console.error("배치 설정 삭제 오류:", error); return { success: false, message: "배치 설정 삭제에 실패했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 사용 가능한 커넥션 목록 조회 */ static async getAvailableConnections(): Promise> { try { const connections: ConnectionInfo[] = []; // 내부 DB 추가 connections.push({ type: 'internal', name: 'Internal Database', db_type: 'postgresql', }); // 외부 DB 연결 조회 const externalConnections = await ExternalDbConnectionService.getConnections({ is_active: 'Y', }); if (externalConnections.success && externalConnections.data) { externalConnections.data.forEach((conn) => { connections.push({ type: 'external', id: conn.id, name: conn.connection_name, db_type: conn.db_type, }); }); } return { success: true, data: connections, }; } catch (error) { console.error("커넥션 목록 조회 오류:", error); return { success: false, message: "커넥션 목록 조회에 실패했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 특정 커넥션의 테이블 목록 조회 */ static async getTablesFromConnection( connectionType: 'internal' | 'external', connectionId?: number ): Promise> { try { let tables: string[] = []; if (connectionType === 'internal') { // 내부 DB 테이블 조회 const result = await prisma.$queryRaw>` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' ORDER BY table_name `; tables = result.map(row => row.table_name); } else if (connectionType === 'external' && connectionId) { // 외부 DB 테이블 조회 const tablesResult = await ExternalDbConnectionService.getTables(connectionId); if (tablesResult.success && tablesResult.data) { tables = tablesResult.data; } } return { success: true, data: tables, }; } catch (error) { console.error("테이블 목록 조회 오류:", error); return { success: false, message: "테이블 목록 조회에 실패했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 특정 테이블의 컬럼 정보 조회 */ static async getTableColumns( connectionType: 'internal' | 'external', tableName: string, connectionId?: number ): Promise> { try { let columns: ColumnInfo[] = []; if (connectionType === 'internal') { // 내부 DB 컬럼 조회 const result = await prisma.$queryRaw>` SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ${tableName} ORDER BY ordinal_position `; columns = result.map(row => ({ column_name: row.column_name, data_type: row.data_type, is_nullable: row.is_nullable === 'YES', column_default: row.column_default, })); } else if (connectionType === 'external' && connectionId) { // 외부 DB 컬럼 조회 const columnsResult = await ExternalDbConnectionService.getTableColumns( connectionId, tableName ); if (columnsResult.success && columnsResult.data) { columns = columnsResult.data.map(col => ({ column_name: col.column_name, data_type: col.data_type, is_nullable: col.is_nullable, column_default: col.column_default, })); } } return { success: true, data: columns, }; } catch (error) { console.error("컬럼 정보 조회 오류:", error); return { success: false, message: "컬럼 정보 조회에 실패했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 배치 매핑 유효성 검사 */ private static async validateBatchMappings( mappings: BatchMapping[] ): Promise { const errors: string[] = []; const warnings: string[] = []; if (!mappings || mappings.length === 0) { errors.push("최소 하나 이상의 매핑이 필요합니다."); return { isValid: false, errors, warnings }; } // n:1 매핑 검사 (여러 FROM이 같은 TO로 매핑되는 것 방지) const toMappings = new Map(); mappings.forEach((mapping, index) => { const toKey = `${mapping.to_connection_type}:${mapping.to_connection_id || 'internal'}:${mapping.to_table_name}:${mapping.to_column_name}`; if (toMappings.has(toKey)) { errors.push( `매핑 ${index + 1}: TO 컬럼 '${mapping.to_table_name}.${mapping.to_column_name}'에 중복 매핑이 있습니다. n:1 매핑은 허용되지 않습니다.` ); } else { toMappings.set(toKey, index); } }); // 1:n 매핑 경고 (같은 FROM에서 여러 TO로 매핑) const fromMappings = new Map(); mappings.forEach((mapping, index) => { const fromKey = `${mapping.from_connection_type}:${mapping.from_connection_id || 'internal'}:${mapping.from_table_name}:${mapping.from_column_name}`; if (!fromMappings.has(fromKey)) { fromMappings.set(fromKey, []); } fromMappings.get(fromKey)!.push(index); }); fromMappings.forEach((indices, fromKey) => { if (indices.length > 1) { const [, , tableName, columnName] = fromKey.split(':'); warnings.push( `FROM 컬럼 '${tableName}.${columnName}'에서 ${indices.length}개의 TO 컬럼으로 매핑됩니다. (1:n 매핑)` ); } }); return { isValid: errors.length === 0, errors, warnings, }; } }