// 외부 DB 연결 서비스 // 작성일: 2024-12-17 import { PrismaClient } from "@prisma/client"; import { ExternalDbConnection, ExternalDbConnectionFilter, ApiResponse, } from "../types/externalDbTypes"; import { PasswordEncryption } from "../utils/passwordEncryption"; 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 : "알 수 없는 오류", }; } } /** * 데이터베이스 연결 테스트 */ static async testConnection( testData: import("../types/externalDbTypes").ConnectionTestRequest ): Promise { const startTime = Date.now(); try { switch (testData.db_type.toLowerCase()) { case "postgresql": return await this.testPostgreSQLConnection(testData, startTime); case "mysql": return await this.testMySQLConnection(testData, startTime); case "oracle": return await this.testOracleConnection(testData, startTime); case "mssql": return await this.testMSSQLConnection(testData, startTime); case "sqlite": return await this.testSQLiteConnection(testData, startTime); default: return { success: false, message: `지원하지 않는 데이터베이스 타입입니다: ${testData.db_type}`, error: { code: "UNSUPPORTED_DB_TYPE", details: `${testData.db_type} 타입은 현재 지원하지 않습니다.`, }, }; } } catch (error) { return { success: false, message: "연결 테스트 중 오류가 발생했습니다.", error: { code: "TEST_ERROR", details: error instanceof Error ? error.message : "알 수 없는 오류", }, }; } } /** * PostgreSQL 연결 테스트 */ private static async testPostgreSQLConnection( testData: import("../types/externalDbTypes").ConnectionTestRequest, startTime: number ): Promise { const { Client } = await import("pg"); const client = new Client({ host: testData.host, port: testData.port, database: testData.database_name, user: testData.username, password: testData.password, connectionTimeoutMillis: (testData.connection_timeout || 30) * 1000, ssl: testData.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false, }); try { await client.connect(); const result = await client.query( "SELECT version(), pg_database_size(current_database()) as size" ); const responseTime = Date.now() - startTime; await client.end(); return { success: true, message: "PostgreSQL 연결이 성공했습니다.", details: { response_time: responseTime, server_version: result.rows[0]?.version || "알 수 없음", database_size: this.formatBytes( parseInt(result.rows[0]?.size || "0") ), }, }; } catch (error) { try { await client.end(); } catch (endError) { // 연결 종료 오류는 무시 } return { success: false, message: "PostgreSQL 연결에 실패했습니다.", error: { code: "CONNECTION_FAILED", details: error instanceof Error ? error.message : "알 수 없는 오류", }, }; } } /** * MySQL 연결 테스트 (모의 구현) */ private static async testMySQLConnection( testData: import("../types/externalDbTypes").ConnectionTestRequest, startTime: number ): Promise { // MySQL 라이브러리가 없으므로 모의 구현 return { success: false, message: "MySQL 연결 테스트는 현재 지원하지 않습니다.", error: { code: "NOT_IMPLEMENTED", details: "MySQL 라이브러리가 설치되지 않았습니다.", }, }; } /** * Oracle 연결 테스트 (모의 구현) */ private static async testOracleConnection( testData: import("../types/externalDbTypes").ConnectionTestRequest, startTime: number ): Promise { return { success: false, message: "Oracle 연결 테스트는 현재 지원하지 않습니다.", error: { code: "NOT_IMPLEMENTED", details: "Oracle 라이브러리가 설치되지 않았습니다.", }, }; } /** * SQL Server 연결 테스트 (모의 구현) */ private static async testMSSQLConnection( testData: import("../types/externalDbTypes").ConnectionTestRequest, startTime: number ): Promise { return { success: false, message: "SQL Server 연결 테스트는 현재 지원하지 않습니다.", error: { code: "NOT_IMPLEMENTED", details: "SQL Server 라이브러리가 설치되지 않았습니다.", }, }; } /** * SQLite 연결 테스트 (모의 구현) */ private static async testSQLiteConnection( testData: import("../types/externalDbTypes").ConnectionTestRequest, startTime: number ): Promise { return { success: false, message: "SQLite 연결 테스트는 현재 지원하지 않습니다.", error: { code: "NOT_IMPLEMENTED", details: "SQLite는 파일 기반이므로 네트워크 연결 테스트가 불가능합니다.", }, }; } /** * 바이트 크기를 읽기 쉬운 형태로 변환 */ 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; } } }