772 lines
24 KiB
TypeScript
772 lines
24 KiB
TypeScript
// 배치관리 전용 컨트롤러 (기존 소스와 완전 분리)
|
|
// 작성일: 2024-12-24
|
|
|
|
import { Request, Response } from "express";
|
|
import { AuthenticatedRequest } from "../types/auth";
|
|
import {
|
|
BatchManagementService,
|
|
BatchConnectionInfo,
|
|
BatchTableInfo,
|
|
BatchColumnInfo,
|
|
} from "../services/batchManagementService";
|
|
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 {
|
|
/**
|
|
* 사용 가능한 커넥션 목록 조회 (회사별)
|
|
*/
|
|
static async getAvailableConnections(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
) {
|
|
try {
|
|
const userCompanyCode = req.user?.companyCode;
|
|
const result =
|
|
await BatchManagementService.getAvailableConnections(userCompanyCode);
|
|
if (result.success) {
|
|
res.json(result);
|
|
} else {
|
|
res.status(500).json(result);
|
|
}
|
|
} catch (error) {
|
|
console.error("커넥션 목록 조회 오류:", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "커넥션 목록 조회 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 특정 커넥션의 테이블 목록 조회 (회사별)
|
|
*/
|
|
static async getTablesFromConnection(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
) {
|
|
try {
|
|
const { type, id } = req.params;
|
|
const userCompanyCode = req.user?.companyCode;
|
|
|
|
if (type !== "internal" && type !== "external") {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)",
|
|
});
|
|
}
|
|
|
|
const connectionId = type === "external" ? Number(id) : undefined;
|
|
const result = await BatchManagementService.getTablesFromConnection(
|
|
type,
|
|
connectionId,
|
|
userCompanyCode
|
|
);
|
|
|
|
if (result.success) {
|
|
return res.json(result);
|
|
} else {
|
|
return res.status(500).json(result);
|
|
}
|
|
} catch (error) {
|
|
console.error("테이블 목록 조회 오류:", error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: "테이블 목록 조회 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 특정 테이블의 컬럼 정보 조회 (회사별)
|
|
*/
|
|
static async getTableColumns(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const { type, id, tableName } = req.params;
|
|
const userCompanyCode = req.user?.companyCode;
|
|
|
|
if (type !== "internal" && type !== "external") {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "올바른 연결 타입을 지정해주세요. (internal 또는 external)",
|
|
});
|
|
}
|
|
|
|
const connectionId = type === "external" ? Number(id) : undefined;
|
|
const result = await BatchManagementService.getTableColumns(
|
|
type,
|
|
connectionId,
|
|
tableName,
|
|
userCompanyCode
|
|
);
|
|
|
|
if (result.success) {
|
|
return res.json(result);
|
|
} else {
|
|
return res.status(500).json(result);
|
|
}
|
|
} catch (error) {
|
|
console.error("컬럼 정보 조회 오류:", error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 배치 설정 생성
|
|
* POST /api/batch-management/batch-configs
|
|
*/
|
|
static async createBatchConfig(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const { batchName, description, cronSchedule, mappings, isActive } =
|
|
req.body;
|
|
|
|
if (
|
|
!batchName ||
|
|
!cronSchedule ||
|
|
!mappings ||
|
|
!Array.isArray(mappings)
|
|
) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message:
|
|
"필수 필드가 누락되었습니다. (batchName, cronSchedule, mappings)",
|
|
});
|
|
}
|
|
|
|
const batchConfig = await BatchService.createBatchConfig({
|
|
batchName,
|
|
description,
|
|
cronSchedule,
|
|
mappings,
|
|
isActive: isActive !== undefined ? isActive : true,
|
|
} as CreateBatchConfigRequest);
|
|
|
|
return res.status(201).json({
|
|
success: true,
|
|
data: batchConfig,
|
|
message: "배치 설정이 성공적으로 생성되었습니다.",
|
|
});
|
|
} catch (error) {
|
|
console.error("배치 설정 생성 오류:", error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: "배치 설정 생성에 실패했습니다.",
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 특정 배치 설정 조회
|
|
* GET /api/batch-management/batch-configs/:id
|
|
*/
|
|
static async getBatchConfigById(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const { id } = req.params;
|
|
console.log("🔍 배치 설정 조회 요청:", id);
|
|
|
|
const result = await BatchService.getBatchConfigById(Number(id));
|
|
|
|
if (!result.success) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: result.message || "배치 설정을 찾을 수 없습니다.",
|
|
});
|
|
}
|
|
|
|
console.log("📋 조회된 배치 설정:", result.data);
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: result.data,
|
|
});
|
|
} catch (error) {
|
|
console.error("❌ 배치 설정 조회 오류:", error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: "배치 설정 조회에 실패했습니다.",
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 배치 설정 목록 조회
|
|
* GET /api/batch-management/batch-configs
|
|
*/
|
|
static async getBatchConfigs(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const { page = 1, limit = 10, search, isActive } = req.query;
|
|
|
|
const filter = {
|
|
page: Number(page),
|
|
limit: Number(limit),
|
|
search: search as string,
|
|
is_active: isActive as string,
|
|
};
|
|
|
|
const result = await BatchService.getBatchConfigs(filter);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.data,
|
|
pagination: result.pagination,
|
|
});
|
|
} catch (error) {
|
|
console.error("배치 설정 목록 조회 오류:", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "배치 설정 목록 조회에 실패했습니다.",
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 배치 수동 실행
|
|
* POST /api/batch-management/batch-configs/:id/execute
|
|
*/
|
|
static async executeBatchConfig(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
if (!id || isNaN(Number(id))) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "올바른 배치 설정 ID를 제공해주세요.",
|
|
});
|
|
}
|
|
|
|
// 배치 설정 조회
|
|
const batchConfigResult = await BatchService.getBatchConfigById(
|
|
Number(id)
|
|
);
|
|
if (!batchConfigResult.success || !batchConfigResult.data) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
message: "배치 설정을 찾을 수 없습니다.",
|
|
});
|
|
}
|
|
|
|
const batchConfig = batchConfigResult.data as BatchConfig;
|
|
const startTime = new Date();
|
|
|
|
console.log(`배치 수동 실행 시작: ${batchConfig.batch_name} (ID: ${id})`);
|
|
|
|
let executionLog: any = null;
|
|
|
|
try {
|
|
// 실행 로그 생성
|
|
const { BatchExecutionLogService } = await import(
|
|
"../services/batchExecutionLogService"
|
|
);
|
|
const logResult = await BatchExecutionLogService.createExecutionLog({
|
|
batch_config_id: Number(id),
|
|
company_code: batchConfig.company_code,
|
|
execution_status: "RUNNING",
|
|
start_time: startTime,
|
|
total_records: 0,
|
|
success_records: 0,
|
|
failed_records: 0,
|
|
});
|
|
|
|
if (!logResult.success || !logResult.data) {
|
|
throw new Error(
|
|
logResult.message || "배치 실행 로그를 생성할 수 없습니다."
|
|
);
|
|
}
|
|
|
|
executionLog = logResult.data;
|
|
|
|
// BatchSchedulerService의 executeBatchConfig 메서드 사용 (중복 로직 제거)
|
|
const { BatchSchedulerService } = await import(
|
|
"../services/batchSchedulerService"
|
|
);
|
|
const result =
|
|
await BatchSchedulerService.executeBatchConfig(batchConfig);
|
|
|
|
// result가 undefined인 경우 처리
|
|
if (!result) {
|
|
throw new Error("배치 실행 결과를 받을 수 없습니다.");
|
|
}
|
|
|
|
const endTime = new Date();
|
|
const duration = endTime.getTime() - startTime.getTime();
|
|
|
|
// 실행 로그 업데이트 (성공)
|
|
await BatchExecutionLogService.updateExecutionLog(executionLog.id, {
|
|
execution_status: "SUCCESS",
|
|
end_time: endTime,
|
|
duration_ms: duration,
|
|
total_records: result.totalRecords,
|
|
success_records: result.successRecords,
|
|
failed_records: result.failedRecords,
|
|
});
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: {
|
|
batchName: batchConfig.batch_name,
|
|
totalRecords: result.totalRecords,
|
|
successRecords: result.successRecords,
|
|
failedRecords: result.failedRecords,
|
|
executionTime: duration,
|
|
},
|
|
message: "배치가 성공적으로 실행되었습니다.",
|
|
});
|
|
} catch (batchError) {
|
|
console.error(`배치 실행 실패: ${batchConfig.batch_name}`, batchError);
|
|
|
|
// 실행 로그 업데이트 (실패) - executionLog가 생성되었을 경우에만
|
|
try {
|
|
const endTime = new Date();
|
|
const duration = endTime.getTime() - startTime.getTime();
|
|
|
|
// executionLog가 정의되어 있는지 확인
|
|
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,
|
|
error_message:
|
|
batchError instanceof Error
|
|
? batchError.message
|
|
: "알 수 없는 오류",
|
|
});
|
|
}
|
|
} catch (logError) {
|
|
console.error("실행 로그 업데이트 실패:", logError);
|
|
}
|
|
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: "배치 실행에 실패했습니다.",
|
|
error:
|
|
batchError instanceof Error
|
|
? batchError.message
|
|
: "알 수 없는 오류",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error(`배치 실행 오류 (ID: ${req.params.id}):`, error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: "배치 실행 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 배치 설정 업데이트
|
|
* PUT /api/batch-management/batch-configs/:id
|
|
*/
|
|
static async updateBatchConfig(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const { id } = req.params;
|
|
const updateData = req.body;
|
|
|
|
if (!id || isNaN(Number(id))) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "올바른 배치 설정 ID를 제공해주세요.",
|
|
});
|
|
}
|
|
|
|
const batchConfig = await BatchService.updateBatchConfig(
|
|
Number(id),
|
|
updateData
|
|
);
|
|
|
|
// 스케줄러에서 배치 스케줄 업데이트 (즉시 실행 비활성화)
|
|
await BatchSchedulerService.updateBatchSchedule(Number(id), false);
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: batchConfig,
|
|
message: "배치 설정이 성공적으로 업데이트되었습니다.",
|
|
});
|
|
} catch (error) {
|
|
console.error("배치 설정 업데이트 오류:", error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: "배치 설정 업데이트에 실패했습니다.",
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* REST API 데이터 미리보기
|
|
*/
|
|
static async previewRestApiData(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const {
|
|
apiUrl,
|
|
apiKey,
|
|
endpoint,
|
|
method = "GET",
|
|
paramType,
|
|
paramName,
|
|
paramValue,
|
|
paramSource,
|
|
requestBody,
|
|
authServiceName, // DB에서 토큰 가져올 서비스명
|
|
dataArrayPath, // 데이터 배열 경로 (예: response, data.items)
|
|
} = req.body;
|
|
|
|
// apiUrl, endpoint는 항상 필수
|
|
if (!apiUrl || !endpoint) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "API URL과 엔드포인트는 필수입니다.",
|
|
});
|
|
}
|
|
|
|
// 토큰 결정: authServiceName이 있으면 DB에서 조회, 없으면 apiKey 사용
|
|
let finalApiKey = apiKey || "";
|
|
if (authServiceName) {
|
|
const companyCode = req.user?.companyCode;
|
|
|
|
// DB에서 토큰 조회 (멀티테넌시: company_code 필터링)
|
|
let tokenQuery: string;
|
|
let tokenParams: any[];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 회사 토큰 조회 가능
|
|
tokenQuery = `SELECT access_token FROM auth_tokens
|
|
WHERE service_name = $1
|
|
ORDER BY created_date DESC LIMIT 1`;
|
|
tokenParams = [authServiceName];
|
|
} else {
|
|
// 일반 회사: 자신의 회사 토큰만 조회
|
|
tokenQuery = `SELECT access_token FROM auth_tokens
|
|
WHERE service_name = $1 AND company_code = $2
|
|
ORDER BY created_date DESC LIMIT 1`;
|
|
tokenParams = [authServiceName, companyCode];
|
|
}
|
|
|
|
const tokenResult = await query<{ access_token: string }>(
|
|
tokenQuery,
|
|
tokenParams
|
|
);
|
|
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}'의 토큰을 찾을 수 없습니다. 먼저 토큰 저장 배치를 실행하세요.`,
|
|
});
|
|
}
|
|
}
|
|
|
|
// 토큰이 없어도 공개 API 호출 가능 (토큰 검증 제거)
|
|
|
|
console.log("REST API 미리보기 요청:", {
|
|
apiUrl,
|
|
endpoint,
|
|
method,
|
|
paramType,
|
|
paramName,
|
|
paramValue,
|
|
paramSource,
|
|
requestBody: requestBody ? "Included" : "None",
|
|
authServiceName: authServiceName || "직접 입력",
|
|
dataArrayPath: dataArrayPath || "전체 응답",
|
|
});
|
|
|
|
// RestApiConnector 사용하여 데이터 조회
|
|
const { RestApiConnector } = await import("../database/RestApiConnector");
|
|
|
|
const connector = new RestApiConnector({
|
|
baseUrl: apiUrl,
|
|
apiKey: finalApiKey,
|
|
timeout: 30000,
|
|
});
|
|
|
|
// 연결 테스트
|
|
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);
|
|
|
|
// Request Body 파싱
|
|
let parsedBody = undefined;
|
|
if (requestBody && typeof requestBody === "string") {
|
|
try {
|
|
parsedBody = JSON.parse(requestBody);
|
|
} catch (e) {
|
|
console.warn("Request Body JSON 파싱 실패:", e);
|
|
// 파싱 실패 시 원본 문자열 사용하거나 무시 (상황에 따라 결정, 여기선 undefined로 처리하거나 에러 반환 가능)
|
|
// 여기서는 경고 로그 남기고 진행
|
|
}
|
|
} else if (requestBody) {
|
|
parsedBody = requestBody;
|
|
}
|
|
|
|
// 데이터 조회 - executeRequest 사용 (POST/PUT/DELETE 지원)
|
|
const result = await connector.executeRequest(
|
|
finalEndpoint,
|
|
method as "GET" | "POST" | "PUT" | "DELETE",
|
|
parsedBody
|
|
);
|
|
|
|
console.log(`[previewRestApiData] executeRequest 결과:`, {
|
|
rowCount: result.rowCount,
|
|
rowsLength: result.rows ? result.rows.length : "undefined",
|
|
firstRow:
|
|
result.rows && result.rows.length > 0 ? result.rows[0] : "no 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) {
|
|
// 첫 번째 객체에서 필드명 추출
|
|
const fields = Object.keys(data[0]);
|
|
console.log(`[previewRestApiData] 추출된 필드:`, fields);
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: {
|
|
fields: fields,
|
|
samples: data,
|
|
totalCount: extractedData.length,
|
|
},
|
|
message: `${fields.length}개 필드, ${extractedData.length}개 레코드를 조회했습니다.`,
|
|
});
|
|
} else {
|
|
return res.json({
|
|
success: true,
|
|
data: {
|
|
fields: [],
|
|
samples: [],
|
|
totalCount: 0,
|
|
},
|
|
message: "API에서 데이터를 가져올 수 없습니다.",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("REST API 미리보기 오류:", error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: "REST API 데이터 미리보기 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* REST API 배치 설정 저장
|
|
*/
|
|
static async saveRestApiBatch(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const {
|
|
batchName,
|
|
batchType,
|
|
cronSchedule,
|
|
description,
|
|
apiMappings,
|
|
authServiceName,
|
|
dataArrayPath,
|
|
saveMode,
|
|
conflictKey,
|
|
} = req.body;
|
|
|
|
if (
|
|
!batchName ||
|
|
!batchType ||
|
|
!cronSchedule ||
|
|
!apiMappings ||
|
|
apiMappings.length === 0
|
|
) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "필수 필드가 누락되었습니다.",
|
|
});
|
|
}
|
|
|
|
console.log("REST API 배치 저장 요청:", {
|
|
batchName,
|
|
batchType,
|
|
cronSchedule,
|
|
description,
|
|
apiMappings,
|
|
authServiceName,
|
|
dataArrayPath,
|
|
saveMode,
|
|
conflictKey,
|
|
});
|
|
|
|
// 🔐 멀티테넌시: 현재 사용자 회사 코드 사용 (프론트에서 받지 않음)
|
|
const companyCode = req.user?.companyCode || "*";
|
|
const userId = req.user?.userId;
|
|
|
|
// BatchService를 사용하여 배치 설정 저장
|
|
const batchConfig: CreateBatchConfigRequest = {
|
|
batchName: batchName,
|
|
description: description || "",
|
|
cronSchedule: cronSchedule,
|
|
isActive: "Y",
|
|
companyCode,
|
|
authServiceName: authServiceName || undefined,
|
|
dataArrayPath: dataArrayPath || undefined,
|
|
saveMode: saveMode || "INSERT",
|
|
conflictKey: conflictKey || undefined,
|
|
mappings: apiMappings,
|
|
};
|
|
|
|
const result = await BatchService.createBatchConfig(batchConfig, userId);
|
|
|
|
if (result.success && result.data) {
|
|
// 스케줄러에 자동 등록 ✅
|
|
try {
|
|
await BatchSchedulerService.scheduleBatch(result.data);
|
|
console.log(
|
|
`✅ 새로운 배치가 스케줄러에 등록되었습니다: ${batchName} (ID: ${result.data.id})`
|
|
);
|
|
} catch (schedulerError) {
|
|
console.error(`❌ 스케줄러 등록 실패: ${batchName}`, schedulerError);
|
|
// 스케줄러 등록 실패해도 배치 저장은 성공으로 처리
|
|
}
|
|
|
|
return res.json({
|
|
success: true,
|
|
message: "REST API 배치가 성공적으로 저장되었습니다.",
|
|
data: result.data,
|
|
});
|
|
} else {
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: result.message || "배치 저장에 실패했습니다.",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("REST API 배치 저장 오류:", error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
message: "배치 저장 중 오류가 발생했습니다.",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 인증 토큰 서비스명 목록 조회
|
|
*/
|
|
static async getAuthServiceNames(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user?.companyCode;
|
|
|
|
// 멀티테넌시: company_code 필터링
|
|
let queryText: string;
|
|
let queryParams: any[] = [];
|
|
|
|
if (companyCode === "*") {
|
|
// 최고 관리자: 모든 서비스 조회
|
|
queryText = `SELECT DISTINCT service_name
|
|
FROM auth_tokens
|
|
WHERE service_name IS NOT NULL
|
|
ORDER BY service_name`;
|
|
} else {
|
|
// 일반 회사: 자신의 회사 서비스만 조회
|
|
queryText = `SELECT DISTINCT service_name
|
|
FROM auth_tokens
|
|
WHERE service_name IS NOT NULL
|
|
AND company_code = $1
|
|
ORDER BY service_name`;
|
|
queryParams = [companyCode];
|
|
}
|
|
|
|
const result = await query<{ service_name: string }>(
|
|
queryText,
|
|
queryParams
|
|
);
|
|
|
|
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: "인증 서비스 목록 조회 중 오류가 발생했습니다.",
|
|
});
|
|
}
|
|
}
|
|
}
|