From b1b9e4ad9396d9d1c07eb9ea4c2f49e604e4dfe7 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 1 Dec 2025 15:30:25 +0900 Subject: [PATCH] =?UTF-8?q?=ED=83=80=EC=9E=85=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/batchController.ts | 15 +-- .../controllers/batchManagementController.ts | 7 +- .../controllers/screenEmbeddingController.ts | 27 ++--- .../src/services/batchExternalDbService.ts | 3 +- .../src/services/batchSchedulerService.ts | 13 ++- backend-node/src/services/batchService.ts | 21 +++- backend-node/src/types/batchTypes.ts | 98 ++++++++++++++++++- 7 files changed, 151 insertions(+), 33 deletions(-) diff --git a/backend-node/src/controllers/batchController.ts b/backend-node/src/controllers/batchController.ts index 638edcd2..009e30a8 100644 --- a/backend-node/src/controllers/batchController.ts +++ b/backend-node/src/controllers/batchController.ts @@ -4,6 +4,7 @@ import { Request, Response } from "express"; import { BatchService } from "../services/batchService"; import { BatchSchedulerService } from "../services/batchSchedulerService"; +import { BatchExternalDbService } from "../services/batchExternalDbService"; import { BatchConfigFilter, CreateBatchConfigRequest, @@ -63,7 +64,7 @@ export class BatchController { res: Response ) { try { - const result = await BatchService.getAvailableConnections(); + const result = await BatchExternalDbService.getAvailableConnections(); if (result.success) { res.json(result); @@ -99,8 +100,8 @@ export class BatchController { } const connectionId = type === "external" ? Number(id) : undefined; - const result = await BatchService.getTablesFromConnection( - type, + const result = await BatchService.getTables( + type as "internal" | "external", connectionId ); @@ -142,10 +143,10 @@ export class BatchController { } const connectionId = type === "external" ? Number(id) : undefined; - const result = await BatchService.getTableColumns( - type, - connectionId, - tableName + const result = await BatchService.getColumns( + tableName, + type as "internal" | "external", + connectionId ); if (result.success) { diff --git a/backend-node/src/controllers/batchManagementController.ts b/backend-node/src/controllers/batchManagementController.ts index 61194485..e1c5b182 100644 --- a/backend-node/src/controllers/batchManagementController.ts +++ b/backend-node/src/controllers/batchManagementController.ts @@ -331,8 +331,11 @@ export class BatchManagementController { const duration = endTime.getTime() - startTime.getTime(); // executionLog가 정의되어 있는지 확인 - if (typeof executionLog !== "undefined") { - await BatchService.updateExecutionLog(executionLog.id, { + if (typeof executionLog !== "undefined" && executionLog) { + const { BatchExecutionLogService } = await import( + "../services/batchExecutionLogService" + ); + await BatchExecutionLogService.updateExecutionLog(executionLog.id, { execution_status: "FAILED", end_time: endTime, duration_ms: duration, diff --git a/backend-node/src/controllers/screenEmbeddingController.ts b/backend-node/src/controllers/screenEmbeddingController.ts index 43087589..497d99db 100644 --- a/backend-node/src/controllers/screenEmbeddingController.ts +++ b/backend-node/src/controllers/screenEmbeddingController.ts @@ -5,6 +5,7 @@ import { Request, Response } from "express"; import { getPool } from "../database/db"; import { logger } from "../utils/logger"; +import { AuthenticatedRequest } from "../types/auth"; const pool = getPool(); @@ -16,7 +17,7 @@ const pool = getPool(); * 화면 임베딩 목록 조회 * GET /api/screen-embedding?parentScreenId=1 */ -export async function getScreenEmbeddings(req: Request, res: Response) { +export async function getScreenEmbeddings(req: AuthenticatedRequest, res: Response) { try { const { parentScreenId } = req.query; const companyCode = req.user!.companyCode; @@ -67,7 +68,7 @@ export async function getScreenEmbeddings(req: Request, res: Response) { * 화면 임베딩 상세 조회 * GET /api/screen-embedding/:id */ -export async function getScreenEmbeddingById(req: Request, res: Response) { +export async function getScreenEmbeddingById(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; const companyCode = req.user!.companyCode; @@ -113,7 +114,7 @@ export async function getScreenEmbeddingById(req: Request, res: Response) { * 화면 임베딩 생성 * POST /api/screen-embedding */ -export async function createScreenEmbedding(req: Request, res: Response) { +export async function createScreenEmbedding(req: AuthenticatedRequest, res: Response) { try { const { parentScreenId, @@ -184,7 +185,7 @@ export async function createScreenEmbedding(req: Request, res: Response) { * 화면 임베딩 수정 * PUT /api/screen-embedding/:id */ -export async function updateScreenEmbedding(req: Request, res: Response) { +export async function updateScreenEmbedding(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; const { position, mode, config } = req.body; @@ -257,7 +258,7 @@ export async function updateScreenEmbedding(req: Request, res: Response) { * 화면 임베딩 삭제 * DELETE /api/screen-embedding/:id */ -export async function deleteScreenEmbedding(req: Request, res: Response) { +export async function deleteScreenEmbedding(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; const companyCode = req.user!.companyCode; @@ -301,7 +302,7 @@ export async function deleteScreenEmbedding(req: Request, res: Response) { * 데이터 전달 설정 조회 * GET /api/screen-data-transfer?sourceScreenId=1&targetScreenId=2 */ -export async function getScreenDataTransfer(req: Request, res: Response) { +export async function getScreenDataTransfer(req: AuthenticatedRequest, res: Response) { try { const { sourceScreenId, targetScreenId } = req.query; const companyCode = req.user!.companyCode; @@ -363,7 +364,7 @@ export async function getScreenDataTransfer(req: Request, res: Response) { * 데이터 전달 설정 생성 * POST /api/screen-data-transfer */ -export async function createScreenDataTransfer(req: Request, res: Response) { +export async function createScreenDataTransfer(req: AuthenticatedRequest, res: Response) { try { const { sourceScreenId, @@ -436,7 +437,7 @@ export async function createScreenDataTransfer(req: Request, res: Response) { * 데이터 전달 설정 수정 * PUT /api/screen-data-transfer/:id */ -export async function updateScreenDataTransfer(req: Request, res: Response) { +export async function updateScreenDataTransfer(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; const { dataReceivers, buttonConfig } = req.body; @@ -504,7 +505,7 @@ export async function updateScreenDataTransfer(req: Request, res: Response) { * 데이터 전달 설정 삭제 * DELETE /api/screen-data-transfer/:id */ -export async function deleteScreenDataTransfer(req: Request, res: Response) { +export async function deleteScreenDataTransfer(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; const companyCode = req.user!.companyCode; @@ -548,7 +549,7 @@ export async function deleteScreenDataTransfer(req: Request, res: Response) { * 분할 패널 설정 조회 * GET /api/screen-split-panel/:screenId */ -export async function getScreenSplitPanel(req: Request, res: Response) { +export async function getScreenSplitPanel(req: AuthenticatedRequest, res: Response) { try { const { screenId } = req.params; const companyCode = req.user!.companyCode; @@ -655,7 +656,7 @@ export async function getScreenSplitPanel(req: Request, res: Response) { * 분할 패널 설정 생성 * POST /api/screen-split-panel */ -export async function createScreenSplitPanel(req: Request, res: Response) { +export async function createScreenSplitPanel(req: AuthenticatedRequest, res: Response) { const client = await pool.connect(); try { @@ -792,7 +793,7 @@ export async function createScreenSplitPanel(req: Request, res: Response) { * 분할 패널 설정 수정 * PUT /api/screen-split-panel/:id */ -export async function updateScreenSplitPanel(req: Request, res: Response) { +export async function updateScreenSplitPanel(req: AuthenticatedRequest, res: Response) { try { const { id } = req.params; const { layoutConfig } = req.body; @@ -845,7 +846,7 @@ export async function updateScreenSplitPanel(req: Request, res: Response) { * 분할 패널 설정 삭제 * DELETE /api/screen-split-panel/:id */ -export async function deleteScreenSplitPanel(req: Request, res: Response) { +export async function deleteScreenSplitPanel(req: AuthenticatedRequest, res: Response) { const client = await pool.connect(); try { diff --git a/backend-node/src/services/batchExternalDbService.ts b/backend-node/src/services/batchExternalDbService.ts index 18524085..303c2d7a 100644 --- a/backend-node/src/services/batchExternalDbService.ts +++ b/backend-node/src/services/batchExternalDbService.ts @@ -203,8 +203,7 @@ export class BatchExternalDbService { // 비밀번호 복호화 if (connection.password) { try { - const passwordEncryption = new PasswordEncryption(); - connection.password = passwordEncryption.decrypt(connection.password); + connection.password = PasswordEncryption.decrypt(connection.password); } catch (error) { console.error("비밀번호 복호화 실패:", error); // 복호화 실패 시 원본 사용 (또는 에러 처리) diff --git a/backend-node/src/services/batchSchedulerService.ts b/backend-node/src/services/batchSchedulerService.ts index a8f755c3..65ac736f 100644 --- a/backend-node/src/services/batchSchedulerService.ts +++ b/backend-node/src/services/batchSchedulerService.ts @@ -1,10 +1,10 @@ -import cron from "node-cron"; +import cron, { ScheduledTask } from "node-cron"; import { BatchService } from "./batchService"; import { BatchExecutionLogService } from "./batchExecutionLogService"; import { logger } from "../utils/logger"; export class BatchSchedulerService { - private static scheduledTasks: Map = new Map(); + private static scheduledTasks: Map = new Map(); /** * 모든 활성 배치의 스케줄링 초기화 @@ -175,7 +175,7 @@ export class BatchSchedulerService { // 실행 로그 업데이트 (실패) if (executionLog) { await BatchExecutionLogService.updateExecutionLog(executionLog.id, { - execution_status: "FAILURE", + execution_status: "FAILED", end_time: new Date(), duration_ms: Date.now() - startTime.getTime(), error_message: @@ -396,4 +396,11 @@ export class BatchSchedulerService { return { totalRecords, successRecords, failedRecords }; } + + /** + * 개별 배치 작업 스케줄링 (scheduleBatch의 별칭) + */ + static async scheduleBatchConfig(config: any) { + return this.scheduleBatch(config); + } } diff --git a/backend-node/src/services/batchService.ts b/backend-node/src/services/batchService.ts index 41f20964..2aefc98b 100644 --- a/backend-node/src/services/batchService.ts +++ b/backend-node/src/services/batchService.ts @@ -16,7 +16,6 @@ import { UpdateBatchConfigRequest, } from "../types/batchTypes"; import { BatchExternalDbService } from "./batchExternalDbService"; -import { DbConnectionManager } from "./dbConnectionManager"; export class BatchService { /** @@ -475,7 +474,13 @@ export class BatchService { try { if (connectionType === "internal") { // 내부 DB 테이블 조회 - const tables = await DbConnectionManager.getInternalTables(); + const tables = await query( + `SELECT table_name, table_type, table_schema + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_type = 'BASE TABLE' + ORDER BY table_name` + ); return { success: true, data: tables, @@ -509,7 +514,13 @@ export class BatchService { try { if (connectionType === "internal") { // 내부 DB 컬럼 조회 - const columns = await DbConnectionManager.getInternalColumns(tableName); + const columns = await query( + `SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = $1 + ORDER BY ordinal_position`, + [tableName] + ); return { success: true, data: columns, @@ -543,7 +554,9 @@ export class BatchService { try { if (connectionType === "internal") { // 내부 DB 데이터 조회 - const data = await DbConnectionManager.getInternalData(tableName, 10); + const data = await query( + `SELECT * FROM ${tableName} LIMIT 10` + ); return { success: true, data, diff --git a/backend-node/src/types/batchTypes.ts b/backend-node/src/types/batchTypes.ts index 1cbec196..15efd003 100644 --- a/backend-node/src/types/batchTypes.ts +++ b/backend-node/src/types/batchTypes.ts @@ -1,4 +1,98 @@ -import { ApiResponse, ColumnInfo } from './batchTypes'; +// 배치관리 타입 정의 +// 작성일: 2024-12-24 + +// 공통 API 응답 타입 +export interface ApiResponse { + success: boolean; + data?: T; + message?: string; + error?: string; + pagination?: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +} + +// 컬럼 정보 타입 +export interface ColumnInfo { + column_name: string; + data_type: string; + is_nullable?: string; + column_default?: string | null; +} + +// 테이블 정보 타입 +export interface TableInfo { + table_name: string; + table_type?: string; + table_schema?: string; +} + +// 연결 정보 타입 +export interface ConnectionInfo { + type: 'internal' | 'external'; + id?: number; + name: string; + db_type?: string; +} + +// 배치 설정 필터 타입 +export interface BatchConfigFilter { + page?: number; + limit?: number; + search?: string; + is_active?: string; + company_code?: string; +} + +// 배치 매핑 타입 +export interface BatchMapping { + id?: number; + batch_config_id?: number; + company_code?: string; + from_connection_type: 'internal' | 'external' | 'restapi'; + from_connection_id?: number; + from_table_name: string; + from_column_name: string; + from_column_type?: string; + from_api_url?: string; + from_api_key?: string; + from_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + from_api_param_type?: 'url' | 'query'; + from_api_param_name?: string; + from_api_param_value?: string; + from_api_param_source?: 'static' | 'dynamic'; + from_api_body?: string; + to_connection_type: 'internal' | 'external' | 'restapi'; + to_connection_id?: number; + to_table_name: string; + to_column_name: string; + to_column_type?: string; + to_api_url?: string; + to_api_key?: string; + to_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + to_api_body?: string; + mapping_order?: number; + created_by?: string; + created_date?: Date; +} + +// 배치 설정 타입 +export interface BatchConfig { + id?: number; + batch_name: string; + description?: string; + cron_schedule: string; + is_active: 'Y' | 'N'; + company_code?: string; + created_by?: string; + created_date?: Date; + updated_by?: string; + updated_date?: Date; + batch_mappings?: BatchMapping[]; +} export interface BatchConnectionInfo { type: 'internal' | 'external'; @@ -27,7 +121,7 @@ export interface BatchMappingRequest { from_api_param_name?: string; // API 파라미터명 from_api_param_value?: string; // API 파라미터 값 또는 템플릿 from_api_param_source?: 'static' | 'dynamic'; // 파라미터 소스 타입 - // 👇 REST API Body 추가 (FROM - REST API에서 POST 요청 시 필요) + // REST API Body 추가 (FROM - REST API에서 POST 요청 시 필요) from_api_body?: string; to_connection_type: 'internal' | 'external' | 'restapi'; to_connection_id?: number;