import { Client } from "pg"; import { DatabaseConnector, ConnectionConfig, QueryResult, } from "../interfaces/DatabaseConnector"; import { ConnectionTestResult, TableInfo } from "../types/externalDbTypes"; export class PostgreSQLConnector implements DatabaseConnector { private client: Client | null = null; private config: ConnectionConfig; constructor(config: ConnectionConfig) { this.config = config; } async connect(): Promise { // 기존 연결이 있다면 먼저 정리 await this.forceDisconnect(); const clientConfig: any = { host: this.config.host, port: this.config.port, database: this.config.database, user: this.config.user, password: this.config.password, // 연결 안정성 개선 (더 보수적인 설정) connectionTimeoutMillis: this.config.connectionTimeoutMillis || 15000, query_timeout: this.config.queryTimeoutMillis || 20000, keepAlive: false, // keepAlive 비활성화 (연결 문제 방지) // SASL 인증 문제 방지 application_name: "PLM-ERP-System", // 추가 안정성 설정 statement_timeout: 20000, idle_in_transaction_session_timeout: 30000, }; if (this.config.ssl != null) { clientConfig.ssl = this.config.ssl; } this.client = new Client(clientConfig); // 연결 시 더 긴 타임아웃 설정 const connectPromise = this.client.connect(); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error("연결 타임아웃")), 20000); }); await Promise.race([connectPromise, timeoutPromise]); console.log( `✅ PostgreSQL 연결 성공: ${this.config.host}:${this.config.port}` ); } // 강제 연결 해제 메서드 추가 private async forceDisconnect(): Promise { if (this.client) { try { await this.client.end(); } catch (error) { console.warn("강제 연결 해제 중 오류 (무시):", error); } finally { this.client = null; } } } async disconnect(): Promise { if (this.client) { try { const endPromise = this.client.end(); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error("연결 해제 타임아웃")), 3000); }); await Promise.race([endPromise, timeoutPromise]); console.log(`✅ PostgreSQL 연결 해제 성공`); } catch (error) { console.warn("연결 해제 중 오류:", error); } finally { this.client = null; } } } async testConnection(): Promise { const startTime = Date.now(); try { await this.connect(); const result = await this.client!.query( "SELECT version(), pg_database_size(current_database()) as size" ); const responseTime = Date.now() - startTime; await this.disconnect(); 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: any) { await this.disconnect(); return { success: false, message: "PostgreSQL 연결에 실패했습니다.", error: { code: "CONNECTION_FAILED", details: error.message || "알 수 없는 오류", }, }; } } async executeQuery(query: string, params: any[] = []): Promise { try { await this.connect(); const result = await this.client!.query(query, params); await this.disconnect(); return { rows: result.rows, rowCount: result.rowCount ?? undefined, fields: result.fields ?? undefined, }; } catch (error: any) { await this.disconnect(); throw new Error(`PostgreSQL 쿼리 실행 실패: ${error.message}`); } } async getTables(): Promise { let tempClient: Client | null = null; try { console.log( `🔍 PostgreSQL 테이블 목록 조회 시작: ${this.config.host}:${this.config.port}` ); // 매번 새로운 연결 생성 const clientConfig: any = { host: this.config.host, port: this.config.port, database: this.config.database, user: this.config.user, password: this.config.password, connectionTimeoutMillis: 10000, query_timeout: 15000, application_name: "PLM-ERP-Tables", }; tempClient = new Client(clientConfig); await tempClient.connect(); const result = await tempClient.query(` SELECT t.table_name, obj_description(quote_ident(t.table_name)::regclass::oid, 'pg_class') as table_description FROM information_schema.tables t WHERE t.table_schema = 'public' AND t.table_type = 'BASE TABLE' ORDER BY t.table_name; `); console.log(`✅ 테이블 목록 조회 성공: ${result.rows.length}개`); return result.rows.map((row) => ({ table_name: row.table_name, description: row.table_description, columns: [], // Columns will be fetched by getColumns })); } catch (error: any) { console.error(`❌ 테이블 목록 조회 실패:`, error.message); throw new Error(`PostgreSQL 테이블 목록 조회 실패: ${error.message}`); } finally { if (tempClient) { try { await tempClient.end(); } catch (endError) { console.warn("테이블 조회 연결 해제 중 오류:", endError); } } } } async getColumns(tableName: string): Promise { let tempClient: Client | null = null; try { console.log( `🔍 PostgreSQL 컬럼 정보 조회 시작: ${this.config.host}:${this.config.port}/${tableName}` ); // 매번 새로운 연결 생성 const clientConfig: any = { host: this.config.host, port: this.config.port, database: this.config.database, user: this.config.user, password: this.config.password, connectionTimeoutMillis: 10000, query_timeout: 15000, application_name: "PLM-ERP-Columns", }; tempClient = new Client(clientConfig); await tempClient.connect(); const result = await tempClient.query( ` SELECT isc.column_name, isc.data_type, isc.is_nullable, isc.column_default, col_description(c.oid, a.attnum) as column_comment, CASE WHEN tc.constraint_type = 'PRIMARY KEY' THEN 'YES' ELSE 'NO' END AS is_primary_key FROM information_schema.columns isc LEFT JOIN pg_class c ON c.relname = isc.table_name AND c.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = isc.table_schema) LEFT JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = isc.column_name LEFT JOIN information_schema.key_column_usage k ON k.table_name = isc.table_name AND k.table_schema = isc.table_schema AND k.column_name = isc.column_name LEFT JOIN information_schema.table_constraints tc ON tc.constraint_name = k.constraint_name AND tc.table_schema = k.table_schema AND tc.table_name = k.table_name AND tc.constraint_type = 'PRIMARY KEY' WHERE isc.table_schema = 'public' AND isc.table_name = $1 ORDER BY isc.ordinal_position; `, [tableName] ); console.log( `✅ 컬럼 정보 조회 성공: ${tableName} - ${result.rows.length}개` ); return result.rows; } catch (error: any) { console.error(`❌ 컬럼 정보 조회 실패: ${tableName} -`, error.message); throw new Error(`PostgreSQL 컬럼 정보 조회 실패: ${error.message}`); } finally { if (tempClient) { try { await tempClient.end(); } catch (endError) { console.warn("컬럼 조회 연결 해제 중 오류:", endError); } } } } private 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]; } }