/** * PostgreSQL Raw Query 기반 데이터베이스 매니저 * * Prisma → Raw Query 전환의 핵심 모듈 * - Connection Pool 기반 안정적인 연결 관리 * - 트랜잭션 지원 * - 타입 안전성 보장 * - 자동 재연결 및 에러 핸들링 */ import { Pool, PoolClient, QueryResult as PgQueryResult, QueryResultRow, } from "pg"; import config from "../config/environment"; // PostgreSQL 연결 풀 let pool: Pool | null = null; /** * 데이터베이스 연결 풀 초기화 */ export const initializePool = (): Pool => { if (pool) { return pool; } // DATABASE_URL 파싱 (postgresql://user:password@host:port/database) const databaseUrl = config.databaseUrl; // URL 파싱 로직 const dbConfig = parseDatabaseUrl(databaseUrl); pool = new Pool({ host: dbConfig.host, port: dbConfig.port, database: dbConfig.database, user: dbConfig.user, password: dbConfig.password, // 연결 풀 설정 min: config.nodeEnv === "production" ? 5 : 2, max: config.nodeEnv === "production" ? 20 : 10, // 타임아웃 설정 connectionTimeoutMillis: 30000, // 30초 idleTimeoutMillis: 600000, // 10분 // 연결 유지 설정 keepAlive: true, keepAliveInitialDelayMillis: 10000, // 쿼리 타임아웃 statement_timeout: 60000, // 60초 (동적 테이블 생성 등 고려) query_timeout: 60000, // Application Name application_name: "WACE-PLM-Backend", }); // 연결 풀 이벤트 핸들러 pool.on("connect", (client) => { if (config.debug) { console.log("✅ PostgreSQL 클라이언트 연결 생성"); } }); pool.on("acquire", (client) => { if (config.debug) { console.log("🔒 PostgreSQL 클라이언트 획득"); } }); pool.on("remove", (client) => { if (config.debug) { console.log("🗑️ PostgreSQL 클라이언트 제거"); } }); pool.on("error", (err, client) => { console.error("❌ PostgreSQL 연결 풀 에러:", err); }); console.log( `🚀 PostgreSQL 연결 풀 초기화 완료: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}` ); return pool; }; /** * DATABASE_URL 파싱 헬퍼 함수 */ function parseDatabaseUrl(url: string) { // postgresql://user:password@host:port/database const regex = /postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/; const match = url.match(regex); if (!match) { // URL 파싱 실패 시 기본값 사용 console.warn("⚠️ DATABASE_URL 파싱 실패, 기본값 사용"); return { host: "localhost", port: 5432, database: "ilshin", user: "postgres", password: "postgres", }; } return { user: decodeURIComponent(match[1]), password: decodeURIComponent(match[2]), host: match[3], port: parseInt(match[4], 10), database: match[5], }; } /** * 연결 풀 가져오기 */ export const getPool = (): Pool => { if (!pool) { return initializePool(); } return pool; }; /** * 기본 쿼리 실행 함수 * * @param text SQL 쿼리 문자열 (Parameterized Query) * @param params 쿼리 파라미터 배열 * @returns 쿼리 결과 배열 * * @example * const users = await query('SELECT * FROM users WHERE user_id = $1', ['user123']); */ export async function query( text: string, params?: any[] ): Promise { const pool = getPool(); const client = await pool.connect(); try { const startTime = Date.now(); const result: PgQueryResult = await client.query(text, params); const duration = Date.now() - startTime; if (config.debug) { console.log("🔍 쿼리 실행:", { query: text, params, rowCount: result.rowCount, duration: `${duration}ms`, }); } return result.rows; } catch (error: any) { console.error("❌ 쿼리 실행 실패:", { query: text, params, error: error.message, }); throw error; } finally { client.release(); } } /** * 단일 행 조회 쿼리 (결과가 없으면 null 반환) * * @param text SQL 쿼리 문자열 * @param params 쿼리 파라미터 * @returns 단일 행 또는 null * * @example * const user = await queryOne('SELECT * FROM users WHERE user_id = $1', ['user123']); */ export async function queryOne( text: string, params?: any[] ): Promise { const rows = await query(text, params); return rows.length > 0 ? rows[0] : null; } /** * 트랜잭션 실행 함수 * * @param callback 트랜잭션 내에서 실행할 함수 * @returns 콜백 함수의 반환값 * * @example * const result = await transaction(async (client) => { * await client.query('INSERT INTO users (...) VALUES (...)', []); * await client.query('INSERT INTO user_roles (...) VALUES (...)', []); * return { success: true }; * }); */ export async function transaction( callback: (client: PoolClient) => Promise ): Promise { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); if (config.debug) { console.log("🔄 트랜잭션 시작"); } const result = await callback(client); await client.query("COMMIT"); if (config.debug) { console.log("✅ 트랜잭션 커밋 완료"); } return result; } catch (error: any) { await client.query("ROLLBACK"); console.error("❌ 트랜잭션 롤백:", error.message); throw error; } finally { client.release(); } } /** * 연결 풀 종료 (앱 종료 시 호출) */ export async function closePool(): Promise { if (pool) { await pool.end(); pool = null; console.log("🛑 PostgreSQL 연결 풀 종료"); } } /** * 연결 풀 상태 확인 */ export function getPoolStatus() { const pool = getPool(); return { totalCount: pool.totalCount, idleCount: pool.idleCount, waitingCount: pool.waitingCount, }; } // Pool 직접 접근 (필요한 경우) export { pool }; // 기본 익스포트 (편의성) export default { query, queryOne, transaction, getPool, initializePool, closePool, getPoolStatus, };