feat: 배치 관리 시스템 구현

 주요 기능:
- 배치 설정 관리 (생성/수정/삭제/실행)
- 배치 실행 로그 관리 및 모니터링
- 배치 스케줄러 자동 실행 (cron 기반)
- 외부 DB 연결을 통한 데이터 동기화
- Oracle, MSSQL, MariaDB 커넥터 지원

🔧 백엔드 구현:
- BatchManagementController: 배치 설정 CRUD
- BatchExecutionLogController: 실행 로그 관리
- BatchSchedulerService: 자동 스케줄링
- BatchExternalDbService: 외부 DB 연동
- 배치 관련 테이블 스키마 추가

🎨 프론트엔드 구현:
- 배치 관리 대시보드 UI
- 배치 생성/수정 폼
- 실행 로그 모니터링 화면
- 수동 실행 및 상태 관리

🛡️ 안전성:
- 기존 시스템과 독립적 구현
- 트랜잭션 기반 안전한 데이터 처리
- 에러 핸들링 및 로깅 강화
This commit is contained in:
2025-09-25 11:04:16 +09:00
parent 4abf5b31c0
commit 949aab0b73
33 changed files with 8549 additions and 961 deletions

View File

@@ -0,0 +1,299 @@
// 배치 실행 로그 서비스
// 작성일: 2024-12-24
import prisma from "../config/database";
import {
BatchExecutionLog,
CreateBatchExecutionLogRequest,
UpdateBatchExecutionLogRequest,
BatchExecutionLogFilter,
BatchExecutionLogWithConfig
} from "../types/batchExecutionLogTypes";
import { ApiResponse } from "../types/batchTypes";
export class BatchExecutionLogService {
/**
* 배치 실행 로그 목록 조회
*/
static async getExecutionLogs(
filter: BatchExecutionLogFilter = {}
): Promise<ApiResponse<BatchExecutionLogWithConfig[]>> {
try {
const {
batch_config_id,
execution_status,
start_date,
end_date,
page = 1,
limit = 50
} = filter;
const skip = (page - 1) * limit;
const take = limit;
// WHERE 조건 구성
const where: any = {};
if (batch_config_id) {
where.batch_config_id = batch_config_id;
}
if (execution_status) {
where.execution_status = execution_status;
}
if (start_date || end_date) {
where.start_time = {};
if (start_date) {
where.start_time.gte = start_date;
}
if (end_date) {
where.start_time.lte = end_date;
}
}
// 로그 조회
const [logs, total] = await Promise.all([
prisma.batch_execution_logs.findMany({
where,
include: {
batch_config: {
select: {
id: true,
batch_name: true,
description: true,
cron_schedule: true,
is_active: true
}
}
},
orderBy: { start_time: 'desc' },
skip,
take
}),
prisma.batch_execution_logs.count({ where })
]);
return {
success: true,
data: logs as BatchExecutionLogWithConfig[],
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
};
} catch (error) {
console.error("배치 실행 로그 조회 실패:", error);
return {
success: false,
message: "배치 실행 로그 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 배치 실행 로그 생성
*/
static async createExecutionLog(
data: CreateBatchExecutionLogRequest
): Promise<ApiResponse<BatchExecutionLog>> {
try {
const log = await prisma.batch_execution_logs.create({
data: {
batch_config_id: data.batch_config_id,
execution_status: data.execution_status,
start_time: data.start_time || new Date(),
end_time: data.end_time,
duration_ms: data.duration_ms,
total_records: data.total_records || 0,
success_records: data.success_records || 0,
failed_records: data.failed_records || 0,
error_message: data.error_message,
error_details: data.error_details,
server_name: data.server_name || process.env.HOSTNAME || 'unknown',
process_id: data.process_id || process.pid?.toString()
}
});
return {
success: true,
data: log as BatchExecutionLog,
message: "배치 실행 로그가 생성되었습니다."
};
} catch (error) {
console.error("배치 실행 로그 생성 실패:", error);
return {
success: false,
message: "배치 실행 로그 생성 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 배치 실행 로그 업데이트
*/
static async updateExecutionLog(
id: number,
data: UpdateBatchExecutionLogRequest
): Promise<ApiResponse<BatchExecutionLog>> {
try {
const log = await prisma.batch_execution_logs.update({
where: { id },
data: {
execution_status: data.execution_status,
end_time: data.end_time,
duration_ms: data.duration_ms,
total_records: data.total_records,
success_records: data.success_records,
failed_records: data.failed_records,
error_message: data.error_message,
error_details: data.error_details
}
});
return {
success: true,
data: log as BatchExecutionLog,
message: "배치 실행 로그가 업데이트되었습니다."
};
} catch (error) {
console.error("배치 실행 로그 업데이트 실패:", error);
return {
success: false,
message: "배치 실행 로그 업데이트 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 배치 실행 로그 삭제
*/
static async deleteExecutionLog(id: number): Promise<ApiResponse<void>> {
try {
await prisma.batch_execution_logs.delete({
where: { id }
});
return {
success: true,
message: "배치 실행 로그가 삭제되었습니다."
};
} catch (error) {
console.error("배치 실행 로그 삭제 실패:", error);
return {
success: false,
message: "배치 실행 로그 삭제 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 특정 배치의 최신 실행 로그 조회
*/
static async getLatestExecutionLog(
batchConfigId: number
): Promise<ApiResponse<BatchExecutionLog | null>> {
try {
const log = await prisma.batch_execution_logs.findFirst({
where: { batch_config_id: batchConfigId },
orderBy: { start_time: 'desc' }
});
return {
success: true,
data: log as BatchExecutionLog | null
};
} catch (error) {
console.error("최신 배치 실행 로그 조회 실패:", error);
return {
success: false,
message: "최신 배치 실행 로그 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 배치 실행 통계 조회
*/
static async getExecutionStats(
batchConfigId?: number,
startDate?: Date,
endDate?: Date
): Promise<ApiResponse<{
total_executions: number;
success_count: number;
failed_count: number;
success_rate: number;
average_duration_ms: number;
total_records_processed: number;
}>> {
try {
const where: any = {};
if (batchConfigId) {
where.batch_config_id = batchConfigId;
}
if (startDate || endDate) {
where.start_time = {};
if (startDate) {
where.start_time.gte = startDate;
}
if (endDate) {
where.start_time.lte = endDate;
}
}
const logs = await prisma.batch_execution_logs.findMany({
where,
select: {
execution_status: true,
duration_ms: true,
total_records: true
}
});
const total_executions = logs.length;
const success_count = logs.filter((log: any) => log.execution_status === 'SUCCESS').length;
const failed_count = logs.filter((log: any) => log.execution_status === 'FAILED').length;
const success_rate = total_executions > 0 ? (success_count / total_executions) * 100 : 0;
const validDurations = logs
.filter((log: any) => log.duration_ms !== null)
.map((log: any) => log.duration_ms!);
const average_duration_ms = validDurations.length > 0
? validDurations.reduce((sum: number, duration: number) => sum + duration, 0) / validDurations.length
: 0;
const total_records_processed = logs
.filter((log: any) => log.total_records !== null)
.reduce((sum: number, log: any) => sum + (log.total_records || 0), 0);
return {
success: true,
data: {
total_executions,
success_count,
failed_count,
success_rate,
average_duration_ms,
total_records_processed
}
};
} catch (error) {
console.error("배치 실행 통계 조회 실패:", error);
return {
success: false,
message: "배치 실행 통계 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
}

View File

@@ -0,0 +1,686 @@
// 배치관리 전용 외부 DB 서비스
// 기존 ExternalDbConnectionService와 분리하여 배치관리 시스템에 특화된 기능 제공
// 작성일: 2024-12-24
import prisma from "../config/database";
import { PasswordEncryption } from "../utils/passwordEncryption";
import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory";
import { ApiResponse, ColumnInfo, TableInfo } from "../types/batchTypes";
export class BatchExternalDbService {
/**
* 배치관리용 외부 DB 연결 목록 조회
*/
static async getAvailableConnections(): Promise<ApiResponse<Array<{
type: 'internal' | 'external';
id?: number;
name: string;
db_type?: string;
}>>> {
try {
const connections: Array<{
type: 'internal' | 'external';
id?: number;
name: string;
db_type?: string;
}> = [];
// 내부 DB 추가
connections.push({
type: 'internal',
name: '내부 데이터베이스 (PostgreSQL)',
db_type: 'postgresql'
});
// 활성화된 외부 DB 연결 조회
const externalConnections = await prisma.external_db_connections.findMany({
where: { is_active: 'Y' },
select: {
id: true,
connection_name: true,
db_type: true,
description: true
},
orderBy: { connection_name: 'asc' }
});
// 외부 DB 연결 추가
externalConnections.forEach(conn => {
connections.push({
type: 'external',
id: conn.id,
name: `${conn.connection_name} (${conn.db_type?.toUpperCase()})`,
db_type: conn.db_type || undefined
});
});
return {
success: true,
data: connections,
message: `${connections.length}개의 연결을 조회했습니다.`
};
} catch (error) {
console.error("배치관리 연결 목록 조회 실패:", error);
return {
success: false,
message: "연결 목록 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 배치관리용 테이블 목록 조회
*/
static async getTablesFromConnection(
connectionType: 'internal' | 'external',
connectionId?: number
): Promise<ApiResponse<TableInfo[]>> {
try {
let tables: TableInfo[] = [];
if (connectionType === 'internal') {
// 내부 DB 테이블 조회
const result = await prisma.$queryRaw<Array<{ table_name: string }>>`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name
`;
tables = result.map(row => ({
table_name: row.table_name,
columns: []
}));
} else if (connectionType === 'external' && connectionId) {
// 외부 DB 테이블 조회
const tablesResult = await this.getExternalTables(connectionId);
if (tablesResult.success && tablesResult.data) {
tables = tablesResult.data;
}
}
return {
success: true,
data: tables,
message: `${tables.length}개의 테이블을 조회했습니다.`
};
} catch (error) {
console.error("배치관리 테이블 목록 조회 실패:", error);
return {
success: false,
message: "테이블 목록 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 배치관리용 테이블 컬럼 정보 조회
*/
static async getTableColumns(
connectionType: 'internal' | 'external',
connectionId: number | undefined,
tableName: string
): Promise<ApiResponse<ColumnInfo[]>> {
try {
console.log(`[BatchExternalDbService] getTableColumns 호출:`, {
connectionType,
connectionId,
tableName
});
let columns: ColumnInfo[] = [];
if (connectionType === 'internal') {
// 내부 DB 컬럼 조회
console.log(`[BatchExternalDbService] 내부 DB 컬럼 조회 시작: ${tableName}`);
const result = await prisma.$queryRaw<Array<{
column_name: string;
data_type: string;
is_nullable: string;
column_default: string | null
}>>`
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = ${tableName}
ORDER BY ordinal_position
`;
console.log(`[BatchExternalDbService] 내부 DB 컬럼 조회 결과:`, result);
columns = result.map(row => ({
column_name: row.column_name,
data_type: row.data_type,
is_nullable: row.is_nullable,
column_default: row.column_default,
}));
} else if (connectionType === 'external' && connectionId) {
// 외부 DB 컬럼 조회
console.log(`[BatchExternalDbService] 외부 DB 컬럼 조회 시작: connectionId=${connectionId}, tableName=${tableName}`);
const columnsResult = await this.getExternalTableColumns(connectionId, tableName);
console.log(`[BatchExternalDbService] 외부 DB 컬럼 조회 결과:`, columnsResult);
if (columnsResult.success && columnsResult.data) {
columns = columnsResult.data;
}
}
console.log(`[BatchExternalDbService] 최종 컬럼 목록:`, columns);
return {
success: true,
data: columns,
message: `${columns.length}개의 컬럼을 조회했습니다.`
};
} catch (error) {
console.error("[BatchExternalDbService] 컬럼 정보 조회 오류:", error);
return {
success: false,
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 외부 DB 테이블 목록 조회 (내부 구현)
*/
private static async getExternalTables(connectionId: number): Promise<ApiResponse<TableInfo[]>> {
try {
// 연결 정보 조회
const connection = await prisma.external_db_connections.findUnique({
where: { id: connectionId }
});
if (!connection) {
return {
success: false,
message: "연결 정보를 찾을 수 없습니다."
};
}
// 비밀번호 복호화
const decryptedPassword = PasswordEncryption.decrypt(connection.password);
if (!decryptedPassword) {
return {
success: false,
message: "비밀번호 복호화에 실패했습니다."
};
}
// 연결 설정 준비
const config = {
host: connection.host,
port: connection.port,
database: connection.database_name,
user: connection.username,
password: decryptedPassword,
connectionTimeoutMillis: connection.connection_timeout != null ? connection.connection_timeout * 1000 : undefined,
queryTimeoutMillis: connection.query_timeout != null ? connection.query_timeout * 1000 : undefined,
ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
};
// DatabaseConnectorFactory를 통한 테이블 목록 조회
const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, connectionId);
const tables = await connector.getTables();
return {
success: true,
message: "테이블 목록을 조회했습니다.",
data: tables
};
} catch (error) {
console.error("외부 DB 테이블 목록 조회 오류:", error);
return {
success: false,
message: "테이블 목록 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 외부 DB 테이블 컬럼 정보 조회 (내부 구현)
*/
private static async getExternalTableColumns(connectionId: number, tableName: string): Promise<ApiResponse<ColumnInfo[]>> {
try {
console.log(`[BatchExternalDbService] getExternalTableColumns 호출: connectionId=${connectionId}, tableName=${tableName}`);
// 연결 정보 조회
const connection = await prisma.external_db_connections.findUnique({
where: { id: connectionId }
});
if (!connection) {
console.log(`[BatchExternalDbService] 연결 정보를 찾을 수 없음: connectionId=${connectionId}`);
return {
success: false,
message: "연결 정보를 찾을 수 없습니다."
};
}
console.log(`[BatchExternalDbService] 연결 정보 조회 성공:`, {
id: connection.id,
connection_name: connection.connection_name,
db_type: connection.db_type,
host: connection.host,
port: connection.port,
database_name: connection.database_name
});
// 비밀번호 복호화
const decryptedPassword = PasswordEncryption.decrypt(connection.password);
// 연결 설정 준비
const config = {
host: connection.host,
port: connection.port,
database: connection.database_name,
user: connection.username,
password: decryptedPassword,
connectionTimeoutMillis: connection.connection_timeout != null ? connection.connection_timeout * 1000 : undefined,
queryTimeoutMillis: connection.query_timeout != null ? connection.query_timeout * 1000 : undefined,
ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
};
console.log(`[BatchExternalDbService] 커넥터 생성 시작: db_type=${connection.db_type}`);
// 데이터베이스 타입에 따른 커넥터 생성
const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, connectionId);
console.log(`[BatchExternalDbService] 커넥터 생성 완료, 컬럼 조회 시작: tableName=${tableName}`);
// 컬럼 정보 조회
console.log(`[BatchExternalDbService] connector.getColumns 호출 전`);
const columns = await connector.getColumns(tableName);
console.log(`[BatchExternalDbService] 원본 컬럼 조회 결과:`, columns);
console.log(`[BatchExternalDbService] 원본 컬럼 개수:`, columns ? columns.length : 'null/undefined');
// 각 데이터베이스 커넥터의 반환 구조가 다르므로 통일된 구조로 변환
const standardizedColumns: ColumnInfo[] = columns.map((col: any) => {
console.log(`[BatchExternalDbService] 컬럼 변환 중:`, col);
// MySQL/MariaDB 구조: {name, dataType, isNullable, defaultValue} (MySQLConnector만)
if (col.name && col.dataType !== undefined) {
const result = {
column_name: col.name,
data_type: col.dataType,
is_nullable: col.isNullable ? 'YES' : 'NO',
column_default: col.defaultValue || null,
};
console.log(`[BatchExternalDbService] MySQL/MariaDB 구조로 변환:`, result);
return result;
}
// PostgreSQL/Oracle/MSSQL/MariaDB 구조: {column_name, data_type, is_nullable, column_default}
else {
const result = {
column_name: col.column_name || col.COLUMN_NAME,
data_type: col.data_type || col.DATA_TYPE,
is_nullable: col.is_nullable || col.IS_NULLABLE || (col.nullable === 'Y' ? 'YES' : 'NO'),
column_default: col.column_default || col.COLUMN_DEFAULT || null,
};
console.log(`[BatchExternalDbService] 표준 구조로 변환:`, result);
return result;
}
});
console.log(`[BatchExternalDbService] 표준화된 컬럼 목록:`, standardizedColumns);
// 빈 배열인 경우 경고 로그
if (!standardizedColumns || standardizedColumns.length === 0) {
console.warn(`[BatchExternalDbService] 컬럼이 비어있음: connectionId=${connectionId}, tableName=${tableName}`);
console.warn(`[BatchExternalDbService] 연결 정보:`, {
db_type: connection.db_type,
host: connection.host,
port: connection.port,
database_name: connection.database_name,
username: connection.username
});
// 테이블 존재 여부 확인
console.warn(`[BatchExternalDbService] 테이블 존재 여부 확인을 위해 테이블 목록 조회 시도`);
try {
const tables = await connector.getTables();
console.warn(`[BatchExternalDbService] 사용 가능한 테이블 목록:`, tables.map(t => t.table_name));
// 테이블명이 정확한지 확인
const tableExists = tables.some(t => t.table_name.toLowerCase() === tableName.toLowerCase());
console.warn(`[BatchExternalDbService] 테이블 존재 여부: ${tableExists}`);
// 정확한 테이블명 찾기
const exactTable = tables.find(t => t.table_name.toLowerCase() === tableName.toLowerCase());
if (exactTable) {
console.warn(`[BatchExternalDbService] 정확한 테이블명: ${exactTable.table_name}`);
}
// 모든 테이블명 출력
console.warn(`[BatchExternalDbService] 모든 테이블명:`, tables.map(t => `"${t.table_name}"`));
// 테이블명 비교
console.warn(`[BatchExternalDbService] 요청된 테이블명: "${tableName}"`);
console.warn(`[BatchExternalDbService] 테이블명 비교 결과:`, tables.map(t => ({
table_name: t.table_name,
matches: t.table_name.toLowerCase() === tableName.toLowerCase(),
exact_match: t.table_name === tableName
})));
// 정확한 테이블명으로 다시 시도
if (exactTable && exactTable.table_name !== tableName) {
console.warn(`[BatchExternalDbService] 정확한 테이블명으로 다시 시도: ${exactTable.table_name}`);
try {
const correctColumns = await connector.getColumns(exactTable.table_name);
console.warn(`[BatchExternalDbService] 정확한 테이블명으로 조회한 컬럼:`, correctColumns);
} catch (correctError) {
console.error(`[BatchExternalDbService] 정확한 테이블명으로 조회 실패:`, correctError);
}
}
} catch (tableError) {
console.error(`[BatchExternalDbService] 테이블 목록 조회 실패:`, tableError);
}
}
return {
success: true,
data: standardizedColumns,
message: "컬럼 정보를 조회했습니다."
};
} catch (error) {
console.error("[BatchExternalDbService] 외부 DB 컬럼 정보 조회 오류:", error);
console.error("[BatchExternalDbService] 오류 스택:", error instanceof Error ? error.stack : 'No stack trace');
return {
success: false,
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 외부 DB 테이블에서 데이터 조회
*/
static async getDataFromTable(
connectionId: number,
tableName: string,
limit: number = 100
): Promise<ApiResponse<any[]>> {
try {
console.log(`[BatchExternalDbService] 외부 DB 데이터 조회: connectionId=${connectionId}, tableName=${tableName}`);
// 외부 DB 연결 정보 조회
const connection = await prisma.external_db_connections.findUnique({
where: { id: connectionId }
});
if (!connection) {
return {
success: false,
message: "외부 DB 연결을 찾을 수 없습니다."
};
}
// 패스워드 복호화
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 커넥터 생성
const connector = await DatabaseConnectorFactory.createConnector(
connection.db_type || 'postgresql',
config,
connectionId
);
// 데이터 조회 (DB 타입에 따라 쿼리 구문 변경)
let query: string;
const dbType = connection.db_type?.toLowerCase() || 'postgresql';
if (dbType === 'oracle') {
query = `SELECT * FROM ${tableName} WHERE ROWNUM <= ${limit}`;
} else {
query = `SELECT * FROM ${tableName} LIMIT ${limit}`;
}
console.log(`[BatchExternalDbService] 실행할 쿼리: ${query}`);
const result = await connector.executeQuery(query);
console.log(`[BatchExternalDbService] 외부 DB 데이터 조회 완료: ${result.rows.length}개 레코드`);
return {
success: true,
data: result.rows
};
} catch (error) {
console.error(`외부 DB 데이터 조회 오류 (connectionId: ${connectionId}, table: ${tableName}):`, error);
return {
success: false,
message: "외부 DB 데이터 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 외부 DB 테이블에서 특정 컬럼들만 조회
*/
static async getDataFromTableWithColumns(
connectionId: number,
tableName: string,
columns: string[],
limit: number = 100
): Promise<ApiResponse<any[]>> {
try {
console.log(`[BatchExternalDbService] 외부 DB 특정 컬럼 조회: connectionId=${connectionId}, tableName=${tableName}, columns=[${columns.join(', ')}]`);
// 외부 DB 연결 정보 조회
const connection = await prisma.external_db_connections.findUnique({
where: { id: connectionId }
});
if (!connection) {
return {
success: false,
message: "외부 DB 연결을 찾을 수 없습니다."
};
}
// 패스워드 복호화
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 커넥터 생성
const connector = await DatabaseConnectorFactory.createConnector(
connection.db_type || 'postgresql',
config,
connectionId
);
// 데이터 조회 (DB 타입에 따라 쿼리 구문 변경)
let query: string;
const dbType = connection.db_type?.toLowerCase() || 'postgresql';
const columnList = columns.join(', ');
if (dbType === 'oracle') {
query = `SELECT ${columnList} FROM ${tableName} WHERE ROWNUM <= ${limit}`;
} else {
query = `SELECT ${columnList} FROM ${tableName} LIMIT ${limit}`;
}
console.log(`[BatchExternalDbService] 실행할 쿼리: ${query}`);
const result = await connector.executeQuery(query);
console.log(`[BatchExternalDbService] 외부 DB 특정 컬럼 조회 완료: ${result.rows.length}개 레코드`);
return {
success: true,
data: result.rows
};
} catch (error) {
console.error(`외부 DB 특정 컬럼 조회 오류 (connectionId: ${connectionId}, table: ${tableName}):`, error);
return {
success: false,
message: "외부 DB 특정 컬럼 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 외부 DB 테이블에 데이터 삽입
*/
static async insertDataToTable(
connectionId: number,
tableName: string,
data: any[]
): Promise<ApiResponse<{ successCount: number; failedCount: number }>> {
try {
console.log(`[BatchExternalDbService] 외부 DB 데이터 삽입: connectionId=${connectionId}, tableName=${tableName}, ${data.length}개 레코드`);
if (!data || data.length === 0) {
return {
success: true,
data: { successCount: 0, failedCount: 0 }
};
}
// 외부 DB 연결 정보 조회
const connection = await prisma.external_db_connections.findUnique({
where: { id: connectionId }
});
if (!connection) {
return {
success: false,
message: "외부 DB 연결을 찾을 수 없습니다."
};
}
// 패스워드 복호화
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 커넥터 생성
const connector = await DatabaseConnectorFactory.createConnector(
connection.db_type || 'postgresql',
config,
connectionId
);
let successCount = 0;
let failedCount = 0;
// 각 레코드를 개별적으로 삽입 (UPSERT 방식으로 중복 처리)
for (const record of data) {
try {
const columns = Object.keys(record);
const values = Object.values(record);
// 값들을 SQL 문자열로 변환 (타입별 처리)
const formattedValues = values.map(value => {
if (value === null || value === undefined) {
return 'NULL';
} else if (value instanceof Date) {
// Date 객체를 MySQL/MariaDB 형식으로 변환
return `'${value.toISOString().slice(0, 19).replace('T', ' ')}'`;
} else if (typeof value === 'string') {
// 문자열이 날짜 형식인지 확인
const dateRegex = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{2}\s+\d{4}\s+\d{2}:\d{2}:\d{2}/;
if (dateRegex.test(value)) {
// JavaScript Date 문자열을 MySQL 형식으로 변환
const date = new Date(value);
return `'${date.toISOString().slice(0, 19).replace('T', ' ')}'`;
} else {
return `'${value.replace(/'/g, "''")}'`; // SQL 인젝션 방지를 위한 간단한 이스케이프
}
} else if (typeof value === 'number') {
return String(value);
} else if (typeof value === 'boolean') {
return value ? '1' : '0';
} else {
// 기타 객체는 문자열로 변환
return `'${String(value).replace(/'/g, "''")}'`;
}
}).join(', ');
// Primary Key 컬럼 추정
const primaryKeyColumn = columns.includes('id') ? 'id' :
columns.includes('user_id') ? 'user_id' :
columns[0];
// UPDATE SET 절 생성 (Primary Key 제외)
const updateColumns = columns.filter(col => col !== primaryKeyColumn);
let query: string;
const dbType = connection.db_type?.toLowerCase() || 'mysql';
if (dbType === 'mysql' || dbType === 'mariadb') {
// MySQL/MariaDB: ON DUPLICATE KEY UPDATE 사용
if (updateColumns.length > 0) {
const updateSet = updateColumns.map(col => `${col} = VALUES(${col})`).join(', ');
query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${formattedValues})
ON DUPLICATE KEY UPDATE ${updateSet}`;
} else {
// Primary Key만 있는 경우 IGNORE 사용
query = `INSERT IGNORE INTO ${tableName} (${columns.join(', ')}) VALUES (${formattedValues})`;
}
} else {
// 다른 DB는 기본 INSERT 사용
query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${formattedValues})`;
}
await connector.executeQuery(query);
successCount++;
} catch (error) {
console.error(`외부 DB 레코드 UPSERT 실패:`, error);
failedCount++;
}
}
console.log(`[BatchExternalDbService] 외부 DB 데이터 삽입 완료: 성공 ${successCount}개, 실패 ${failedCount}`);
return {
success: true,
data: { successCount, failedCount }
};
} catch (error) {
console.error(`외부 DB 데이터 삽입 오류 (connectionId: ${connectionId}, table: ${tableName}):`, error);
return {
success: false,
message: "외부 DB 데이터 삽입 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
}

View File

@@ -0,0 +1,370 @@
// 배치관리 전용 서비스 (기존 소스와 완전 분리)
// 작성일: 2024-12-24
import prisma from "../config/database";
import { PasswordEncryption } from "../utils/passwordEncryption";
import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory";
// 배치관리 전용 타입 정의
export interface BatchConnectionInfo {
type: 'internal' | 'external';
id?: number;
name: string;
db_type?: string;
}
export interface BatchTableInfo {
table_name: string;
columns: BatchColumnInfo[];
description?: string | null;
}
export interface BatchColumnInfo {
column_name: string;
data_type: string;
is_nullable?: string;
column_default?: string | null;
}
export interface BatchApiResponse<T = unknown> {
success: boolean;
data?: T;
message?: string;
error?: string;
}
export class BatchManagementService {
/**
* 배치관리용 연결 목록 조회
*/
static async getAvailableConnections(): Promise<BatchApiResponse<BatchConnectionInfo[]>> {
try {
const connections: BatchConnectionInfo[] = [];
// 내부 DB 추가
connections.push({
type: 'internal',
name: '내부 데이터베이스 (PostgreSQL)',
db_type: 'postgresql'
});
// 활성화된 외부 DB 연결 조회
const externalConnections = await prisma.external_db_connections.findMany({
where: { is_active: 'Y' },
select: {
id: true,
connection_name: true,
db_type: true,
description: true
},
orderBy: { connection_name: 'asc' }
});
// 외부 DB 연결 추가
externalConnections.forEach(conn => {
connections.push({
type: 'external',
id: conn.id,
name: `${conn.connection_name} (${conn.db_type?.toUpperCase()})`,
db_type: conn.db_type || undefined
});
});
return {
success: true,
data: connections,
message: `${connections.length}개의 연결을 조회했습니다.`
};
} catch (error) {
console.error("배치관리 연결 목록 조회 실패:", error);
return {
success: false,
message: "연결 목록 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 배치관리용 테이블 목록 조회
*/
static async getTablesFromConnection(
connectionType: 'internal' | 'external',
connectionId?: number
): Promise<BatchApiResponse<BatchTableInfo[]>> {
try {
let tables: BatchTableInfo[] = [];
if (connectionType === 'internal') {
// 내부 DB 테이블 조회
const result = await prisma.$queryRaw<Array<{ table_name: string }>>`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name
`;
tables = result.map(row => ({
table_name: row.table_name,
columns: []
}));
} else if (connectionType === 'external' && connectionId) {
// 외부 DB 테이블 조회
const tablesResult = await this.getExternalTables(connectionId);
if (tablesResult.success && tablesResult.data) {
tables = tablesResult.data;
}
}
return {
success: true,
data: tables,
message: `${tables.length}개의 테이블을 조회했습니다.`
};
} catch (error) {
console.error("배치관리 테이블 목록 조회 실패:", error);
return {
success: false,
message: "테이블 목록 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 배치관리용 테이블 컬럼 정보 조회
*/
static async getTableColumns(
connectionType: 'internal' | 'external',
connectionId: number | undefined,
tableName: string
): Promise<BatchApiResponse<BatchColumnInfo[]>> {
try {
console.log(`[BatchManagementService] getTableColumns 호출:`, {
connectionType,
connectionId,
tableName
});
let columns: BatchColumnInfo[] = [];
if (connectionType === 'internal') {
// 내부 DB 컬럼 조회
console.log(`[BatchManagementService] 내부 DB 컬럼 조회 시작: ${tableName}`);
const result = await prisma.$queryRaw<Array<{
column_name: string;
data_type: string;
is_nullable: string;
column_default: string | null
}>>`
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = ${tableName}
ORDER BY ordinal_position
`;
console.log(`[BatchManagementService] 내부 DB 컬럼 조회 결과:`, result);
columns = result.map(row => ({
column_name: row.column_name,
data_type: row.data_type,
is_nullable: row.is_nullable,
column_default: row.column_default,
}));
} else if (connectionType === 'external' && connectionId) {
// 외부 DB 컬럼 조회
console.log(`[BatchManagementService] 외부 DB 컬럼 조회 시작: connectionId=${connectionId}, tableName=${tableName}`);
const columnsResult = await this.getExternalTableColumns(connectionId, tableName);
console.log(`[BatchManagementService] 외부 DB 컬럼 조회 결과:`, columnsResult);
if (columnsResult.success && columnsResult.data) {
columns = columnsResult.data;
}
}
console.log(`[BatchManagementService] 최종 컬럼 목록:`, columns);
return {
success: true,
data: columns,
message: `${columns.length}개의 컬럼을 조회했습니다.`
};
} catch (error) {
console.error("[BatchManagementService] 컬럼 정보 조회 오류:", error);
return {
success: false,
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 외부 DB 테이블 목록 조회 (내부 구현)
*/
private static async getExternalTables(connectionId: number): Promise<BatchApiResponse<BatchTableInfo[]>> {
try {
// 연결 정보 조회
const connection = await prisma.external_db_connections.findUnique({
where: { id: connectionId }
});
if (!connection) {
return {
success: false,
message: "연결 정보를 찾을 수 없습니다."
};
}
// 비밀번호 복호화
const decryptedPassword = PasswordEncryption.decrypt(connection.password);
if (!decryptedPassword) {
return {
success: false,
message: "비밀번호 복호화에 실패했습니다."
};
}
// 연결 설정 준비
const config = {
host: connection.host,
port: connection.port,
database: connection.database_name,
user: connection.username,
password: decryptedPassword,
connectionTimeoutMillis: connection.connection_timeout != null ? connection.connection_timeout * 1000 : undefined,
queryTimeoutMillis: connection.query_timeout != null ? connection.query_timeout * 1000 : undefined,
ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
};
// DatabaseConnectorFactory를 통한 테이블 목록 조회
const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, connectionId);
const tables = await connector.getTables();
return {
success: true,
message: "테이블 목록을 조회했습니다.",
data: tables
};
} catch (error) {
console.error("외부 DB 테이블 목록 조회 오류:", error);
return {
success: false,
message: "테이블 목록 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 외부 DB 테이블 컬럼 정보 조회 (내부 구현)
*/
private static async getExternalTableColumns(connectionId: number, tableName: string): Promise<BatchApiResponse<BatchColumnInfo[]>> {
try {
console.log(`[BatchManagementService] getExternalTableColumns 호출: connectionId=${connectionId}, tableName=${tableName}`);
// 연결 정보 조회
const connection = await prisma.external_db_connections.findUnique({
where: { id: connectionId }
});
if (!connection) {
console.log(`[BatchManagementService] 연결 정보를 찾을 수 없음: connectionId=${connectionId}`);
return {
success: false,
message: "연결 정보를 찾을 수 없습니다."
};
}
console.log(`[BatchManagementService] 연결 정보 조회 성공:`, {
id: connection.id,
connection_name: connection.connection_name,
db_type: connection.db_type,
host: connection.host,
port: connection.port,
database_name: connection.database_name
});
// 비밀번호 복호화
const decryptedPassword = PasswordEncryption.decrypt(connection.password);
// 연결 설정 준비
const config = {
host: connection.host,
port: connection.port,
database: connection.database_name,
user: connection.username,
password: decryptedPassword,
connectionTimeoutMillis: connection.connection_timeout != null ? connection.connection_timeout * 1000 : undefined,
queryTimeoutMillis: connection.query_timeout != null ? connection.query_timeout * 1000 : undefined,
ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
};
console.log(`[BatchManagementService] 커넥터 생성 시작: db_type=${connection.db_type}`);
// 데이터베이스 타입에 따른 커넥터 생성
const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, connectionId);
console.log(`[BatchManagementService] 커넥터 생성 완료, 컬럼 조회 시작: tableName=${tableName}`);
// 컬럼 정보 조회
console.log(`[BatchManagementService] connector.getColumns 호출 전`);
const columns = await connector.getColumns(tableName);
console.log(`[BatchManagementService] 원본 컬럼 조회 결과:`, columns);
console.log(`[BatchManagementService] 원본 컬럼 개수:`, columns ? columns.length : 'null/undefined');
// 각 데이터베이스 커넥터의 반환 구조가 다르므로 통일된 구조로 변환
const standardizedColumns: BatchColumnInfo[] = columns.map((col: any) => {
console.log(`[BatchManagementService] 컬럼 변환 중:`, col);
// MySQL/MariaDB 구조: {name, dataType, isNullable, defaultValue} (MySQLConnector만)
if (col.name && col.dataType !== undefined) {
const result = {
column_name: col.name,
data_type: col.dataType,
is_nullable: col.isNullable ? 'YES' : 'NO',
column_default: col.defaultValue || null,
};
console.log(`[BatchManagementService] MySQL/MariaDB 구조로 변환:`, result);
return result;
}
// PostgreSQL/Oracle/MSSQL/MariaDB 구조: {column_name, data_type, is_nullable, column_default}
else {
const result = {
column_name: col.column_name || col.COLUMN_NAME,
data_type: col.data_type || col.DATA_TYPE,
is_nullable: col.is_nullable || col.IS_NULLABLE || (col.nullable === 'Y' ? 'YES' : 'NO'),
column_default: col.column_default || col.COLUMN_DEFAULT || null,
};
console.log(`[BatchManagementService] 표준 구조로 변환:`, result);
return result;
}
});
console.log(`[BatchManagementService] 표준화된 컬럼 목록:`, standardizedColumns);
return {
success: true,
data: standardizedColumns,
message: "컬럼 정보를 조회했습니다."
};
} catch (error) {
console.error("[BatchManagementService] 외부 DB 컬럼 정보 조회 오류:", error);
console.error("[BatchManagementService] 오류 스택:", error instanceof Error ? error.stack : 'No stack trace');
return {
success: false,
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
}

View File

@@ -0,0 +1,397 @@
// 배치 스케줄러 서비스
// 작성일: 2024-12-24
import * as cron from 'node-cron';
import prisma from '../config/database';
import { BatchService } from './batchService';
import { BatchExecutionLogService } from './batchExecutionLogService';
import { logger } from '../utils/logger';
export class BatchSchedulerService {
private static scheduledTasks: Map<number, cron.ScheduledTask> = new Map();
private static isInitialized = false;
/**
* 스케줄러 초기화
*/
static async initialize() {
if (this.isInitialized) {
logger.info('배치 스케줄러가 이미 초기화되었습니다.');
return;
}
try {
logger.info('배치 스케줄러 초기화 시작...');
// 활성화된 배치 설정들을 로드하여 스케줄 등록
await this.loadActiveBatchConfigs();
this.isInitialized = true;
logger.info('배치 스케줄러 초기화 완료');
} catch (error) {
logger.error('배치 스케줄러 초기화 실패:', error);
throw error;
}
}
/**
* 활성화된 배치 설정들을 로드하여 스케줄 등록
*/
private static async loadActiveBatchConfigs() {
try {
const activeConfigs = await prisma.batch_configs.findMany({
where: {
is_active: 'Y'
},
include: {
batch_mappings: true
}
});
logger.info(`활성화된 배치 설정 ${activeConfigs.length}개 발견`);
for (const config of activeConfigs) {
await this.scheduleBatchConfig(config);
}
} catch (error) {
logger.error('활성화된 배치 설정 로드 실패:', error);
throw error;
}
}
/**
* 배치 설정을 스케줄에 등록
*/
static async scheduleBatchConfig(config: any) {
try {
const { id, batch_name, cron_schedule } = config;
// 기존 스케줄이 있다면 제거
if (this.scheduledTasks.has(id)) {
this.scheduledTasks.get(id)?.stop();
this.scheduledTasks.delete(id);
}
// cron 스케줄 유효성 검사
if (!cron.validate(cron_schedule)) {
logger.error(`잘못된 cron 스케줄: ${cron_schedule} (배치 ID: ${id})`);
return;
}
// 새로운 스케줄 등록
const task = cron.schedule(cron_schedule, async () => {
await this.executeBatchConfig(config);
});
this.scheduledTasks.set(id, task);
logger.info(`배치 스케줄 등록 완료: ${batch_name} (ID: ${id}, Schedule: ${cron_schedule})`);
} catch (error) {
logger.error(`배치 스케줄 등록 실패 (ID: ${config.id}):`, error);
}
}
/**
* 배치 설정 스케줄 제거
*/
static async unscheduleBatchConfig(batchConfigId: number) {
try {
if (this.scheduledTasks.has(batchConfigId)) {
this.scheduledTasks.get(batchConfigId)?.stop();
this.scheduledTasks.delete(batchConfigId);
logger.info(`배치 스케줄 제거 완료 (ID: ${batchConfigId})`);
}
} catch (error) {
logger.error(`배치 스케줄 제거 실패 (ID: ${batchConfigId}):`, error);
}
}
/**
* 배치 설정 업데이트 시 스케줄 재등록
*/
static async updateBatchSchedule(configId: number) {
try {
// 기존 스케줄 제거
await this.unscheduleBatchConfig(configId);
// 업데이트된 배치 설정 조회
const config = await prisma.batch_configs.findUnique({
where: { id: configId },
include: { batch_mappings: true }
});
if (!config) {
logger.warn(`배치 설정을 찾을 수 없습니다: ID ${configId}`);
return;
}
// 활성화된 배치만 다시 스케줄 등록
if (config.is_active === 'Y') {
await this.scheduleBatchConfig(config);
logger.info(`배치 스케줄 업데이트 완료: ${config.batch_name} (ID: ${configId})`);
} else {
logger.info(`비활성화된 배치 스케줄 제거: ${config.batch_name} (ID: ${configId})`);
}
} catch (error) {
logger.error(`배치 스케줄 업데이트 실패: ID ${configId}`, error);
}
}
/**
* 배치 설정 실행
*/
private static async executeBatchConfig(config: any) {
const startTime = new Date();
let executionLog: any = null;
try {
logger.info(`배치 실행 시작: ${config.batch_name} (ID: ${config.id})`);
// 실행 로그 생성
const executionLogResponse = await BatchExecutionLogService.createExecutionLog({
batch_config_id: config.id,
execution_status: 'RUNNING',
start_time: startTime,
total_records: 0,
success_records: 0,
failed_records: 0
});
if (!executionLogResponse.success || !executionLogResponse.data) {
logger.error(`배치 실행 로그 생성 실패: ${config.batch_name}`, executionLogResponse.message);
return;
}
executionLog = executionLogResponse.data;
// 실제 배치 실행 로직 (수동 실행과 동일한 로직 사용)
const result = await this.executeBatchMappings(config);
// 실행 로그 업데이트 (성공)
await BatchExecutionLogService.updateExecutionLog(executionLog.id, {
execution_status: 'SUCCESS',
end_time: new Date(),
duration_ms: Date.now() - startTime.getTime(),
total_records: result.totalRecords,
success_records: result.successRecords,
failed_records: result.failedRecords
});
logger.info(`배치 실행 완료: ${config.batch_name} (처리된 레코드: ${result.totalRecords})`);
} catch (error) {
logger.error(`배치 실행 실패: ${config.batch_name}`, error);
// 실행 로그 업데이트 (실패)
if (executionLog) {
await BatchExecutionLogService.updateExecutionLog(executionLog.id, {
execution_status: 'FAILED',
end_time: new Date(),
duration_ms: Date.now() - startTime.getTime(),
error_message: error instanceof Error ? error.message : '알 수 없는 오류',
error_details: error instanceof Error ? error.stack : String(error)
});
}
}
}
/**
* 배치 매핑 실행 (수동 실행과 동일한 로직)
*/
private static async executeBatchMappings(config: any) {
let totalRecords = 0;
let successRecords = 0;
let failedRecords = 0;
if (!config.batch_mappings || config.batch_mappings.length === 0) {
logger.warn(`배치 매핑이 없습니다: ${config.batch_name}`);
return { totalRecords, successRecords, failedRecords };
}
// 테이블별로 매핑을 그룹화
const tableGroups = new Map<string, typeof config.batch_mappings>();
for (const mapping of config.batch_mappings) {
const key = `${mapping.from_connection_type}:${mapping.from_connection_id || 'internal'}:${mapping.from_table_name}`;
if (!tableGroups.has(key)) {
tableGroups.set(key, []);
}
tableGroups.get(key)!.push(mapping);
}
// 각 테이블 그룹별로 처리
for (const [tableKey, mappings] of tableGroups) {
try {
const firstMapping = mappings[0];
logger.info(`테이블 처리 시작: ${tableKey} -> ${mappings.length}개 컬럼 매핑`);
// FROM 테이블에서 매핑된 컬럼들만 조회
const fromColumns = mappings.map((m: any) => m.from_column_name);
const fromData = await BatchService.getDataFromTableWithColumns(
firstMapping.from_table_name,
fromColumns,
firstMapping.from_connection_type as 'internal' | 'external',
firstMapping.from_connection_id || undefined
);
totalRecords += fromData.length;
// 컬럼 매핑 적용하여 TO 테이블 형식으로 변환
const mappedData = fromData.map(row => {
const mappedRow: any = {};
for (const mapping of mappings) {
mappedRow[mapping.to_column_name] = row[mapping.from_column_name];
}
return mappedRow;
});
// TO 테이블에 데이터 삽입
const insertResult = await BatchService.insertDataToTable(
firstMapping.to_table_name,
mappedData,
firstMapping.to_connection_type as 'internal' | 'external',
firstMapping.to_connection_id || undefined
);
successRecords += insertResult.successCount;
failedRecords += insertResult.failedCount;
logger.info(`테이블 처리 완료: ${insertResult.successCount}개 성공, ${insertResult.failedCount}개 실패`);
} catch (error) {
logger.error(`테이블 처리 실패: ${tableKey}`, error);
failedRecords += 1;
}
}
return { totalRecords, successRecords, failedRecords };
}
/**
* 배치 매핑 처리 (기존 메서드 - 사용 안 함)
*/
private static async processBatchMappings(config: any) {
const { batch_mappings } = config;
let totalRecords = 0;
let successRecords = 0;
let failedRecords = 0;
if (!batch_mappings || batch_mappings.length === 0) {
logger.warn(`배치 매핑이 없습니다: ${config.batch_name}`);
return { totalRecords, successRecords, failedRecords };
}
for (const mapping of batch_mappings) {
try {
logger.info(`매핑 처리 시작: ${mapping.from_table_name} -> ${mapping.to_table_name}`);
// FROM 테이블에서 데이터 조회
const fromData = await this.getDataFromSource(mapping);
totalRecords += fromData.length;
// TO 테이블에 데이터 삽입
const insertResult = await this.insertDataToTarget(mapping, fromData);
successRecords += insertResult.successCount;
failedRecords += insertResult.failedCount;
logger.info(`매핑 처리 완료: ${insertResult.successCount}개 성공, ${insertResult.failedCount}개 실패`);
} catch (error) {
logger.error(`매핑 처리 실패: ${mapping.from_table_name} -> ${mapping.to_table_name}`, error);
failedRecords += 1;
}
}
return { totalRecords, successRecords, failedRecords };
}
/**
* FROM 테이블에서 데이터 조회
*/
private static async getDataFromSource(mapping: any) {
try {
if (mapping.from_connection_type === 'internal') {
// 내부 DB에서 조회
const result = await prisma.$queryRawUnsafe(
`SELECT * FROM ${mapping.from_table_name}`
);
return result as any[];
} else {
// 외부 DB에서 조회 (구현 필요)
logger.warn('외부 DB 조회는 아직 구현되지 않았습니다.');
return [];
}
} catch (error) {
logger.error(`FROM 테이블 데이터 조회 실패: ${mapping.from_table_name}`, error);
throw error;
}
}
/**
* TO 테이블에 데이터 삽입
*/
private static async insertDataToTarget(mapping: any, data: any[]) {
let successCount = 0;
let failedCount = 0;
try {
if (mapping.to_connection_type === 'internal') {
// 내부 DB에 삽입
for (const record of data) {
try {
// 매핑된 컬럼만 추출
const mappedData = this.mapColumns(record, mapping);
await prisma.$executeRawUnsafe(
`INSERT INTO ${mapping.to_table_name} (${Object.keys(mappedData).join(', ')}) VALUES (${Object.values(mappedData).map(() => '?').join(', ')})`,
...Object.values(mappedData)
);
successCount++;
} catch (error) {
logger.error(`레코드 삽입 실패:`, error);
failedCount++;
}
}
} else {
// 외부 DB에 삽입 (구현 필요)
logger.warn('외부 DB 삽입은 아직 구현되지 않았습니다.');
failedCount = data.length;
}
} catch (error) {
logger.error(`TO 테이블 데이터 삽입 실패: ${mapping.to_table_name}`, error);
throw error;
}
return { successCount, failedCount };
}
/**
* 컬럼 매핑
*/
private static mapColumns(record: any, mapping: any) {
const mappedData: any = {};
// 단순한 컬럼 매핑 (실제로는 더 복잡한 로직 필요)
mappedData[mapping.to_column_name] = record[mapping.from_column_name];
return mappedData;
}
/**
* 모든 스케줄 중지
*/
static async stopAllSchedules() {
try {
for (const [id, task] of this.scheduledTasks) {
task.stop();
logger.info(`배치 스케줄 중지: ID ${id}`);
}
this.scheduledTasks.clear();
this.isInitialized = false;
logger.info('모든 배치 스케줄이 중지되었습니다.');
} catch (error) {
logger.error('배치 스케줄 중지 실패:', error);
}
}
/**
* 현재 등록된 스케줄 목록 조회
*/
static getScheduledTasks() {
return Array.from(this.scheduledTasks.keys());
}
}

View File

@@ -1,7 +1,7 @@
// 배치관리 서비스
// 작성일: 2024-12-24
import { PrismaClient } from "@prisma/client";
import prisma from "../config/database";
import {
BatchConfig,
BatchMapping,
@@ -12,12 +12,12 @@ import {
ConnectionInfo,
TableInfo,
ColumnInfo,
CreateBatchConfigRequest,
UpdateBatchConfigRequest,
} from "../types/batchTypes";
import { ExternalDbConnectionService } from "./externalDbConnectionService";
import { BatchExternalDbService } from "./batchExternalDbService";
import { DbConnectionManager } from "./dbConnectionManager";
const prisma = new PrismaClient();
export class BatchService {
/**
* 배치 설정 목록 조회
@@ -55,17 +55,32 @@ export class BatchService {
];
}
const batchConfigs = await prisma.batch_configs.findMany({
where,
include: {
batch_mappings: true,
},
orderBy: [{ is_active: "desc" }, { batch_name: "asc" }],
});
const page = filter.page || 1;
const limit = filter.limit || 10;
const skip = (page - 1) * limit;
const [batchConfigs, total] = await Promise.all([
prisma.batch_configs.findMany({
where,
include: {
batch_mappings: true,
},
orderBy: [{ is_active: "desc" }, { batch_name: "asc" }],
skip,
take: limit,
}),
prisma.batch_configs.count({ where }),
]);
return {
success: true,
data: batchConfigs as BatchConfig[],
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
};
} catch (error) {
console.error("배치 설정 목록 조회 오류:", error);
@@ -122,28 +137,18 @@ export class BatchService {
* 배치 설정 생성
*/
static async createBatchConfig(
data: BatchMappingRequest,
data: CreateBatchConfigRequest,
userId?: string
): Promise<ApiResponse<BatchConfig>> {
try {
// 매핑 유효성 검사
const validation = await this.validateBatchMappings(data.mappings);
if (!validation.isValid) {
return {
success: false,
message: "매핑 유효성 검사 실패",
error: validation.errors.join(", "),
};
}
// 트랜잭션으로 배치 설정과 매핑 생성
const result = await prisma.$transaction(async (tx) => {
// 배치 설정 생성
const batchConfig = await tx.batch_configs.create({
data: {
batch_name: data.batch_name,
batch_name: data.batchName,
description: data.description,
cron_schedule: data.cron_schedule,
cron_schedule: data.cronSchedule,
created_by: userId,
updated_by: userId,
},
@@ -198,7 +203,7 @@ export class BatchService {
*/
static async updateBatchConfig(
id: number,
data: Partial<BatchMappingRequest>,
data: UpdateBatchConfigRequest,
userId?: string
): Promise<ApiResponse<BatchConfig>> {
try {
@@ -215,18 +220,6 @@ export class BatchService {
};
}
// 매핑이 제공된 경우 유효성 검사
if (data.mappings) {
const validation = await this.validateBatchMappings(data.mappings);
if (!validation.isValid) {
return {
success: false,
message: "매핑 유효성 검사 실패",
error: validation.errors.join(", "),
};
}
}
// 트랜잭션으로 업데이트
const result = await prisma.$transaction(async (tx) => {
// 배치 설정 업데이트
@@ -234,9 +227,10 @@ export class BatchService {
updated_by: userId,
};
if (data.batch_name) updateData.batch_name = data.batch_name;
if (data.batchName) updateData.batch_name = data.batchName;
if (data.description !== undefined) updateData.description = data.description;
if (data.cron_schedule) updateData.cron_schedule = data.cron_schedule;
if (data.cronSchedule) updateData.cron_schedule = data.cronSchedule;
if (data.isActive !== undefined) updateData.is_active = data.isActive;
const batchConfig = await tx.batch_configs.update({
where: { id },
@@ -354,16 +348,14 @@ export class BatchService {
});
// 외부 DB 연결 조회
const externalConnections = await ExternalDbConnectionService.getConnections({
is_active: 'Y',
});
const externalConnections = await BatchExternalDbService.getAvailableConnections();
if (externalConnections.success && externalConnections.data) {
externalConnections.data.forEach((conn) => {
connections.push({
type: 'external',
id: conn.id,
name: conn.connection_name,
name: conn.name,
db_type: conn.db_type,
});
});
@@ -389,9 +381,9 @@ export class BatchService {
static async getTablesFromConnection(
connectionType: 'internal' | 'external',
connectionId?: number
): Promise<ApiResponse<string[]>> {
): Promise<ApiResponse<TableInfo[]>> {
try {
let tables: string[] = [];
let tables: TableInfo[] = [];
if (connectionType === 'internal') {
// 내부 DB 테이블 조회
@@ -402,10 +394,13 @@ export class BatchService {
AND table_type = 'BASE TABLE'
ORDER BY table_name
`;
tables = result.map(row => row.table_name);
tables = result.map(row => ({
table_name: row.table_name,
columns: []
}));
} else if (connectionType === 'external' && connectionId) {
// 외부 DB 테이블 조회
const tablesResult = await ExternalDbConnectionService.getTables(connectionId);
const tablesResult = await BatchExternalDbService.getTablesFromConnection(connectionType, connectionId);
if (tablesResult.success && tablesResult.data) {
tables = tablesResult.data;
}
@@ -430,14 +425,22 @@ export class BatchService {
*/
static async getTableColumns(
connectionType: 'internal' | 'external',
tableName: string,
connectionId?: number
connectionId: number | undefined,
tableName: string
): Promise<ApiResponse<ColumnInfo[]>> {
try {
console.log(`[BatchService] getTableColumns 호출:`, {
connectionType,
connectionId,
tableName
});
let columns: ColumnInfo[] = [];
if (connectionType === 'internal') {
// 내부 DB 컬럼 조회
console.log(`[BatchService] 내부 DB 컬럼 조회 시작: ${tableName}`);
const result = await prisma.$queryRaw<Array<{
column_name: string;
data_type: string;
@@ -455,26 +458,31 @@ export class BatchService {
ORDER BY ordinal_position
`;
console.log(`[BatchService] 내부 DB 컬럼 조회 결과:`, result);
columns = result.map(row => ({
column_name: row.column_name,
data_type: row.data_type,
is_nullable: row.is_nullable === 'YES',
is_nullable: row.is_nullable,
column_default: row.column_default,
}));
} else if (connectionType === 'external' && connectionId) {
// 외부 DB 컬럼 조회
const columnsResult = await ExternalDbConnectionService.getTableColumns(
console.log(`[BatchService] 외부 DB 컬럼 조회 시작: connectionId=${connectionId}, tableName=${tableName}`);
const columnsResult = await BatchExternalDbService.getTableColumns(
connectionType,
connectionId,
tableName
);
console.log(`[BatchService] 외부 DB 컬럼 조회 결과:`, columnsResult);
if (columnsResult.success && columnsResult.data) {
columns = columnsResult.data.map(col => ({
column_name: col.column_name,
data_type: col.data_type,
is_nullable: col.is_nullable,
column_default: col.column_default,
}));
columns = columnsResult.data;
}
console.log(`[BatchService] 외부 DB 컬럼:`, columns);
}
return {
@@ -491,6 +499,228 @@ export class BatchService {
}
}
/**
* 배치 실행 로그 생성
*/
static async createExecutionLog(data: {
batch_config_id: number;
execution_status: string;
start_time: Date;
total_records: number;
success_records: number;
failed_records: number;
}): Promise<any> {
try {
const executionLog = await prisma.batch_execution_logs.create({
data: {
batch_config_id: data.batch_config_id,
execution_status: data.execution_status,
start_time: data.start_time,
total_records: data.total_records,
success_records: data.success_records,
failed_records: data.failed_records,
},
});
return executionLog;
} catch (error) {
console.error("배치 실행 로그 생성 오류:", error);
throw error;
}
}
/**
* 배치 실행 로그 업데이트
*/
static async updateExecutionLog(
id: number,
data: {
execution_status?: string;
end_time?: Date;
duration_ms?: number;
total_records?: number;
success_records?: number;
failed_records?: number;
error_message?: string;
}
): Promise<void> {
try {
await prisma.batch_execution_logs.update({
where: { id },
data,
});
} catch (error) {
console.error("배치 실행 로그 업데이트 오류:", error);
throw error;
}
}
/**
* 테이블에서 데이터 조회 (연결 타입에 따라 내부/외부 DB 구분)
*/
static async getDataFromTable(
tableName: string,
connectionType: 'internal' | 'external' = 'internal',
connectionId?: number
): Promise<any[]> {
try {
console.log(`[BatchService] 테이블에서 데이터 조회: ${tableName} (${connectionType}${connectionId ? `:${connectionId}` : ''})`);
if (connectionType === 'internal') {
// 내부 DB에서 데이터 조회
const result = await prisma.$queryRawUnsafe(`SELECT * FROM ${tableName} LIMIT 100`);
console.log(`[BatchService] 내부 DB 데이터 조회 결과: ${Array.isArray(result) ? result.length : 0}개 레코드`);
return result as any[];
} else if (connectionType === 'external' && connectionId) {
// 외부 DB에서 데이터 조회
const result = await BatchExternalDbService.getDataFromTable(connectionId, tableName);
if (result.success && result.data) {
console.log(`[BatchService] 외부 DB 데이터 조회 결과: ${result.data.length}개 레코드`);
return result.data;
} else {
console.error(`외부 DB 데이터 조회 실패: ${result.message}`);
return [];
}
} else {
throw new Error(`잘못된 연결 타입 또는 연결 ID: ${connectionType}, ${connectionId}`);
}
} catch (error) {
console.error(`테이블 데이터 조회 오류 (${tableName}):`, error);
throw error;
}
}
/**
* 테이블에서 특정 컬럼들만 조회 (연결 타입에 따라 내부/외부 DB 구분)
*/
static async getDataFromTableWithColumns(
tableName: string,
columns: string[],
connectionType: 'internal' | 'external' = 'internal',
connectionId?: number
): Promise<any[]> {
try {
console.log(`[BatchService] 테이블에서 특정 컬럼 데이터 조회: ${tableName} (${columns.join(', ')}) (${connectionType}${connectionId ? `:${connectionId}` : ''})`);
if (connectionType === 'internal') {
// 내부 DB에서 특정 컬럼만 조회
const columnList = columns.join(', ');
const result = await prisma.$queryRawUnsafe(`SELECT ${columnList} FROM ${tableName} LIMIT 100`);
console.log(`[BatchService] 내부 DB 특정 컬럼 조회 결과: ${Array.isArray(result) ? result.length : 0}개 레코드`);
return result as any[];
} else if (connectionType === 'external' && connectionId) {
// 외부 DB에서 특정 컬럼만 조회
const result = await BatchExternalDbService.getDataFromTableWithColumns(connectionId, tableName, columns);
if (result.success && result.data) {
console.log(`[BatchService] 외부 DB 특정 컬럼 조회 결과: ${result.data.length}개 레코드`);
return result.data;
} else {
console.error(`외부 DB 특정 컬럼 조회 실패: ${result.message}`);
return [];
}
} else {
throw new Error(`잘못된 연결 타입 또는 연결 ID: ${connectionType}, ${connectionId}`);
}
} catch (error) {
console.error(`테이블 특정 컬럼 조회 오류 (${tableName}):`, error);
throw error;
}
}
/**
* 테이블에 데이터 삽입 (연결 타입에 따라 내부/외부 DB 구분)
*/
static async insertDataToTable(
tableName: string,
data: any[],
connectionType: 'internal' | 'external' = 'internal',
connectionId?: number
): Promise<{
successCount: number;
failedCount: number;
}> {
try {
console.log(`[BatchService] 테이블에 데이터 삽입: ${tableName} (${connectionType}${connectionId ? `:${connectionId}` : ''}), ${data.length}개 레코드`);
if (!data || data.length === 0) {
return { successCount: 0, failedCount: 0 };
}
if (connectionType === 'internal') {
// 내부 DB에 데이터 삽입
let successCount = 0;
let failedCount = 0;
// 각 레코드를 개별적으로 삽입 (UPSERT 방식으로 중복 처리)
for (const record of data) {
try {
// 동적 UPSERT 쿼리 생성 (PostgreSQL ON CONFLICT 사용)
const columns = Object.keys(record);
const values = Object.values(record).map(value => {
// Date 객체를 ISO 문자열로 변환 (PostgreSQL이 자동으로 파싱)
if (value instanceof Date) {
return value.toISOString();
}
// JavaScript Date 문자열을 Date 객체로 변환 후 ISO 문자열로
if (typeof value === 'string') {
const dateRegex = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{2}\s+\d{4}\s+\d{2}:\d{2}:\d{2}/;
if (dateRegex.test(value)) {
return new Date(value).toISOString();
}
}
return value;
});
const placeholders = values.map((_, index) => `$${index + 1}`).join(', ');
// Primary Key 컬럼 추정 (일반적으로 id 또는 첫 번째 컬럼)
const primaryKeyColumn = columns.includes('id') ? 'id' :
columns.includes('user_id') ? 'user_id' :
columns[0];
// UPDATE SET 절 생성 (Primary Key 제외)
const updateColumns = columns.filter(col => col !== primaryKeyColumn);
const updateSet = updateColumns.map(col => `${col} = EXCLUDED.${col}`).join(', ');
let query: string;
if (updateSet) {
// UPSERT: 중복 시 업데이트
query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})
ON CONFLICT (${primaryKeyColumn}) DO UPDATE SET ${updateSet}`;
} else {
// Primary Key만 있는 경우 중복 시 무시
query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})
ON CONFLICT (${primaryKeyColumn}) DO NOTHING`;
}
await prisma.$executeRawUnsafe(query, ...values);
successCount++;
} catch (error) {
console.error(`레코드 UPSERT 실패:`, error);
failedCount++;
}
}
console.log(`[BatchService] 내부 DB 데이터 삽입 완료: 성공 ${successCount}개, 실패 ${failedCount}`);
return { successCount, failedCount };
} else if (connectionType === 'external' && connectionId) {
// 외부 DB에 데이터 삽입
const result = await BatchExternalDbService.insertDataToTable(connectionId, tableName, data);
if (result.success && result.data) {
console.log(`[BatchService] 외부 DB 데이터 삽입 완료: 성공 ${result.data.successCount}개, 실패 ${result.data.failedCount}`);
return result.data;
} else {
console.error(`외부 DB 데이터 삽입 실패: ${result.message}`);
return { successCount: 0, failedCount: data.length };
}
} else {
throw new Error(`잘못된 연결 타입 또는 연결 ID: ${connectionType}, ${connectionId}`);
}
} catch (error) {
console.error(`테이블 데이터 삽입 오류 (${tableName}):`, error);
throw error;
}
}
/**
* 배치 매핑 유효성 검사
*/

View File

@@ -1,7 +1,7 @@
// 외부 DB 연결 서비스
// 작성일: 2024-12-17
import { PrismaClient } from "@prisma/client";
import prisma from "../config/database";
import {
ExternalDbConnection,
ExternalDbConnectionFilter,
@@ -9,9 +9,7 @@ import {
TableInfo,
} from "../types/externalDbTypes";
import { PasswordEncryption } from "../utils/passwordEncryption";
import { DbConnectionManager } from "./dbConnectionManager";
const prisma = new PrismaClient();
import { DatabaseConnectorFactory } from "../database/DatabaseConnectorFactory";
export class ExternalDbConnectionService {
/**
@@ -81,6 +79,93 @@ export class ExternalDbConnectionService {
}
}
/**
* DB 타입별로 그룹화된 외부 DB 연결 목록 조회
*/
static async getConnectionsGroupedByType(
filter: ExternalDbConnectionFilter = {}
): Promise<ApiResponse<Record<string, ExternalDbConnection[]>>> {
try {
// 기본 연결 목록 조회
const connectionsResult = await this.getConnections(filter);
if (!connectionsResult.success || !connectionsResult.data) {
return {
success: false,
message: "연결 목록 조회에 실패했습니다."
};
}
// DB 타입 카테고리 정보 조회
const categories = await prisma.db_type_categories.findMany({
where: { is_active: true },
orderBy: [
{ sort_order: 'asc' },
{ display_name: 'asc' }
]
});
// DB 타입별로 그룹화
const groupedConnections: Record<string, any> = {};
// 카테고리 정보를 포함한 그룹 초기화
categories.forEach((category: any) => {
groupedConnections[category.type_code] = {
category: {
type_code: category.type_code,
display_name: category.display_name,
icon: category.icon,
color: category.color,
sort_order: category.sort_order
},
connections: []
};
});
// 연결을 해당 타입 그룹에 배치
connectionsResult.data.forEach(connection => {
if (groupedConnections[connection.db_type]) {
groupedConnections[connection.db_type].connections.push(connection);
} else {
// 카테고리에 없는 DB 타입인 경우 기타 그룹에 추가
if (!groupedConnections['other']) {
groupedConnections['other'] = {
category: {
type_code: 'other',
display_name: '기타',
icon: 'database',
color: '#6B7280',
sort_order: 999
},
connections: []
};
}
groupedConnections['other'].connections.push(connection);
}
});
// 연결이 없는 빈 그룹 제거
Object.keys(groupedConnections).forEach(key => {
if (groupedConnections[key].connections.length === 0) {
delete groupedConnections[key];
}
});
return {
success: true,
data: groupedConnections,
message: `DB 타입별로 그룹화된 연결 목록을 조회했습니다.`
};
} catch (error) {
console.error("그룹화된 연결 목록 조회 실패:", error);
return {
success: false,
message: "그룹화된 연결 목록 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
/**
* 특정 외부 DB 연결 조회
*/
@@ -239,13 +324,40 @@ export class ExternalDbConnectionService {
}
}
// 비밀번호가 변경되는 경우, 연결 테스트 먼저 수행
if (data.password && data.password !== "***ENCRYPTED***") {
// 임시 연결 설정으로 테스트
const testConfig = {
host: data.host || existingConnection.host,
port: data.port || existingConnection.port,
database: data.database_name || existingConnection.database_name,
user: data.username || existingConnection.username,
password: data.password, // 새로 입력된 비밀번호로 테스트
connectionTimeoutMillis: data.connection_timeout != null ? data.connection_timeout * 1000 : undefined,
queryTimeoutMillis: data.query_timeout != null ? data.query_timeout * 1000 : undefined,
ssl: (data.ssl_enabled || existingConnection.ssl_enabled) === "Y" ? { rejectUnauthorized: false } : false
};
// 연결 테스트 수행
const connector = await DatabaseConnectorFactory.createConnector(existingConnection.db_type, testConfig, id);
const testResult = await connector.testConnection();
if (!testResult.success) {
return {
success: false,
message: "새로운 연결 정보로 테스트에 실패했습니다. 수정할 수 없습니다.",
error: testResult.error ? `${testResult.error.code}: ${testResult.error.details}` : undefined
};
}
}
// 업데이트 데이터 준비
const updateData: any = {
...data,
updated_date: new Date(),
};
// 비밀번호가 변경된 경우 암호화
// 비밀번호가 변경된 경우 암호화 (연결 테스트 통과 후)
if (data.password && data.password !== "***ENCRYPTED***") {
updateData.password = PasswordEncryption.encrypt(data.password);
} else {
@@ -320,7 +432,8 @@ export class ExternalDbConnectionService {
* 데이터베이스 연결 테스트 (ID 기반)
*/
static async testConnectionById(
id: number
id: number,
testData?: { password?: string }
): Promise<import("../types/externalDbTypes").ConnectionTestResult> {
try {
// 저장된 연결 정보 조회
@@ -339,9 +452,17 @@ export class ExternalDbConnectionService {
};
}
// 비밀번호 복호화
const decryptedPassword = await this.getDecryptedPassword(id);
if (!decryptedPassword) {
// 비밀번호 결정 (테스트용 비밀번호가 제공된 경우 그것을 사용, 아니면 저장된 비밀번호 복호화)
let password: string | null;
if (testData?.password) {
password = testData.password;
console.log(`🔍 [연결테스트] 새로 입력된 비밀번호 사용: ${password.substring(0, 3)}***`);
} else {
password = await this.getDecryptedPassword(id);
console.log(`🔍 [연결테스트] 저장된 비밀번호 사용: ${password ? password.substring(0, 3) + '***' : 'null'}`);
}
if (!password) {
return {
success: false,
message: "비밀번호 복호화에 실패했습니다.",
@@ -358,14 +479,46 @@ export class ExternalDbConnectionService {
port: connection.port,
database: connection.database_name,
user: connection.username,
password: decryptedPassword,
password: password,
connectionTimeoutMillis: connection.connection_timeout != null ? connection.connection_timeout * 1000 : undefined,
queryTimeoutMillis: connection.query_timeout != null ? connection.query_timeout * 1000 : undefined,
ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
};
// DbConnectionManager를 통한 연결 테스트
return await DbConnectionManager.testConnection(id, connection.db_type, config);
// 연결 테스트용 임시 커넥터 생성 (캐시 사용하지 않음)
let connector: any;
switch (connection.db_type.toLowerCase()) {
case 'postgresql':
const { PostgreSQLConnector } = await import('../database/PostgreSQLConnector');
connector = new PostgreSQLConnector(config);
break;
case 'oracle':
const { OracleConnector } = await import('../database/OracleConnector');
connector = new OracleConnector(config);
break;
case 'mariadb':
case 'mysql':
const { MariaDBConnector } = await import('../database/MariaDBConnector');
connector = new MariaDBConnector(config);
break;
case 'mssql':
const { MSSQLConnector } = await import('../database/MSSQLConnector');
connector = new MSSQLConnector(config);
break;
default:
throw new Error(`지원하지 않는 데이터베이스 타입: ${connection.db_type}`);
}
console.log(`🔍 [연결테스트] 새 커넥터로 DB 연결 시도 - Host: ${config.host}, DB: ${config.database}, User: ${config.user}`);
const testResult = await connector.testConnection();
console.log(`🔍 [연결테스트] 결과 - Success: ${testResult.success}, Message: ${testResult.message}`);
return {
success: testResult.success,
message: testResult.message,
details: testResult.details
};
} catch (error) {
return {
success: false,
@@ -416,7 +569,7 @@ export class ExternalDbConnectionService {
}
// DB 타입 유효성 검사
const validDbTypes = ["mysql", "postgresql", "oracle", "mssql", "sqlite"];
const validDbTypes = ["mysql", "postgresql", "oracle", "mssql", "sqlite", "mariadb"];
if (!validDbTypes.includes(data.db_type)) {
throw new Error("지원하지 않는 DB 타입입니다.");
}
@@ -487,8 +640,9 @@ export class ExternalDbConnectionService {
ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
};
// DbConnectionManager를 통한 쿼리 실행
const result = await DbConnectionManager.executeQuery(id, connection.db_type, config, query);
// DatabaseConnectorFactory를 통한 쿼리 실행
const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, id);
const result = await connector.executeQuery(query);
return {
success: true,
@@ -595,8 +749,9 @@ export class ExternalDbConnectionService {
ssl: connection.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
};
// DbConnectionManager를 통한 테이블 목록 조회
const tables = await DbConnectionManager.getTables(id, connection.db_type, config);
// DatabaseConnectorFactory를 통한 테이블 목록 조회
const connector = await DatabaseConnectorFactory.createConnector(connection.db_type, config, id);
const tables = await connector.getTables();
return {
success: true,
@@ -676,4 +831,58 @@ export class ExternalDbConnectionService {
}
}
/**
* 특정 테이블의 컬럼 정보 조회
*/
static async getTableColumns(connectionId: number, tableName: string): Promise<ApiResponse<any[]>> {
let client: any = null;
try {
const connection = await this.getConnectionById(connectionId);
if (!connection.success || !connection.data) {
return {
success: false,
message: "연결 정보를 찾을 수 없습니다."
};
}
const connectionData = connection.data;
// 비밀번호 복호화
const decryptedPassword = PasswordEncryption.decrypt(connectionData.password);
// 연결 설정 준비
const config = {
host: connectionData.host,
port: connectionData.port,
database: connectionData.database_name,
user: connectionData.username, // ConnectionConfig에서는 user 사용
password: decryptedPassword,
connectionTimeoutMillis: connectionData.connection_timeout != null ? connectionData.connection_timeout * 1000 : undefined,
queryTimeoutMillis: connectionData.query_timeout != null ? connectionData.query_timeout * 1000 : undefined,
ssl: connectionData.ssl_enabled === "Y" ? { rejectUnauthorized: false } : false
};
// 데이터베이스 타입에 따른 커넥터 생성
const connector = await DatabaseConnectorFactory.createConnector(connectionData.db_type, config, connectionId);
// 컬럼 정보 조회
const columns = await connector.getColumns(tableName);
return {
success: true,
data: columns,
message: "컬럼 정보를 조회했습니다."
};
} catch (error) {
console.error("컬럼 정보 조회 오류:", error);
return {
success: false,
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
error: error instanceof Error ? error.message : "알 수 없는 오류"
};
}
}
}