import { PrismaClient } from "@prisma/client"; import { logger } from "../utils/logger"; const prisma = new PrismaClient(); // 조건 노드 타입 정의 interface ConditionNode { type: "group" | "condition"; operator?: "AND" | "OR"; children?: ConditionNode[]; field?: string; operator_type?: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE"; 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, companyCode: string ): Promise { 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, companyCode: string ): Promise { const startTime = Date.now(); let executedActions = 0; let failedActions = 0; const errors: string[] = []; try { const control = diagram.control as unknown as ConditionControl; const category = diagram.category as unknown as ConnectionCategory; const plan = diagram.plan as unknown 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 ): Promise { 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 ): 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)); default: return false; } } /** * 대상 액션 실행 */ private static async executeTargetAction( action: TargetAction, sourceData: Record, companyCode: string ): Promise { // 필드 매핑을 통해 대상 데이터 생성 const targetData: Record = {}; 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 ): Promise { // 동적 테이블 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, conditions?: ConditionNode ): Promise { // 조건이 없으면 실행하지 않음 (안전장치) 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, conditions?: ConditionNode ): Promise { // 조건이 없으면 실행하지 않음 (안전장치) 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 ): Promise { // 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, 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 unknown 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;