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

271 lines
8.5 KiB
TypeScript

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<void> {
// 기존 연결이 있다면 먼저 정리
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<void> {
if (this.client) {
try {
await this.client.end();
} catch (error) {
console.warn("강제 연결 해제 중 오류 (무시):", error);
} finally {
this.client = null;
}
}
}
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;
}
}
}
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"
);
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<QueryResult> {
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<TableInfo[]> {
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<any[]> {
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];
}
}