// 배치관리 전용 서비스 (기존 소스와 완전 분리) // 작성일: 2024-12-24 import { query, queryOne } from "../database/db"; import { PasswordEncryption } from "../utils/passwordEncryption"; import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory"; // 배치관리 전용 타입 정의 export interface BatchConnectionInfo { type: "internal" | "external"; id?: number; name: string; db_type?: string; } export interface BatchTableInfo { table_name: string; columns: BatchColumnInfo[]; description?: string | null; } export interface BatchColumnInfo { column_name: string; data_type: string; is_nullable?: string; column_default?: string | null; } export interface BatchApiResponse { success: boolean; data?: T; message?: string; error?: string; } export class BatchManagementService { /** * 배치관리용 연결 목록 조회 */ static async getAvailableConnections(): Promise< BatchApiResponse > { try { const connections: BatchConnectionInfo[] = []; // 내부 DB 추가 connections.push({ type: "internal", name: "내부 데이터베이스 (PostgreSQL)", db_type: "postgresql", }); // 활성화된 외부 DB 연결 조회 const externalConnections = await query<{ id: number; connection_name: string; db_type: string; description: string; }>( `SELECT id, connection_name, db_type, description FROM external_db_connections WHERE is_active = 'Y' ORDER BY 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: BatchTableInfo[] = []; if (connectionType === "internal") { // 내부 DB 테이블 조회 const result = await query<{ table_name: string }>( `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(`[BatchManagementService] getTableColumns 호출:`, { connectionType, connectionId, tableName, }); let columns: BatchColumnInfo[] = []; if (connectionType === "internal") { // 내부 DB 컬럼 조회 console.log( `[BatchManagementService] 내부 DB 컬럼 조회 시작: ${tableName}` ); const result = await query<{ column_name: string; data_type: string; is_nullable: string; column_default: string | null; }>( `SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 ORDER BY ordinal_position`, [tableName] ); console.log(`[BatchManagementService] 쿼리 결과:`, result); console.log(`[BatchManagementService] 내부 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( `[BatchManagementService] 외부 DB 컬럼 조회 시작: connectionId=${connectionId}, tableName=${tableName}` ); const columnsResult = await this.getExternalTableColumns( connectionId, tableName ); console.log( `[BatchManagementService] 외부 DB 컬럼 조회 결과:`, columnsResult ); if (columnsResult.success && columnsResult.data) { columns = columnsResult.data; } } console.log(`[BatchManagementService] 최종 컬럼 목록:`, columns); return { success: true, data: columns, message: `${columns.length}개의 컬럼을 조회했습니다.`, }; } catch (error) { console.error("[BatchManagementService] 컬럼 정보 조회 오류:", error); return { success: false, message: "컬럼 정보 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 외부 DB 테이블 목록 조회 (내부 구현) */ private static async getExternalTables( connectionId: number ): Promise> { try { // 연결 정보 조회 const connection = await queryOne( `SELECT * FROM external_db_connections WHERE id = $1`, [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( `[BatchManagementService] getExternalTableColumns 호출: connectionId=${connectionId}, tableName=${tableName}` ); // 연결 정보 조회 const connection = await queryOne( `SELECT * FROM external_db_connections WHERE id = $1`, [connectionId] ); if (!connection) { console.log( `[BatchManagementService] 연결 정보를 찾을 수 없음: connectionId=${connectionId}` ); return { success: false, message: "연결 정보를 찾을 수 없습니다.", }; } console.log(`[BatchManagementService] 연결 정보 조회 성공:`, { 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( `[BatchManagementService] 커넥터 생성 시작: db_type=${connection.db_type}` ); // 데이터베이스 타입에 따른 커넥터 생성 const connector = await DatabaseConnectorFactory.createConnector( connection.db_type, config, connectionId ); console.log( `[BatchManagementService] 커넥터 생성 완료, 컬럼 조회 시작: tableName=${tableName}` ); // 컬럼 정보 조회 console.log(`[BatchManagementService] connector.getColumns 호출 전`); const columns = await connector.getColumns(tableName); console.log(`[BatchManagementService] 원본 컬럼 조회 결과:`, columns); console.log( `[BatchManagementService] 원본 컬럼 개수:`, columns ? columns.length : "null/undefined" ); // 각 데이터베이스 커넥터의 반환 구조가 다르므로 통일된 구조로 변환 const standardizedColumns: BatchColumnInfo[] = columns.map((col: any) => { console.log(`[BatchManagementService] 컬럼 변환 중:`, 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( `[BatchManagementService] 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(`[BatchManagementService] 표준 구조로 변환:`, result); return result; } }); console.log( `[BatchManagementService] 표준화된 컬럼 목록:`, standardizedColumns ); return { success: true, data: standardizedColumns, message: "컬럼 정보를 조회했습니다.", }; } catch (error) { console.error( "[BatchManagementService] 외부 DB 컬럼 정보 조회 오류:", error ); console.error( "[BatchManagementService] 오류 스택:", error instanceof Error ? error.stack : "No stack trace" ); return { success: false, message: "컬럼 정보 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } }