배치 UPSERT 기능 및 고정값 매핑 버그 수정

This commit is contained in:
dohyeons
2025-12-04 17:26:29 +09:00
parent 7a2f80b646
commit ef3b85f343
9 changed files with 1176 additions and 576 deletions

View File

@@ -1,7 +1,7 @@
// 배치관리 전용 컨트롤러 (기존 소스와 완전 분리)
// 작성일: 2024-12-24
import { Response } from "express";
import { Request, Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import {
BatchManagementService,
@@ -13,6 +13,7 @@ import { BatchService } from "../services/batchService";
import { BatchSchedulerService } from "../services/batchSchedulerService";
import { BatchExternalDbService } from "../services/batchExternalDbService";
import { CreateBatchConfigRequest, BatchConfig } from "../types/batchTypes";
import { query } from "../database/db";
export class BatchManagementController {
/**
@@ -422,6 +423,8 @@ export class BatchManagementController {
paramValue,
paramSource,
requestBody,
authServiceName, // DB에서 토큰 가져올 서비스명
dataArrayPath, // 데이터 배열 경로 (예: response, data.items)
} = req.body;
// apiUrl, endpoint는 항상 필수
@@ -432,15 +435,36 @@ export class BatchManagementController {
});
}
// GET 요청일 때만 API Key 필수 (POST/PUT/DELETE는 선택)
if ((!method || method === "GET") && !apiKey) {
// 토큰 결정: authServiceName이 있으면 DB에서 조회, 없으면 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({
success: false,
message: "GET 메서드에서는 API Key가 필요합니다.",
message: "인증 토큰이 필요합니다. 직접 입력하거나 DB에서 선택하세요.",
});
}
console.log("🔍 REST API 미리보기 요청:", {
console.log("REST API 미리보기 요청:", {
apiUrl,
endpoint,
method,
@@ -449,6 +473,8 @@ export class BatchManagementController {
paramValue,
paramSource,
requestBody: requestBody ? "Included" : "None",
authServiceName: authServiceName || "직접 입력",
dataArrayPath: dataArrayPath || "전체 응답",
});
// RestApiConnector 사용하여 데이터 조회
@@ -456,7 +482,7 @@ export class BatchManagementController {
const connector = new RestApiConnector({
baseUrl: apiUrl,
apiKey: apiKey || "",
apiKey: finalApiKey,
timeout: 30000,
});
@@ -511,8 +537,50 @@ export class BatchManagementController {
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) {
// 첫 번째 객체에서 필드명 추출
@@ -524,9 +592,9 @@ export class BatchManagementController {
data: {
fields: fields,
samples: data,
totalCount: result.rowCount || data.length,
totalCount: extractedData.length,
},
message: `${fields.length}개 필드, ${result.rowCount || data.length}개 레코드를 조회했습니다.`,
message: `${fields.length}개 필드, ${extractedData.length}개 레코드를 조회했습니다.`,
});
} else {
return res.json({
@@ -554,8 +622,17 @@ export class BatchManagementController {
*/
static async saveRestApiBatch(req: AuthenticatedRequest, res: Response) {
try {
const { batchName, batchType, cronSchedule, description, apiMappings } =
req.body;
const {
batchName,
batchType,
cronSchedule,
description,
apiMappings,
authServiceName,
dataArrayPath,
saveMode,
conflictKey,
} = req.body;
if (
!batchName ||
@@ -576,6 +653,10 @@ export class BatchManagementController {
cronSchedule,
description,
apiMappings,
authServiceName,
dataArrayPath,
saveMode,
conflictKey,
});
// 🔐 멀티테넌시: 현재 사용자 회사 코드 사용 (프론트에서 받지 않음)
@@ -589,6 +670,10 @@ export class BatchManagementController {
cronSchedule: cronSchedule,
isActive: "Y",
companyCode,
authServiceName: authServiceName || undefined,
dataArrayPath: dataArrayPath || undefined,
saveMode: saveMode || "INSERT",
conflictKey: conflictKey || undefined,
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: "인증 서비스 목록 조회 중 오류가 발생했습니다.",
});
}
}
}