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

275 lines
6.1 KiB
TypeScript

/**
* 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<User>('SELECT * FROM users WHERE user_id = $1', ['user123']);
*/
export async function query<T extends QueryResultRow = any>(
text: string,
params?: any[]
): Promise<T[]> {
const pool = getPool();
const client = await pool.connect();
try {
const startTime = Date.now();
const result: PgQueryResult<T> = 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<User>('SELECT * FROM users WHERE user_id = $1', ['user123']);
*/
export async function queryOne<T extends QueryResultRow = any>(
text: string,
params?: any[]
): Promise<T | null> {
const rows = await query<T>(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<T>(
callback: (client: PoolClient) => Promise<T>
): Promise<T> {
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<void> {
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,
};