배치 UPSERT 기능 및 고정값 매핑 버그 수정
This commit is contained in:
parent
7a2f80b646
commit
ef3b85f343
|
|
@ -1,7 +1,7 @@
|
||||||
// 배치관리 전용 컨트롤러 (기존 소스와 완전 분리)
|
// 배치관리 전용 컨트롤러 (기존 소스와 완전 분리)
|
||||||
// 작성일: 2024-12-24
|
// 작성일: 2024-12-24
|
||||||
|
|
||||||
import { Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { AuthenticatedRequest } from "../types/auth";
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
import {
|
import {
|
||||||
BatchManagementService,
|
BatchManagementService,
|
||||||
|
|
@ -13,6 +13,7 @@ import { BatchService } from "../services/batchService";
|
||||||
import { BatchSchedulerService } from "../services/batchSchedulerService";
|
import { BatchSchedulerService } from "../services/batchSchedulerService";
|
||||||
import { BatchExternalDbService } from "../services/batchExternalDbService";
|
import { BatchExternalDbService } from "../services/batchExternalDbService";
|
||||||
import { CreateBatchConfigRequest, BatchConfig } from "../types/batchTypes";
|
import { CreateBatchConfigRequest, BatchConfig } from "../types/batchTypes";
|
||||||
|
import { query } from "../database/db";
|
||||||
|
|
||||||
export class BatchManagementController {
|
export class BatchManagementController {
|
||||||
/**
|
/**
|
||||||
|
|
@ -422,6 +423,8 @@ export class BatchManagementController {
|
||||||
paramValue,
|
paramValue,
|
||||||
paramSource,
|
paramSource,
|
||||||
requestBody,
|
requestBody,
|
||||||
|
authServiceName, // DB에서 토큰 가져올 서비스명
|
||||||
|
dataArrayPath, // 데이터 배열 경로 (예: response, data.items)
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// apiUrl, endpoint는 항상 필수
|
// apiUrl, endpoint는 항상 필수
|
||||||
|
|
@ -432,15 +435,36 @@ export class BatchManagementController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET 요청일 때만 API Key 필수 (POST/PUT/DELETE는 선택)
|
// 토큰 결정: authServiceName이 있으면 DB에서 조회, 없으면 apiKey 사용
|
||||||
if ((!method || method === "GET") && !apiKey) {
|
let finalApiKey = apiKey || "";
|
||||||
|
if (authServiceName) {
|
||||||
|
// DB에서 토큰 조회
|
||||||
|
const tokenResult = await query<{ access_token: string }>(
|
||||||
|
`SELECT access_token FROM auth_tokens
|
||||||
|
WHERE service_name = $1
|
||||||
|
ORDER BY created_date DESC LIMIT 1`,
|
||||||
|
[authServiceName]
|
||||||
|
);
|
||||||
|
if (tokenResult.length > 0 && tokenResult[0].access_token) {
|
||||||
|
finalApiKey = tokenResult[0].access_token;
|
||||||
|
console.log(`auth_tokens에서 토큰 조회 성공: ${authServiceName}`);
|
||||||
|
} else {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `서비스 '${authServiceName}'의 토큰을 찾을 수 없습니다. 먼저 토큰 저장 배치를 실행하세요.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 토큰이 없으면 에러 (직접 입력도 안 하고 DB 선택도 안 한 경우)
|
||||||
|
if (!finalApiKey) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "GET 메서드에서는 API Key가 필요합니다.",
|
message: "인증 토큰이 필요합니다. 직접 입력하거나 DB에서 선택하세요.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🔍 REST API 미리보기 요청:", {
|
console.log("REST API 미리보기 요청:", {
|
||||||
apiUrl,
|
apiUrl,
|
||||||
endpoint,
|
endpoint,
|
||||||
method,
|
method,
|
||||||
|
|
@ -449,6 +473,8 @@ export class BatchManagementController {
|
||||||
paramValue,
|
paramValue,
|
||||||
paramSource,
|
paramSource,
|
||||||
requestBody: requestBody ? "Included" : "None",
|
requestBody: requestBody ? "Included" : "None",
|
||||||
|
authServiceName: authServiceName || "직접 입력",
|
||||||
|
dataArrayPath: dataArrayPath || "전체 응답",
|
||||||
});
|
});
|
||||||
|
|
||||||
// RestApiConnector 사용하여 데이터 조회
|
// RestApiConnector 사용하여 데이터 조회
|
||||||
|
|
@ -456,7 +482,7 @@ export class BatchManagementController {
|
||||||
|
|
||||||
const connector = new RestApiConnector({
|
const connector = new RestApiConnector({
|
||||||
baseUrl: apiUrl,
|
baseUrl: apiUrl,
|
||||||
apiKey: apiKey || "",
|
apiKey: finalApiKey,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -511,8 +537,50 @@ export class BatchManagementController {
|
||||||
result.rows && result.rows.length > 0 ? result.rows[0] : "no data",
|
result.rows && result.rows.length > 0 ? result.rows[0] : "no data",
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = result.rows.slice(0, 5); // 최대 5개 샘플만
|
// 데이터 배열 추출 헬퍼 함수
|
||||||
console.log(`[previewRestApiData] 슬라이스된 데이터:`, data);
|
const getValueByPath = (obj: any, path: string): any => {
|
||||||
|
if (!path) return obj;
|
||||||
|
const keys = path.split(".");
|
||||||
|
let current = obj;
|
||||||
|
for (const key of keys) {
|
||||||
|
if (current === null || current === undefined) return undefined;
|
||||||
|
current = current[key];
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
};
|
||||||
|
|
||||||
|
// dataArrayPath가 있으면 해당 경로에서 배열 추출
|
||||||
|
let extractedData: any[] = [];
|
||||||
|
if (dataArrayPath) {
|
||||||
|
// result.rows가 단일 객체일 수 있음 (API 응답 전체)
|
||||||
|
const rawData = result.rows.length === 1 ? result.rows[0] : result.rows;
|
||||||
|
const arrayData = getValueByPath(rawData, dataArrayPath);
|
||||||
|
|
||||||
|
if (Array.isArray(arrayData)) {
|
||||||
|
extractedData = arrayData;
|
||||||
|
console.log(
|
||||||
|
`[previewRestApiData] '${dataArrayPath}' 경로에서 ${arrayData.length}개 항목 추출`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`[previewRestApiData] '${dataArrayPath}' 경로가 배열이 아님:`,
|
||||||
|
typeof arrayData
|
||||||
|
);
|
||||||
|
// 배열이 아니면 단일 객체로 처리
|
||||||
|
if (arrayData) {
|
||||||
|
extractedData = [arrayData];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// dataArrayPath가 없으면 기존 로직 사용
|
||||||
|
extractedData = result.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = extractedData.slice(0, 5); // 최대 5개 샘플만
|
||||||
|
console.log(
|
||||||
|
`[previewRestApiData] 슬라이스된 데이터 (${extractedData.length}개 중 ${data.length}개):`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
// 첫 번째 객체에서 필드명 추출
|
// 첫 번째 객체에서 필드명 추출
|
||||||
|
|
@ -524,9 +592,9 @@ export class BatchManagementController {
|
||||||
data: {
|
data: {
|
||||||
fields: fields,
|
fields: fields,
|
||||||
samples: data,
|
samples: data,
|
||||||
totalCount: result.rowCount || data.length,
|
totalCount: extractedData.length,
|
||||||
},
|
},
|
||||||
message: `${fields.length}개 필드, ${result.rowCount || data.length}개 레코드를 조회했습니다.`,
|
message: `${fields.length}개 필드, ${extractedData.length}개 레코드를 조회했습니다.`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
@ -554,8 +622,17 @@ export class BatchManagementController {
|
||||||
*/
|
*/
|
||||||
static async saveRestApiBatch(req: AuthenticatedRequest, res: Response) {
|
static async saveRestApiBatch(req: AuthenticatedRequest, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { batchName, batchType, cronSchedule, description, apiMappings } =
|
const {
|
||||||
req.body;
|
batchName,
|
||||||
|
batchType,
|
||||||
|
cronSchedule,
|
||||||
|
description,
|
||||||
|
apiMappings,
|
||||||
|
authServiceName,
|
||||||
|
dataArrayPath,
|
||||||
|
saveMode,
|
||||||
|
conflictKey,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!batchName ||
|
!batchName ||
|
||||||
|
|
@ -576,6 +653,10 @@ export class BatchManagementController {
|
||||||
cronSchedule,
|
cronSchedule,
|
||||||
description,
|
description,
|
||||||
apiMappings,
|
apiMappings,
|
||||||
|
authServiceName,
|
||||||
|
dataArrayPath,
|
||||||
|
saveMode,
|
||||||
|
conflictKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🔐 멀티테넌시: 현재 사용자 회사 코드 사용 (프론트에서 받지 않음)
|
// 🔐 멀티테넌시: 현재 사용자 회사 코드 사용 (프론트에서 받지 않음)
|
||||||
|
|
@ -589,6 +670,10 @@ export class BatchManagementController {
|
||||||
cronSchedule: cronSchedule,
|
cronSchedule: cronSchedule,
|
||||||
isActive: "Y",
|
isActive: "Y",
|
||||||
companyCode,
|
companyCode,
|
||||||
|
authServiceName: authServiceName || undefined,
|
||||||
|
dataArrayPath: dataArrayPath || undefined,
|
||||||
|
saveMode: saveMode || "INSERT",
|
||||||
|
conflictKey: conflictKey || undefined,
|
||||||
mappings: apiMappings,
|
mappings: apiMappings,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -625,4 +710,31 @@ export class BatchManagementController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증 토큰 서비스명 목록 조회
|
||||||
|
*/
|
||||||
|
static async getAuthServiceNames(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const result = await query<{ service_name: string }>(
|
||||||
|
`SELECT DISTINCT service_name
|
||||||
|
FROM auth_tokens
|
||||||
|
WHERE service_name IS NOT NULL
|
||||||
|
ORDER BY service_name`
|
||||||
|
);
|
||||||
|
|
||||||
|
const serviceNames = result.map((row) => row.service_name);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: serviceNames,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("인증 서비스 목록 조회 오류:", error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "인증 서비스 목록 조회 중 오류가 발생했습니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,4 +79,10 @@ router.post("/rest-api/preview", authenticateToken, BatchManagementController.pr
|
||||||
*/
|
*/
|
||||||
router.post("/rest-api/save", authenticateToken, BatchManagementController.saveRestApiBatch);
|
router.post("/rest-api/save", authenticateToken, BatchManagementController.saveRestApiBatch);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/batch-management/auth-services
|
||||||
|
* 인증 토큰 서비스명 목록 조회
|
||||||
|
*/
|
||||||
|
router.get("/auth-services", authenticateToken, BatchManagementController.getAuthServiceNames);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import cron, { ScheduledTask } from "node-cron";
|
||||||
import { BatchService } from "./batchService";
|
import { BatchService } from "./batchService";
|
||||||
import { BatchExecutionLogService } from "./batchExecutionLogService";
|
import { BatchExecutionLogService } from "./batchExecutionLogService";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
import { query } from "../database/db";
|
||||||
|
|
||||||
export class BatchSchedulerService {
|
export class BatchSchedulerService {
|
||||||
private static scheduledTasks: Map<number, ScheduledTask> = new Map();
|
private static scheduledTasks: Map<number, ScheduledTask> = new Map();
|
||||||
|
|
@ -214,9 +215,16 @@ export class BatchSchedulerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 테이블별로 매핑을 그룹화
|
// 테이블별로 매핑을 그룹화
|
||||||
|
// 고정값 매핑(mapping_type === 'fixed')은 별도 그룹으로 분리하지 않고 나중에 처리
|
||||||
const tableGroups = new Map<string, typeof config.batch_mappings>();
|
const tableGroups = new Map<string, typeof config.batch_mappings>();
|
||||||
|
const fixedMappingsGlobal: typeof config.batch_mappings = [];
|
||||||
|
|
||||||
for (const mapping of config.batch_mappings) {
|
for (const mapping of config.batch_mappings) {
|
||||||
|
// 고정값 매핑은 별도로 모아둠 (FROM 소스가 필요 없음)
|
||||||
|
if (mapping.mapping_type === "fixed") {
|
||||||
|
fixedMappingsGlobal.push(mapping);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const key = `${mapping.from_connection_type}:${mapping.from_connection_id || "internal"}:${mapping.from_table_name}`;
|
const key = `${mapping.from_connection_type}:${mapping.from_connection_id || "internal"}:${mapping.from_table_name}`;
|
||||||
if (!tableGroups.has(key)) {
|
if (!tableGroups.has(key)) {
|
||||||
tableGroups.set(key, []);
|
tableGroups.set(key, []);
|
||||||
|
|
@ -224,6 +232,14 @@ export class BatchSchedulerService {
|
||||||
tableGroups.get(key)!.push(mapping);
|
tableGroups.get(key)!.push(mapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 고정값 매핑만 있고 일반 매핑이 없는 경우 처리
|
||||||
|
if (tableGroups.size === 0 && fixedMappingsGlobal.length > 0) {
|
||||||
|
logger.warn(
|
||||||
|
`일반 매핑이 없고 고정값 매핑만 있습니다. 고정값만으로는 배치를 실행할 수 없습니다.`
|
||||||
|
);
|
||||||
|
return { totalRecords, successRecords, failedRecords };
|
||||||
|
}
|
||||||
|
|
||||||
// 각 테이블 그룹별로 처리
|
// 각 테이블 그룹별로 처리
|
||||||
for (const [tableKey, mappings] of tableGroups) {
|
for (const [tableKey, mappings] of tableGroups) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -244,10 +260,31 @@ export class BatchSchedulerService {
|
||||||
"./batchExternalDbService"
|
"./batchExternalDbService"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// auth_service_name이 설정된 경우 auth_tokens에서 토큰 조회
|
||||||
|
let apiKey = firstMapping.from_api_key || "";
|
||||||
|
if (config.auth_service_name) {
|
||||||
|
const tokenResult = await query<{ access_token: string }>(
|
||||||
|
`SELECT access_token FROM auth_tokens
|
||||||
|
WHERE service_name = $1
|
||||||
|
ORDER BY created_date DESC LIMIT 1`,
|
||||||
|
[config.auth_service_name]
|
||||||
|
);
|
||||||
|
if (tokenResult.length > 0 && tokenResult[0].access_token) {
|
||||||
|
apiKey = tokenResult[0].access_token;
|
||||||
|
logger.info(
|
||||||
|
`auth_tokens에서 토큰 조회 성공: ${config.auth_service_name}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`auth_tokens에서 토큰을 찾을 수 없음: ${config.auth_service_name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 👇 Body 파라미터 추가 (POST 요청 시)
|
// 👇 Body 파라미터 추가 (POST 요청 시)
|
||||||
const apiResult = await BatchExternalDbService.getDataFromRestApi(
|
const apiResult = await BatchExternalDbService.getDataFromRestApi(
|
||||||
firstMapping.from_api_url!,
|
firstMapping.from_api_url!,
|
||||||
firstMapping.from_api_key!,
|
apiKey,
|
||||||
firstMapping.from_table_name,
|
firstMapping.from_table_name,
|
||||||
(firstMapping.from_api_method as
|
(firstMapping.from_api_method as
|
||||||
| "GET"
|
| "GET"
|
||||||
|
|
@ -266,7 +303,36 @@ export class BatchSchedulerService {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (apiResult.success && apiResult.data) {
|
if (apiResult.success && apiResult.data) {
|
||||||
fromData = apiResult.data;
|
// 데이터 배열 경로가 설정되어 있으면 해당 경로에서 배열 추출
|
||||||
|
if (config.data_array_path) {
|
||||||
|
const extractArrayByPath = (obj: any, path: string): any[] => {
|
||||||
|
if (!path) return Array.isArray(obj) ? obj : [obj];
|
||||||
|
const keys = path.split(".");
|
||||||
|
let current = obj;
|
||||||
|
for (const key of keys) {
|
||||||
|
if (current === null || current === undefined) return [];
|
||||||
|
current = current[key];
|
||||||
|
}
|
||||||
|
return Array.isArray(current)
|
||||||
|
? current
|
||||||
|
: current
|
||||||
|
? [current]
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// apiResult.data가 단일 객체인 경우 (API 응답 전체)
|
||||||
|
const rawData =
|
||||||
|
Array.isArray(apiResult.data) && apiResult.data.length === 1
|
||||||
|
? apiResult.data[0]
|
||||||
|
: apiResult.data;
|
||||||
|
|
||||||
|
fromData = extractArrayByPath(rawData, config.data_array_path);
|
||||||
|
logger.info(
|
||||||
|
`데이터 배열 경로 '${config.data_array_path}'에서 ${fromData.length}개 레코드 추출`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
fromData = apiResult.data;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`REST API 데이터 조회 실패: ${apiResult.message}`);
|
throw new Error(`REST API 데이터 조회 실패: ${apiResult.message}`);
|
||||||
}
|
}
|
||||||
|
|
@ -298,6 +364,11 @@ export class BatchSchedulerService {
|
||||||
const mappedData = fromData.map((row) => {
|
const mappedData = fromData.map((row) => {
|
||||||
const mappedRow: any = {};
|
const mappedRow: any = {};
|
||||||
for (const mapping of mappings) {
|
for (const mapping of mappings) {
|
||||||
|
// 고정값 매핑은 이미 분리되어 있으므로 여기서는 처리하지 않음
|
||||||
|
if (mapping.mapping_type === "fixed") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// DB → REST API 배치인지 확인
|
// DB → REST API 배치인지 확인
|
||||||
if (
|
if (
|
||||||
firstMapping.to_connection_type === "restapi" &&
|
firstMapping.to_connection_type === "restapi" &&
|
||||||
|
|
@ -315,6 +386,13 @@ export class BatchSchedulerService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 고정값 매핑 적용 (전역으로 분리된 fixedMappingsGlobal 사용)
|
||||||
|
for (const fixedMapping of fixedMappingsGlobal) {
|
||||||
|
// from_column_name에 고정값이 저장되어 있음
|
||||||
|
mappedRow[fixedMapping.to_column_name] =
|
||||||
|
fixedMapping.from_column_name;
|
||||||
|
}
|
||||||
|
|
||||||
// 멀티테넌시: TO가 DB일 때 company_code 자동 주입
|
// 멀티테넌시: TO가 DB일 때 company_code 자동 주입
|
||||||
// - 배치 설정에 company_code가 있고
|
// - 배치 설정에 company_code가 있고
|
||||||
// - 매핑에서 company_code를 명시적으로 다루지 않은 경우만
|
// - 매핑에서 company_code를 명시적으로 다루지 않은 경우만
|
||||||
|
|
@ -384,12 +462,14 @@ export class BatchSchedulerService {
|
||||||
insertResult = { successCount: 0, failedCount: 0 };
|
insertResult = { successCount: 0, failedCount: 0 };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// DB에 데이터 삽입
|
// DB에 데이터 삽입 (save_mode, conflict_key 지원)
|
||||||
insertResult = await BatchService.insertDataToTable(
|
insertResult = await BatchService.insertDataToTable(
|
||||||
firstMapping.to_table_name,
|
firstMapping.to_table_name,
|
||||||
mappedData,
|
mappedData,
|
||||||
firstMapping.to_connection_type as "internal" | "external",
|
firstMapping.to_connection_type as "internal" | "external",
|
||||||
firstMapping.to_connection_id || undefined
|
firstMapping.to_connection_id || undefined,
|
||||||
|
(config.save_mode as "INSERT" | "UPSERT") || "INSERT",
|
||||||
|
config.conflict_key || undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -176,8 +176,8 @@ export class BatchService {
|
||||||
// 배치 설정 생성
|
// 배치 설정 생성
|
||||||
const batchConfigResult = await client.query(
|
const batchConfigResult = await client.query(
|
||||||
`INSERT INTO batch_configs
|
`INSERT INTO batch_configs
|
||||||
(batch_name, description, cron_schedule, is_active, company_code, created_by, created_date, updated_date)
|
(batch_name, description, cron_schedule, is_active, company_code, save_mode, conflict_key, auth_service_name, data_array_path, created_by, created_date, updated_date)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW())
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
data.batchName,
|
data.batchName,
|
||||||
|
|
@ -185,6 +185,10 @@ export class BatchService {
|
||||||
data.cronSchedule,
|
data.cronSchedule,
|
||||||
data.isActive || "Y",
|
data.isActive || "Y",
|
||||||
data.companyCode,
|
data.companyCode,
|
||||||
|
data.saveMode || "INSERT",
|
||||||
|
data.conflictKey || null,
|
||||||
|
data.authServiceName || null,
|
||||||
|
data.dataArrayPath || null,
|
||||||
userId,
|
userId,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
@ -201,37 +205,38 @@ export class BatchService {
|
||||||
from_column_type, from_api_url, from_api_key, from_api_method, from_api_param_type,
|
from_column_type, from_api_url, from_api_key, from_api_method, from_api_param_type,
|
||||||
from_api_param_name, from_api_param_value, from_api_param_source, from_api_body,
|
from_api_param_name, from_api_param_value, from_api_param_source, from_api_body,
|
||||||
to_connection_type, to_connection_id, to_table_name, to_column_name, to_column_type,
|
to_connection_type, to_connection_id, to_table_name, to_column_name, to_column_type,
|
||||||
to_api_url, to_api_key, to_api_method, to_api_body, mapping_order, created_by, created_date)
|
to_api_url, to_api_key, to_api_method, to_api_body, mapping_order, mapping_type, created_by, created_date)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, NOW())
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, NOW())
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
batchConfig.id,
|
batchConfig.id,
|
||||||
data.companyCode, // 멀티테넌시: 배치 설정과 동일한 company_code 사용
|
data.companyCode, // 멀티테넌시: 배치 설정과 동일한 company_code 사용
|
||||||
mapping.from_connection_type,
|
mapping.from_connection_type,
|
||||||
mapping.from_connection_id,
|
mapping.from_connection_id,
|
||||||
mapping.from_table_name,
|
mapping.from_table_name,
|
||||||
mapping.from_column_name,
|
mapping.from_column_name,
|
||||||
mapping.from_column_type,
|
mapping.from_column_type,
|
||||||
mapping.from_api_url,
|
mapping.from_api_url,
|
||||||
mapping.from_api_key,
|
mapping.from_api_key,
|
||||||
mapping.from_api_method,
|
mapping.from_api_method,
|
||||||
mapping.from_api_param_type,
|
mapping.from_api_param_type,
|
||||||
mapping.from_api_param_name,
|
mapping.from_api_param_name,
|
||||||
mapping.from_api_param_value,
|
mapping.from_api_param_value,
|
||||||
mapping.from_api_param_source,
|
mapping.from_api_param_source,
|
||||||
mapping.from_api_body, // FROM REST API Body
|
mapping.from_api_body, // FROM REST API Body
|
||||||
mapping.to_connection_type,
|
mapping.to_connection_type,
|
||||||
mapping.to_connection_id,
|
mapping.to_connection_id,
|
||||||
mapping.to_table_name,
|
mapping.to_table_name,
|
||||||
mapping.to_column_name,
|
mapping.to_column_name,
|
||||||
mapping.to_column_type,
|
mapping.to_column_type,
|
||||||
mapping.to_api_url,
|
mapping.to_api_url,
|
||||||
mapping.to_api_key,
|
mapping.to_api_key,
|
||||||
mapping.to_api_method,
|
mapping.to_api_method,
|
||||||
mapping.to_api_body,
|
mapping.to_api_body,
|
||||||
mapping.mapping_order || index + 1,
|
mapping.mapping_order || index + 1,
|
||||||
userId,
|
mapping.mapping_type || "direct", // 매핑 타입: direct 또는 fixed
|
||||||
]
|
userId,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
mappings.push(mappingResult.rows[0]);
|
mappings.push(mappingResult.rows[0]);
|
||||||
}
|
}
|
||||||
|
|
@ -311,6 +316,18 @@ export class BatchService {
|
||||||
updateFields.push(`is_active = $${paramIndex++}`);
|
updateFields.push(`is_active = $${paramIndex++}`);
|
||||||
updateValues.push(data.isActive);
|
updateValues.push(data.isActive);
|
||||||
}
|
}
|
||||||
|
if (data.saveMode !== undefined) {
|
||||||
|
updateFields.push(`save_mode = $${paramIndex++}`);
|
||||||
|
updateValues.push(data.saveMode);
|
||||||
|
}
|
||||||
|
if (data.conflictKey !== undefined) {
|
||||||
|
updateFields.push(`conflict_key = $${paramIndex++}`);
|
||||||
|
updateValues.push(data.conflictKey || null);
|
||||||
|
}
|
||||||
|
if (data.authServiceName !== undefined) {
|
||||||
|
updateFields.push(`auth_service_name = $${paramIndex++}`);
|
||||||
|
updateValues.push(data.authServiceName || null);
|
||||||
|
}
|
||||||
|
|
||||||
// 배치 설정 업데이트
|
// 배치 설정 업데이트
|
||||||
const batchConfigResult = await client.query(
|
const batchConfigResult = await client.query(
|
||||||
|
|
@ -339,8 +356,8 @@ export class BatchService {
|
||||||
from_column_type, from_api_url, from_api_key, from_api_method, from_api_param_type,
|
from_column_type, from_api_url, from_api_key, from_api_method, from_api_param_type,
|
||||||
from_api_param_name, from_api_param_value, from_api_param_source, from_api_body,
|
from_api_param_name, from_api_param_value, from_api_param_source, from_api_body,
|
||||||
to_connection_type, to_connection_id, to_table_name, to_column_name, to_column_type,
|
to_connection_type, to_connection_id, to_table_name, to_column_name, to_column_type,
|
||||||
to_api_url, to_api_key, to_api_method, to_api_body, mapping_order, created_by, created_date)
|
to_api_url, to_api_key, to_api_method, to_api_body, mapping_order, mapping_type, created_by, created_date)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, NOW())
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, NOW())
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
id,
|
id,
|
||||||
|
|
@ -368,6 +385,7 @@ export class BatchService {
|
||||||
mapping.to_api_method,
|
mapping.to_api_method,
|
||||||
mapping.to_api_body,
|
mapping.to_api_body,
|
||||||
mapping.mapping_order || index + 1,
|
mapping.mapping_order || index + 1,
|
||||||
|
mapping.mapping_type || "direct", // 매핑 타입: direct 또는 fixed
|
||||||
userId,
|
userId,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
@ -554,9 +572,7 @@ export class BatchService {
|
||||||
try {
|
try {
|
||||||
if (connectionType === "internal") {
|
if (connectionType === "internal") {
|
||||||
// 내부 DB 데이터 조회
|
// 내부 DB 데이터 조회
|
||||||
const data = await query<any>(
|
const data = await query<any>(`SELECT * FROM ${tableName} LIMIT 10`);
|
||||||
`SELECT * FROM ${tableName} LIMIT 10`
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data,
|
data,
|
||||||
|
|
@ -729,19 +745,27 @@ export class BatchService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 테이블에 데이터 삽입 (연결 타입에 따라 내부/외부 DB 구분)
|
* 테이블에 데이터 삽입 (연결 타입에 따라 내부/외부 DB 구분)
|
||||||
|
* @param tableName 테이블명
|
||||||
|
* @param data 삽입할 데이터 배열
|
||||||
|
* @param connectionType 연결 타입 (internal/external)
|
||||||
|
* @param connectionId 외부 연결 ID
|
||||||
|
* @param saveMode 저장 모드 (INSERT/UPSERT)
|
||||||
|
* @param conflictKey UPSERT 시 충돌 기준 컬럼명
|
||||||
*/
|
*/
|
||||||
static async insertDataToTable(
|
static async insertDataToTable(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
data: any[],
|
data: any[],
|
||||||
connectionType: "internal" | "external" = "internal",
|
connectionType: "internal" | "external" = "internal",
|
||||||
connectionId?: number
|
connectionId?: number,
|
||||||
|
saveMode: "INSERT" | "UPSERT" = "INSERT",
|
||||||
|
conflictKey?: string
|
||||||
): Promise<{
|
): Promise<{
|
||||||
successCount: number;
|
successCount: number;
|
||||||
failedCount: number;
|
failedCount: number;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
console.log(
|
console.log(
|
||||||
`[BatchService] 테이블에 데이터 삽입: ${tableName} (${connectionType}${connectionId ? `:${connectionId}` : ""}), ${data.length}개 레코드`
|
`[BatchService] 테이블에 데이터 ${saveMode}: ${tableName} (${connectionType}${connectionId ? `:${connectionId}` : ""}), ${data.length}개 레코드${conflictKey ? `, 충돌키: ${conflictKey}` : ""}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
|
|
@ -753,24 +777,45 @@ export class BatchService {
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let failedCount = 0;
|
let failedCount = 0;
|
||||||
|
|
||||||
// 각 레코드를 개별적으로 삽입 (UPSERT 방식으로 중복 처리)
|
// 각 레코드를 개별적으로 삽입
|
||||||
for (const record of data) {
|
for (const record of data) {
|
||||||
try {
|
try {
|
||||||
const columns = Object.keys(record);
|
const columns = Object.keys(record);
|
||||||
const values = Object.values(record);
|
const values = Object.values(record);
|
||||||
const placeholders = values
|
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
||||||
.map((_, i) => `$${i + 1}`)
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
const queryStr = `INSERT INTO ${tableName} (${columns.join(
|
let queryStr: string;
|
||||||
", "
|
|
||||||
)}) VALUES (${placeholders})`;
|
if (saveMode === "UPSERT" && conflictKey) {
|
||||||
|
// UPSERT 모드: ON CONFLICT DO UPDATE
|
||||||
|
// 충돌 키를 제외한 컬럼들만 UPDATE
|
||||||
|
const updateColumns = columns.filter(
|
||||||
|
(col) => col !== conflictKey
|
||||||
|
);
|
||||||
|
const updateSet = updateColumns
|
||||||
|
.map((col) => `${col} = EXCLUDED.${col}`)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
// updated_date 컬럼이 있으면 현재 시간으로 업데이트
|
||||||
|
const hasUpdatedDate = columns.includes("updated_date");
|
||||||
|
const finalUpdateSet = hasUpdatedDate
|
||||||
|
? `${updateSet}, updated_date = NOW()`
|
||||||
|
: updateSet;
|
||||||
|
|
||||||
|
queryStr = `INSERT INTO ${tableName} (${columns.join(", ")})
|
||||||
|
VALUES (${placeholders})
|
||||||
|
ON CONFLICT (${conflictKey})
|
||||||
|
DO UPDATE SET ${finalUpdateSet}`;
|
||||||
|
} else {
|
||||||
|
// INSERT 모드: 기존 방식
|
||||||
|
queryStr = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
|
||||||
|
}
|
||||||
|
|
||||||
await query(queryStr, values);
|
await query(queryStr, values);
|
||||||
successCount++;
|
successCount++;
|
||||||
} catch (insertError) {
|
} catch (insertError) {
|
||||||
console.error(
|
console.error(
|
||||||
`내부 DB 데이터 삽입 실패 (${tableName}):`,
|
`내부 DB 데이터 ${saveMode} 실패 (${tableName}):`,
|
||||||
insertError
|
insertError
|
||||||
);
|
);
|
||||||
failedCount++;
|
failedCount++;
|
||||||
|
|
@ -779,7 +824,13 @@ export class BatchService {
|
||||||
|
|
||||||
return { successCount, failedCount };
|
return { successCount, failedCount };
|
||||||
} else if (connectionType === "external" && connectionId) {
|
} else if (connectionType === "external" && connectionId) {
|
||||||
// 외부 DB에 데이터 삽입
|
// 외부 DB에 데이터 삽입 (UPSERT는 내부 DB만 지원)
|
||||||
|
if (saveMode === "UPSERT") {
|
||||||
|
console.warn(
|
||||||
|
`[BatchService] 외부 DB는 UPSERT를 지원하지 않습니다. INSERT로 실행합니다.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await BatchExternalDbService.insertDataToTable(
|
const result = await BatchExternalDbService.insertDataToTable(
|
||||||
connectionId,
|
connectionId,
|
||||||
tableName,
|
tableName,
|
||||||
|
|
@ -799,7 +850,7 @@ export class BatchService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`데이터 삽입 오류 (${tableName}):`, error);
|
console.error(`데이터 ${saveMode} 오류 (${tableName}):`, error);
|
||||||
return { successCount: 0, failedCount: data ? data.length : 0 };
|
return { successCount: 0, failedCount: data ? data.length : 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export interface TableInfo {
|
||||||
|
|
||||||
// 연결 정보 타입
|
// 연결 정보 타입
|
||||||
export interface ConnectionInfo {
|
export interface ConnectionInfo {
|
||||||
type: 'internal' | 'external';
|
type: "internal" | "external";
|
||||||
id?: number;
|
id?: number;
|
||||||
name: string;
|
name: string;
|
||||||
db_type?: string;
|
db_type?: string;
|
||||||
|
|
@ -52,27 +52,27 @@ export interface BatchMapping {
|
||||||
id?: number;
|
id?: number;
|
||||||
batch_config_id?: number;
|
batch_config_id?: number;
|
||||||
company_code?: string;
|
company_code?: string;
|
||||||
from_connection_type: 'internal' | 'external' | 'restapi';
|
from_connection_type: "internal" | "external" | "restapi";
|
||||||
from_connection_id?: number;
|
from_connection_id?: number;
|
||||||
from_table_name: string;
|
from_table_name: string;
|
||||||
from_column_name: string;
|
from_column_name: string;
|
||||||
from_column_type?: string;
|
from_column_type?: string;
|
||||||
from_api_url?: string;
|
from_api_url?: string;
|
||||||
from_api_key?: string;
|
from_api_key?: string;
|
||||||
from_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
from_api_method?: "GET" | "POST" | "PUT" | "DELETE";
|
||||||
from_api_param_type?: 'url' | 'query';
|
from_api_param_type?: "url" | "query";
|
||||||
from_api_param_name?: string;
|
from_api_param_name?: string;
|
||||||
from_api_param_value?: string;
|
from_api_param_value?: string;
|
||||||
from_api_param_source?: 'static' | 'dynamic';
|
from_api_param_source?: "static" | "dynamic";
|
||||||
from_api_body?: string;
|
from_api_body?: string;
|
||||||
to_connection_type: 'internal' | 'external' | 'restapi';
|
to_connection_type: "internal" | "external" | "restapi";
|
||||||
to_connection_id?: number;
|
to_connection_id?: number;
|
||||||
to_table_name: string;
|
to_table_name: string;
|
||||||
to_column_name: string;
|
to_column_name: string;
|
||||||
to_column_type?: string;
|
to_column_type?: string;
|
||||||
to_api_url?: string;
|
to_api_url?: string;
|
||||||
to_api_key?: string;
|
to_api_key?: string;
|
||||||
to_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
to_api_method?: "GET" | "POST" | "PUT" | "DELETE";
|
||||||
to_api_body?: string;
|
to_api_body?: string;
|
||||||
mapping_order?: number;
|
mapping_order?: number;
|
||||||
created_by?: string;
|
created_by?: string;
|
||||||
|
|
@ -85,8 +85,12 @@ export interface BatchConfig {
|
||||||
batch_name: string;
|
batch_name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
cron_schedule: string;
|
cron_schedule: string;
|
||||||
is_active: 'Y' | 'N';
|
is_active: "Y" | "N";
|
||||||
company_code?: string;
|
company_code?: string;
|
||||||
|
save_mode?: "INSERT" | "UPSERT"; // 저장 모드 (기본: INSERT)
|
||||||
|
conflict_key?: string; // UPSERT 시 충돌 기준 컬럼명
|
||||||
|
auth_service_name?: string; // REST API 인증에 사용할 토큰 서비스명
|
||||||
|
data_array_path?: string; // REST API 응답에서 데이터 배열 경로 (예: response, data.items)
|
||||||
created_by?: string;
|
created_by?: string;
|
||||||
created_date?: Date;
|
created_date?: Date;
|
||||||
updated_by?: string;
|
updated_by?: string;
|
||||||
|
|
@ -95,7 +99,7 @@ export interface BatchConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BatchConnectionInfo {
|
export interface BatchConnectionInfo {
|
||||||
type: 'internal' | 'external';
|
type: "internal" | "external";
|
||||||
id?: number;
|
id?: number;
|
||||||
name: string;
|
name: string;
|
||||||
db_type?: string;
|
db_type?: string;
|
||||||
|
|
@ -109,38 +113,43 @@ export interface BatchColumnInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BatchMappingRequest {
|
export interface BatchMappingRequest {
|
||||||
from_connection_type: 'internal' | 'external' | 'restapi';
|
from_connection_type: "internal" | "external" | "restapi" | "fixed";
|
||||||
from_connection_id?: number;
|
from_connection_id?: number;
|
||||||
from_table_name: string;
|
from_table_name: string;
|
||||||
from_column_name: string;
|
from_column_name: string;
|
||||||
from_column_type?: string;
|
from_column_type?: string;
|
||||||
from_api_url?: string;
|
from_api_url?: string;
|
||||||
from_api_key?: string;
|
from_api_key?: string;
|
||||||
from_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
from_api_method?: "GET" | "POST" | "PUT" | "DELETE";
|
||||||
from_api_param_type?: 'url' | 'query'; // API 파라미터 타입
|
from_api_param_type?: "url" | "query"; // API 파라미터 타입
|
||||||
from_api_param_name?: string; // API 파라미터명
|
from_api_param_name?: string; // API 파라미터명
|
||||||
from_api_param_value?: string; // API 파라미터 값 또는 템플릿
|
from_api_param_value?: string; // API 파라미터 값 또는 템플릿
|
||||||
from_api_param_source?: 'static' | 'dynamic'; // 파라미터 소스 타입
|
from_api_param_source?: "static" | "dynamic"; // 파라미터 소스 타입
|
||||||
// REST API Body 추가 (FROM - REST API에서 POST 요청 시 필요)
|
// REST API Body 추가 (FROM - REST API에서 POST 요청 시 필요)
|
||||||
from_api_body?: string;
|
from_api_body?: string;
|
||||||
to_connection_type: 'internal' | 'external' | 'restapi';
|
to_connection_type: "internal" | "external" | "restapi";
|
||||||
to_connection_id?: number;
|
to_connection_id?: number;
|
||||||
to_table_name: string;
|
to_table_name: string;
|
||||||
to_column_name: string;
|
to_column_name: string;
|
||||||
to_column_type?: string;
|
to_column_type?: string;
|
||||||
to_api_url?: string;
|
to_api_url?: string;
|
||||||
to_api_key?: string;
|
to_api_key?: string;
|
||||||
to_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
to_api_method?: "GET" | "POST" | "PUT" | "DELETE";
|
||||||
to_api_body?: string; // Request Body 템플릿 (DB → REST API 배치용)
|
to_api_body?: string; // Request Body 템플릿 (DB → REST API 배치용)
|
||||||
mapping_order?: number;
|
mapping_order?: number;
|
||||||
|
mapping_type?: "direct" | "fixed"; // 매핑 타입: direct (API 필드) 또는 fixed (고정값)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateBatchConfigRequest {
|
export interface CreateBatchConfigRequest {
|
||||||
batchName: string;
|
batchName: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
cronSchedule: string;
|
cronSchedule: string;
|
||||||
isActive: 'Y' | 'N';
|
isActive: "Y" | "N";
|
||||||
companyCode: string;
|
companyCode: string;
|
||||||
|
saveMode?: "INSERT" | "UPSERT";
|
||||||
|
conflictKey?: string;
|
||||||
|
authServiceName?: string;
|
||||||
|
dataArrayPath?: string; // REST API 응답에서 데이터 배열 경로
|
||||||
mappings: BatchMappingRequest[];
|
mappings: BatchMappingRequest[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,7 +157,11 @@ export interface UpdateBatchConfigRequest {
|
||||||
batchName?: string;
|
batchName?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
cronSchedule?: string;
|
cronSchedule?: string;
|
||||||
isActive?: 'Y' | 'N';
|
isActive?: "Y" | "N";
|
||||||
|
saveMode?: "INSERT" | "UPSERT";
|
||||||
|
conflictKey?: string;
|
||||||
|
authServiceName?: string;
|
||||||
|
dataArrayPath?: string; // REST API 응답에서 데이터 배열 경로
|
||||||
mappings?: BatchMappingRequest[];
|
mappings?: BatchMappingRequest[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -58,6 +58,10 @@ export default function BatchEditPage() {
|
||||||
const [cronSchedule, setCronSchedule] = useState("0 12 * * *");
|
const [cronSchedule, setCronSchedule] = useState("0 12 * * *");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [isActive, setIsActive] = useState("Y");
|
const [isActive, setIsActive] = useState("Y");
|
||||||
|
const [saveMode, setSaveMode] = useState<"INSERT" | "UPSERT">("INSERT");
|
||||||
|
const [conflictKey, setConflictKey] = useState("");
|
||||||
|
const [authServiceName, setAuthServiceName] = useState("");
|
||||||
|
const [authServiceNames, setAuthServiceNames] = useState<string[]>([]);
|
||||||
|
|
||||||
// 연결 정보
|
// 연결 정보
|
||||||
const [connections, setConnections] = useState<ConnectionInfo[]>([]);
|
const [connections, setConnections] = useState<ConnectionInfo[]>([]);
|
||||||
|
|
@ -87,9 +91,20 @@ export default function BatchEditPage() {
|
||||||
if (batchId) {
|
if (batchId) {
|
||||||
loadBatchConfig();
|
loadBatchConfig();
|
||||||
loadConnections();
|
loadConnections();
|
||||||
|
loadAuthServiceNames();
|
||||||
}
|
}
|
||||||
}, [batchId]);
|
}, [batchId]);
|
||||||
|
|
||||||
|
// 인증 서비스명 목록 로드
|
||||||
|
const loadAuthServiceNames = async () => {
|
||||||
|
try {
|
||||||
|
const names = await BatchAPI.getAuthServiceNames();
|
||||||
|
setAuthServiceNames(names);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("인증 서비스 목록 로드 실패:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 연결 정보가 로드된 후 배치 설정의 연결 정보 설정
|
// 연결 정보가 로드된 후 배치 설정의 연결 정보 설정
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (batchConfig && connections.length > 0 && batchConfig.batch_mappings && batchConfig.batch_mappings.length > 0) {
|
if (batchConfig && connections.length > 0 && batchConfig.batch_mappings && batchConfig.batch_mappings.length > 0) {
|
||||||
|
|
@ -184,6 +199,9 @@ export default function BatchEditPage() {
|
||||||
setCronSchedule(config.cron_schedule);
|
setCronSchedule(config.cron_schedule);
|
||||||
setDescription(config.description || "");
|
setDescription(config.description || "");
|
||||||
setIsActive(config.is_active || "Y");
|
setIsActive(config.is_active || "Y");
|
||||||
|
setSaveMode((config as any).save_mode || "INSERT");
|
||||||
|
setConflictKey((config as any).conflict_key || "");
|
||||||
|
setAuthServiceName((config as any).auth_service_name || "");
|
||||||
|
|
||||||
if (config.batch_mappings && config.batch_mappings.length > 0) {
|
if (config.batch_mappings && config.batch_mappings.length > 0) {
|
||||||
console.log("📊 매핑 정보:", config.batch_mappings);
|
console.log("📊 매핑 정보:", config.batch_mappings);
|
||||||
|
|
@ -460,7 +478,10 @@ export default function BatchEditPage() {
|
||||||
description,
|
description,
|
||||||
cronSchedule,
|
cronSchedule,
|
||||||
isActive,
|
isActive,
|
||||||
mappings
|
mappings,
|
||||||
|
saveMode,
|
||||||
|
conflictKey: saveMode === "UPSERT" ? conflictKey : undefined,
|
||||||
|
authServiceName: authServiceName || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success("배치 설정이 성공적으로 수정되었습니다.");
|
toast.success("배치 설정이 성공적으로 수정되었습니다.");
|
||||||
|
|
@ -558,6 +579,68 @@ export default function BatchEditPage() {
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="isActive">활성화</Label>
|
<Label htmlFor="isActive">활성화</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 저장 모드 설정 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="saveMode">저장 모드</Label>
|
||||||
|
<Select
|
||||||
|
value={saveMode}
|
||||||
|
onValueChange={(value: "INSERT" | "UPSERT") => setSaveMode(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="저장 모드 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="INSERT">INSERT (항상 새로 추가)</SelectItem>
|
||||||
|
<SelectItem value="UPSERT">UPSERT (있으면 업데이트, 없으면 추가)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
UPSERT: 동일한 키가 있으면 업데이트, 없으면 새로 추가합니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{saveMode === "UPSERT" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="conflictKey">충돌 기준 컬럼 *</Label>
|
||||||
|
<Input
|
||||||
|
id="conflictKey"
|
||||||
|
value={conflictKey}
|
||||||
|
onChange={(e) => setConflictKey(e.target.value)}
|
||||||
|
placeholder="예: device_serial_number"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
UPSERT 시 중복 여부를 판단할 컬럼명을 입력하세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 인증 토큰 서비스 설정 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="authServiceName">인증 토큰 서비스</Label>
|
||||||
|
<Select
|
||||||
|
value={authServiceName || "none"}
|
||||||
|
onValueChange={(value) => setAuthServiceName(value === "none" ? "" : value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="인증 토큰 서비스 선택 (선택사항)" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">사용 안 함</SelectItem>
|
||||||
|
{authServiceNames.map((name) => (
|
||||||
|
<SelectItem key={name} value={name}>
|
||||||
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
REST API 호출 시 auth_tokens 테이블에서 토큰을 가져와 Authorization 헤더에 설정합니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ export interface BatchConfig {
|
||||||
cron_schedule: string;
|
cron_schedule: string;
|
||||||
is_active?: string;
|
is_active?: string;
|
||||||
company_code?: string;
|
company_code?: string;
|
||||||
|
save_mode?: 'INSERT' | 'UPSERT'; // 저장 모드 (기본: INSERT)
|
||||||
|
conflict_key?: string; // UPSERT 시 충돌 기준 컬럼명
|
||||||
|
auth_service_name?: string; // REST API 인증에 사용할 토큰 서비스명
|
||||||
created_date?: Date;
|
created_date?: Date;
|
||||||
created_by?: string;
|
created_by?: string;
|
||||||
updated_date?: Date;
|
updated_date?: Date;
|
||||||
|
|
@ -386,6 +389,26 @@ export class BatchAPI {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* auth_tokens 테이블의 서비스명 목록 조회
|
||||||
|
*/
|
||||||
|
static async getAuthServiceNames(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<{
|
||||||
|
success: boolean;
|
||||||
|
data: string[];
|
||||||
|
}>(`/batch-management/auth-services`);
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data || [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("인증 서비스 목록 조회 오류:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BatchJob export 추가 (이미 위에서 interface로 정의됨)
|
// BatchJob export 추가 (이미 위에서 interface로 정의됨)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { apiClient } from "./client";
|
||||||
|
|
||||||
// 배치관리 전용 타입 정의
|
// 배치관리 전용 타입 정의
|
||||||
export interface BatchConnectionInfo {
|
export interface BatchConnectionInfo {
|
||||||
type: 'internal' | 'external';
|
type: "internal" | "external";
|
||||||
id?: number;
|
id?: number;
|
||||||
name: string;
|
name: string;
|
||||||
db_type?: string;
|
db_type?: string;
|
||||||
|
|
@ -39,9 +39,7 @@ class BatchManagementAPIClass {
|
||||||
*/
|
*/
|
||||||
static async getAvailableConnections(): Promise<BatchConnectionInfo[]> {
|
static async getAvailableConnections(): Promise<BatchConnectionInfo[]> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get<BatchApiResponse<BatchConnectionInfo[]>>(
|
const response = await apiClient.get<BatchApiResponse<BatchConnectionInfo[]>>(`${this.BASE_PATH}/connections`);
|
||||||
`${this.BASE_PATH}/connections`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.data.success) {
|
if (!response.data.success) {
|
||||||
throw new Error(response.data.message || "커넥션 목록 조회에 실패했습니다.");
|
throw new Error(response.data.message || "커넥션 목록 조회에 실패했습니다.");
|
||||||
|
|
@ -58,15 +56,15 @@ class BatchManagementAPIClass {
|
||||||
* 특정 커넥션의 테이블 목록 조회
|
* 특정 커넥션의 테이블 목록 조회
|
||||||
*/
|
*/
|
||||||
static async getTablesFromConnection(
|
static async getTablesFromConnection(
|
||||||
connectionType: 'internal' | 'external',
|
connectionType: "internal" | "external",
|
||||||
connectionId?: number
|
connectionId?: number,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
let url = `${this.BASE_PATH}/connections/${connectionType}`;
|
let url = `${this.BASE_PATH}/connections/${connectionType}`;
|
||||||
if (connectionType === 'external' && connectionId) {
|
if (connectionType === "external" && connectionId) {
|
||||||
url += `/${connectionId}`;
|
url += `/${connectionId}`;
|
||||||
}
|
}
|
||||||
url += '/tables';
|
url += "/tables";
|
||||||
|
|
||||||
const response = await apiClient.get<BatchApiResponse<string[]>>(url);
|
const response = await apiClient.get<BatchApiResponse<string[]>>(url);
|
||||||
|
|
||||||
|
|
@ -85,13 +83,13 @@ class BatchManagementAPIClass {
|
||||||
* 특정 테이블의 컬럼 정보 조회
|
* 특정 테이블의 컬럼 정보 조회
|
||||||
*/
|
*/
|
||||||
static async getTableColumns(
|
static async getTableColumns(
|
||||||
connectionType: 'internal' | 'external',
|
connectionType: "internal" | "external",
|
||||||
tableName: string,
|
tableName: string,
|
||||||
connectionId?: number
|
connectionId?: number,
|
||||||
): Promise<BatchColumnInfo[]> {
|
): Promise<BatchColumnInfo[]> {
|
||||||
try {
|
try {
|
||||||
let url = `${this.BASE_PATH}/connections/${connectionType}`;
|
let url = `${this.BASE_PATH}/connections/${connectionType}`;
|
||||||
if (connectionType === 'external' && connectionId) {
|
if (connectionType === "external" && connectionId) {
|
||||||
url += `/${connectionId}`;
|
url += `/${connectionId}`;
|
||||||
}
|
}
|
||||||
url += `/tables/${encodeURIComponent(tableName)}/columns`;
|
url += `/tables/${encodeURIComponent(tableName)}/columns`;
|
||||||
|
|
@ -120,14 +118,16 @@ class BatchManagementAPIClass {
|
||||||
apiUrl: string,
|
apiUrl: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
|
||||||
paramInfo?: {
|
paramInfo?: {
|
||||||
paramType: 'url' | 'query';
|
paramType: "url" | "query";
|
||||||
paramName: string;
|
paramName: string;
|
||||||
paramValue: string;
|
paramValue: string;
|
||||||
paramSource: 'static' | 'dynamic';
|
paramSource: "static" | "dynamic";
|
||||||
},
|
},
|
||||||
requestBody?: string
|
requestBody?: string,
|
||||||
|
authServiceName?: string, // DB에서 토큰 가져올 서비스명
|
||||||
|
dataArrayPath?: string, // 데이터 배열 경로 (예: response, data.items)
|
||||||
): Promise<{
|
): Promise<{
|
||||||
fields: string[];
|
fields: string[];
|
||||||
samples: any[];
|
samples: any[];
|
||||||
|
|
@ -139,7 +139,7 @@ class BatchManagementAPIClass {
|
||||||
apiKey,
|
apiKey,
|
||||||
endpoint,
|
endpoint,
|
||||||
method,
|
method,
|
||||||
requestBody
|
requestBody,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 파라미터 정보가 있으면 추가
|
// 파라미터 정보가 있으면 추가
|
||||||
|
|
@ -150,11 +150,23 @@ class BatchManagementAPIClass {
|
||||||
requestData.paramSource = paramInfo.paramSource;
|
requestData.paramSource = paramInfo.paramSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await apiClient.post<BatchApiResponse<{
|
// DB에서 토큰 가져올 서비스명 추가
|
||||||
fields: string[];
|
if (authServiceName) {
|
||||||
samples: any[];
|
requestData.authServiceName = authServiceName;
|
||||||
totalCount: number;
|
}
|
||||||
}>>(`${this.BASE_PATH}/rest-api/preview`, requestData);
|
|
||||||
|
// 데이터 배열 경로 추가
|
||||||
|
if (dataArrayPath) {
|
||||||
|
requestData.dataArrayPath = dataArrayPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await apiClient.post<
|
||||||
|
BatchApiResponse<{
|
||||||
|
fields: string[];
|
||||||
|
samples: any[];
|
||||||
|
totalCount: number;
|
||||||
|
}>
|
||||||
|
>(`${this.BASE_PATH}/rest-api/preview`, requestData);
|
||||||
|
|
||||||
if (!response.data.success) {
|
if (!response.data.success) {
|
||||||
throw new Error(response.data.message || "REST API 미리보기에 실패했습니다.");
|
throw new Error(response.data.message || "REST API 미리보기에 실패했습니다.");
|
||||||
|
|
@ -167,6 +179,24 @@ class BatchManagementAPIClass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증 서비스명 목록 조회
|
||||||
|
*/
|
||||||
|
static async getAuthServiceNames(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<BatchApiResponse<string[]>>(`${this.BASE_PATH}/auth-services`);
|
||||||
|
|
||||||
|
if (!response.data.success) {
|
||||||
|
throw new Error(response.data.message || "인증 서비스 목록 조회에 실패했습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("인증 서비스 목록 조회 오류:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* REST API 배치 저장
|
* REST API 배치 저장
|
||||||
*/
|
*/
|
||||||
|
|
@ -176,15 +206,17 @@ class BatchManagementAPIClass {
|
||||||
cronSchedule: string;
|
cronSchedule: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
apiMappings: any[];
|
apiMappings: any[];
|
||||||
}): Promise<{ success: boolean; message: string; data?: any; }> {
|
authServiceName?: string;
|
||||||
|
dataArrayPath?: string; // REST API 응답에서 데이터 배열 경로
|
||||||
|
saveMode?: "INSERT" | "UPSERT";
|
||||||
|
conflictKey?: string;
|
||||||
|
}): Promise<{ success: boolean; message: string; data?: any }> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post<BatchApiResponse<any>>(
|
const response = await apiClient.post<BatchApiResponse<any>>(`${this.BASE_PATH}/rest-api/save`, batchData);
|
||||||
`${this.BASE_PATH}/rest-api/save`, batchData
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
success: response.data.success,
|
success: response.data.success,
|
||||||
message: response.data.message || "",
|
message: response.data.message || "",
|
||||||
data: response.data.data
|
data: response.data.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("REST API 배치 저장 오류:", error);
|
console.error("REST API 배치 저장 오류:", error);
|
||||||
|
|
@ -193,4 +225,4 @@ class BatchManagementAPIClass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BatchManagementAPI = BatchManagementAPIClass;
|
export const BatchManagementAPI = BatchManagementAPIClass;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue