922 lines
30 KiB
TypeScript
922 lines
30 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;
|
||
|
||
console.log("🔍 관계 상세 정보:", {
|
||
connectionType,
|
||
hasExternalCallConfig: !!relationships.externalCallConfig,
|
||
externalCallConfig: relationships.externalCallConfig,
|
||
hasDataSaveConfig: !!relationships.dataSaveConfig,
|
||
dataSaveConfig: relationships.dataSaveConfig,
|
||
});
|
||
|
||
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 {
|
||
console.log("🔍 외부 호출 실행 시작 - relationships 구조:", relationships);
|
||
|
||
const externalCallConfig = relationships.externalCallConfig;
|
||
console.log("🔍 externalCallConfig:", externalCallConfig);
|
||
|
||
if (!externalCallConfig) {
|
||
console.error("❌ 외부 호출 설정이 없습니다. relationships 구조:", relationships);
|
||
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" && requestBody ? JSON.parse(requestBody) : formData,
|
||
};
|
||
|
||
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) {
|
||
console.log(`📥 데이터 매핑 설정 발견 - HTTP 메서드: ${restApiSettings.httpMethod}`);
|
||
console.log("📥 매핑 설정:", externalCallConfig.dataMappingConfig.inboundMapping);
|
||
console.log("📥 응답 데이터:", responseData);
|
||
|
||
await this.processInboundMapping(externalCallConfig.dataMappingConfig.inboundMapping, responseData, context);
|
||
} else {
|
||
console.log(`ℹ️ 데이터 매핑 설정이 없습니다 - HTTP 메서드: ${restApiSettings.httpMethod}`);
|
||
}
|
||
|
||
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})`);
|
||
console.log("📥 받은 formData:", formData);
|
||
console.log("📥 formData 키들:", Object.keys(formData));
|
||
|
||
// 🔥 UPDATE 액션의 경우 formData를 기본으로 시작 (기본키 포함)
|
||
const mappedData: Record<string, any> = action.actionType === "update" ? { ...formData } : {};
|
||
|
||
// 필드 매핑 처리 (기존 데이터에 덮어쓰기)
|
||
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;
|
||
console.log(`🔧 정적 값 매핑: ${mapping.targetField} = ${value}`);
|
||
} else {
|
||
// 필드 매핑 처리
|
||
const sourceField = mapping.fromField?.columnName;
|
||
if (sourceField && formData[sourceField] !== undefined) {
|
||
mappedData[mapping.toField.columnName] = formData[sourceField];
|
||
console.log(`🔧 필드 매핑: ${sourceField} → ${mapping.toField.columnName} = ${formData[sourceField]}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log("📋 최종 매핑된 데이터:", mappedData);
|
||
console.log("🔑 기본키 포함 여부 체크:", {
|
||
hasId: "id" in mappedData,
|
||
keys: Object.keys(mappedData),
|
||
values: Object.values(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 {
|
||
console.log(`💾 테이블 데이터 저장 시작: ${tableName}`, {
|
||
actionType,
|
||
data,
|
||
connection,
|
||
});
|
||
|
||
// 데이터 저장 API 호출 (apiClient 사용)
|
||
const response = await apiClient.post("/dataflow/execute-data-action", {
|
||
tableName,
|
||
data,
|
||
actionType,
|
||
connection,
|
||
});
|
||
|
||
console.log(`✅ 테이블 데이터 저장 성공: ${tableName}`, response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error("테이블 데이터 저장 오류:", error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 조건 평가
|
||
*/
|
||
private static evaluateConditions(
|
||
conditions: any[],
|
||
formData: Record<string, any>,
|
||
context: ButtonExecutionContext,
|
||
): boolean {
|
||
console.log("🔍 조건 평가 시작:", {
|
||
conditions,
|
||
formDataKeys: Object.keys(formData),
|
||
formData,
|
||
contextData: context.contextData,
|
||
});
|
||
|
||
for (const condition of conditions) {
|
||
const fieldValue = formData[condition.field];
|
||
const conditionValue = condition.value;
|
||
const operator = condition.operator;
|
||
|
||
console.log("🔍 개별 조건 검증:", {
|
||
field: condition.field,
|
||
operator,
|
||
expectedValue: conditionValue,
|
||
actualValue: fieldValue,
|
||
formDataHasField: condition.field in formData,
|
||
allFormDataKeys: Object.keys(formData),
|
||
});
|
||
|
||
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})`);
|
||
console.log("❌ 사용 가능한 필드들:", Object.keys(formData));
|
||
console.log("❌ 전체 formData:", formData);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
console.log("✅ 모든 조건 만족");
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 다양한 API 응답 구조에서 실제 데이터 추출
|
||
*/
|
||
private static extractActualData(responseData: any): any {
|
||
console.log(`🔍 데이터 추출 시작 - 원본 타입: ${typeof responseData}`);
|
||
|
||
// null이나 undefined인 경우
|
||
if (!responseData) {
|
||
console.log("⚠️ 응답 데이터가 null 또는 undefined");
|
||
return [];
|
||
}
|
||
|
||
// 이미 배열인 경우 (직접 배열 응답)
|
||
if (Array.isArray(responseData)) {
|
||
console.log("✅ 직접 배열 응답 감지");
|
||
return responseData;
|
||
}
|
||
|
||
// 문자열인 경우 JSON 파싱 시도
|
||
if (typeof responseData === "string") {
|
||
console.log("🔄 JSON 문자열 파싱 시도");
|
||
try {
|
||
const parsed = JSON.parse(responseData);
|
||
console.log("✅ JSON 파싱 성공, 재귀 호출");
|
||
return this.extractActualData(parsed);
|
||
} catch (error) {
|
||
console.log("⚠️ JSON 파싱 실패, 원본 문자열 반환:", error);
|
||
return [responseData];
|
||
}
|
||
}
|
||
|
||
// 객체가 아닌 경우 (숫자 등)
|
||
if (typeof responseData !== "object") {
|
||
console.log(`⚠️ 객체가 아닌 응답: ${typeof responseData}`);
|
||
return [responseData];
|
||
}
|
||
|
||
// 일반적인 데이터 필드명들을 우선순위대로 확인
|
||
const commonDataFields = [
|
||
"data", // { data: [...] }
|
||
"result", // { result: [...] }
|
||
"results", // { results: [...] }
|
||
"items", // { items: [...] }
|
||
"list", // { list: [...] }
|
||
"records", // { records: [...] }
|
||
"rows", // { rows: [...] }
|
||
"content", // { content: [...] }
|
||
"payload", // { payload: [...] }
|
||
"response", // { response: [...] }
|
||
];
|
||
|
||
for (const field of commonDataFields) {
|
||
if (responseData[field] !== undefined) {
|
||
console.log(`✅ '${field}' 필드에서 데이터 추출`);
|
||
|
||
const extractedData = responseData[field];
|
||
|
||
// 추출된 데이터가 문자열인 경우 JSON 파싱 시도
|
||
if (typeof extractedData === "string") {
|
||
console.log("🔄 추출된 데이터가 JSON 문자열, 파싱 시도");
|
||
try {
|
||
const parsed = JSON.parse(extractedData);
|
||
console.log("✅ JSON 파싱 성공, 재귀 호출");
|
||
return this.extractActualData(parsed);
|
||
} catch (error) {
|
||
console.log("⚠️ JSON 파싱 실패, 원본 문자열 반환:", error);
|
||
return [extractedData];
|
||
}
|
||
}
|
||
|
||
// 추출된 데이터가 객체이고 또 다른 중첩 구조일 수 있으므로 재귀 호출
|
||
if (typeof extractedData === "object" && !Array.isArray(extractedData)) {
|
||
console.log("🔄 중첩된 객체 감지, 재귀 추출 시도");
|
||
return this.extractActualData(extractedData);
|
||
}
|
||
|
||
return extractedData;
|
||
}
|
||
}
|
||
|
||
// 특별한 필드가 없는 경우, 객체의 값들 중에서 배열을 찾기
|
||
const objectValues = Object.values(responseData);
|
||
const arrayValue = objectValues.find((value) => Array.isArray(value));
|
||
|
||
if (arrayValue) {
|
||
console.log("✅ 객체 값 중 배열 발견");
|
||
return arrayValue;
|
||
}
|
||
|
||
// 객체의 값들 중에서 객체를 찾아서 재귀 탐색
|
||
for (const value of objectValues) {
|
||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||
console.log("🔄 객체 값에서 재귀 탐색");
|
||
const nestedResult = this.extractActualData(value);
|
||
if (Array.isArray(nestedResult) && nestedResult.length > 0) {
|
||
return nestedResult;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 모든 시도가 실패한 경우, 원본 객체를 단일 항목 배열로 반환
|
||
console.log("📦 원본 객체를 단일 항목으로 처리");
|
||
return [responseData];
|
||
}
|
||
|
||
/**
|
||
* 인바운드 데이터 매핑 처리
|
||
*/
|
||
private static async processInboundMapping(
|
||
inboundMapping: any,
|
||
responseData: any,
|
||
context: ButtonExecutionContext,
|
||
): Promise<void> {
|
||
try {
|
||
console.log("📥 인바운드 데이터 매핑 처리 시작");
|
||
console.log("📥 원본 응답 데이터:", responseData);
|
||
|
||
const targetTable = inboundMapping.targetTable;
|
||
const fieldMappings = inboundMapping.fieldMappings || [];
|
||
const insertMode = inboundMapping.insertMode || "insert";
|
||
|
||
console.log("📥 매핑 설정:", {
|
||
targetTable,
|
||
fieldMappings,
|
||
insertMode,
|
||
});
|
||
|
||
// 응답 데이터에서 실제 데이터 추출 (다양한 구조 지원)
|
||
const actualData = this.extractActualData(responseData);
|
||
|
||
console.log("📥 추출된 실제 데이터:", actualData);
|
||
|
||
// 배열이 아닌 경우 배열로 변환
|
||
const dataArray = Array.isArray(actualData) ? actualData : [actualData];
|
||
|
||
console.log("📥 처리할 데이터 배열:", dataArray);
|
||
|
||
if (dataArray.length === 0) {
|
||
console.log("⚠️ 처리할 데이터가 없습니다");
|
||
return;
|
||
}
|
||
|
||
for (const item of dataArray) {
|
||
const mappedData: Record<string, any> = {};
|
||
|
||
console.log("📥 개별 아이템 처리:", item);
|
||
|
||
// 필드 매핑 적용
|
||
for (const mapping of fieldMappings) {
|
||
const sourceValue = item[mapping.sourceField];
|
||
console.log(`📥 필드 매핑: ${mapping.sourceField} -> ${mapping.targetField} = ${sourceValue}`);
|
||
|
||
if (sourceValue !== undefined && sourceValue !== null) {
|
||
mappedData[mapping.targetField] = sourceValue;
|
||
}
|
||
}
|
||
|
||
console.log("📋 매핑된 데이터:", mappedData);
|
||
|
||
// 매핑된 데이터가 비어있지 않은 경우에만 저장
|
||
if (Object.keys(mappedData).length > 0) {
|
||
await this.saveDataToTable(targetTable, mappedData, insertMode);
|
||
} else {
|
||
console.log("⚠️ 매핑된 데이터가 비어있어 저장을 건너뜁니다");
|
||
}
|
||
}
|
||
|
||
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 || "작업 중 오류가 발생했습니다.");
|
||
}
|
||
}
|