ERP-node/backend-node/src/services/flowDataMoveService.ts

594 lines
17 KiB
TypeScript
Raw Normal View History

2025-10-20 10:55:33 +09:00
/**
2025-10-20 15:53:00 +09:00
* ( )
* - 방식: 같은
* - 방식: 다른
* - 방식:
2025-10-20 10:55:33 +09:00
*/
import db from "../database/db";
2025-10-20 17:50:27 +09:00
import { FlowAuditLog, FlowIntegrationContext } from "../types/flow";
2025-10-20 10:55:33 +09:00
import { FlowDefinitionService } from "./flowDefinitionService";
2025-10-20 15:53:00 +09:00
import { FlowStepService } from "./flowStepService";
2025-10-20 17:50:27 +09:00
import { FlowExternalDbIntegrationService } from "./flowExternalDbIntegrationService";
2025-10-20 10:55:33 +09:00
export class FlowDataMoveService {
private flowDefinitionService: FlowDefinitionService;
2025-10-20 15:53:00 +09:00
private flowStepService: FlowStepService;
2025-10-20 17:50:27 +09:00
private externalDbIntegrationService: FlowExternalDbIntegrationService;
2025-10-20 10:55:33 +09:00
constructor() {
this.flowDefinitionService = new FlowDefinitionService();
2025-10-20 15:53:00 +09:00
this.flowStepService = new FlowStepService();
2025-10-20 17:50:27 +09:00
this.externalDbIntegrationService = new FlowExternalDbIntegrationService();
2025-10-20 10:55:33 +09:00
}
/**
2025-10-20 15:53:00 +09:00
* ( )
2025-10-20 10:55:33 +09:00
*/
async moveDataToStep(
flowId: number,
2025-10-20 15:53:00 +09:00
fromStepId: number,
2025-10-20 10:55:33 +09:00
toStepId: number,
2025-10-20 15:53:00 +09:00
dataId: any,
userId: string = "system",
additionalData?: Record<string, any>
): Promise<{ success: boolean; targetDataId?: any; message?: string }> {
return await db.transaction(async (client) => {
try {
// 1. 단계 정보 조회
const fromStep = await this.flowStepService.findById(fromStepId);
const toStep = await this.flowStepService.findById(toStepId);
2025-10-20 10:55:33 +09:00
2025-10-20 15:53:00 +09:00
if (!fromStep || !toStep) {
throw new Error("유효하지 않은 단계입니다");
}
let targetDataId = dataId;
let sourceTable = fromStep.tableName;
let targetTable = toStep.tableName || fromStep.tableName;
// 2. 이동 방식에 따라 처리
switch (toStep.moveType || "status") {
case "status":
// 상태 변경 방식
await this.moveByStatusChange(
client,
fromStep,
toStep,
dataId,
additionalData
);
break;
case "table":
// 테이블 이동 방식
targetDataId = await this.moveByTableTransfer(
client,
fromStep,
toStep,
dataId,
additionalData
);
targetTable = toStep.targetTable || toStep.tableName;
break;
2025-10-20 10:55:33 +09:00
2025-10-20 15:53:00 +09:00
case "both":
// 하이브리드 방식: 둘 다 수행
await this.moveByStatusChange(
client,
fromStep,
toStep,
dataId,
additionalData
);
targetDataId = await this.moveByTableTransfer(
client,
fromStep,
toStep,
dataId,
additionalData
);
targetTable = toStep.targetTable || toStep.tableName;
break;
default:
throw new Error(`지원하지 않는 이동 방식: ${toStep.moveType}`);
}
// 3. 매핑 테이블 업데이트 (테이블 이동 방식일 때)
if (toStep.moveType === "table" || toStep.moveType === "both") {
await this.updateDataMapping(
client,
flowId,
toStepId,
fromStepId,
dataId,
targetDataId
);
}
2025-10-20 17:50:27 +09:00
// 4. 외부 DB 연동 실행 (설정된 경우)
if (
toStep.integrationType &&
toStep.integrationType !== "internal" &&
toStep.integrationConfig
) {
await this.executeExternalIntegration(
toStep,
flowId,
targetDataId,
sourceTable,
userId,
additionalData
);
}
// 5. 감사 로그 기록
2025-10-20 15:53:00 +09:00
await this.logDataMove(client, {
2025-10-20 10:55:33 +09:00
flowId,
fromStepId,
toStepId,
2025-10-20 15:53:00 +09:00
moveType: toStep.moveType || "status",
sourceTable,
targetTable,
sourceDataId: String(dataId),
targetDataId: String(targetDataId),
statusFrom: fromStep.statusValue,
statusTo: toStep.statusValue,
2025-10-20 10:55:33 +09:00
userId,
2025-10-20 15:53:00 +09:00
});
return {
success: true,
targetDataId,
message: "데이터가 성공적으로 이동되었습니다",
};
} catch (error: any) {
console.error("데이터 이동 실패:", error);
throw error;
}
2025-10-20 10:55:33 +09:00
});
}
2025-10-20 15:53:00 +09:00
/**
*
*/
private async moveByStatusChange(
client: any,
fromStep: any,
toStep: any,
dataId: any,
additionalData?: Record<string, any>
): Promise<void> {
const statusColumn = toStep.statusColumn || "flow_status";
const tableName = fromStep.tableName;
// 추가 필드 업데이트 준비
const updates: string[] = [`${statusColumn} = $2`, `updated_at = NOW()`];
const values: any[] = [dataId, toStep.statusValue];
let paramIndex = 3;
// 추가 데이터가 있으면 함께 업데이트
if (additionalData) {
for (const [key, value] of Object.entries(additionalData)) {
updates.push(`${key} = $${paramIndex}`);
values.push(value);
paramIndex++;
}
}
const updateQuery = `
UPDATE ${tableName}
SET ${updates.join(", ")}
WHERE id = $1
`;
const result = await client.query(updateQuery, values);
if (result.rowCount === 0) {
throw new Error(`데이터를 찾을 수 없습니다: ${dataId}`);
}
}
/**
*
*/
private async moveByTableTransfer(
client: any,
fromStep: any,
toStep: any,
dataId: any,
additionalData?: Record<string, any>
): Promise<any> {
const sourceTable = fromStep.tableName;
const targetTable = toStep.targetTable || toStep.tableName;
const fieldMappings = toStep.fieldMappings || {};
// 1. 소스 데이터 조회
const selectQuery = `SELECT * FROM ${sourceTable} WHERE id = $1`;
const sourceResult = await client.query(selectQuery, [dataId]);
if (sourceResult.length === 0) {
throw new Error(`소스 데이터를 찾을 수 없습니다: ${dataId}`);
}
const sourceData = sourceResult[0];
// 2. 필드 매핑 적용
const mappedData: Record<string, any> = {};
// 매핑 정의가 있으면 적용
for (const [sourceField, targetField] of Object.entries(fieldMappings)) {
if (sourceData[sourceField] !== undefined) {
mappedData[targetField as string] = sourceData[sourceField];
}
}
// 추가 데이터 병합
if (additionalData) {
Object.assign(mappedData, additionalData);
}
// 3. 타겟 테이블에 데이터 삽입
if (Object.keys(mappedData).length === 0) {
throw new Error("매핑할 데이터가 없습니다");
}
const columns = Object.keys(mappedData);
const values = Object.values(mappedData);
const placeholders = columns.map((_, i) => `$${i + 1}`).join(", ");
const insertQuery = `
INSERT INTO ${targetTable} (${columns.join(", ")})
VALUES (${placeholders})
RETURNING id
`;
const insertResult = await client.query(insertQuery, values);
return insertResult[0].id;
}
/**
*
*/
private async updateDataMapping(
client: any,
flowId: number,
currentStepId: number,
prevStepId: number,
sourceDataId: any,
targetDataId: any
): Promise<void> {
// 기존 매핑 조회
const selectQuery = `
SELECT id, step_data_map
FROM flow_data_mapping
WHERE flow_definition_id = $1
AND step_data_map->$2 = $3
`;
const mappingResult = await client.query(selectQuery, [
flowId,
String(prevStepId),
JSON.stringify(String(sourceDataId)),
]);
const stepDataMap: Record<string, string> =
mappingResult.length > 0 ? mappingResult[0].step_data_map : {};
// 새 단계 데이터 추가
stepDataMap[String(currentStepId)] = String(targetDataId);
if (mappingResult.length > 0) {
// 기존 매핑 업데이트
const updateQuery = `
UPDATE flow_data_mapping
SET current_step_id = $1,
step_data_map = $2,
updated_at = NOW()
WHERE id = $3
`;
await client.query(updateQuery, [
currentStepId,
JSON.stringify(stepDataMap),
mappingResult[0].id,
]);
} else {
// 새 매핑 생성
const insertQuery = `
INSERT INTO flow_data_mapping
(flow_definition_id, current_step_id, step_data_map)
VALUES ($1, $2, $3)
`;
await client.query(insertQuery, [
flowId,
currentStepId,
JSON.stringify(stepDataMap),
]);
}
}
/**
*
*/
private async logDataMove(client: any, params: any): Promise<void> {
const query = `
INSERT INTO flow_audit_log (
flow_definition_id, from_step_id, to_step_id,
move_type, source_table, target_table,
source_data_id, target_data_id,
status_from, status_to,
changed_by, note
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
`;
await client.query(query, [
params.flowId,
params.fromStepId,
params.toStepId,
params.moveType,
params.sourceTable,
params.targetTable,
params.sourceDataId,
params.targetDataId,
params.statusFrom,
params.statusTo,
params.userId,
params.note || null,
]);
}
2025-10-20 10:55:33 +09:00
/**
*
*/
async moveBatchData(
flowId: number,
2025-10-20 15:53:00 +09:00
fromStepId: number,
2025-10-20 10:55:33 +09:00
toStepId: number,
2025-10-20 15:53:00 +09:00
dataIds: any[],
userId: string = "system"
): Promise<{ success: boolean; results: any[] }> {
const results = [];
for (const dataId of dataIds) {
try {
const result = await this.moveDataToStep(
flowId,
fromStepId,
toStepId,
dataId,
userId
);
results.push({ dataId, ...result });
} catch (error: any) {
results.push({ dataId, success: false, message: error.message });
}
2025-10-20 10:55:33 +09:00
}
2025-10-20 15:53:00 +09:00
return {
success: results.every((r) => r.success),
results,
};
2025-10-20 10:55:33 +09:00
}
/**
*
*/
2025-10-20 15:53:00 +09:00
async getAuditLogs(flowId: number, dataId: string): Promise<FlowAuditLog[]> {
2025-10-20 10:55:33 +09:00
const query = `
SELECT
fal.*,
fs_from.step_name as from_step_name,
fs_to.step_name as to_step_name
FROM flow_audit_log fal
LEFT JOIN flow_step fs_from ON fal.from_step_id = fs_from.id
LEFT JOIN flow_step fs_to ON fal.to_step_id = fs_to.id
2025-10-20 15:53:00 +09:00
WHERE fal.flow_definition_id = $1
AND (fal.source_data_id = $2 OR fal.target_data_id = $2)
2025-10-20 10:55:33 +09:00
ORDER BY fal.changed_at DESC
`;
2025-10-20 15:53:00 +09:00
const result = await db.query(query, [flowId, dataId]);
2025-10-20 10:55:33 +09:00
return result.map((row) => ({
id: row.id,
flowDefinitionId: row.flow_definition_id,
2025-10-20 15:53:00 +09:00
tableName: row.table_name || row.source_table,
recordId: row.record_id || row.source_data_id,
2025-10-20 10:55:33 +09:00
fromStepId: row.from_step_id,
toStepId: row.to_step_id,
changedBy: row.changed_by,
changedAt: row.changed_at,
note: row.note,
fromStepName: row.from_step_name,
toStepName: row.to_step_name,
2025-10-20 15:53:00 +09:00
moveType: row.move_type,
sourceTable: row.source_table,
targetTable: row.target_table,
sourceDataId: row.source_data_id,
targetDataId: row.target_data_id,
statusFrom: row.status_from,
statusTo: row.status_to,
2025-10-20 10:55:33 +09:00
}));
}
/**
*
*/
async getFlowAuditLogs(
flowId: number,
limit: number = 100
): Promise<FlowAuditLog[]> {
const query = `
SELECT
fal.*,
fs_from.step_name as from_step_name,
fs_to.step_name as to_step_name
FROM flow_audit_log fal
LEFT JOIN flow_step fs_from ON fal.from_step_id = fs_from.id
LEFT JOIN flow_step fs_to ON fal.to_step_id = fs_to.id
WHERE fal.flow_definition_id = $1
ORDER BY fal.changed_at DESC
LIMIT $2
`;
const result = await db.query(query, [flowId, limit]);
return result.map((row) => ({
id: row.id,
flowDefinitionId: row.flow_definition_id,
2025-10-20 15:53:00 +09:00
tableName: row.table_name || row.source_table,
recordId: row.record_id || row.source_data_id,
2025-10-20 10:55:33 +09:00
fromStepId: row.from_step_id,
toStepId: row.to_step_id,
changedBy: row.changed_by,
changedAt: row.changed_at,
note: row.note,
fromStepName: row.from_step_name,
toStepName: row.to_step_name,
2025-10-20 15:53:00 +09:00
moveType: row.move_type,
sourceTable: row.source_table,
targetTable: row.target_table,
sourceDataId: row.source_data_id,
targetDataId: row.target_data_id,
statusFrom: row.status_from,
statusTo: row.status_to,
2025-10-20 10:55:33 +09:00
}));
}
2025-10-20 17:50:27 +09:00
/**
* DB
*/
private async executeExternalIntegration(
toStep: any,
flowId: number,
dataId: any,
tableName: string | undefined,
userId: string,
additionalData?: Record<string, any>
): Promise<void> {
const startTime = Date.now();
try {
// 연동 컨텍스트 구성
const context: FlowIntegrationContext = {
flowId,
stepId: toStep.id,
dataId,
tableName,
currentUser: userId,
variables: {
...additionalData,
stepName: toStep.stepName,
stepId: toStep.id,
},
};
// 연동 타입별 처리
switch (toStep.integrationType) {
case "external_db":
const result = await this.externalDbIntegrationService.execute(
context,
toStep.integrationConfig
);
// 연동 로그 기록
await this.logIntegration(
flowId,
toStep.id,
dataId,
toStep.integrationType,
toStep.integrationConfig.connectionId,
toStep.integrationConfig,
result.data,
result.success ? "success" : "failed",
result.error?.message,
Date.now() - startTime,
userId
);
if (!result.success) {
throw new Error(
`외부 DB 연동 실패: ${result.error?.message || "알 수 없는 오류"}`
);
}
break;
case "rest_api":
// REST API 연동 (추후 구현)
console.warn("REST API 연동은 아직 구현되지 않았습니다");
break;
case "webhook":
// Webhook 연동 (추후 구현)
console.warn("Webhook 연동은 아직 구현되지 않았습니다");
break;
case "hybrid":
// 복합 연동 (추후 구현)
console.warn("복합 연동은 아직 구현되지 않았습니다");
break;
default:
throw new Error(`지원하지 않는 연동 타입: ${toStep.integrationType}`);
}
} catch (error: any) {
console.error("외부 연동 실행 실패:", error);
// 연동 실패 로그 기록
await this.logIntegration(
flowId,
toStep.id,
dataId,
toStep.integrationType,
toStep.integrationConfig?.connectionId,
toStep.integrationConfig,
null,
"failed",
error.message,
Date.now() - startTime,
userId
);
throw error;
}
}
/**
*
*/
private async logIntegration(
flowId: number,
stepId: number,
dataId: any,
integrationType: string,
connectionId: number | undefined,
requestPayload: any,
responsePayload: any,
status: "success" | "failed" | "timeout" | "rollback",
errorMessage: string | undefined,
executionTimeMs: number,
userId: string
): Promise<void> {
const query = `
INSERT INTO flow_integration_log (
flow_definition_id, step_id, data_id, integration_type, connection_id,
request_payload, response_payload, status, error_message,
execution_time_ms, executed_by, executed_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW())
`;
await db.query(query, [
flowId,
stepId,
String(dataId),
integrationType,
connectionId || null,
requestPayload ? JSON.stringify(requestPayload) : null,
responsePayload ? JSON.stringify(responsePayload) : null,
status,
errorMessage || null,
executionTimeMs,
userId,
]);
}
2025-10-20 10:55:33 +09:00
}