// 배치관리 전용 외부 DB 서비스 // 기존 ExternalDbConnectionService와 분리하여 배치관리 시스템에 특화된 기능 제공 // 작성일: 2024-12-24 import prisma from "../config/database"; import { PasswordEncryption } from "../utils/passwordEncryption"; import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory"; import { ApiResponse, ColumnInfo, TableInfo } from "../types/batchTypes"; export class BatchExternalDbService { /** * 배치관리용 외부 DB 연결 목록 조회 */ static async getAvailableConnections(): Promise>> { try { const connections: Array<{ type: 'internal' | 'external'; id?: number; name: string; db_type?: string; }> = []; // 내부 DB 추가 connections.push({ type: 'internal', name: '내부 데이터베이스 (PostgreSQL)', db_type: 'postgresql' }); // 활성화된 외부 DB 연결 조회 const externalConnections = await prisma.external_db_connections.findMany({ where: { is_active: 'Y' }, select: { id: true, connection_name: true, db_type: true, description: true }, orderBy: { connection_name: 'asc' } }); // 외부 DB 연결 추가 externalConnections.forEach(conn => { connections.push({ type: 'external', id: conn.id, name: `${conn.connection_name} (${conn.db_type?.toUpperCase()})`, db_type: conn.db_type || undefined }); }); return { success: true, data: connections, message: `${connections.length}개의 연결을 조회했습니다.` }; } 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: TableInfo[] = []; 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 => ({ table_name: row.table_name, columns: [] })); } else if (connectionType === 'external' && connectionId) { // 외부 DB 테이블 조회 const tablesResult = await this.getExternalTables(connectionId); if (tablesResult.success && tablesResult.data) { tables = tablesResult.data; } } return { success: true, data: tables, message: `${tables.length}개의 테이블을 조회했습니다.` }; } catch (error) { console.error("배치관리 테이블 목록 조회 실패:", error); return { success: false, message: "테이블 목록 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }; } } /** * 배치관리용 테이블 컬럼 정보 조회 */ static async getTableColumns( connectionType: 'internal' | 'external', connectionId: number | undefined, tableName: string ): Promise> { try { console.log(`[BatchExternalDbService] getTableColumns 호출:`, { connectionType, connectionId, tableName }); let columns: ColumnInfo[] = []; if (connectionType === 'internal') { // 내부 DB 컬럼 조회 console.log(`[BatchExternalDbService] 내부 DB 컬럼 조회 시작: ${tableName}`); 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 `; console.log(`[BatchExternalDbService] 내부 DB 컬럼 조회 결과:`, result); columns = result.map(row => ({ column_name: row.column_name, data_type: row.data_type, is_nullable: row.is_nullable, column_default: row.column_default, })); } else if (connectionType === 'external' && connectionId) { // 외부 DB 컬럼 조회 console.log(`[BatchExternalDbService] 외부 DB 컬럼 조회 시작: connectionId=${connectionId}, tableName=${tableName}`); const columnsResult = await this.getExternalTableColumns(connectionId, tableName); console.log(`[BatchExternalDbService] 외부 DB 컬럼 조회 결과:`, columnsResult); if (columnsResult.success && columnsResult.data) { columns = columnsResult.data; } } console.log(`[BatchExternalDbService] 최종 컬럼 목록:`, columns); return { success: true, data: columns, message: `${columns.length}개의 컬럼을 조회했습니다.` }; } catch (error) { console.error("[BatchExternalDbService] 컬럼 정보 조회 오류:", error); return { success: false, message: "컬럼 정보 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }; } } /** * 외부 DB 테이블 목록 조회 (내부 구현) */ private static async getExternalTables(connectionId: number): Promise> { try { // 연결 정보 조회 const connection = await prisma.external_db_connections.findUnique({ where: { id: connectionId } }); if (!connection) { return { success: false, message: "연결 정보를 찾을 수 없습니다." }; } // 비밀번호 복호화 const decryptedPassword = PasswordEncryption.decrypt(connection.password); if (!decryptedPassword) { return { success: false, message: "비밀번호 복호화에 실패했습니다." }; } // 연결 설정 준비 const config = { host: connection.host, port: connection.port, database: connection.database_name, user: connection.username, password: decryptedPassword, connectionTimeoutMillis: connection.connection_timeout != null ? connection.connection_timeout * 1000 : undefined, queryTimeoutMillis: connection.query_timeout != null ? connection.query_timeout * 1000 : undefined, ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false }; // DatabaseConnectorFactory를 통한 테이블 목록 조회 const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, connectionId); const tables = await connector.getTables(); return { success: true, message: "테이블 목록을 조회했습니다.", data: tables }; } catch (error) { console.error("외부 DB 테이블 목록 조회 오류:", error); return { success: false, message: "테이블 목록 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }; } } /** * 외부 DB 테이블 컬럼 정보 조회 (내부 구현) */ private static async getExternalTableColumns(connectionId: number, tableName: string): Promise> { try { console.log(`[BatchExternalDbService] getExternalTableColumns 호출: connectionId=${connectionId}, tableName=${tableName}`); // 연결 정보 조회 const connection = await prisma.external_db_connections.findUnique({ where: { id: connectionId } }); if (!connection) { console.log(`[BatchExternalDbService] 연결 정보를 찾을 수 없음: connectionId=${connectionId}`); return { success: false, message: "연결 정보를 찾을 수 없습니다." }; } console.log(`[BatchExternalDbService] 연결 정보 조회 성공:`, { id: connection.id, connection_name: connection.connection_name, db_type: connection.db_type, host: connection.host, port: connection.port, database_name: connection.database_name }); // 비밀번호 복호화 const decryptedPassword = PasswordEncryption.decrypt(connection.password); // 연결 설정 준비 const config = { host: connection.host, port: connection.port, database: connection.database_name, user: connection.username, password: decryptedPassword, connectionTimeoutMillis: connection.connection_timeout != null ? connection.connection_timeout * 1000 : undefined, queryTimeoutMillis: connection.query_timeout != null ? connection.query_timeout * 1000 : undefined, ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false }; console.log(`[BatchExternalDbService] 커넥터 생성 시작: db_type=${connection.db_type}`); // 데이터베이스 타입에 따른 커넥터 생성 const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, connectionId); console.log(`[BatchExternalDbService] 커넥터 생성 완료, 컬럼 조회 시작: tableName=${tableName}`); // 컬럼 정보 조회 console.log(`[BatchExternalDbService] connector.getColumns 호출 전`); const columns = await connector.getColumns(tableName); console.log(`[BatchExternalDbService] 원본 컬럼 조회 결과:`, columns); console.log(`[BatchExternalDbService] 원본 컬럼 개수:`, columns ? columns.length : 'null/undefined'); // 각 데이터베이스 커넥터의 반환 구조가 다르므로 통일된 구조로 변환 const standardizedColumns: ColumnInfo[] = columns.map((col: any) => { console.log(`[BatchExternalDbService] 컬럼 변환 중:`, col); // MySQL/MariaDB 구조: {name, dataType, isNullable, defaultValue} (MySQLConnector만) if (col.name && col.dataType !== undefined) { const result = { column_name: col.name, data_type: col.dataType, is_nullable: col.isNullable ? 'YES' : 'NO', column_default: col.defaultValue || null, }; console.log(`[BatchExternalDbService] MySQL/MariaDB 구조로 변환:`, result); return result; } // PostgreSQL/Oracle/MSSQL/MariaDB 구조: {column_name, data_type, is_nullable, column_default} else { const result = { column_name: col.column_name || col.COLUMN_NAME, data_type: col.data_type || col.DATA_TYPE, is_nullable: col.is_nullable || col.IS_NULLABLE || (col.nullable === 'Y' ? 'YES' : 'NO'), column_default: col.column_default || col.COLUMN_DEFAULT || null, }; console.log(`[BatchExternalDbService] 표준 구조로 변환:`, result); return result; } }); console.log(`[BatchExternalDbService] 표준화된 컬럼 목록:`, standardizedColumns); // 빈 배열인 경우 경고 로그 if (!standardizedColumns || standardizedColumns.length === 0) { console.warn(`[BatchExternalDbService] 컬럼이 비어있음: connectionId=${connectionId}, tableName=${tableName}`); console.warn(`[BatchExternalDbService] 연결 정보:`, { db_type: connection.db_type, host: connection.host, port: connection.port, database_name: connection.database_name, username: connection.username }); // 테이블 존재 여부 확인 console.warn(`[BatchExternalDbService] 테이블 존재 여부 확인을 위해 테이블 목록 조회 시도`); try { const tables = await connector.getTables(); console.warn(`[BatchExternalDbService] 사용 가능한 테이블 목록:`, tables.map(t => t.table_name)); // 테이블명이 정확한지 확인 const tableExists = tables.some(t => t.table_name.toLowerCase() === tableName.toLowerCase()); console.warn(`[BatchExternalDbService] 테이블 존재 여부: ${tableExists}`); // 정확한 테이블명 찾기 const exactTable = tables.find(t => t.table_name.toLowerCase() === tableName.toLowerCase()); if (exactTable) { console.warn(`[BatchExternalDbService] 정확한 테이블명: ${exactTable.table_name}`); } // 모든 테이블명 출력 console.warn(`[BatchExternalDbService] 모든 테이블명:`, tables.map(t => `"${t.table_name}"`)); // 테이블명 비교 console.warn(`[BatchExternalDbService] 요청된 테이블명: "${tableName}"`); console.warn(`[BatchExternalDbService] 테이블명 비교 결과:`, tables.map(t => ({ table_name: t.table_name, matches: t.table_name.toLowerCase() === tableName.toLowerCase(), exact_match: t.table_name === tableName }))); // 정확한 테이블명으로 다시 시도 if (exactTable && exactTable.table_name !== tableName) { console.warn(`[BatchExternalDbService] 정확한 테이블명으로 다시 시도: ${exactTable.table_name}`); try { const correctColumns = await connector.getColumns(exactTable.table_name); console.warn(`[BatchExternalDbService] 정확한 테이블명으로 조회한 컬럼:`, correctColumns); } catch (correctError) { console.error(`[BatchExternalDbService] 정확한 테이블명으로 조회 실패:`, correctError); } } } catch (tableError) { console.error(`[BatchExternalDbService] 테이블 목록 조회 실패:`, tableError); } } return { success: true, data: standardizedColumns, message: "컬럼 정보를 조회했습니다." }; } catch (error) { console.error("[BatchExternalDbService] 외부 DB 컬럼 정보 조회 오류:", error); console.error("[BatchExternalDbService] 오류 스택:", error instanceof Error ? error.stack : 'No stack trace'); return { success: false, message: "컬럼 정보 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }; } } /** * 외부 DB 테이블에서 데이터 조회 */ static async getDataFromTable( connectionId: number, tableName: string, limit: number = 100 ): Promise> { try { console.log(`[BatchExternalDbService] 외부 DB 데이터 조회: connectionId=${connectionId}, tableName=${tableName}`); // 외부 DB 연결 정보 조회 const connection = await prisma.external_db_connections.findUnique({ where: { id: connectionId } }); if (!connection) { return { success: false, message: "외부 DB 연결을 찾을 수 없습니다." }; } // 패스워드 복호화 const decryptedPassword = PasswordEncryption.decrypt(connection.password); // DB 연결 설정 const config = { host: connection.host, port: connection.port, user: connection.username, password: decryptedPassword, database: connection.database_name, }; // DB 커넥터 생성 const connector = await DatabaseConnectorFactory.createConnector( connection.db_type || 'postgresql', config, connectionId ); // 데이터 조회 (DB 타입에 따라 쿼리 구문 변경) let query: string; const dbType = connection.db_type?.toLowerCase() || 'postgresql'; if (dbType === 'oracle') { query = `SELECT * FROM ${tableName} WHERE ROWNUM <= ${limit}`; } else { query = `SELECT * FROM ${tableName} LIMIT ${limit}`; } console.log(`[BatchExternalDbService] 실행할 쿼리: ${query}`); const result = await connector.executeQuery(query); console.log(`[BatchExternalDbService] 외부 DB 데이터 조회 완료: ${result.rows.length}개 레코드`); return { success: true, data: result.rows }; } catch (error) { console.error(`외부 DB 데이터 조회 오류 (connectionId: ${connectionId}, table: ${tableName}):`, error); return { success: false, message: "외부 DB 데이터 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }; } } /** * 외부 DB 테이블에서 특정 컬럼들만 조회 */ static async getDataFromTableWithColumns( connectionId: number, tableName: string, columns: string[], limit: number = 100 ): Promise> { try { console.log(`[BatchExternalDbService] 외부 DB 특정 컬럼 조회: connectionId=${connectionId}, tableName=${tableName}, columns=[${columns.join(', ')}]`); // 외부 DB 연결 정보 조회 const connection = await prisma.external_db_connections.findUnique({ where: { id: connectionId } }); if (!connection) { return { success: false, message: "외부 DB 연결을 찾을 수 없습니다." }; } // 패스워드 복호화 const decryptedPassword = PasswordEncryption.decrypt(connection.password); // DB 연결 설정 const config = { host: connection.host, port: connection.port, user: connection.username, password: decryptedPassword, database: connection.database_name, }; // DB 커넥터 생성 const connector = await DatabaseConnectorFactory.createConnector( connection.db_type || 'postgresql', config, connectionId ); // 데이터 조회 (DB 타입에 따라 쿼리 구문 변경) let query: string; const dbType = connection.db_type?.toLowerCase() || 'postgresql'; const columnList = columns.join(', '); if (dbType === 'oracle') { query = `SELECT ${columnList} FROM ${tableName} WHERE ROWNUM <= ${limit}`; } else { query = `SELECT ${columnList} FROM ${tableName} LIMIT ${limit}`; } console.log(`[BatchExternalDbService] 실행할 쿼리: ${query}`); const result = await connector.executeQuery(query); console.log(`[BatchExternalDbService] 외부 DB 특정 컬럼 조회 완료: ${result.rows.length}개 레코드`); return { success: true, data: result.rows }; } catch (error) { console.error(`외부 DB 특정 컬럼 조회 오류 (connectionId: ${connectionId}, table: ${tableName}):`, error); return { success: false, message: "외부 DB 특정 컬럼 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }; } } /** * 외부 DB 테이블에 데이터 삽입 */ static async insertDataToTable( connectionId: number, tableName: string, data: any[] ): Promise> { try { console.log(`[BatchExternalDbService] 외부 DB 데이터 삽입: connectionId=${connectionId}, tableName=${tableName}, ${data.length}개 레코드`); if (!data || data.length === 0) { return { success: true, data: { successCount: 0, failedCount: 0 } }; } // 외부 DB 연결 정보 조회 const connection = await prisma.external_db_connections.findUnique({ where: { id: connectionId } }); if (!connection) { return { success: false, message: "외부 DB 연결을 찾을 수 없습니다." }; } // 패스워드 복호화 const decryptedPassword = PasswordEncryption.decrypt(connection.password); // DB 연결 설정 const config = { host: connection.host, port: connection.port, user: connection.username, password: decryptedPassword, database: connection.database_name, }; // DB 커넥터 생성 const connector = await DatabaseConnectorFactory.createConnector( connection.db_type || 'postgresql', config, connectionId ); let successCount = 0; let failedCount = 0; // 각 레코드를 개별적으로 삽입 (UPSERT 방식으로 중복 처리) for (const record of data) { try { const columns = Object.keys(record); const values = Object.values(record); // 값들을 SQL 문자열로 변환 (타입별 처리) const formattedValues = values.map(value => { if (value === null || value === undefined) { return 'NULL'; } else if (value instanceof Date) { // Date 객체를 MySQL/MariaDB 형식으로 변환 return `'${value.toISOString().slice(0, 19).replace('T', ' ')}'`; } else if (typeof value === 'string') { // 문자열이 날짜 형식인지 확인 const dateRegex = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{2}\s+\d{4}\s+\d{2}:\d{2}:\d{2}/; if (dateRegex.test(value)) { // JavaScript Date 문자열을 MySQL 형식으로 변환 const date = new Date(value); return `'${date.toISOString().slice(0, 19).replace('T', ' ')}'`; } else { return `'${value.replace(/'/g, "''")}'`; // SQL 인젝션 방지를 위한 간단한 이스케이프 } } else if (typeof value === 'number') { return String(value); } else if (typeof value === 'boolean') { return value ? '1' : '0'; } else { // 기타 객체는 문자열로 변환 return `'${String(value).replace(/'/g, "''")}'`; } }).join(', '); // Primary Key 컬럼 추정 const primaryKeyColumn = columns.includes('id') ? 'id' : columns.includes('user_id') ? 'user_id' : columns[0]; // UPDATE SET 절 생성 (Primary Key 제외) const updateColumns = columns.filter(col => col !== primaryKeyColumn); let query: string; const dbType = connection.db_type?.toLowerCase() || 'mysql'; if (dbType === 'mysql' || dbType === 'mariadb') { // MySQL/MariaDB: ON DUPLICATE KEY UPDATE 사용 if (updateColumns.length > 0) { const updateSet = updateColumns.map(col => `${col} = VALUES(${col})`).join(', '); query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${formattedValues}) ON DUPLICATE KEY UPDATE ${updateSet}`; } else { // Primary Key만 있는 경우 IGNORE 사용 query = `INSERT IGNORE INTO ${tableName} (${columns.join(', ')}) VALUES (${formattedValues})`; } } else { // 다른 DB는 기본 INSERT 사용 query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${formattedValues})`; } await connector.executeQuery(query); successCount++; } catch (error) { console.error(`외부 DB 레코드 UPSERT 실패:`, error); failedCount++; } } console.log(`[BatchExternalDbService] 외부 DB 데이터 삽입 완료: 성공 ${successCount}개, 실패 ${failedCount}개`); return { success: true, data: { successCount, failedCount } }; } catch (error) { console.error(`외부 DB 데이터 삽입 오류 (connectionId: ${connectionId}, table: ${tableName}):`, error); return { success: false, message: "외부 DB 데이터 삽입 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }; } } }