diff --git a/backend-node/prisma/schema.prisma b/backend-node/prisma/schema.prisma index 3e8812ab..7cd671d2 100644 --- a/backend-node/prisma/schema.prisma +++ b/backend-node/prisma/schema.prisma @@ -111,6 +111,10 @@ model batch_mappings { from_api_url String? @db.VarChar(500) from_api_key String? @db.VarChar(200) from_api_method String? @db.VarChar(10) + from_api_param_type String? @db.VarChar(10) // 'url' 또는 'query' + from_api_param_name String? @db.VarChar(100) // 파라미터명 + from_api_param_value String? @db.VarChar(500) // 파라미터 값 또는 템플릿 + from_api_param_source String? @db.VarChar(10) // 'static' 또는 'dynamic' to_connection_type String @db.VarChar(20) to_connection_id Int? to_table_name String @db.VarChar(100) diff --git a/backend-node/src/controllers/batchController.ts b/backend-node/src/controllers/batchController.ts index ba270f41..f91b5d25 100644 --- a/backend-node/src/controllers/batchController.ts +++ b/backend-node/src/controllers/batchController.ts @@ -3,6 +3,7 @@ import { Request, Response } from "express"; import { BatchService } from "../services/batchService"; +import { BatchSchedulerService } from "../services/batchSchedulerService"; import { BatchConfigFilter, CreateBatchConfigRequest, UpdateBatchConfigRequest } from "../types/batchTypes"; export interface AuthenticatedRequest extends Request { @@ -190,6 +191,11 @@ export class BatchController { cronSchedule, mappings } as CreateBatchConfigRequest); + + // 생성된 배치가 활성화 상태라면 스케줄러에 등록 + if (batchConfig.data && batchConfig.data.is_active === 'Y' && batchConfig.data.id) { + await BatchSchedulerService.updateBatchSchedule(batchConfig.data.id); + } return res.status(201).json({ success: true, @@ -235,6 +241,9 @@ export class BatchController { message: "배치 설정을 찾을 수 없습니다." }); } + + // 스케줄러에서 배치 스케줄 업데이트 (활성화 시 즉시 스케줄 등록) + await BatchSchedulerService.updateBatchSchedule(Number(id)); return res.json({ success: true, diff --git a/backend-node/src/controllers/batchManagementController.ts b/backend-node/src/controllers/batchManagementController.ts index 4381a340..cbff6bc3 100644 --- a/backend-node/src/controllers/batchManagementController.ts +++ b/backend-node/src/controllers/batchManagementController.ts @@ -282,7 +282,13 @@ export class BatchManagementController { firstMapping.from_api_key!, firstMapping.from_table_name, firstMapping.from_api_method as 'GET' | 'POST' | 'PUT' | 'DELETE' || 'GET', - mappings.map(m => m.from_column_name) + mappings.map(m => m.from_column_name), + 100, // limit + // 파라미터 정보 전달 + firstMapping.from_api_param_type, + firstMapping.from_api_param_name, + firstMapping.from_api_param_value, + firstMapping.from_api_param_source ); console.log(`API 조회 결과:`, { @@ -482,7 +488,16 @@ export class BatchManagementController { */ static async previewRestApiData(req: AuthenticatedRequest, res: Response) { try { - const { apiUrl, apiKey, endpoint, method = 'GET' } = req.body; + const { + apiUrl, + apiKey, + endpoint, + method = 'GET', + paramType, + paramName, + paramValue, + paramSource + } = req.body; if (!apiUrl || !apiKey || !endpoint) { return res.status(400).json({ @@ -491,6 +506,15 @@ export class BatchManagementController { }); } + console.log("🔍 REST API 미리보기 요청:", { + apiUrl, + endpoint, + paramType, + paramName, + paramValue, + paramSource + }); + // RestApiConnector 사용하여 데이터 조회 const { RestApiConnector } = await import('../database/RestApiConnector'); @@ -503,8 +527,28 @@ export class BatchManagementController { // 연결 테스트 await connector.connect(); + // 파라미터가 있는 경우 엔드포인트 수정 + let finalEndpoint = endpoint; + if (paramType && paramName && paramValue) { + if (paramType === 'url') { + // URL 파라미터: /api/users/{userId} → /api/users/123 + if (endpoint.includes(`{${paramName}}`)) { + finalEndpoint = endpoint.replace(`{${paramName}}`, paramValue); + } else { + // 엔드포인트에 {paramName}이 없으면 뒤에 추가 + finalEndpoint = `${endpoint}/${paramValue}`; + } + } else if (paramType === 'query') { + // 쿼리 파라미터: /api/users?userId=123 + const separator = endpoint.includes('?') ? '&' : '?'; + finalEndpoint = `${endpoint}${separator}${paramName}=${paramValue}`; + } + } + + console.log("🔗 최종 엔드포인트:", finalEndpoint); + // 데이터 조회 (최대 5개만) - GET 메서드만 지원 - const result = await connector.executeQuery(endpoint, method); + const result = await connector.executeQuery(finalEndpoint, method); console.log(`[previewRestApiData] executeQuery 결과:`, { rowCount: result.rowCount, rowsLength: result.rows ? result.rows.length : 'undefined', diff --git a/backend-node/src/services/batchExternalDbService.ts b/backend-node/src/services/batchExternalDbService.ts index 470c3b75..d5670f04 100644 --- a/backend-node/src/services/batchExternalDbService.ts +++ b/backend-node/src/services/batchExternalDbService.ts @@ -697,7 +697,12 @@ export class BatchExternalDbService { endpoint: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', columns?: string[], - limit: number = 100 + limit: number = 100, + // 파라미터 정보 추가 + paramType?: 'url' | 'query', + paramName?: string, + paramValue?: string, + paramSource?: 'static' | 'dynamic' ): Promise> { try { console.log(`[BatchExternalDbService] REST API 데이터 조회: ${apiUrl}${endpoint}`); @@ -712,8 +717,33 @@ export class BatchExternalDbService { // 연결 테스트 await connector.connect(); + // 파라미터가 있는 경우 엔드포인트 수정 + const { logger } = await import('../utils/logger'); + logger.info(`[BatchExternalDbService] 파라미터 정보`, { + paramType, paramName, paramValue, paramSource + }); + + let finalEndpoint = endpoint; + if (paramType && paramName && paramValue) { + if (paramType === 'url') { + // URL 파라미터: /api/users/{userId} → /api/users/123 + if (endpoint.includes(`{${paramName}}`)) { + finalEndpoint = endpoint.replace(`{${paramName}}`, paramValue); + } else { + // 엔드포인트에 {paramName}이 없으면 뒤에 추가 + finalEndpoint = `${endpoint}/${paramValue}`; + } + } else if (paramType === 'query') { + // 쿼리 파라미터: /api/users?userId=123 + const separator = endpoint.includes('?') ? '&' : '?'; + finalEndpoint = `${endpoint}${separator}${paramName}=${paramValue}`; + } + + logger.info(`[BatchExternalDbService] 파라미터 적용된 엔드포인트: ${finalEndpoint}`); + } + // 데이터 조회 - const result = await connector.executeQuery(endpoint, method); + const result = await connector.executeQuery(finalEndpoint, method); let data = result.rows; // 컬럼 필터링 (지정된 컬럼만 추출) @@ -734,7 +764,8 @@ export class BatchExternalDbService { data = data.slice(0, limit); } - console.log(`[BatchExternalDbService] REST API 데이터 조회 완료: ${data.length}개 레코드`); + logger.info(`[BatchExternalDbService] REST API 데이터 조회 완료: ${data.length}개 레코드`); + logger.info(`[BatchExternalDbService] 조회된 데이터`, { data }); return { success: true, diff --git a/backend-node/src/services/batchSchedulerService.ts b/backend-node/src/services/batchSchedulerService.ts index 3d032291..4a46f595 100644 --- a/backend-node/src/services/batchSchedulerService.ts +++ b/backend-node/src/services/batchSchedulerService.ts @@ -112,7 +112,7 @@ export class BatchSchedulerService { /** * 배치 설정 업데이트 시 스케줄 재등록 */ - static async updateBatchSchedule(configId: number) { + static async updateBatchSchedule(configId: number, executeImmediately: boolean = true) { try { // 기존 스케줄 제거 await this.unscheduleBatchConfig(configId); @@ -132,6 +132,12 @@ export class BatchSchedulerService { if (config.is_active === 'Y') { await this.scheduleBatchConfig(config); logger.info(`배치 스케줄 업데이트 완료: ${config.batch_name} (ID: ${configId})`); + + // 활성화 시 즉시 실행 (옵션) + if (executeImmediately) { + logger.info(`🚀 배치 활성화 즉시 실행: ${config.batch_name} (ID: ${configId})`); + await this.executeBatchConfig(config); + } } else { logger.info(`비활성화된 배치 스케줄 제거: ${config.batch_name} (ID: ${configId})`); } @@ -239,7 +245,13 @@ export class BatchSchedulerService { firstMapping.from_api_key!, firstMapping.from_table_name, firstMapping.from_api_method as 'GET' | 'POST' | 'PUT' | 'DELETE' || 'GET', - mappings.map((m: any) => m.from_column_name) + mappings.map((m: any) => m.from_column_name), + 100, // limit + // 파라미터 정보 전달 + firstMapping.from_api_param_type, + firstMapping.from_api_param_name, + firstMapping.from_api_param_value, + firstMapping.from_api_param_source ); if (apiResult.success && apiResult.data) { diff --git a/backend-node/src/services/batchService.ts b/backend-node/src/services/batchService.ts index edac1629..4120e47f 100644 --- a/backend-node/src/services/batchService.ts +++ b/backend-node/src/services/batchService.ts @@ -168,6 +168,10 @@ export class BatchService { from_api_url: mapping.from_api_url, from_api_key: mapping.from_api_key, from_api_method: mapping.from_api_method, + from_api_param_type: mapping.from_api_param_type, + from_api_param_name: mapping.from_api_param_name, + from_api_param_value: mapping.from_api_param_value, + from_api_param_source: mapping.from_api_param_source, to_connection_type: mapping.to_connection_type, to_connection_id: mapping.to_connection_id, to_table_name: mapping.to_table_name, @@ -176,7 +180,7 @@ export class BatchService { to_api_url: mapping.to_api_url, to_api_key: mapping.to_api_key, to_api_method: mapping.to_api_method, - // to_api_body: mapping.to_api_body, // Request Body 템플릿 추가 - 임시 주석 처리 + to_api_body: mapping.to_api_body, mapping_order: mapping.mapping_order || index + 1, created_by: userId, }, @@ -260,11 +264,22 @@ export class BatchService { from_table_name: mapping.from_table_name, from_column_name: mapping.from_column_name, from_column_type: mapping.from_column_type, + from_api_url: mapping.from_api_url, + from_api_key: mapping.from_api_key, + from_api_method: mapping.from_api_method, + from_api_param_type: mapping.from_api_param_type, + from_api_param_name: mapping.from_api_param_name, + from_api_param_value: mapping.from_api_param_value, + from_api_param_source: mapping.from_api_param_source, to_connection_type: mapping.to_connection_type, to_connection_id: mapping.to_connection_id, to_table_name: mapping.to_table_name, to_column_name: mapping.to_column_name, to_column_type: mapping.to_column_type, + to_api_url: mapping.to_api_url, + to_api_key: mapping.to_api_key, + to_api_method: mapping.to_api_method, + to_api_body: mapping.to_api_body, mapping_order: mapping.mapping_order || index + 1, created_by: userId, }, @@ -707,18 +722,39 @@ export class BatchService { const updateColumns = columns.filter(col => col !== primaryKeyColumn); const updateSet = updateColumns.map(col => `${col} = EXCLUDED.${col}`).join(', '); + // 먼저 해당 레코드가 존재하는지 확인 + const checkQuery = `SELECT COUNT(*) as count FROM ${tableName} WHERE ${primaryKeyColumn} = $1`; + const existsResult = await prisma.$queryRawUnsafe(checkQuery, record[primaryKeyColumn]); + const exists = (existsResult as any)[0]?.count > 0; + let query: string; - if (updateSet) { - // UPSERT: 중복 시 업데이트 - query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) - ON CONFLICT (${primaryKeyColumn}) DO UPDATE SET ${updateSet}`; + if (exists && updateSet) { + // 기존 레코드가 있으면 UPDATE (값이 다른 경우에만) + const whereConditions = updateColumns.map((col, index) => + `${col} IS DISTINCT FROM $${index + 2}` + ).join(' OR '); + + query = `UPDATE ${tableName} SET ${updateSet.replace(/EXCLUDED\./g, '')} + WHERE ${primaryKeyColumn} = $1 AND (${whereConditions})`; + + // 파라미터: [primaryKeyValue, ...updateValues] + const updateValues = [record[primaryKeyColumn], ...updateColumns.map(col => record[col])]; + const updateResult = await prisma.$executeRawUnsafe(query, ...updateValues); + + if (updateResult > 0) { + console.log(`[BatchService] 레코드 업데이트: ${primaryKeyColumn}=${record[primaryKeyColumn]}`); + } else { + console.log(`[BatchService] 레코드 변경사항 없음: ${primaryKeyColumn}=${record[primaryKeyColumn]}`); + } + } else if (!exists) { + // 새 레코드 삽입 + query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`; + await prisma.$executeRawUnsafe(query, ...values); + console.log(`[BatchService] 새 레코드 삽입: ${primaryKeyColumn}=${record[primaryKeyColumn]}`); } else { - // Primary Key만 있는 경우 중복 시 무시 - query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) - ON CONFLICT (${primaryKeyColumn}) DO NOTHING`; + console.log(`[BatchService] 레코드 이미 존재 (변경사항 없음): ${primaryKeyColumn}=${record[primaryKeyColumn]}`); } - await prisma.$executeRawUnsafe(query, ...values); successCount++; } catch (error) { console.error(`레코드 UPSERT 실패:`, error); diff --git a/backend-node/src/types/batchTypes.ts b/backend-node/src/types/batchTypes.ts index e2a676ef..24158a3d 100644 --- a/backend-node/src/types/batchTypes.ts +++ b/backend-node/src/types/batchTypes.ts @@ -37,6 +37,10 @@ export interface BatchMapping { from_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; // REST API 전용 from_api_url?: string; // REST API 서버 URL from_api_key?: string; // REST API 키 + from_api_param_type?: 'url' | 'query'; // API 파라미터 타입 + from_api_param_name?: string; // API 파라미터명 + from_api_param_value?: string; // API 파라미터 값 또는 템플릿 + from_api_param_source?: 'static' | 'dynamic'; // 파라미터 소스 타입 // TO 정보 to_connection_type: 'internal' | 'external' | 'restapi'; @@ -92,6 +96,10 @@ export interface BatchMappingRequest { from_api_url?: string; from_api_key?: string; from_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + from_api_param_type?: 'url' | 'query'; // API 파라미터 타입 + from_api_param_name?: string; // API 파라미터명 + from_api_param_value?: string; // API 파라미터 값 또는 템플릿 + from_api_param_source?: 'static' | 'dynamic'; // 파라미터 소스 타입 to_connection_type: 'internal' | 'external' | 'restapi'; to_connection_id?: number; to_table_name: string; diff --git a/frontend/app/(main)/admin/batch-management-new/page.tsx b/frontend/app/(main)/admin/batch-management-new/page.tsx index 53a840ff..f70d711a 100644 --- a/frontend/app/(main)/admin/batch-management-new/page.tsx +++ b/frontend/app/(main)/admin/batch-management-new/page.tsx @@ -53,6 +53,12 @@ export default function BatchManagementNewPage() { const [fromApiKey, setFromApiKey] = useState(""); const [fromEndpoint, setFromEndpoint] = useState(""); const [fromApiMethod, setFromApiMethod] = useState<'GET'>('GET'); // GET만 지원 + + // REST API 파라미터 설정 + const [apiParamType, setApiParamType] = useState<'none' | 'url' | 'query'>('none'); + const [apiParamName, setApiParamName] = useState(""); // 파라미터명 (예: userId, id) + const [apiParamValue, setApiParamValue] = useState(""); // 파라미터 값 또는 템플릿 + const [apiParamSource, setApiParamSource] = useState<'static' | 'dynamic'>('static'); // 정적 값 또는 동적 값 // DB → REST API용 상태 const [fromConnection, setFromConnection] = useState(null); @@ -309,7 +315,14 @@ export default function BatchManagementNewPage() { fromApiUrl, fromApiKey, fromEndpoint, - fromApiMethod + fromApiMethod, + // 파라미터 정보 추가 + apiParamType !== 'none' ? { + paramType: apiParamType, + paramName: apiParamName, + paramValue: apiParamValue, + paramSource: apiParamSource + } : undefined ); console.log("API 미리보기 결과:", result); @@ -371,6 +384,11 @@ export default function BatchManagementNewPage() { from_api_url: fromApiUrl, from_api_key: fromApiKey, from_api_method: fromApiMethod, + // API 파라미터 정보 추가 + from_api_param_type: apiParamType !== 'none' ? apiParamType : undefined, + from_api_param_name: apiParamType !== 'none' ? apiParamName : undefined, + from_api_param_value: apiParamType !== 'none' ? apiParamValue : undefined, + from_api_param_source: apiParamType !== 'none' ? apiParamSource : undefined, to_connection_type: toConnection?.type === 'internal' ? 'internal' : 'external', to_connection_id: toConnection?.type === 'internal' ? undefined : toConnection?.id, to_table_name: toTable, @@ -661,14 +679,119 @@ export default function BatchManagementNewPage() { + {/* API 파라미터 설정 */} +
+
+ +

특정 사용자나 조건으로 데이터를 조회할 때 사용합니다.

+
+ +
+ + +
+ + {apiParamType !== 'none' && ( + <> +
+
+ + setApiParamName(e.target.value)} + placeholder="userId, id, email 등" + /> +
+
+ + +
+
+ +
+ + setApiParamValue(e.target.value)} + placeholder={ + apiParamSource === 'static' + ? "123, john@example.com 등" + : "{{user_id}}, {{email}} 등 (실행 시 치환됨)" + } + /> + {apiParamSource === 'dynamic' && ( +

+ 동적값은 배치 실행 시 설정된 값으로 치환됩니다. 예: {`{{user_id}}`} → 실제 사용자 ID +

+ )} +
+ + {apiParamType === 'url' && ( +
+
URL 파라미터 예시
+
+ 엔드포인트: /api/users/{`{${apiParamName || 'userId'}}`} +
+
+ 실제 호출: /api/users/{apiParamValue || '123'} +
+
+ )} + + {apiParamType === 'query' && ( +
+
쿼리 파라미터 예시
+
+ 실제 호출: {fromEndpoint || '/api/users'}?{apiParamName || 'userId'}={apiParamValue || '123'} +
+
+ )} + + )} +
+ {fromApiUrl && fromApiKey && fromEndpoint && (
API 호출 미리보기
- {fromApiMethod} {fromApiUrl}{fromEndpoint} + {fromApiMethod} {fromApiUrl} + {apiParamType === 'url' && apiParamName && apiParamValue + ? fromEndpoint.replace(`{${apiParamName}}`, apiParamValue) || fromEndpoint + `/${apiParamValue}` + : fromEndpoint + } + {apiParamType === 'query' && apiParamName && apiParamValue + ? `?${apiParamName}=${apiParamValue}` + : '' + }
Headers: X-API-Key: {fromApiKey.substring(0, 10)}...
+ {apiParamType !== 'none' && apiParamName && apiParamValue && ( +
+ 파라미터: {apiParamName} = {apiParamValue} ({apiParamSource === 'static' ? '고정값' : '동적값'}) +
+ )}