// 외부 DB 연결 서비스 // 작성일: 2024-12-17 import { PrismaClient } from "@prisma/client"; import { ExternalDbConnection, ExternalDbConnectionFilter, ApiResponse, TableInfo, } from "../types/externalDbTypes"; import { PasswordEncryption } from "../utils/passwordEncryption"; import { DbConnectionManager } from "./dbConnectionManager"; const prisma = new PrismaClient(); export class ExternalDbConnectionService { /** * 외부 DB 연결 목록 조회 */ static async getConnections( filter: ExternalDbConnectionFilter ): Promise> { try { const where: any = {}; // 필터 조건 적용 if (filter.db_type) { where.db_type = filter.db_type; } 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 = [ { connection_name: { contains: filter.search.trim(), mode: "insensitive", }, }, { description: { contains: filter.search.trim(), mode: "insensitive", }, }, ]; } const connections = await prisma.external_db_connections.findMany({ where, orderBy: [{ is_active: "desc" }, { connection_name: "asc" }], }); // 비밀번호는 반환하지 않음 (보안) const safeConnections = connections.map((conn) => ({ ...conn, password: "***ENCRYPTED***", // 실제 비밀번호 대신 마스킹 description: conn.description || undefined, })) as ExternalDbConnection[]; return { success: true, data: safeConnections, message: `${connections.length}개의 연결 설정을 조회했습니다.`, }; } catch (error) { console.error("외부 DB 연결 목록 조회 실패:", error); return { success: false, message: "연결 목록 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 특정 외부 DB 연결 조회 */ static async getConnectionById( id: number ): Promise> { try { const connection = await prisma.external_db_connections.findUnique({ where: { id }, }); if (!connection) { return { success: false, message: "해당 연결 설정을 찾을 수 없습니다.", }; } // 비밀번호는 반환하지 않음 (보안) const safeConnection = { ...connection, password: "***ENCRYPTED***", description: connection.description || undefined, } as ExternalDbConnection; return { success: true, data: safeConnection, message: "연결 설정을 조회했습니다.", }; } catch (error) { console.error("외부 DB 연결 조회 실패:", error); return { success: false, message: "연결 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 새 외부 DB 연결 생성 */ static async createConnection( data: ExternalDbConnection ): Promise> { try { // 데이터 검증 this.validateConnectionData(data); // 연결명 중복 확인 const existingConnection = await prisma.external_db_connections.findFirst( { where: { connection_name: data.connection_name, company_code: data.company_code, }, } ); if (existingConnection) { return { success: false, message: "이미 존재하는 연결명입니다.", }; } // 비밀번호 암호화 const encryptedPassword = PasswordEncryption.encrypt(data.password); const newConnection = await prisma.external_db_connections.create({ data: { connection_name: data.connection_name, description: data.description, db_type: data.db_type, host: data.host, port: data.port, database_name: data.database_name, username: data.username, password: encryptedPassword, connection_timeout: data.connection_timeout, query_timeout: data.query_timeout, max_connections: data.max_connections, ssl_enabled: data.ssl_enabled, ssl_cert_path: data.ssl_cert_path, connection_options: data.connection_options as any, company_code: data.company_code, is_active: data.is_active, created_by: data.created_by, updated_by: data.updated_by, created_date: new Date(), updated_date: new Date(), }, }); // 비밀번호는 반환하지 않음 const safeConnection = { ...newConnection, password: "***ENCRYPTED***", description: newConnection.description || undefined, } as ExternalDbConnection; return { success: true, data: safeConnection, message: "연결 설정이 생성되었습니다.", }; } catch (error) { console.error("외부 DB 연결 생성 실패:", error); return { success: false, message: "연결 생성 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 외부 DB 연결 수정 */ static async updateConnection( id: number, data: Partial ): Promise> { try { // 기존 연결 확인 const existingConnection = await prisma.external_db_connections.findUnique({ where: { id }, }); if (!existingConnection) { return { success: false, message: "해당 연결 설정을 찾을 수 없습니다.", }; } // 연결명 중복 확인 (자신 제외) if (data.connection_name) { const duplicateConnection = await prisma.external_db_connections.findFirst({ where: { connection_name: data.connection_name, company_code: data.company_code || existingConnection.company_code, id: { not: id }, }, }); if (duplicateConnection) { return { success: false, message: "이미 존재하는 연결명입니다.", }; } } // 업데이트 데이터 준비 const updateData: any = { ...data, updated_date: new Date(), }; // 비밀번호가 변경된 경우 암호화 if (data.password && data.password !== "***ENCRYPTED***") { updateData.password = PasswordEncryption.encrypt(data.password); } else { // 비밀번호 필드 제거 (변경하지 않음) delete updateData.password; } const updatedConnection = await prisma.external_db_connections.update({ where: { id }, data: updateData, }); // 비밀번호는 반환하지 않음 const safeConnection = { ...updatedConnection, password: "***ENCRYPTED***", description: updatedConnection.description || undefined, } as ExternalDbConnection; return { success: true, data: safeConnection, message: "연결 설정이 수정되었습니다.", }; } catch (error) { console.error("외부 DB 연결 수정 실패:", error); return { success: false, message: "연결 수정 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 외부 DB 연결 삭제 (물리 삭제) */ static async deleteConnection(id: number): Promise> { try { const existingConnection = await prisma.external_db_connections.findUnique({ where: { id }, }); if (!existingConnection) { return { success: false, message: "해당 연결 설정을 찾을 수 없습니다.", }; } // 물리 삭제 (실제 데이터 삭제) await prisma.external_db_connections.delete({ where: { id }, }); return { success: true, message: "연결 설정이 삭제되었습니다.", }; } catch (error) { console.error("외부 DB 연결 삭제 실패:", error); return { success: false, message: "연결 삭제 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류", }; } } /** * 데이터베이스 연결 테스트 (ID 기반) */ static async testConnectionById( id: number ): Promise { try { // 저장된 연결 정보 조회 const connection = await prisma.external_db_connections.findUnique({ where: { id } }); if (!connection) { return { success: false, message: "연결 정보를 찾을 수 없습니다.", error: { code: "CONNECTION_NOT_FOUND", details: `ID ${id}에 해당하는 연결 정보가 없습니다.` } }; } // 비밀번호 복호화 const decryptedPassword = await this.getDecryptedPassword(id); if (!decryptedPassword) { return { success: false, message: "비밀번호 복호화에 실패했습니다.", error: { code: "DECRYPTION_FAILED", details: "저장된 비밀번호를 복호화할 수 없습니다." } }; } // 연결 설정 준비 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 }; // DbConnectionManager를 통한 연결 테스트 return await DbConnectionManager.testConnection(id, connection.db_type, config); } catch (error) { return { success: false, message: "연결 테스트 중 오류가 발생했습니다.", error: { code: "TEST_ERROR", details: error instanceof Error ? error.message : "알 수 없는 오류", }, }; } } /** * 바이트 크기를 읽기 쉬운 형태로 변환 */ private static formatBytes(bytes: number): string { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; } /** * 연결 데이터 검증 */ private static validateConnectionData(data: ExternalDbConnection): void { const requiredFields = [ "connection_name", "db_type", "host", "port", "database_name", "username", "password", "company_code", ]; for (const field of requiredFields) { if (!data[field as keyof ExternalDbConnection]) { throw new Error(`필수 필드가 누락되었습니다: ${field}`); } } // 포트 번호 유효성 검사 if (data.port < 1 || data.port > 65535) { throw new Error("유효하지 않은 포트 번호입니다. (1-65535)"); } // DB 타입 유효성 검사 const validDbTypes = ["mysql", "postgresql", "oracle", "mssql", "sqlite"]; if (!validDbTypes.includes(data.db_type)) { throw new Error("지원하지 않는 DB 타입입니다."); } } /** * 저장된 연결의 실제 비밀번호 조회 (내부용) */ static async getDecryptedPassword(id: number): Promise { try { const connection = await prisma.external_db_connections.findUnique({ where: { id }, select: { password: true }, }); if (!connection) { return null; } return PasswordEncryption.decrypt(connection.password); } catch (error) { console.error("비밀번호 복호화 실패:", error); return null; } } /** * SQL 쿼리 실행 */ static async executeQuery( id: number, query: string ): Promise> { try { // 연결 정보 조회 console.log("연결 정보 조회 시작:", { id }); const connection = await prisma.external_db_connections.findUnique({ where: { id } }); console.log("조회된 연결 정보:", connection); if (!connection) { console.log("연결 정보를 찾을 수 없음:", { id }); return { success: false, message: "연결 정보를 찾을 수 없습니다." }; } // 비밀번호 복호화 const decryptedPassword = await this.getDecryptedPassword(id); 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 }; // DbConnectionManager를 통한 쿼리 실행 const result = await DbConnectionManager.executeQuery(id, connection.db_type, config, query); return { success: true, message: "쿼리가 성공적으로 실행되었습니다.", data: result.rows }; } catch (error) { console.error("쿼리 실행 오류:", error); return { success: false, message: "쿼리 실행 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }; } } /** * PostgreSQL 쿼리 실행 */ private static async executePostgreSQLQuery( connection: any, password: string, query: string ): Promise> { const { Client } = await import("pg"); const client = new Client({ host: connection.host, port: connection.port, database: connection.database_name, user: connection.username, password: password, connectionTimeoutMillis: (connection.connection_timeout || 30) * 1000, ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false, }); try { await client.connect(); console.log("DB 연결 정보:", { host: connection.host, port: connection.port, database: connection.database_name, user: connection.username }); console.log("쿼리 실행:", query); const result = await client.query(query); console.log("쿼리 결과:", result.rows); await client.end(); return { success: true, message: "쿼리가 성공적으로 실행되었습니다.", data: result.rows }; } catch (error) { try { await client.end(); } catch (endError) { // 연결 종료 오류는 무시 } return { success: false, message: "쿼리 실행 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }; } } /** * 데이터베이스 테이블 목록 조회 */ static async getTables(id: number): Promise> { try { // 연결 정보 조회 const connection = await prisma.external_db_connections.findUnique({ where: { id } }); if (!connection) { return { success: false, message: "연결 정보를 찾을 수 없습니다." }; } // 비밀번호 복호화 const decryptedPassword = await this.getDecryptedPassword(id); 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 }; // DbConnectionManager를 통한 테이블 목록 조회 const tables = await DbConnectionManager.getTables(id, connection.db_type, config); return { success: true, message: "테이블 목록을 조회했습니다.", data: tables }; } catch (error) { console.error("테이블 목록 조회 오류:", error); return { success: false, message: "테이블 목록 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }; } } /** * PostgreSQL 테이블 목록 조회 */ private static async getPostgreSQLTables( connection: any, password: string ): Promise> { const { Client } = await import("pg"); const client = new Client({ host: connection.host, port: connection.port, database: connection.database_name, user: connection.username, password: password, connectionTimeoutMillis: (connection.connection_timeout || 30) * 1000, ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false }); try { await client.connect(); // 테이블 목록과 각 테이블의 컬럼 정보 조회 const result = await client.query(` SELECT t.table_name, array_agg( json_build_object( 'column_name', c.column_name, 'data_type', c.data_type, 'is_nullable', c.is_nullable, 'column_default', c.column_default ) ) as columns, obj_description(quote_ident(t.table_name)::regclass::oid, 'pg_class') as table_description FROM information_schema.tables t LEFT JOIN information_schema.columns c ON c.table_name = t.table_name AND c.table_schema = t.table_schema WHERE t.table_schema = 'public' AND t.table_type = 'BASE TABLE' GROUP BY t.table_name ORDER BY t.table_name `); await client.end(); return { success: true, data: result.rows.map(row => ({ table_name: row.table_name, columns: row.columns || [], description: row.table_description })) as TableInfo[], message: "테이블 목록을 조회했습니다." }; } catch (error) { await client.end(); return { success: false, message: "테이블 목록 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "알 수 없는 오류" }; } } }