백엔드 구현
This commit is contained in:
parent
441a5712c1
commit
f50dd520ae
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
|
import { ApiResponse } from "../types/common";
|
||||||
|
import { EventTriggerService } from "../services/eventTriggerService";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조건부 연결 조건 테스트
|
||||||
|
*/
|
||||||
|
export async function testConditionalConnection(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info("=== 조건부 연결 조건 테스트 시작 ===");
|
||||||
|
|
||||||
|
const { diagramId } = req.params;
|
||||||
|
const { testData } = req.body;
|
||||||
|
const companyCode = req.user?.company_code;
|
||||||
|
|
||||||
|
if (!companyCode) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "회사 코드가 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_COMPANY_CODE",
|
||||||
|
details: "인증된 사용자의 회사 코드를 찾을 수 없습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!diagramId || !testData) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "다이어그램 ID와 테스트 데이터가 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_REQUIRED_FIELDS",
|
||||||
|
details: "diagramId와 testData가 필요합니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await EventTriggerService.testConditionalConnection(
|
||||||
|
parseInt(diagramId),
|
||||||
|
testData,
|
||||||
|
companyCode
|
||||||
|
);
|
||||||
|
|
||||||
|
const response: ApiResponse<any> = {
|
||||||
|
success: true,
|
||||||
|
message: "조건부 연결 테스트를 성공적으로 완료했습니다.",
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("조건부 연결 테스트 실패:", error);
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "조건부 연결 테스트에 실패했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "CONDITIONAL_CONNECTION_TEST_FAILED",
|
||||||
|
details:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "알 수 없는 오류가 발생했습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조건부 연결 액션 수동 실행
|
||||||
|
*/
|
||||||
|
export async function executeConditionalActions(
|
||||||
|
req: AuthenticatedRequest,
|
||||||
|
res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
logger.info("=== 조건부 연결 액션 수동 실행 시작 ===");
|
||||||
|
|
||||||
|
const { diagramId } = req.params;
|
||||||
|
const { triggerType, tableName, data } = req.body;
|
||||||
|
const companyCode = req.user?.company_code;
|
||||||
|
|
||||||
|
if (!companyCode) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "회사 코드가 필요합니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_COMPANY_CODE",
|
||||||
|
details: "인증된 사용자의 회사 코드를 찾을 수 없습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!diagramId || !triggerType || !tableName || !data) {
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "필수 필드가 누락되었습니다.",
|
||||||
|
error: {
|
||||||
|
code: "MISSING_REQUIRED_FIELDS",
|
||||||
|
details: "diagramId, triggerType, tableName, data가 모두 필요합니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await EventTriggerService.executeEventTriggers(
|
||||||
|
triggerType,
|
||||||
|
tableName,
|
||||||
|
data,
|
||||||
|
companyCode
|
||||||
|
);
|
||||||
|
|
||||||
|
const response: ApiResponse<any[]> = {
|
||||||
|
success: true,
|
||||||
|
message: "조건부 연결 액션을 성공적으로 실행했습니다.",
|
||||||
|
data: results,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.status(200).json(response);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("조건부 연결 액션 실행 실패:", error);
|
||||||
|
const response: ApiResponse<null> = {
|
||||||
|
success: false,
|
||||||
|
message: "조건부 연결 액션 실행에 실패했습니다.",
|
||||||
|
error: {
|
||||||
|
code: "CONDITIONAL_ACTION_EXECUTION_FAILED",
|
||||||
|
details:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "알 수 없는 오류가 발생했습니다.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import { logger } from "../utils/logger";
|
||||||
import { AuthenticatedRequest } from "../types/auth";
|
import { AuthenticatedRequest } from "../types/auth";
|
||||||
import { ApiResponse } from "../types/common";
|
import { ApiResponse } from "../types/common";
|
||||||
import { DataflowService } from "../services/dataflowService";
|
import { DataflowService } from "../services/dataflowService";
|
||||||
|
import { EventTriggerService } from "../services/eventTriggerService";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 테이블 관계 생성
|
* 테이블 관계 생성
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ import {
|
||||||
copyDiagram,
|
copyDiagram,
|
||||||
deleteDiagram,
|
deleteDiagram,
|
||||||
} from "../controllers/dataflowController";
|
} from "../controllers/dataflowController";
|
||||||
|
import {
|
||||||
|
testConditionalConnection,
|
||||||
|
executeConditionalActions,
|
||||||
|
} from "../controllers/conditionalConnectionController";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
@ -128,4 +132,18 @@ router.get(
|
||||||
getDiagramRelationshipsByRelationshipId
|
getDiagramRelationshipsByRelationshipId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ==================== 조건부 연결 관리 라우트 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조건부 연결 조건 테스트
|
||||||
|
* POST /api/dataflow/diagrams/:diagramId/test-conditions
|
||||||
|
*/
|
||||||
|
router.post("/diagrams/:diagramId/test-conditions", testConditionalConnection);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조건부 연결 액션 수동 실행
|
||||||
|
* POST /api/dataflow/diagrams/:diagramId/execute-actions
|
||||||
|
*/
|
||||||
|
router.post("/diagrams/:diagramId/execute-actions", executeConditionalActions);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import prisma from "../config/database";
|
import prisma from "../config/database";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { EventTriggerService } from "./eventTriggerService";
|
||||||
|
|
||||||
export interface FormDataResult {
|
export interface FormDataResult {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -247,6 +248,22 @@ export class DynamicFormService {
|
||||||
// 결과를 표준 형식으로 변환
|
// 결과를 표준 형식으로 변환
|
||||||
const insertedRecord = Array.isArray(result) ? result[0] : result;
|
const insertedRecord = Array.isArray(result) ? result[0] : result;
|
||||||
|
|
||||||
|
// 🔥 조건부 연결 실행 (INSERT 트리거)
|
||||||
|
try {
|
||||||
|
if (company_code) {
|
||||||
|
await EventTriggerService.executeEventTriggers(
|
||||||
|
"insert",
|
||||||
|
tableName,
|
||||||
|
insertedRecord as Record<string, any>,
|
||||||
|
company_code
|
||||||
|
);
|
||||||
|
console.log("🚀 조건부 연결 트리거 실행 완료 (INSERT)");
|
||||||
|
}
|
||||||
|
} catch (triggerError) {
|
||||||
|
console.error("⚠️ 조건부 연결 트리거 실행 오류:", triggerError);
|
||||||
|
// 트리거 오류는 로그만 남기고 메인 저장 프로세스는 계속 진행
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: insertedRecord.id || insertedRecord.objid || 0,
|
id: insertedRecord.id || insertedRecord.objid || 0,
|
||||||
screenId: screenId,
|
screenId: screenId,
|
||||||
|
|
@ -343,6 +360,22 @@ export class DynamicFormService {
|
||||||
|
|
||||||
const updatedRecord = Array.isArray(result) ? result[0] : result;
|
const updatedRecord = Array.isArray(result) ? result[0] : result;
|
||||||
|
|
||||||
|
// 🔥 조건부 연결 실행 (UPDATE 트리거)
|
||||||
|
try {
|
||||||
|
if (company_code) {
|
||||||
|
await EventTriggerService.executeEventTriggers(
|
||||||
|
"update",
|
||||||
|
tableName,
|
||||||
|
updatedRecord as Record<string, any>,
|
||||||
|
company_code
|
||||||
|
);
|
||||||
|
console.log("🚀 조건부 연결 트리거 실행 완료 (UPDATE)");
|
||||||
|
}
|
||||||
|
} catch (triggerError) {
|
||||||
|
console.error("⚠️ 조건부 연결 트리거 실행 오류:", triggerError);
|
||||||
|
// 트리거 오류는 로그만 남기고 메인 업데이트 프로세스는 계속 진행
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: updatedRecord.id || updatedRecord.objid || id,
|
id: updatedRecord.id || updatedRecord.objid || id,
|
||||||
screenId: 0, // 실제 테이블에는 screenId가 없으므로 0으로 설정
|
screenId: 0, // 실제 테이블에는 screenId가 없으므로 0으로 설정
|
||||||
|
|
@ -362,7 +395,11 @@ export class DynamicFormService {
|
||||||
/**
|
/**
|
||||||
* 폼 데이터 삭제 (실제 테이블에서 직접 삭제)
|
* 폼 데이터 삭제 (실제 테이블에서 직접 삭제)
|
||||||
*/
|
*/
|
||||||
async deleteFormData(id: number, tableName: string): Promise<void> {
|
async deleteFormData(
|
||||||
|
id: number,
|
||||||
|
tableName: string,
|
||||||
|
companyCode?: string
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log("🗑️ 서비스: 실제 테이블에서 폼 데이터 삭제 시작:", {
|
console.log("🗑️ 서비스: 실제 테이블에서 폼 데이터 삭제 시작:", {
|
||||||
id,
|
id,
|
||||||
|
|
@ -382,6 +419,28 @@ export class DynamicFormService {
|
||||||
const result = await prisma.$queryRawUnsafe(deleteQuery, id);
|
const result = await prisma.$queryRawUnsafe(deleteQuery, id);
|
||||||
|
|
||||||
console.log("✅ 서비스: 실제 테이블 삭제 성공:", result);
|
console.log("✅ 서비스: 실제 테이블 삭제 성공:", result);
|
||||||
|
|
||||||
|
// 🔥 조건부 연결 실행 (DELETE 트리거)
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
companyCode &&
|
||||||
|
result &&
|
||||||
|
Array.isArray(result) &&
|
||||||
|
result.length > 0
|
||||||
|
) {
|
||||||
|
const deletedRecord = result[0] as Record<string, any>;
|
||||||
|
await EventTriggerService.executeEventTriggers(
|
||||||
|
"delete",
|
||||||
|
tableName,
|
||||||
|
deletedRecord,
|
||||||
|
companyCode
|
||||||
|
);
|
||||||
|
console.log("🚀 조건부 연결 트리거 실행 완료 (DELETE)");
|
||||||
|
}
|
||||||
|
} catch (triggerError) {
|
||||||
|
console.error("⚠️ 조건부 연결 트리거 실행 오류:", triggerError);
|
||||||
|
// 트리거 오류는 로그만 남기고 메인 삭제 프로세스는 계속 진행
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ 서비스: 실제 테이블 삭제 실패:", error);
|
console.error("❌ 서비스: 실제 테이블 삭제 실패:", error);
|
||||||
throw new Error(`실제 테이블 삭제 실패: ${error}`);
|
throw new Error(`실제 테이블 삭제 실패: ${error}`);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,581 @@
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { logger } from "../config/logger.js";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
// 조건 노드 타입 정의
|
||||||
|
interface ConditionNode {
|
||||||
|
type: "group" | "condition";
|
||||||
|
operator?: "AND" | "OR";
|
||||||
|
children?: ConditionNode[];
|
||||||
|
field?: string;
|
||||||
|
operator_type?:
|
||||||
|
| "="
|
||||||
|
| "!="
|
||||||
|
| ">"
|
||||||
|
| "<"
|
||||||
|
| ">="
|
||||||
|
| "<="
|
||||||
|
| "LIKE"
|
||||||
|
| "NOT_LIKE"
|
||||||
|
| "CONTAINS"
|
||||||
|
| "STARTS_WITH"
|
||||||
|
| "ENDS_WITH"
|
||||||
|
| "IN"
|
||||||
|
| "NOT_IN"
|
||||||
|
| "IS_NULL"
|
||||||
|
| "IS_NOT_NULL"
|
||||||
|
| "BETWEEN"
|
||||||
|
| "NOT_BETWEEN";
|
||||||
|
value?: any;
|
||||||
|
dataType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 조건 제어 정보
|
||||||
|
interface ConditionControl {
|
||||||
|
triggerType: "insert" | "update" | "delete" | "insert_update";
|
||||||
|
conditionTree: ConditionNode | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 연결 카테고리 정보
|
||||||
|
interface ConnectionCategory {
|
||||||
|
type: "simple-key" | "data-save" | "external-call" | "conditional-link";
|
||||||
|
rollbackOnError?: boolean;
|
||||||
|
enableLogging?: boolean;
|
||||||
|
maxRetryCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 대상 액션
|
||||||
|
interface TargetAction {
|
||||||
|
id: string;
|
||||||
|
actionType: "insert" | "update" | "delete" | "upsert";
|
||||||
|
targetTable: string;
|
||||||
|
enabled: boolean;
|
||||||
|
fieldMappings: FieldMapping[];
|
||||||
|
conditions?: ConditionNode;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 필드 매핑
|
||||||
|
interface FieldMapping {
|
||||||
|
sourceField: string;
|
||||||
|
targetField: string;
|
||||||
|
transformFunction?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 실행 계획
|
||||||
|
interface ExecutionPlan {
|
||||||
|
sourceTable: string;
|
||||||
|
targetActions: TargetAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 실행 결과
|
||||||
|
interface ExecutionResult {
|
||||||
|
success: boolean;
|
||||||
|
executedActions: number;
|
||||||
|
failedActions: number;
|
||||||
|
errors: string[];
|
||||||
|
executionTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조건부 연결 실행을 위한 이벤트 트리거 서비스
|
||||||
|
*/
|
||||||
|
export class EventTriggerService {
|
||||||
|
/**
|
||||||
|
* 특정 테이블에 대한 이벤트 트리거 실행
|
||||||
|
*/
|
||||||
|
static async executeEventTriggers(
|
||||||
|
triggerType: "insert" | "update" | "delete",
|
||||||
|
tableName: string,
|
||||||
|
data: Record<string, any>,
|
||||||
|
companyCode: string
|
||||||
|
): Promise<ExecutionResult[]> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const results: ExecutionResult[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 해당 테이블과 트리거 타입에 맞는 조건부 연결들 조회
|
||||||
|
const diagrams = await prisma.dataflow_diagrams.findMany({
|
||||||
|
where: {
|
||||||
|
company_code: companyCode,
|
||||||
|
control: {
|
||||||
|
path: ["triggerType"],
|
||||||
|
equals:
|
||||||
|
triggerType === "insert"
|
||||||
|
? "insert"
|
||||||
|
: triggerType === "update"
|
||||||
|
? ["update", "insert_update"]
|
||||||
|
: triggerType === "delete"
|
||||||
|
? "delete"
|
||||||
|
: triggerType,
|
||||||
|
},
|
||||||
|
plan: {
|
||||||
|
path: ["sourceTable"],
|
||||||
|
equals: tableName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Found ${diagrams.length} conditional connections for table ${tableName} with trigger ${triggerType}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 각 다이어그램에 대해 조건부 연결 실행
|
||||||
|
for (const diagram of diagrams) {
|
||||||
|
try {
|
||||||
|
const result = await this.executeDiagramTrigger(
|
||||||
|
diagram,
|
||||||
|
data,
|
||||||
|
companyCode
|
||||||
|
);
|
||||||
|
results.push(result);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error executing diagram ${diagram.diagram_id}:`, error);
|
||||||
|
results.push({
|
||||||
|
success: false,
|
||||||
|
executedActions: 0,
|
||||||
|
failedActions: 1,
|
||||||
|
errors: [error instanceof Error ? error.message : "Unknown error"],
|
||||||
|
executionTime: Date.now() - startTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error in executeEventTriggers:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 단일 다이어그램의 트리거 실행
|
||||||
|
*/
|
||||||
|
private static async executeDiagramTrigger(
|
||||||
|
diagram: any,
|
||||||
|
data: Record<string, any>,
|
||||||
|
companyCode: string
|
||||||
|
): Promise<ExecutionResult> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
let executedActions = 0;
|
||||||
|
let failedActions = 0;
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const control = diagram.control as ConditionControl;
|
||||||
|
const category = diagram.category as ConnectionCategory;
|
||||||
|
const plan = diagram.plan as ExecutionPlan;
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Executing diagram ${diagram.diagram_id} (${diagram.diagram_name})`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 조건 평가
|
||||||
|
if (control.conditionTree) {
|
||||||
|
const conditionMet = await this.evaluateCondition(
|
||||||
|
control.conditionTree,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (!conditionMet) {
|
||||||
|
logger.info(
|
||||||
|
`Conditions not met for diagram ${diagram.diagram_id}, skipping execution`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
executedActions: 0,
|
||||||
|
failedActions: 0,
|
||||||
|
errors: [],
|
||||||
|
executionTime: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 대상 액션들 실행
|
||||||
|
for (const action of plan.targetActions) {
|
||||||
|
if (!action.enabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.executeTargetAction(action, data, companyCode);
|
||||||
|
executedActions++;
|
||||||
|
|
||||||
|
if (category.enableLogging) {
|
||||||
|
logger.info(
|
||||||
|
`Successfully executed action ${action.id} on table ${action.targetTable}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
failedActions++;
|
||||||
|
const errorMsg =
|
||||||
|
error instanceof Error ? error.message : "Unknown error";
|
||||||
|
errors.push(`Action ${action.id}: ${errorMsg}`);
|
||||||
|
|
||||||
|
logger.error(`Failed to execute action ${action.id}:`, error);
|
||||||
|
|
||||||
|
// 오류 시 롤백 처리
|
||||||
|
if (category.rollbackOnError) {
|
||||||
|
logger.warn(`Rolling back due to error in action ${action.id}`);
|
||||||
|
// TODO: 롤백 로직 구현
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: failedActions === 0,
|
||||||
|
executedActions,
|
||||||
|
failedActions,
|
||||||
|
errors,
|
||||||
|
executionTime: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error executing diagram ${diagram.diagram_id}:`, error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
executedActions: 0,
|
||||||
|
failedActions: 1,
|
||||||
|
errors: [error instanceof Error ? error.message : "Unknown error"],
|
||||||
|
executionTime: Date.now() - startTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조건 평가
|
||||||
|
*/
|
||||||
|
private static async evaluateCondition(
|
||||||
|
condition: ConditionNode,
|
||||||
|
data: Record<string, any>
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (condition.type === "group") {
|
||||||
|
if (!condition.children || condition.children.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
condition.children.map((child) => this.evaluateCondition(child, data))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (condition.operator === "OR") {
|
||||||
|
return results.some((result) => result);
|
||||||
|
} else {
|
||||||
|
// AND
|
||||||
|
return results.every((result) => result);
|
||||||
|
}
|
||||||
|
} else if (condition.type === "condition") {
|
||||||
|
return this.evaluateSingleCondition(condition, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 단일 조건 평가
|
||||||
|
*/
|
||||||
|
private static evaluateSingleCondition(
|
||||||
|
condition: ConditionNode,
|
||||||
|
data: Record<string, any>
|
||||||
|
): boolean {
|
||||||
|
const { field, operator_type, value } = condition;
|
||||||
|
|
||||||
|
if (!field || !operator_type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldValue = data[field];
|
||||||
|
|
||||||
|
switch (operator_type) {
|
||||||
|
case "=":
|
||||||
|
return fieldValue == value;
|
||||||
|
case "!=":
|
||||||
|
return fieldValue != value;
|
||||||
|
case ">":
|
||||||
|
return Number(fieldValue) > Number(value);
|
||||||
|
case "<":
|
||||||
|
return Number(fieldValue) < Number(value);
|
||||||
|
case ">=":
|
||||||
|
return Number(fieldValue) >= Number(value);
|
||||||
|
case "<=":
|
||||||
|
return Number(fieldValue) <= Number(value);
|
||||||
|
case "LIKE":
|
||||||
|
return String(fieldValue).includes(String(value));
|
||||||
|
case "NOT_LIKE":
|
||||||
|
return !String(fieldValue).includes(String(value));
|
||||||
|
case "CONTAINS":
|
||||||
|
return String(fieldValue)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(String(value).toLowerCase());
|
||||||
|
case "STARTS_WITH":
|
||||||
|
return String(fieldValue).startsWith(String(value));
|
||||||
|
case "ENDS_WITH":
|
||||||
|
return String(fieldValue).endsWith(String(value));
|
||||||
|
case "IN":
|
||||||
|
return Array.isArray(value) && value.includes(fieldValue);
|
||||||
|
case "NOT_IN":
|
||||||
|
return Array.isArray(value) && !value.includes(fieldValue);
|
||||||
|
case "IS_NULL":
|
||||||
|
return fieldValue == null || fieldValue === undefined;
|
||||||
|
case "IS_NOT_NULL":
|
||||||
|
return fieldValue != null && fieldValue !== undefined;
|
||||||
|
case "BETWEEN":
|
||||||
|
if (Array.isArray(value) && value.length === 2) {
|
||||||
|
const numValue = Number(fieldValue);
|
||||||
|
return numValue >= Number(value[0]) && numValue <= Number(value[1]);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
case "NOT_BETWEEN":
|
||||||
|
if (Array.isArray(value) && value.length === 2) {
|
||||||
|
const numValue = Number(fieldValue);
|
||||||
|
return !(
|
||||||
|
numValue >= Number(value[0]) && numValue <= Number(value[1])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 대상 액션 실행
|
||||||
|
*/
|
||||||
|
private static async executeTargetAction(
|
||||||
|
action: TargetAction,
|
||||||
|
sourceData: Record<string, any>,
|
||||||
|
companyCode: string
|
||||||
|
): Promise<void> {
|
||||||
|
// 필드 매핑을 통해 대상 데이터 생성
|
||||||
|
const targetData: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const mapping of action.fieldMappings) {
|
||||||
|
let value = sourceData[mapping.sourceField];
|
||||||
|
|
||||||
|
// 변환 함수 적용
|
||||||
|
if (mapping.transformFunction) {
|
||||||
|
value = this.applyTransformFunction(value, mapping.transformFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기본값 설정
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
value = mapping.defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetData[mapping.targetField] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 회사 코드 추가
|
||||||
|
targetData.company_code = companyCode;
|
||||||
|
|
||||||
|
// 액션 타입별 실행
|
||||||
|
switch (action.actionType) {
|
||||||
|
case "insert":
|
||||||
|
await this.executeInsertAction(action.targetTable, targetData);
|
||||||
|
break;
|
||||||
|
case "update":
|
||||||
|
await this.executeUpdateAction(
|
||||||
|
action.targetTable,
|
||||||
|
targetData,
|
||||||
|
action.conditions
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
await this.executeDeleteAction(
|
||||||
|
action.targetTable,
|
||||||
|
targetData,
|
||||||
|
action.conditions
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "upsert":
|
||||||
|
await this.executeUpsertAction(action.targetTable, targetData);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported action type: ${action.actionType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* INSERT 액션 실행
|
||||||
|
*/
|
||||||
|
private static async executeInsertAction(
|
||||||
|
tableName: string,
|
||||||
|
data: Record<string, any>
|
||||||
|
): Promise<void> {
|
||||||
|
// 동적 테이블 INSERT 실행
|
||||||
|
const sql = `INSERT INTO ${tableName} (${Object.keys(data).join(", ")}) VALUES (${Object.keys(
|
||||||
|
data
|
||||||
|
)
|
||||||
|
.map(() => "?")
|
||||||
|
.join(", ")})`;
|
||||||
|
|
||||||
|
await prisma.$executeRawUnsafe(sql, ...Object.values(data));
|
||||||
|
logger.info(`Inserted data into ${tableName}:`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UPDATE 액션 실행
|
||||||
|
*/
|
||||||
|
private static async executeUpdateAction(
|
||||||
|
tableName: string,
|
||||||
|
data: Record<string, any>,
|
||||||
|
conditions?: ConditionNode
|
||||||
|
): Promise<void> {
|
||||||
|
// 조건이 없으면 실행하지 않음 (안전장치)
|
||||||
|
if (!conditions) {
|
||||||
|
throw new Error(
|
||||||
|
"UPDATE action requires conditions to prevent accidental mass updates"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 동적 테이블 UPDATE 실행
|
||||||
|
const setClause = Object.keys(data)
|
||||||
|
.map((key) => `${key} = ?`)
|
||||||
|
.join(", ");
|
||||||
|
const whereClause = this.buildWhereClause(conditions);
|
||||||
|
|
||||||
|
const sql = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause}`;
|
||||||
|
|
||||||
|
await prisma.$executeRawUnsafe(sql, ...Object.values(data));
|
||||||
|
logger.info(`Updated data in ${tableName}:`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE 액션 실행
|
||||||
|
*/
|
||||||
|
private static async executeDeleteAction(
|
||||||
|
tableName: string,
|
||||||
|
data: Record<string, any>,
|
||||||
|
conditions?: ConditionNode
|
||||||
|
): Promise<void> {
|
||||||
|
// 조건이 없으면 실행하지 않음 (안전장치)
|
||||||
|
if (!conditions) {
|
||||||
|
throw new Error(
|
||||||
|
"DELETE action requires conditions to prevent accidental mass deletions"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 동적 테이블 DELETE 실행
|
||||||
|
const whereClause = this.buildWhereClause(conditions);
|
||||||
|
const sql = `DELETE FROM ${tableName} WHERE ${whereClause}`;
|
||||||
|
|
||||||
|
await prisma.$executeRawUnsafe(sql);
|
||||||
|
logger.info(`Deleted data from ${tableName} with conditions`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UPSERT 액션 실행
|
||||||
|
*/
|
||||||
|
private static async executeUpsertAction(
|
||||||
|
tableName: string,
|
||||||
|
data: Record<string, any>
|
||||||
|
): Promise<void> {
|
||||||
|
// PostgreSQL UPSERT 구현
|
||||||
|
const columns = Object.keys(data);
|
||||||
|
const values = Object.values(data);
|
||||||
|
const conflictColumns = ["id", "company_code"]; // 기본 충돌 컬럼
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
INSERT INTO ${tableName} (${columns.join(", ")})
|
||||||
|
VALUES (${columns.map(() => "?").join(", ")})
|
||||||
|
ON CONFLICT (${conflictColumns.join(", ")})
|
||||||
|
DO UPDATE SET ${columns.map((col) => `${col} = EXCLUDED.${col}`).join(", ")}
|
||||||
|
`;
|
||||||
|
|
||||||
|
await prisma.$executeRawUnsafe(sql, ...values);
|
||||||
|
logger.info(`Upserted data into ${tableName}:`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WHERE 절 구성
|
||||||
|
*/
|
||||||
|
private static buildWhereClause(conditions: ConditionNode): string {
|
||||||
|
// 간단한 WHERE 절 구성 (실제 구현에서는 더 복잡한 로직 필요)
|
||||||
|
if (
|
||||||
|
conditions.type === "condition" &&
|
||||||
|
conditions.field &&
|
||||||
|
conditions.operator_type
|
||||||
|
) {
|
||||||
|
return `${conditions.field} ${conditions.operator_type} '${conditions.value}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "1=1"; // 기본값
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 변환 함수 적용
|
||||||
|
*/
|
||||||
|
private static applyTransformFunction(
|
||||||
|
value: any,
|
||||||
|
transformFunction: string
|
||||||
|
): any {
|
||||||
|
try {
|
||||||
|
// 안전한 변환 함수들만 허용
|
||||||
|
switch (transformFunction) {
|
||||||
|
case "UPPER":
|
||||||
|
return String(value).toUpperCase();
|
||||||
|
case "LOWER":
|
||||||
|
return String(value).toLowerCase();
|
||||||
|
case "TRIM":
|
||||||
|
return String(value).trim();
|
||||||
|
case "NOW":
|
||||||
|
return new Date();
|
||||||
|
case "UUID":
|
||||||
|
return require("crypto").randomUUID();
|
||||||
|
default:
|
||||||
|
logger.warn(`Unknown transform function: ${transformFunction}`);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Error applying transform function ${transformFunction}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조건부 연결 테스트 (개발/디버깅용)
|
||||||
|
*/
|
||||||
|
static async testConditionalConnection(
|
||||||
|
diagramId: number,
|
||||||
|
testData: Record<string, any>,
|
||||||
|
companyCode: string
|
||||||
|
): Promise<{ conditionMet: boolean; result?: ExecutionResult }> {
|
||||||
|
try {
|
||||||
|
const diagram = await prisma.dataflow_diagrams.findUnique({
|
||||||
|
where: { diagram_id: diagramId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!diagram) {
|
||||||
|
throw new Error(`Diagram ${diagramId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const control = diagram.control as ConditionControl;
|
||||||
|
|
||||||
|
// 조건 평가만 수행
|
||||||
|
const conditionMet = control.conditionTree
|
||||||
|
? await this.evaluateCondition(control.conditionTree, testData)
|
||||||
|
: true;
|
||||||
|
|
||||||
|
if (conditionMet) {
|
||||||
|
// 실제 실행 (테스트 모드)
|
||||||
|
const result = await this.executeDiagramTrigger(
|
||||||
|
diagram,
|
||||||
|
testData,
|
||||||
|
companyCode
|
||||||
|
);
|
||||||
|
return { conditionMet: true, result };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { conditionMet: false };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error testing conditional connection:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventTriggerService;
|
||||||
Loading…
Reference in New Issue