ERP-node/frontend/lib/utils/nodeFlowButtonExecutor.ts

190 lines
5.4 KiB
TypeScript
Raw Normal View History

feat: 노드 기반 데이터 플로우 시스템 구현 - 노드 에디터 UI 구현 (React Flow 기반) - TableSource, DataTransform, INSERT, UPDATE, DELETE, UPSERT 노드 - 드래그앤드롭 노드 추가 및 연결 - 속성 패널을 통한 노드 설정 - 실시간 필드 라벨 표시 (column_labels 테이블 연동) - 데이터 변환 노드 (DataTransform) 기능 - EXPLODE: 구분자로 1개 행 → 여러 행 확장 - UPPERCASE, LOWERCASE, TRIM, CONCAT, SPLIT, REPLACE 등 12가지 변환 타입 - In-place 변환 지원 (타겟 필드 생략 시 소스 필드 덮어쓰기) - 변환된 필드가 하위 액션 노드에 자동 전달 - 노드 플로우 실행 엔진 - 위상 정렬을 통한 노드 실행 순서 결정 - 레벨별 병렬 실행 (Promise.allSettled) - 부분 실패 허용 (한 노드 실패 시 연결된 하위 노드만 스킵) - 트랜잭션 기반 안전한 데이터 처리 - UPSERT 액션 로직 구현 - DB 제약 조건 없이 SELECT → UPDATE or INSERT 방식 - 복합 충돌 키 지원 (예: sales_no + product_name) - 파라미터 인덱스 정확한 매핑 - 데이터 소스 자동 감지 - 테이블 선택 데이터 (selectedRowsData) 자동 주입 - 폼 입력 데이터 (formData) 자동 주입 - TableSource 노드가 외부 데이터 우선 사용 - 버튼 컴포넌트 통합 - 기존 관계 실행 + 새 노드 플로우 실행 하이브리드 지원 - 노드 플로우 선택 UI 추가 - API 클라이언트 통합 (Axios) - 개발 문서 작성 - 노드 기반 제어 시스템 개선 계획 - 노드 연결 규칙 설계 - 노드 실행 엔진 설계 - 노드 구조 개선안 - 버튼 통합 분석
2025-10-02 16:22:29 +09:00
/**
*
*/
import { executeNodeFlow, ExecutionResult } from "../api/nodeFlows";
import { logger } from "../utils/logger";
import { toast } from "sonner";
import type { ButtonDataflowConfig, ExtendedControlContext } from "@/types/control-management";
export interface ButtonExecutionContext {
buttonId: string;
screenId?: number;
companyCode?: string;
userId?: string;
formData: Record<string, any>;
selectedRows?: any[];
selectedRowsData?: Record<string, any>[];
controlDataSource?: "form" | "table-selection" | "both";
onRefresh?: () => void;
onClose?: () => void;
}
export interface FlowExecutionResult {
success: boolean;
message: string;
executionTime?: number;
data?: ExecutionResult;
}
/**
*
*/
export async function executeButtonWithFlow(
flowConfig: ButtonDataflowConfig["flowConfig"],
context: ButtonExecutionContext,
originalAction?: () => Promise<void>,
): Promise<FlowExecutionResult> {
if (!flowConfig) {
throw new Error("플로우 설정이 없습니다.");
}
const { flowId, flowName, executionTiming = "before" } = flowConfig;
logger.info(`🚀 노드 플로우 실행 시작:`, {
flowId,
flowName,
timing: executionTiming,
contextKeys: Object.keys(context),
});
try {
// 컨텍스트 데이터 준비
const contextData = prepareContextData(context);
// 타이밍에 따라 실행
switch (executionTiming) {
case "before":
// 1. 플로우 먼저 실행
const beforeResult = await executeNodeFlow(flowId, contextData);
if (!beforeResult.success) {
toast.error(`플로우 실행 실패: ${beforeResult.message}`);
return {
success: false,
message: beforeResult.message,
data: beforeResult,
};
}
toast.success(`플로우 실행 완료: ${flowName}`);
// 2. 원래 버튼 액션 실행
if (originalAction) {
await originalAction();
}
return {
success: true,
message: "플로우 및 버튼 액션이 성공적으로 실행되었습니다.",
executionTime: beforeResult.executionTime,
data: beforeResult,
};
case "after":
// 1. 원래 버튼 액션 먼저 실행
if (originalAction) {
await originalAction();
}
// 2. 플로우 실행
const afterResult = await executeNodeFlow(flowId, contextData);
if (!afterResult.success) {
toast.warning(`버튼 액션은 성공했으나 플로우 실행 실패: ${afterResult.message}`);
} else {
toast.success(`플로우 실행 완료: ${flowName}`);
}
return {
success: afterResult.success,
message: afterResult.message,
executionTime: afterResult.executionTime,
data: afterResult,
};
case "replace":
// 플로우만 실행 (원래 액션 대체)
const replaceResult = await executeNodeFlow(flowId, contextData);
if (!replaceResult.success) {
toast.error(`플로우 실행 실패: ${replaceResult.message}`);
} else {
toast.success(`플로우 실행 완료: ${flowName}`, {
description: `${replaceResult.summary.success}/${replaceResult.summary.total} 노드 성공`,
});
}
return {
success: replaceResult.success,
message: replaceResult.message,
executionTime: replaceResult.executionTime,
data: replaceResult,
};
default:
throw new Error(`지원하지 않는 실행 타이밍: ${executionTiming}`);
}
} catch (error) {
logger.error("플로우 실행 오류:", error);
const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.";
toast.error(`플로우 실행 오류: ${errorMessage}`);
return {
success: false,
message: errorMessage,
};
}
}
/**
*
*/
function prepareContextData(context: ButtonExecutionContext): Record<string, any> {
return {
buttonId: context.buttonId,
screenId: context.screenId,
companyCode: context.companyCode,
userId: context.userId,
formData: context.formData || {},
selectedRowsData: context.selectedRowsData || [],
controlDataSource: context.controlDataSource || "form",
};
}
/**
*
*/
export function handleFlowExecutionResult(result: FlowExecutionResult, context: ButtonExecutionContext): void {
if (result.success) {
logger.info("✅ 플로우 실행 성공:", result);
// 성공 시 데이터 새로고침
if (context.onRefresh) {
context.onRefresh();
}
// 실행 결과 요약 표시
if (result.data) {
const { summary } = result.data;
console.log("📊 플로우 실행 요약:", {
전체: summary.total,
성공: summary.success,
실패: summary.failed,
스킵: summary.skipped,
: `${result.executionTime}ms`,
});
}
} else {
logger.error("❌ 플로우 실행 실패:", result);
// 실패한 노드 정보 표시
if (result.data) {
const failedNodes = result.data.nodes.filter((n) => n.status === "failed");
if (failedNodes.length > 0) {
console.error("❌ 실패한 노드들:", failedNodes);
}
}
}
}