ERP-node/frontend/lib/utils/improvedButtonActionExecuto...

782 lines
23 KiB
TypeScript
Raw Normal View History

2025-09-29 12:17:10 +09:00
/**
* 🔥
*
* :
* 1. Before
* 2. (replace가 )
* 3. After
*/
import { toast } from "sonner";
import { apiClient } from "@/lib/api/client";
import { ExtendedButtonTypeConfig, ButtonDataflowConfig } from "@/types/control-management";
import { ButtonActionType } from "@/types/unified-core";
// ===== 인터페이스 정의 =====
export interface ButtonExecutionContext {
buttonId: string;
screenId: string;
userId: string;
companyCode: string;
startTime: number;
formData?: Record<string, any>;
selectedRows?: any[];
tableData?: any[];
}
export interface ExecutionResult {
success: boolean;
message: string;
executionTime: number;
data?: any;
error?: string;
}
export interface ButtonExecutionResult {
success: boolean;
results: ExecutionResult[];
executionTime: number;
error?: string;
}
interface ControlConfig {
type: "relationship";
relationshipConfig: {
relationshipId: string;
relationshipName: string;
executionTiming: "before" | "after" | "replace";
contextData?: Record<string, any>;
};
}
interface ExecutionPlan {
beforeControls: ControlConfig[];
afterControls: ControlConfig[];
hasReplaceControl: boolean;
}
// ===== 메인 실행기 클래스 =====
export class ImprovedButtonActionExecutor {
/**
* 🔥
*/
static async executeButtonAction(
buttonConfig: ExtendedButtonTypeConfig,
formData: Record<string, any>,
context: ButtonExecutionContext
): Promise<ButtonExecutionResult> {
console.log("🔥 ImprovedButtonActionExecutor 시작:", {
buttonConfig,
formData,
context,
});
const executionPlan = this.createExecutionPlan(buttonConfig);
const results: ExecutionResult[] = [];
console.log("📋 생성된 실행 계획:", {
beforeControls: executionPlan.beforeControls,
afterControls: executionPlan.afterControls,
hasReplaceControl: executionPlan.hasReplaceControl,
});
try {
console.log("🚀 버튼 액션 실행 시작:", {
actionType: buttonConfig.actionType,
hasControls: executionPlan.beforeControls.length + executionPlan.afterControls.length > 0,
hasReplace: executionPlan.hasReplaceControl,
});
// 1. Before 타이밍 제어 실행
if (executionPlan.beforeControls.length > 0) {
console.log("⏰ Before 제어 실행 시작");
const beforeResults = await this.executeControls(
executionPlan.beforeControls,
formData,
context
);
results.push(...beforeResults);
// Before 제어 중 실패가 있으면 중단
const hasFailure = beforeResults.some(r => !r.success);
if (hasFailure) {
throw new Error("Before 제어 실행 중 오류가 발생했습니다.");
}
}
// 2. 메인 액션 실행 (replace가 아닌 경우에만)
if (!executionPlan.hasReplaceControl) {
console.log("⚡ 메인 액션 실행:", buttonConfig.actionType);
const mainResult = await this.executeMainAction(
buttonConfig,
formData,
context
);
results.push(mainResult);
if (!mainResult.success) {
throw new Error("메인 액션 실행 중 오류가 발생했습니다.");
}
} else {
console.log("🔄 Replace 모드: 메인 액션 건너뜀");
}
// 3. After 타이밍 제어 실행
if (executionPlan.afterControls.length > 0) {
console.log("⏰ After 제어 실행 시작");
const afterResults = await this.executeControls(
executionPlan.afterControls,
formData,
context
);
results.push(...afterResults);
}
const totalExecutionTime = Date.now() - context.startTime;
console.log("✅ 버튼 액션 실행 완료:", `${totalExecutionTime}ms`);
return {
success: true,
results,
executionTime: totalExecutionTime,
};
} catch (error) {
console.error("❌ 버튼 액션 실행 실패:", error);
// 롤백 처리
await this.handleExecutionError(error, results, buttonConfig);
return {
success: false,
results,
executionTime: Date.now() - context.startTime,
error: error.message,
};
}
}
/**
* 🔥
*/
private static createExecutionPlan(buttonConfig: ExtendedButtonTypeConfig): ExecutionPlan {
const plan: ExecutionPlan = {
beforeControls: [],
afterControls: [],
hasReplaceControl: false,
};
const dataflowConfig = buttonConfig.dataflowConfig;
if (!dataflowConfig) {
console.log("⚠️ dataflowConfig가 없습니다");
return plan;
}
// enableDataflowControl 체크를 제거하고 dataflowConfig만 있으면 실행
console.log("📋 실행 계획 생성:", {
controlMode: dataflowConfig.controlMode,
hasRelationshipConfig: !!dataflowConfig.relationshipConfig,
enableDataflowControl: buttonConfig.enableDataflowControl,
});
// 관계 기반 제어만 지원
if (dataflowConfig.controlMode === "relationship" && dataflowConfig.relationshipConfig) {
const control: ControlConfig = {
type: "relationship",
relationshipConfig: dataflowConfig.relationshipConfig,
};
switch (dataflowConfig.relationshipConfig.executionTiming) {
case "before":
plan.beforeControls.push(control);
break;
case "after":
plan.afterControls.push(control);
break;
case "replace":
plan.afterControls.push(control); // Replace는 after로 처리하되 플래그 설정
plan.hasReplaceControl = true;
break;
}
}
return plan;
}
/**
* 🔥 ( )
*/
private static async executeControls(
controls: ControlConfig[],
formData: Record<string, any>,
context: ButtonExecutionContext
): Promise<ExecutionResult[]> {
const results: ExecutionResult[] = [];
for (const control of controls) {
try {
// 관계 실행만 지원
const result = await this.executeRelationship(
control.relationshipConfig,
formData,
context
);
results.push(result);
// 제어 실행 실패 시 중단
if (!result.success) {
throw new Error(result.message);
}
} catch (error) {
console.error(`제어 실행 실패 (${control.type}):`, error);
results.push({
success: false,
message: `${control.type} 제어 실행 실패: ${error.message}`,
executionTime: 0,
error: error.message,
});
throw error;
}
}
return results;
}
/**
* 🔥
*/
private static async executeRelationship(
config: {
relationshipId: string;
relationshipName: string;
executionTiming: "before" | "after" | "replace";
contextData?: Record<string, any>;
},
formData: Record<string, any>,
context: ButtonExecutionContext
): Promise<ExecutionResult> {
try {
console.log(`🔗 관계 실행 시작: ${config.relationshipName} (ID: ${config.relationshipId})`);
// 1. 관계 정보 조회
const relationshipData = await this.getRelationshipData(config.relationshipId);
if (!relationshipData) {
throw new Error(`관계 정보를 찾을 수 없습니다: ${config.relationshipId}`);
}
console.log(`📋 관계 데이터 로드 완료:`, relationshipData);
// 2. 관계 타입에 따른 실행
const relationships = relationshipData.relationships;
const connectionType = relationships.connectionType;
let result: ExecutionResult;
if (connectionType === "external_call") {
// 외부 호출 실행
result = await this.executeExternalCall(relationships, formData, context);
} else if (connectionType === "data_save") {
// 데이터 저장 실행
result = await this.executeDataSave(relationships, formData, context);
} else {
throw new Error(`지원하지 않는 연결 타입: ${connectionType}`);
}
console.log(`✅ 관계 실행 완료: ${config.relationshipName}`, result);
if (result.success) {
toast.success(`관계 '${config.relationshipName}' 실행 완료`);
} else {
toast.error(`관계 '${config.relationshipName}' 실행 실패: ${result.message}`);
}
return result;
} catch (error: any) {
console.error(`❌ 관계 실행 실패: ${config.relationshipName}`, error);
const errorResult = {
success: false,
message: `관계 '${config.relationshipName}' 실행 실패: ${error.message}`,
executionTime: 0,
error: error.message,
};
toast.error(errorResult.message);
return errorResult;
}
}
/**
*
*/
private static async getRelationshipData(relationshipId: string): Promise<any> {
try {
console.log(`🔍 관계 데이터 조회 시작: ${relationshipId}`);
const response = await apiClient.get(`/dataflow-diagrams/${relationshipId}`);
console.log(`✅ 관계 데이터 조회 성공:`, response.data);
if (!response.data.success) {
throw new Error(response.data.message || '관계 데이터 조회 실패');
}
return response.data.data;
} catch (error) {
console.error('관계 데이터 조회 오류:', error);
throw error;
}
}
/**
*
*/
private static async executeExternalCall(
relationships: any,
formData: Record<string, any>,
context: ButtonExecutionContext
): Promise<ExecutionResult> {
try {
const externalCallConfig = relationships.externalCallConfig;
if (!externalCallConfig) {
throw new Error('외부 호출 설정이 없습니다');
}
const restApiSettings = externalCallConfig.restApiSettings;
if (!restApiSettings) {
throw new Error('REST API 설정이 없습니다');
}
console.log(`🌐 외부 API 호출: ${restApiSettings.apiUrl}`);
// API 호출 준비
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...restApiSettings.headers,
};
// 인증 처리
if (restApiSettings.authentication?.type === 'api-key') {
headers['Authorization'] = `Bearer ${restApiSettings.authentication.apiKey}`;
}
// 요청 바디 준비 (템플릿 처리)
let requestBody = restApiSettings.bodyTemplate || '';
if (requestBody) {
// 간단한 템플릿 치환 ({{변수명}} 형태)
requestBody = requestBody.replace(/\{\{(\w+)\}\}/g, (match: string, key: string) => {
return formData[key] || (context as any).contextData?.[key] || new Date().toISOString();
});
}
// 백엔드 프록시를 통한 외부 API 호출 (CORS 문제 해결)
console.log(`🌐 백엔드 프록시를 통한 외부 API 호출 준비:`, {
originalUrl: restApiSettings.apiUrl,
method: restApiSettings.httpMethod || 'GET',
headers,
body: restApiSettings.httpMethod !== 'GET' ? requestBody : undefined,
});
// 백엔드 프록시 API 호출 - GenericApiSettings 형식에 맞게 전달
const requestPayload = {
diagramId: relationships.diagramId || 45, // 관계 ID 사용
relationshipId: relationships.relationshipId || "relationship-45",
settings: {
callType: "rest-api",
apiType: "generic",
url: restApiSettings.apiUrl,
method: restApiSettings.httpMethod || 'POST',
headers: restApiSettings.headers || {},
body: requestBody,
authentication: restApiSettings.authentication || { type: 'none' },
timeout: restApiSettings.timeout || 30000,
retryCount: restApiSettings.retryCount || 3,
},
templateData: restApiSettings.httpMethod !== 'GET' ? JSON.parse(requestBody) : {},
};
console.log(`📤 백엔드로 전송할 데이터:`, requestPayload);
const proxyResponse = await apiClient.post(`/external-calls/execute`, requestPayload);
console.log(`📡 백엔드 프록시 응답:`, proxyResponse.data);
if (!proxyResponse.data.success) {
throw new Error(`프록시 API 호출 실패: ${proxyResponse.data.error || proxyResponse.data.message}`);
}
const responseData = proxyResponse.data.result;
console.log(`✅ 외부 API 호출 성공 (프록시):`, responseData);
// 데이터 매핑 처리 (inbound mapping)
if (externalCallConfig.dataMappingConfig?.inboundMapping) {
await this.processInboundMapping(
externalCallConfig.dataMappingConfig.inboundMapping,
responseData,
context
);
}
return {
success: true,
message: '외부 호출 실행 완료',
executionTime: Date.now() - context.startTime,
data: responseData,
};
} catch (error: any) {
console.error('외부 호출 실행 오류:', error);
return {
success: false,
message: `외부 호출 실행 실패: ${error.message}`,
executionTime: Date.now() - context.startTime,
error: error.message,
};
}
}
/**
*
*/
private static async executeDataSave(
relationships: any,
formData: Record<string, any>,
context: ButtonExecutionContext
): Promise<ExecutionResult> {
try {
console.log(`💾 데이터 저장 실행 시작`);
// 제어 조건 확인
const controlConditions = relationships.controlConditions || [];
if (controlConditions.length > 0) {
const conditionsMet = this.evaluateConditions(controlConditions, formData, context);
if (!conditionsMet) {
return {
success: false,
message: '제어 조건을 만족하지 않아 데이터 저장을 건너뜁니다',
executionTime: Date.now() - context.startTime,
};
}
}
// 액션 그룹 실행
const actionGroups = relationships.actionGroups || [];
const results = [];
for (const actionGroup of actionGroups) {
if (!actionGroup.isEnabled) {
console.log(`⏭️ 비활성화된 액션 그룹 건너뜀: ${actionGroup.name}`);
continue;
}
console.log(`🎯 액션 그룹 실행: ${actionGroup.name}`);
for (const action of actionGroup.actions) {
if (!action.isEnabled) {
console.log(`⏭️ 비활성화된 액션 건너뜀: ${action.name}`);
continue;
}
const actionResult = await this.executeDataAction(
action,
relationships,
formData,
context
);
results.push(actionResult);
if (!actionResult.success) {
console.error(`❌ 액션 실행 실패: ${action.name}`, actionResult);
}
}
}
const successCount = results.filter(r => r.success).length;
const totalCount = results.length;
return {
success: successCount > 0,
message: `데이터 저장 완료: ${successCount}/${totalCount} 액션 성공`,
executionTime: Date.now() - context.startTime,
data: {
results,
successCount,
totalCount,
},
};
} catch (error: any) {
console.error('데이터 저장 실행 오류:', error);
return {
success: false,
message: `데이터 저장 실행 실패: ${error.message}`,
executionTime: Date.now() - context.startTime,
error: error.message,
};
}
}
/**
*
*/
private static async executeDataAction(
action: any,
relationships: any,
formData: Record<string, any>,
context: ButtonExecutionContext
): Promise<ExecutionResult> {
try {
console.log(`🔧 데이터 액션 실행: ${action.name} (${action.actionType})`);
// 필드 매핑 처리
const mappedData: Record<string, any> = {};
for (const mapping of action.fieldMappings) {
if (mapping.valueType === 'static') {
// 정적 값 처리
let value = mapping.value;
if (value === '#NOW') {
value = new Date().toISOString();
}
mappedData[mapping.targetField] = value;
} else {
// 필드 매핑 처리
const sourceField = mapping.fromField?.columnName;
if (sourceField && formData[sourceField] !== undefined) {
mappedData[mapping.toField.columnName] = formData[sourceField];
}
}
}
console.log(`📋 매핑된 데이터:`, mappedData);
// 대상 연결 정보
const toConnection = relationships.toConnection;
const targetTable = relationships.toTable?.tableName;
if (!targetTable) {
throw new Error('대상 테이블이 지정되지 않았습니다');
}
// 데이터 저장 API 호출
const saveResult = await this.saveDataToTable(
targetTable,
mappedData,
action.actionType,
toConnection
);
return {
success: true,
message: `데이터 액션 "${action.name}" 실행 완료`,
executionTime: Date.now() - context.startTime,
data: saveResult,
};
} catch (error: any) {
console.error(`데이터 액션 실행 오류: ${action.name}`, error);
return {
success: false,
message: `데이터 액션 실행 실패: ${error.message}`,
executionTime: Date.now() - context.startTime,
error: error.message,
};
}
}
/**
*
*/
private static async saveDataToTable(
tableName: string,
data: Record<string, any>,
actionType: string,
connection?: any
): Promise<any> {
try {
// 데이터 저장 API 호출
const response = await fetch('/api/dataflow/execute-data-action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
body: JSON.stringify({
tableName,
data,
actionType,
connection,
}),
});
if (!response.ok) {
throw new Error(`데이터 저장 API 호출 실패: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('데이터 저장 오류:', error);
throw error;
}
}
/**
*
*/
private static evaluateConditions(
conditions: any[],
formData: Record<string, any>,
context: ButtonExecutionContext
): boolean {
for (const condition of conditions) {
const fieldValue = formData[condition.field];
const conditionValue = condition.value;
const operator = condition.operator;
let conditionMet = false;
switch (operator) {
case '=':
conditionMet = fieldValue === conditionValue;
break;
case '!=':
conditionMet = fieldValue !== conditionValue;
break;
case '>':
conditionMet = Number(fieldValue) > Number(conditionValue);
break;
case '<':
conditionMet = Number(fieldValue) < Number(conditionValue);
break;
case '>=':
conditionMet = Number(fieldValue) >= Number(conditionValue);
break;
case '<=':
conditionMet = Number(fieldValue) <= Number(conditionValue);
break;
default:
console.warn(`지원하지 않는 연산자: ${operator}`);
conditionMet = true;
}
if (!conditionMet) {
console.log(`❌ 조건 불만족: ${condition.field} ${operator} ${conditionValue} (실제값: ${fieldValue})`);
return false;
}
}
console.log(`✅ 모든 조건 만족`);
return true;
}
/**
*
*/
private static async processInboundMapping(
inboundMapping: any,
responseData: any,
context: ButtonExecutionContext
): Promise<void> {
try {
console.log(`📥 인바운드 데이터 매핑 처리 시작`);
const targetTable = inboundMapping.targetTable;
const fieldMappings = inboundMapping.fieldMappings || [];
const insertMode = inboundMapping.insertMode || 'insert';
// 응답 데이터가 배열인 경우 각 항목 처리
const dataArray = Array.isArray(responseData) ? responseData : [responseData];
for (const item of dataArray) {
const mappedData: Record<string, any> = {};
// 필드 매핑 적용
for (const mapping of fieldMappings) {
const sourceValue = item[mapping.sourceField];
if (sourceValue !== undefined) {
mappedData[mapping.targetField] = sourceValue;
}
}
console.log(`📋 매핑된 데이터:`, mappedData);
// 데이터 저장
await this.saveDataToTable(targetTable, mappedData, insertMode);
}
console.log(`✅ 인바운드 데이터 매핑 완료`);
} catch (error) {
console.error('인바운드 데이터 매핑 오류:', error);
throw error;
}
}
/**
* 🔥
*/
private static async executeMainAction(
buttonConfig: ExtendedButtonTypeConfig,
formData: Record<string, any>,
context: ButtonExecutionContext
): Promise<ExecutionResult> {
try {
// 기존 ButtonActionExecutor 로직을 여기서 호출하거나
// 간단한 액션들을 직접 구현
const startTime = performance.now();
// 임시 구현 - 실제로는 기존 ButtonActionExecutor를 호출해야 함
const result = {
success: true,
message: `${buttonConfig.actionType} 액션 실행 완료`,
executionTime: performance.now() - startTime,
data: { actionType: buttonConfig.actionType, formData },
};
console.log("✅ 메인 액션 실행 완료:", result.message);
return result;
} catch (error) {
console.error("메인 액션 실행 오류:", error);
return {
success: false,
message: `${buttonConfig.actionType} 액션 실행 실패: ${error.message}`,
executionTime: 0,
error: error.message,
};
}
}
/**
* 🔥
*/
private static async handleExecutionError(
error: Error,
results: ExecutionResult[],
buttonConfig: ExtendedButtonTypeConfig
): Promise<void> {
console.error("🔄 실행 오류 처리 시작:", error.message);
// 롤백이 필요한 경우 처리
const rollbackNeeded = buttonConfig.dataflowConfig?.executionOptions?.rollbackOnError;
if (rollbackNeeded) {
console.log("🔄 롤백 처리 시작...");
// 성공한 결과들을 역순으로 롤백
const successfulResults = results.filter(r => r.success).reverse();
for (const result of successfulResults) {
try {
// 롤백 로직 구현 (필요시)
console.log("🔄 롤백:", result.message);
} catch (rollbackError) {
console.error("롤백 실패:", rollbackError);
}
}
}
// 오류 토스트 표시
toast.error(error.message || "작업 중 오류가 발생했습니다.");
}
}