2025-09-24 18:23:57 +09:00
|
|
|
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> {
|
2025-09-24 18:23:57 +09:00
|
|
|
// 기존 연결이 있다면 먼저 정리
|
|
|
|
|
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,
|
2025-09-24 18:23:57 +09:00
|
|
|
// 연결 안정성 개선 (더 보수적인 설정)
|
|
|
|
|
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);
|
2025-09-24 18:23:57 +09:00
|
|
|
|
|
|
|
|
// 연결 시 더 긴 타임아웃 설정
|
|
|
|
|
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) {
|
2025-09-24 18:23:57 +09:00
|
|
|
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();
|
2025-09-24 18:23:57 +09:00
|
|
|
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 || "알 수 없음",
|
2025-09-24 18:23:57 +09:00
|
|
|
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 || "알 수 없는 오류",
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async executeQuery(query: string): Promise<QueryResult> {
|
|
|
|
|
try {
|
|
|
|
|
await this.connect();
|
|
|
|
|
const result = await this.client!.query(query);
|
|
|
|
|
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[]> {
|
2025-09-24 18:23:57 +09:00
|
|
|
let tempClient: Client | null = null;
|
2025-09-23 10:45:53 +09:00
|
|
|
try {
|
2025-09-24 18:23:57 +09:00
|
|
|
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;
|
|
|
|
|
`);
|
2025-09-24 18:23:57 +09:00
|
|
|
|
|
|
|
|
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) {
|
2025-09-24 18:23:57 +09:00
|
|
|
console.error(`❌ 테이블 목록 조회 실패:`, error.message);
|
2025-09-23 10:45:53 +09:00
|
|
|
throw new Error(`PostgreSQL 테이블 목록 조회 실패: ${error.message}`);
|
2025-09-24 18:23:57 +09:00
|
|
|
} 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[]> {
|
2025-09-24 18:23:57 +09:00
|
|
|
let tempClient: Client | null = null;
|
2025-09-23 10:45:53 +09:00
|
|
|
try {
|
2025-09-24 18:23:57 +09:00
|
|
|
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,
|
2025-09-24 18:23:57 +09:00
|
|
|
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) {
|
2025-09-24 18:23:57 +09:00
|
|
|
console.error(`❌ 컬럼 정보 조회 실패: ${tableName} -`, error.message);
|
2025-09-23 10:45:53 +09:00
|
|
|
throw new Error(`PostgreSQL 컬럼 정보 조회 실패: ${error.message}`);
|
2025-09-24 18:23:57 +09:00
|
|
|
} 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];
|
|
|
|
|
}
|
2025-09-24 18:23:57 +09:00
|
|
|
}
|