Merge pull request '외부 DB 연결 끊김 오류 해결' (#279) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/279
This commit is contained in:
commit
3f1ecfab15
|
|
@ -1,43 +1,25 @@
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { pool, queryOne } from "../database/db";
|
|
||||||
import logger from "../utils/logger";
|
import logger from "../utils/logger";
|
||||||
import { PasswordEncryption } from "../utils/passwordEncryption";
|
import { ExternalDbConnectionPoolService } from "../services/externalDbConnectionPoolService";
|
||||||
import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory";
|
|
||||||
|
|
||||||
// 외부 DB 커넥터를 가져오는 헬퍼 함수
|
// 외부 DB 커넥터를 가져오는 헬퍼 함수 (연결 풀 사용)
|
||||||
export async function getExternalDbConnector(connectionId: number) {
|
export async function getExternalDbConnector(connectionId: number) {
|
||||||
// 외부 DB 연결 정보 조회
|
const poolService = ExternalDbConnectionPoolService.getInstance();
|
||||||
const connection = await queryOne<any>(
|
|
||||||
`SELECT * FROM external_db_connections WHERE id = $1`,
|
|
||||||
[connectionId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!connection) {
|
// 연결 풀 래퍼를 반환 (executeQuery 메서드를 가진 객체)
|
||||||
throw new Error(`외부 DB 연결 정보를 찾을 수 없습니다. ID: ${connectionId}`);
|
return {
|
||||||
}
|
executeQuery: async (sql: string, params?: any[]) => {
|
||||||
|
const result = await poolService.executeQuery(connectionId, sql, params);
|
||||||
// 패스워드 복호화
|
return { rows: result };
|
||||||
const decryptedPassword = PasswordEncryption.decrypt(connection.password);
|
},
|
||||||
|
|
||||||
// DB 연결 설정
|
|
||||||
const config = {
|
|
||||||
host: connection.host,
|
|
||||||
port: connection.port,
|
|
||||||
user: connection.username,
|
|
||||||
password: decryptedPassword,
|
|
||||||
database: connection.database_name,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// DB 커넥터 생성
|
|
||||||
return await DatabaseConnectorFactory.createConnector(
|
|
||||||
connection.db_type || "mariadb",
|
|
||||||
config,
|
|
||||||
connectionId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 동적 계층 구조 데이터 조회 (범용)
|
// 동적 계층 구조 데이터 조회 (범용)
|
||||||
export const getHierarchyData = async (req: Request, res: Response): Promise<Response> => {
|
export const getHierarchyData = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
try {
|
try {
|
||||||
const { externalDbConnectionId, hierarchyConfig } = req.body;
|
const { externalDbConnectionId, hierarchyConfig } = req.body;
|
||||||
|
|
||||||
|
|
@ -48,7 +30,9 @@ export const getHierarchyData = async (req: Request, res: Response): Promise<Res
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const connector = await getExternalDbConnector(Number(externalDbConnectionId));
|
const connector = await getExternalDbConnector(
|
||||||
|
Number(externalDbConnectionId)
|
||||||
|
);
|
||||||
const config = JSON.parse(hierarchyConfig);
|
const config = JSON.parse(hierarchyConfig);
|
||||||
|
|
||||||
const result: any = {
|
const result: any = {
|
||||||
|
|
@ -69,7 +53,7 @@ export const getHierarchyData = async (req: Request, res: Response): Promise<Res
|
||||||
for (const level of config.levels) {
|
for (const level of config.levels) {
|
||||||
const levelQuery = `SELECT * FROM ${level.tableName} LIMIT 1000`;
|
const levelQuery = `SELECT * FROM ${level.tableName} LIMIT 1000`;
|
||||||
const levelResult = await connector.executeQuery(levelQuery);
|
const levelResult = await connector.executeQuery(levelQuery);
|
||||||
|
|
||||||
result.levels.push({
|
result.levels.push({
|
||||||
level: level.level,
|
level: level.level,
|
||||||
name: level.name,
|
name: level.name,
|
||||||
|
|
@ -94,7 +78,10 @@ export const getHierarchyData = async (req: Request, res: Response): Promise<Res
|
||||||
logger.info("동적 계층 구조 데이터 조회", {
|
logger.info("동적 계층 구조 데이터 조회", {
|
||||||
externalDbConnectionId,
|
externalDbConnectionId,
|
||||||
warehouseCount: result.warehouse?.length || 0,
|
warehouseCount: result.warehouse?.length || 0,
|
||||||
levelCounts: result.levels.map((l: any) => ({ level: l.level, count: l.data.length })),
|
levelCounts: result.levels.map((l: any) => ({
|
||||||
|
level: l.level,
|
||||||
|
count: l.data.length,
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
@ -112,22 +99,35 @@ export const getHierarchyData = async (req: Request, res: Response): Promise<Res
|
||||||
};
|
};
|
||||||
|
|
||||||
// 특정 레벨의 하위 데이터 조회
|
// 특정 레벨의 하위 데이터 조회
|
||||||
export const getChildrenData = async (req: Request, res: Response): Promise<Response> => {
|
export const getChildrenData = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
try {
|
try {
|
||||||
const { externalDbConnectionId, hierarchyConfig, parentLevel, parentKey } = req.body;
|
const { externalDbConnectionId, hierarchyConfig, parentLevel, parentKey } =
|
||||||
|
req.body;
|
||||||
|
|
||||||
if (!externalDbConnectionId || !hierarchyConfig || !parentLevel || !parentKey) {
|
if (
|
||||||
|
!externalDbConnectionId ||
|
||||||
|
!hierarchyConfig ||
|
||||||
|
!parentLevel ||
|
||||||
|
!parentKey
|
||||||
|
) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "필수 파라미터가 누락되었습니다.",
|
message: "필수 파라미터가 누락되었습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const connector = await getExternalDbConnector(Number(externalDbConnectionId));
|
const connector = await getExternalDbConnector(
|
||||||
|
Number(externalDbConnectionId)
|
||||||
|
);
|
||||||
const config = JSON.parse(hierarchyConfig);
|
const config = JSON.parse(hierarchyConfig);
|
||||||
|
|
||||||
// 다음 레벨 찾기
|
// 다음 레벨 찾기
|
||||||
const nextLevel = config.levels?.find((l: any) => l.level === parentLevel + 1);
|
const nextLevel = config.levels?.find(
|
||||||
|
(l: any) => l.level === parentLevel + 1
|
||||||
|
);
|
||||||
|
|
||||||
if (!nextLevel) {
|
if (!nextLevel) {
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
@ -168,7 +168,10 @@ export const getChildrenData = async (req: Request, res: Response): Promise<Resp
|
||||||
};
|
};
|
||||||
|
|
||||||
// 창고 목록 조회 (사용자 지정 테이블) - 레거시, 호환성 유지
|
// 창고 목록 조회 (사용자 지정 테이블) - 레거시, 호환성 유지
|
||||||
export const getWarehouses = async (req: Request, res: Response): Promise<Response> => {
|
export const getWarehouses = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
try {
|
try {
|
||||||
const { externalDbConnectionId, tableName } = req.query;
|
const { externalDbConnectionId, tableName } = req.query;
|
||||||
|
|
||||||
|
|
@ -186,7 +189,9 @@ export const getWarehouses = async (req: Request, res: Response): Promise<Respon
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const connector = await getExternalDbConnector(Number(externalDbConnectionId));
|
const connector = await getExternalDbConnector(
|
||||||
|
Number(externalDbConnectionId)
|
||||||
|
);
|
||||||
|
|
||||||
// 테이블명을 사용하여 모든 컬럼 조회
|
// 테이블명을 사용하여 모든 컬럼 조회
|
||||||
const query = `SELECT * FROM ${tableName} LIMIT 100`;
|
const query = `SELECT * FROM ${tableName} LIMIT 100`;
|
||||||
|
|
@ -215,7 +220,10 @@ export const getWarehouses = async (req: Request, res: Response): Promise<Respon
|
||||||
};
|
};
|
||||||
|
|
||||||
// 구역 목록 조회 (사용자 지정 테이블) - 레거시, 호환성 유지
|
// 구역 목록 조회 (사용자 지정 테이블) - 레거시, 호환성 유지
|
||||||
export const getAreas = async (req: Request, res: Response): Promise<Response> => {
|
export const getAreas = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
try {
|
try {
|
||||||
const { externalDbConnectionId, warehouseKey, tableName } = req.query;
|
const { externalDbConnectionId, warehouseKey, tableName } = req.query;
|
||||||
|
|
||||||
|
|
@ -226,7 +234,9 @@ export const getAreas = async (req: Request, res: Response): Promise<Response> =
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const connector = await getExternalDbConnector(Number(externalDbConnectionId));
|
const connector = await getExternalDbConnector(
|
||||||
|
Number(externalDbConnectionId)
|
||||||
|
);
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
SELECT * FROM ${tableName}
|
SELECT * FROM ${tableName}
|
||||||
|
|
@ -258,7 +268,10 @@ export const getAreas = async (req: Request, res: Response): Promise<Response> =
|
||||||
};
|
};
|
||||||
|
|
||||||
// 위치 목록 조회 (사용자 지정 테이블) - 레거시, 호환성 유지
|
// 위치 목록 조회 (사용자 지정 테이블) - 레거시, 호환성 유지
|
||||||
export const getLocations = async (req: Request, res: Response): Promise<Response> => {
|
export const getLocations = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
try {
|
try {
|
||||||
const { externalDbConnectionId, areaKey, tableName } = req.query;
|
const { externalDbConnectionId, areaKey, tableName } = req.query;
|
||||||
|
|
||||||
|
|
@ -269,7 +282,9 @@ export const getLocations = async (req: Request, res: Response): Promise<Respons
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const connector = await getExternalDbConnector(Number(externalDbConnectionId));
|
const connector = await getExternalDbConnector(
|
||||||
|
Number(externalDbConnectionId)
|
||||||
|
);
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
SELECT * FROM ${tableName}
|
SELECT * FROM ${tableName}
|
||||||
|
|
@ -301,28 +316,38 @@ export const getLocations = async (req: Request, res: Response): Promise<Respons
|
||||||
};
|
};
|
||||||
|
|
||||||
// 자재 목록 조회 (동적 컬럼 매핑 지원)
|
// 자재 목록 조회 (동적 컬럼 매핑 지원)
|
||||||
export const getMaterials = async (req: Request, res: Response): Promise<Response> => {
|
export const getMaterials = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
externalDbConnectionId,
|
externalDbConnectionId,
|
||||||
locaKey,
|
locaKey,
|
||||||
tableName,
|
tableName,
|
||||||
keyColumn,
|
keyColumn,
|
||||||
locationKeyColumn,
|
locationKeyColumn,
|
||||||
layerColumn
|
layerColumn,
|
||||||
} = req.query;
|
} = req.query;
|
||||||
|
|
||||||
if (!externalDbConnectionId || !locaKey || !tableName || !locationKeyColumn) {
|
if (
|
||||||
|
!externalDbConnectionId ||
|
||||||
|
!locaKey ||
|
||||||
|
!tableName ||
|
||||||
|
!locationKeyColumn
|
||||||
|
) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "필수 파라미터가 누락되었습니다.",
|
message: "필수 파라미터가 누락되었습니다.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const connector = await getExternalDbConnector(Number(externalDbConnectionId));
|
const connector = await getExternalDbConnector(
|
||||||
|
Number(externalDbConnectionId)
|
||||||
|
);
|
||||||
|
|
||||||
// 동적 쿼리 생성
|
// 동적 쿼리 생성
|
||||||
const orderByClause = layerColumn ? `ORDER BY ${layerColumn}` : '';
|
const orderByClause = layerColumn ? `ORDER BY ${layerColumn}` : "";
|
||||||
const query = `
|
const query = `
|
||||||
SELECT * FROM ${tableName}
|
SELECT * FROM ${tableName}
|
||||||
WHERE ${locationKeyColumn} = '${locaKey}'
|
WHERE ${locationKeyColumn} = '${locaKey}'
|
||||||
|
|
@ -356,7 +381,10 @@ export const getMaterials = async (req: Request, res: Response): Promise<Respons
|
||||||
};
|
};
|
||||||
|
|
||||||
// 자재 개수 조회 (여러 Location 일괄) - 레거시, 호환성 유지
|
// 자재 개수 조회 (여러 Location 일괄) - 레거시, 호환성 유지
|
||||||
export const getMaterialCounts = async (req: Request, res: Response): Promise<Response> => {
|
export const getMaterialCounts = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
): Promise<Response> => {
|
||||||
try {
|
try {
|
||||||
const { externalDbConnectionId, locationKeys, tableName } = req.body;
|
const { externalDbConnectionId, locationKeys, tableName } = req.body;
|
||||||
|
|
||||||
|
|
@ -367,7 +395,9 @@ export const getMaterialCounts = async (req: Request, res: Response): Promise<Re
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const connector = await getExternalDbConnector(Number(externalDbConnectionId));
|
const connector = await getExternalDbConnector(
|
||||||
|
Number(externalDbConnectionId)
|
||||||
|
);
|
||||||
|
|
||||||
const keysString = locationKeys.map((key: string) => `'${key}'`).join(",");
|
const keysString = locationKeys.map((key: string) => `'${key}'`).join(",");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
|
||||||
lastUsedAt: Date;
|
lastUsedAt: Date;
|
||||||
activeConnections = 0;
|
activeConnections = 0;
|
||||||
maxConnections: number;
|
maxConnections: number;
|
||||||
|
private isPoolClosed = false;
|
||||||
|
|
||||||
constructor(config: ExternalDbConnection) {
|
constructor(config: ExternalDbConnection) {
|
||||||
this.connectionId = config.id!;
|
this.connectionId = config.id!;
|
||||||
|
|
@ -131,6 +132,9 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
|
||||||
waitForConnections: true,
|
waitForConnections: true,
|
||||||
queueLimit: 0,
|
queueLimit: 0,
|
||||||
connectTimeout: (config.connection_timeout || 30) * 1000,
|
connectTimeout: (config.connection_timeout || 30) * 1000,
|
||||||
|
// 연결 유지 및 자동 재연결 설정
|
||||||
|
enableKeepAlive: true,
|
||||||
|
keepAliveInitialDelay: 10000, // 10초마다 keep-alive 패킷 전송
|
||||||
ssl:
|
ssl:
|
||||||
config.ssl_enabled === "Y" ? { rejectUnauthorized: false } : undefined,
|
config.ssl_enabled === "Y" ? { rejectUnauthorized: false } : undefined,
|
||||||
});
|
});
|
||||||
|
|
@ -149,15 +153,46 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
|
||||||
`[${this.dbType.toUpperCase()}] 연결 반환 (${this.activeConnections}/${this.maxConnections})`
|
`[${this.dbType.toUpperCase()}] 연결 반환 (${this.activeConnections}/${this.maxConnections})`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 연결 오류 이벤트 처리
|
||||||
|
this.pool.on("error", (err) => {
|
||||||
|
logger.error(`[${this.dbType.toUpperCase()}] 연결 풀 오류:`, err);
|
||||||
|
// 연결이 닫힌 경우 플래그 설정
|
||||||
|
if (err.message.includes("closed state")) {
|
||||||
|
this.isPoolClosed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async query(sql: string, params?: any[]): Promise<any> {
|
async query(sql: string, params?: any[]): Promise<any> {
|
||||||
this.lastUsedAt = new Date();
|
this.lastUsedAt = new Date();
|
||||||
const [rows] = await this.pool.execute(sql, params);
|
|
||||||
return rows;
|
// 연결 풀이 닫힌 상태인지 확인
|
||||||
|
if (this.isPoolClosed) {
|
||||||
|
throw new Error("연결 풀이 닫힌 상태입니다. 재연결이 필요합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [rows] = await this.pool.execute(sql, params);
|
||||||
|
return rows;
|
||||||
|
} catch (error: any) {
|
||||||
|
// 연결 닫힘 오류 감지
|
||||||
|
if (
|
||||||
|
error.message.includes("closed state") ||
|
||||||
|
error.code === "PROTOCOL_CONNECTION_LOST" ||
|
||||||
|
error.code === "ECONNRESET"
|
||||||
|
) {
|
||||||
|
this.isPoolClosed = true;
|
||||||
|
logger.warn(
|
||||||
|
`[${this.dbType.toUpperCase()}] 연결 끊김 감지 (ID: ${this.connectionId})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnect(): Promise<void> {
|
async disconnect(): Promise<void> {
|
||||||
|
this.isPoolClosed = true;
|
||||||
await this.pool.end();
|
await this.pool.end();
|
||||||
logger.info(
|
logger.info(
|
||||||
`[${this.dbType.toUpperCase()}] 연결 풀 종료 (ID: ${this.connectionId})`
|
`[${this.dbType.toUpperCase()}] 연결 풀 종료 (ID: ${this.connectionId})`
|
||||||
|
|
@ -165,6 +200,10 @@ class MySQLPoolWrapper implements ConnectionPoolWrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
isHealthy(): boolean {
|
isHealthy(): boolean {
|
||||||
|
// 연결 풀이 닫혔으면 비정상
|
||||||
|
if (this.isPoolClosed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return this.activeConnections < this.maxConnections;
|
return this.activeConnections < this.maxConnections;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -230,9 +269,11 @@ export class ExternalDbConnectionPoolService {
|
||||||
): Promise<ConnectionPoolWrapper> {
|
): Promise<ConnectionPoolWrapper> {
|
||||||
logger.info(`🔧 새 연결 풀 생성 중 (ID: ${connectionId})...`);
|
logger.info(`🔧 새 연결 풀 생성 중 (ID: ${connectionId})...`);
|
||||||
|
|
||||||
// DB 연결 정보 조회
|
// DB 연결 정보 조회 (실제 비밀번호 포함)
|
||||||
const connectionResult =
|
const connectionResult =
|
||||||
await ExternalDbConnectionService.getConnectionById(connectionId);
|
await ExternalDbConnectionService.getConnectionByIdWithPassword(
|
||||||
|
connectionId
|
||||||
|
);
|
||||||
|
|
||||||
if (!connectionResult.success || !connectionResult.data) {
|
if (!connectionResult.success || !connectionResult.data) {
|
||||||
throw new Error(`연결 정보를 찾을 수 없습니다 (ID: ${connectionId})`);
|
throw new Error(`연결 정보를 찾을 수 없습니다 (ID: ${connectionId})`);
|
||||||
|
|
@ -296,16 +337,19 @@ export class ExternalDbConnectionPoolService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 쿼리 실행 (자동으로 연결 풀 관리)
|
* 쿼리 실행 (자동으로 연결 풀 관리 + 재시도 로직)
|
||||||
*/
|
*/
|
||||||
async executeQuery(
|
async executeQuery(
|
||||||
connectionId: number,
|
connectionId: number,
|
||||||
sql: string,
|
sql: string,
|
||||||
params?: any[]
|
params?: any[],
|
||||||
|
retryCount = 0
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const pool = await this.getPool(connectionId);
|
const MAX_RETRIES = 2;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const pool = await this.getPool(connectionId);
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`📊 쿼리 실행 (ID: ${connectionId}): ${sql.substring(0, 100)}...`
|
`📊 쿼리 실행 (ID: ${connectionId}): ${sql.substring(0, 100)}...`
|
||||||
);
|
);
|
||||||
|
|
@ -314,7 +358,29 @@ export class ExternalDbConnectionPoolService {
|
||||||
`✅ 쿼리 완료 (ID: ${connectionId}), 결과: ${result.length}건`
|
`✅ 쿼리 완료 (ID: ${connectionId}), 결과: ${result.length}건`
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
|
// 연결 끊김 오류인 경우 재시도
|
||||||
|
const isConnectionError =
|
||||||
|
error.message?.includes("closed state") ||
|
||||||
|
error.message?.includes("연결 풀이 닫힌 상태") ||
|
||||||
|
error.code === "PROTOCOL_CONNECTION_LOST" ||
|
||||||
|
error.code === "ECONNRESET" ||
|
||||||
|
error.code === "ETIMEDOUT";
|
||||||
|
|
||||||
|
if (isConnectionError && retryCount < MAX_RETRIES) {
|
||||||
|
logger.warn(
|
||||||
|
`🔄 연결 오류 감지, 재시도 중... (${retryCount + 1}/${MAX_RETRIES}) (ID: ${connectionId})`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 기존 풀 제거 후 새로 생성
|
||||||
|
await this.removePool(connectionId);
|
||||||
|
|
||||||
|
// 잠시 대기 후 재시도
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
return this.executeQuery(connectionId, sql, params, retryCount + 1);
|
||||||
|
}
|
||||||
|
|
||||||
logger.error(`❌ 쿼리 실행 실패 (ID: ${connectionId}):`, error);
|
logger.error(`❌ 쿼리 실행 실패 (ID: ${connectionId}):`, error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue