ERP-node/backend-node/src/database/PostgreSQLConnector.ts

253 lines
7.8 KiB
TypeScript
Raw Normal View History

import { Client } from "pg";
import {
DatabaseConnector,
ConnectionConfig,
QueryResult,
} from "../interfaces/DatabaseConnector";
import { ConnectionTestResult, TableInfo } from "../types/externalDbTypes";
2025-09-23 10:45:53 +09:00
export class PostgreSQLConnector implements DatabaseConnector {
private client: Client | null = null;
private config: ConnectionConfig;
constructor(config: ConnectionConfig) {
this.config = config;
}
async connect(): Promise<void> {
// 기존 연결이 있다면 먼저 정리
await this.forceDisconnect();
2025-09-23 10:45:53 +09:00
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,
2025-09-23 10:45:53 +09:00
};
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<void> {
if (this.client) {
try {
await this.client.end();
} catch (error) {
console.warn("강제 연결 해제 중 오류 (무시):", error);
} finally {
this.client = null;
}
}
2025-09-23 10:45:53 +09:00
}
async disconnect(): Promise<void> {
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;
}
2025-09-23 10:45:53 +09:00
}
}
async testConnection(): Promise<ConnectionTestResult> {
const startTime = Date.now();
try {
await this.connect();
const result = await this.client!.query(
"SELECT version(), pg_database_size(current_database()) as size"
);
2025-09-23 10:45:53 +09:00
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")
),
2025-09-23 10:45:53 +09:00
},
};
} catch (error: any) {
await this.disconnect();
return {
success: false,
message: "PostgreSQL 연결에 실패했습니다.",
error: {
code: "CONNECTION_FAILED",
details: error.message || "알 수 없는 오류",
},
};
}
}
2025-09-29 15:21:14 +09:00
async executeQuery(query: string, params: any[] = []): Promise<QueryResult> {
2025-09-23 10:45:53 +09:00
try {
await this.connect();
2025-09-29 15:21:14 +09:00
const result = await this.client!.query(query, params);
2025-09-23 10:45:53 +09:00
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<TableInfo[]> {
let tempClient: Client | null = null;
2025-09-23 10:45:53 +09:00
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(`
2025-09-23 10:45:53 +09:00
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}`);
2025-09-23 10:45:53 +09:00
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);
2025-09-23 10:45:53 +09:00
throw new Error(`PostgreSQL 테이블 목록 조회 실패: ${error.message}`);
} finally {
if (tempClient) {
try {
await tempClient.end();
} catch (endError) {
console.warn("테이블 조회 연결 해제 중 오류:", endError);
}
}
2025-09-23 10:45:53 +09:00
}
}
async getColumns(tableName: string): Promise<any[]> {
let tempClient: Client | null = null;
2025-09-23 10:45:53 +09:00
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(
`
2025-09-23 10:45:53 +09:00
SELECT
column_name,
data_type,
is_nullable,
column_default,
col_description(c.oid, a.attnum) as column_comment
FROM information_schema.columns isc
LEFT JOIN pg_class c ON c.relname = isc.table_name
LEFT JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = isc.column_name
WHERE isc.table_schema = 'public' AND isc.table_name = $1
ORDER BY isc.ordinal_position;
`,
[tableName]
);
console.log(
`✅ 컬럼 정보 조회 성공: ${tableName} - ${result.rows.length}`
);
2025-09-23 10:45:53 +09:00
return result.rows;
} catch (error: any) {
console.error(`❌ 컬럼 정보 조회 실패: ${tableName} -`, error.message);
2025-09-23 10:45:53 +09:00
throw new Error(`PostgreSQL 컬럼 정보 조회 실패: ${error.message}`);
} finally {
if (tempClient) {
try {
await tempClient.end();
} catch (endError) {
console.warn("컬럼 조회 연결 해제 중 오류:", endError);
}
}
2025-09-23 10:45:53 +09:00
}
}
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];
}
}