782 lines
23 KiB
TypeScript
782 lines
23 KiB
TypeScript
|
|
/**
|
||
|
|
* 🔥 개선된 버튼 액션 실행기
|
||
|
|
*
|
||
|
|
* 계획서에 따른 새로운 실행 플로우:
|
||
|
|
* 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 || "작업 중 오류가 발생했습니다.");
|
||
|
|
}
|
||
|
|
}
|