jskim-node #423
|
|
@ -768,4 +768,42 @@ export class BatchManagementController {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 노드 플로우 목록 조회 (배치 설정에서 노드 플로우 선택용)
|
||||
* GET /api/batch-management/node-flows
|
||||
* 멀티테넌시: 최고 관리자는 전체, 일반 회사는 자기 회사 플로우만
|
||||
*/
|
||||
static async getNodeFlows(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
|
||||
let queryText: string;
|
||||
let queryParams: any[] = [];
|
||||
|
||||
if (companyCode === "*") {
|
||||
queryText = `SELECT flow_id, flow_name, flow_description AS description, created_at AS created_date
|
||||
FROM node_flows
|
||||
ORDER BY flow_name`;
|
||||
} else {
|
||||
queryText = `SELECT flow_id, flow_name, flow_description AS description, created_at AS created_date
|
||||
FROM node_flows
|
||||
WHERE company_code = $1
|
||||
ORDER BY flow_name`;
|
||||
queryParams = [companyCode ?? ""];
|
||||
}
|
||||
|
||||
const result = await query(queryText, queryParams);
|
||||
const data = Array.isArray(result) ? result : [];
|
||||
|
||||
return res.json({ success: true, data });
|
||||
} catch (error) {
|
||||
console.error("노드 플로우 목록 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "노드 플로우 목록 조회 실패",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,4 +85,10 @@ router.post("/rest-api/save", authenticateToken, BatchManagementController.saveR
|
|||
*/
|
||||
router.get("/auth-services", authenticateToken, BatchManagementController.getAuthServiceNames);
|
||||
|
||||
/**
|
||||
* GET /api/batch-management/node-flows
|
||||
* 노드 플로우 목록 조회 (배치 설정에서 노드 플로우 선택용)
|
||||
*/
|
||||
router.get("/node-flows", authenticateToken, BatchManagementController.getNodeFlows);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -165,8 +165,20 @@ export class BatchSchedulerService {
|
|||
|
||||
executionLog = executionLogResponse.data;
|
||||
|
||||
// 실제 배치 실행 로직 (수동 실행과 동일한 로직 사용)
|
||||
const result = await this.executeBatchMappings(config);
|
||||
// 실행 유형 분기: node_flow면 노드 플로우 실행, 아니면 매핑 배치 실행
|
||||
let result: {
|
||||
totalRecords: number;
|
||||
successRecords: number;
|
||||
failedRecords: number;
|
||||
};
|
||||
if (
|
||||
config.execution_type === "node_flow" &&
|
||||
config.node_flow_id != null
|
||||
) {
|
||||
result = await this.executeNodeFlow(config);
|
||||
} else {
|
||||
result = await this.executeBatchMappings(config);
|
||||
}
|
||||
|
||||
// 실행 로그 업데이트 (성공)
|
||||
await BatchExecutionLogService.updateExecutionLog(executionLog.id, {
|
||||
|
|
@ -207,6 +219,67 @@ export class BatchSchedulerService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 노드 플로우 실행 (execution_type === 'node_flow'일 때)
|
||||
* node_flows 테이블의 플로우를 NodeFlowExecutionService로 실행하고 결과를 배치 로그 형식으로 반환
|
||||
*/
|
||||
private static async executeNodeFlow(config: any): Promise<{
|
||||
totalRecords: number;
|
||||
successRecords: number;
|
||||
failedRecords: number;
|
||||
}> {
|
||||
const { NodeFlowExecutionService } = await import(
|
||||
"./nodeFlowExecutionService"
|
||||
);
|
||||
|
||||
// 플로우 존재 여부 확인
|
||||
const flowCheck = await query<{ flow_id: number; flow_name: string }>(
|
||||
"SELECT flow_id, flow_name FROM node_flows WHERE flow_id = $1",
|
||||
[config.node_flow_id]
|
||||
);
|
||||
if (flowCheck.length === 0) {
|
||||
throw new Error(
|
||||
`노드 플로우를 찾을 수 없습니다 (flow_id: ${config.node_flow_id})`
|
||||
);
|
||||
}
|
||||
|
||||
// node_flow_context: DB JSONB는 객체로 올 수 있고, 문자열로 올 수 있음. 안전 파싱
|
||||
let contextObj: Record<string, any> = {};
|
||||
if (config.node_flow_context != null) {
|
||||
if (typeof config.node_flow_context === "string") {
|
||||
try {
|
||||
contextObj = JSON.parse(config.node_flow_context);
|
||||
} catch {
|
||||
contextObj = {};
|
||||
}
|
||||
} else if (
|
||||
typeof config.node_flow_context === "object" &&
|
||||
!Array.isArray(config.node_flow_context)
|
||||
) {
|
||||
contextObj = { ...config.node_flow_context };
|
||||
}
|
||||
}
|
||||
|
||||
const contextData: Record<string, any> = {
|
||||
...contextObj,
|
||||
_batchId: config.id,
|
||||
_batchName: config.batch_name,
|
||||
_companyCode: config.company_code,
|
||||
_executedBy: "batch_system",
|
||||
};
|
||||
|
||||
const flowResult = await NodeFlowExecutionService.executeFlow(
|
||||
config.node_flow_id,
|
||||
contextData
|
||||
);
|
||||
|
||||
return {
|
||||
totalRecords: flowResult.summary.total,
|
||||
successRecords: flowResult.summary.success,
|
||||
failedRecords: flowResult.summary.failed,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 매핑 실행 (수동 실행과 동일한 로직)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -176,8 +176,8 @@ export class BatchService {
|
|||
// 배치 설정 생성
|
||||
const batchConfigResult = await client.query(
|
||||
`INSERT INTO batch_configs
|
||||
(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, $7, $8, $9, $10, NOW(), NOW())
|
||||
(batch_name, description, cron_schedule, is_active, company_code, save_mode, conflict_key, auth_service_name, data_array_path, execution_type, node_flow_id, node_flow_context, created_by, created_date, updated_date)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, NOW(), NOW())
|
||||
RETURNING *`,
|
||||
[
|
||||
data.batchName,
|
||||
|
|
@ -189,6 +189,11 @@ export class BatchService {
|
|||
data.conflictKey || null,
|
||||
data.authServiceName || null,
|
||||
data.dataArrayPath || null,
|
||||
data.executionType || "mapping",
|
||||
data.nodeFlowId ?? null,
|
||||
data.nodeFlowContext != null
|
||||
? JSON.stringify(data.nodeFlowContext)
|
||||
: null,
|
||||
userId,
|
||||
]
|
||||
);
|
||||
|
|
@ -332,6 +337,22 @@ export class BatchService {
|
|||
updateFields.push(`data_array_path = $${paramIndex++}`);
|
||||
updateValues.push(data.dataArrayPath || null);
|
||||
}
|
||||
if (data.executionType !== undefined) {
|
||||
updateFields.push(`execution_type = $${paramIndex++}`);
|
||||
updateValues.push(data.executionType);
|
||||
}
|
||||
if (data.nodeFlowId !== undefined) {
|
||||
updateFields.push(`node_flow_id = $${paramIndex++}`);
|
||||
updateValues.push(data.nodeFlowId ?? null);
|
||||
}
|
||||
if (data.nodeFlowContext !== undefined) {
|
||||
updateFields.push(`node_flow_context = $${paramIndex++}`);
|
||||
updateValues.push(
|
||||
data.nodeFlowContext != null
|
||||
? JSON.stringify(data.nodeFlowContext)
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
// 배치 설정 업데이트
|
||||
const batchConfigResult = await client.query(
|
||||
|
|
|
|||
|
|
@ -91,6 +91,12 @@ export interface BatchConfig {
|
|||
conflict_key?: string; // UPSERT 시 충돌 기준 컬럼명
|
||||
auth_service_name?: string; // REST API 인증에 사용할 토큰 서비스명
|
||||
data_array_path?: string; // REST API 응답에서 데이터 배열 경로 (예: response, data.items)
|
||||
/** 실행 유형: mapping(테이블 매핑) | node_flow(노드 플로우) */
|
||||
execution_type?: "mapping" | "node_flow";
|
||||
/** 노드 플로우 실행 시 사용할 flow_id (node_flows.flow_id) */
|
||||
node_flow_id?: number;
|
||||
/** 노드 플로우 실행 시 전달할 컨텍스트 (Record<string, any>) */
|
||||
node_flow_context?: Record<string, any>;
|
||||
created_by?: string;
|
||||
created_date?: Date;
|
||||
updated_by?: string;
|
||||
|
|
@ -150,6 +156,9 @@ export interface CreateBatchConfigRequest {
|
|||
conflictKey?: string;
|
||||
authServiceName?: string;
|
||||
dataArrayPath?: string; // REST API 응답에서 데이터 배열 경로
|
||||
executionType?: "mapping" | "node_flow";
|
||||
nodeFlowId?: number;
|
||||
nodeFlowContext?: Record<string, any>;
|
||||
mappings: BatchMappingRequest[];
|
||||
}
|
||||
|
||||
|
|
@ -162,6 +171,9 @@ export interface UpdateBatchConfigRequest {
|
|||
conflictKey?: string;
|
||||
authServiceName?: string;
|
||||
dataArrayPath?: string; // REST API 응답에서 데이터 배열 경로
|
||||
executionType?: "mapping" | "node_flow";
|
||||
nodeFlowId?: number;
|
||||
nodeFlowContext?: Record<string, any>;
|
||||
mappings?: BatchMappingRequest[];
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue