feat: 레거시 src/services/dataflowDiagramService.ts Prisma 제거
변경사항:
1. src/services/dataflowDiagramService.ts:
- PrismaClient import 제거
- database/db의 query, queryOne import 추가
- 모든 Prisma 호출 Raw Query로 전환:
✅ getDataflowDiagrams: findMany + count → query + queryOne
✅ getDataflowDiagramById: findFirst → queryOne
✅ createDataflowDiagram: create → queryOne
✅ updateDataflowDiagram: update → queryOne (동적 UPDATE)
✅ deleteDataflowDiagram: delete → query
✅ copyDataflowDiagram: findFirst → queryOne
2. src/database/db.ts 생성:
- backend-node/src/database/db.ts 복사
- 레거시 코드와 호환성 유지
최종 확인:
- ✅ src/ 디렉토리: Prisma 호출 0개
- ✅ backend-node/ 디렉토리: Prisma 호출 0개
- ✅ 전체 프로젝트: Prisma 완전 제거
This commit is contained in:
parent
440803e203
commit
fab8909195
|
|
@ -0,0 +1,274 @@
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { query, queryOne } from "../database/db";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export interface DataflowDiagram {
|
export interface DataflowDiagram {
|
||||||
diagram_id: number;
|
diagram_id: number;
|
||||||
|
|
@ -49,18 +47,33 @@ export class DataflowDiagramService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const [diagrams, total] = await Promise.all([
|
// WHERE 절 구성
|
||||||
prisma.dataflow_diagrams.findMany({
|
const whereParts: string[] = ["company_code = $1"];
|
||||||
where: whereClause,
|
const params: any[] = [companyCode];
|
||||||
orderBy: { created_at: "desc" },
|
|
||||||
skip,
|
if (searchTerm) {
|
||||||
take: size,
|
whereParts.push("diagram_name ILIKE $2");
|
||||||
}),
|
params.push(`%${searchTerm}%`);
|
||||||
prisma.dataflow_diagrams.count({
|
}
|
||||||
where: whereClause,
|
|
||||||
}),
|
const whereSQL = whereParts.join(" AND ");
|
||||||
|
|
||||||
|
const [diagrams, totalResult] = await Promise.all([
|
||||||
|
query<DataflowDiagram>(
|
||||||
|
`SELECT * FROM dataflow_diagrams
|
||||||
|
WHERE ${whereSQL}
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
|
||||||
|
[...params, size, skip]
|
||||||
|
),
|
||||||
|
queryOne<{ count: string }>(
|
||||||
|
`SELECT COUNT(*) as count FROM dataflow_diagrams WHERE ${whereSQL}`,
|
||||||
|
params
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const total = parseInt(totalResult?.count || "0", 10);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
diagrams,
|
diagrams,
|
||||||
pagination: {
|
pagination: {
|
||||||
|
|
@ -79,12 +92,11 @@ export class DataflowDiagramService {
|
||||||
diagramId: number,
|
diagramId: number,
|
||||||
companyCode: string
|
companyCode: string
|
||||||
): Promise<DataflowDiagram | null> {
|
): Promise<DataflowDiagram | null> {
|
||||||
return await prisma.dataflow_diagrams.findFirst({
|
return await queryOne<DataflowDiagram>(
|
||||||
where: {
|
`SELECT * FROM dataflow_diagrams
|
||||||
diagram_id: diagramId,
|
WHERE diagram_id = $1 AND company_code = $2`,
|
||||||
company_code: companyCode,
|
[diagramId, companyCode]
|
||||||
},
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -93,14 +105,19 @@ export class DataflowDiagramService {
|
||||||
async createDataflowDiagram(
|
async createDataflowDiagram(
|
||||||
data: CreateDataflowDiagramData
|
data: CreateDataflowDiagramData
|
||||||
): Promise<DataflowDiagram> {
|
): Promise<DataflowDiagram> {
|
||||||
return await prisma.dataflow_diagrams.create({
|
const result = await queryOne<DataflowDiagram>(
|
||||||
data: {
|
`INSERT INTO dataflow_diagrams
|
||||||
diagram_name: data.diagram_name,
|
(diagram_name, relationships, company_code, created_by, created_at, updated_at)
|
||||||
relationships: data.relationships,
|
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||||
company_code: data.company_code,
|
RETURNING *`,
|
||||||
created_by: data.created_by,
|
[
|
||||||
},
|
data.diagram_name,
|
||||||
});
|
JSON.stringify(data.relationships),
|
||||||
|
data.company_code,
|
||||||
|
data.created_by || null,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return result!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -120,17 +137,33 @@ export class DataflowDiagramService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.dataflow_diagrams.update({
|
// 동적 UPDATE 쿼리 생성
|
||||||
where: {
|
const updateFields: string[] = ["updated_at = NOW()"];
|
||||||
diagram_id: diagramId,
|
const params: any[] = [];
|
||||||
},
|
let paramIndex = 1;
|
||||||
data: {
|
|
||||||
...(data.diagram_name && { diagram_name: data.diagram_name }),
|
if (data.diagram_name !== undefined) {
|
||||||
...(data.relationships && { relationships: data.relationships }),
|
updateFields.push(`diagram_name = $${paramIndex++}`);
|
||||||
...(data.updated_by && { updated_by: data.updated_by }),
|
params.push(data.diagram_name);
|
||||||
updated_at: new Date(),
|
}
|
||||||
},
|
if (data.relationships !== undefined) {
|
||||||
});
|
updateFields.push(`relationships = $${paramIndex++}`);
|
||||||
|
params.push(JSON.stringify(data.relationships));
|
||||||
|
}
|
||||||
|
if (data.updated_by !== undefined) {
|
||||||
|
updateFields.push(`updated_by = $${paramIndex++}`);
|
||||||
|
params.push(data.updated_by);
|
||||||
|
}
|
||||||
|
|
||||||
|
params.push(diagramId);
|
||||||
|
|
||||||
|
return await queryOne<DataflowDiagram>(
|
||||||
|
`UPDATE dataflow_diagrams
|
||||||
|
SET ${updateFields.join(", ")}
|
||||||
|
WHERE diagram_id = $${paramIndex}
|
||||||
|
RETURNING *`,
|
||||||
|
params
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -149,11 +182,10 @@ export class DataflowDiagramService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.dataflow_diagrams.delete({
|
await query(
|
||||||
where: {
|
`DELETE FROM dataflow_diagrams WHERE diagram_id = $1`,
|
||||||
diagram_id: diagramId,
|
[diagramId]
|
||||||
},
|
);
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -184,12 +216,11 @@ export class DataflowDiagramService {
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
copyName = `${baseName} (${counter})`;
|
copyName = `${baseName} (${counter})`;
|
||||||
const existing = await prisma.dataflow_diagrams.findFirst({
|
const existing = await queryOne<DataflowDiagram>(
|
||||||
where: {
|
`SELECT * FROM dataflow_diagrams
|
||||||
company_code: companyCode,
|
WHERE company_code = $1 AND diagram_name = $2`,
|
||||||
diagram_name: copyName,
|
[companyCode, copyName]
|
||||||
},
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (!existing) break;
|
if (!existing) break;
|
||||||
counter++;
|
counter++;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue