- Added new entries to .gitignore for multi-agent MCP task queue and related rules. - Removed "즉시 저장" (quick insert) options from the ScreenSettingModal and BasicTab components to streamline button configurations. - Cleaned up unused event options in the V2ButtonConfigPanel to enhance clarity and maintainability. These changes aim to improve project organization and simplify the user interface by eliminating redundant options.
298 lines
7.0 KiB
TypeScript
298 lines
7.0 KiB
TypeScript
/**
|
|
* PostgreSQL Raw Query 기반 데이터베이스 매니저
|
|
*
|
|
* Prisma → Raw Query 전환의 핵심 모듈
|
|
* - Connection Pool 기반 안정적인 연결 관리
|
|
* - 트랜잭션 지원
|
|
* - 타입 안전성 보장
|
|
* - 자동 재연결 및 에러 핸들링
|
|
*/
|
|
|
|
import {
|
|
Pool,
|
|
PoolClient,
|
|
QueryResult as PgQueryResult,
|
|
QueryResultRow,
|
|
types,
|
|
} from "pg";
|
|
import config from "../config/environment";
|
|
|
|
// DATE 타입(OID 1082)을 문자열로 반환 (타임존 변환에 의한 -1day 버그 방지)
|
|
types.setTypeParser(1082, (val: string) => val);
|
|
|
|
// 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) => {
|
|
client.query("SET timezone = 'Asia/Seoul'");
|
|
if (config.debug) {
|
|
console.log("✅ PostgreSQL 클라이언트 연결 생성 (timezone: Asia/Seoul)");
|
|
}
|
|
});
|
|
|
|
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);
|
|
// 연결 풀 에러 발생 시 자동 재연결 시도
|
|
// Pool은 자동으로 연결을 재생성하므로 별도 처리 불필요
|
|
// 다만, 연속 에러 발생 시 알림이 필요할 수 있음
|
|
});
|
|
|
|
// 연결 풀 상태 체크 (5분마다)
|
|
setInterval(() => {
|
|
if (pool) {
|
|
const status = {
|
|
totalCount: pool.totalCount,
|
|
idleCount: pool.idleCount,
|
|
waitingCount: pool.waitingCount,
|
|
};
|
|
// 대기 중인 연결이 많으면 경고
|
|
if (status.waitingCount > 5) {
|
|
console.warn("⚠️ PostgreSQL 연결 풀 대기열 증가:", status);
|
|
}
|
|
}
|
|
}, 5 * 60 * 1000);
|
|
|
|
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,
|
|
};
|