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;
|
2025-09-29 13:32:59 +09:00
|
|
|
|
|
|
|
|
|
|
console.log(`🔍 관계 상세 정보:`, {
|
|
|
|
|
|
connectionType,
|
|
|
|
|
|
hasExternalCallConfig: !!relationships.externalCallConfig,
|
|
|
|
|
|
externalCallConfig: relationships.externalCallConfig,
|
|
|
|
|
|
hasDataSaveConfig: !!relationships.dataSaveConfig,
|
|
|
|
|
|
dataSaveConfig: relationships.dataSaveConfig,
|
|
|
|
|
|
});
|
2025-09-29 12:17:10 +09:00
|
|
|
|
|
|
|
|
|
|
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 {
|
2025-09-29 13:32:59 +09:00
|
|
|
|
console.log(`🔍 외부 호출 실행 시작 - relationships 구조:`, relationships);
|
|
|
|
|
|
|
2025-09-29 12:17:10 +09:00
|
|
|
|
const externalCallConfig = relationships.externalCallConfig;
|
2025-09-29 13:32:59 +09:00
|
|
|
|
console.log(`🔍 externalCallConfig:`, externalCallConfig);
|
|
|
|
|
|
|
2025-09-29 12:17:10 +09:00
|
|
|
|
if (!externalCallConfig) {
|
2025-09-29 13:32:59 +09:00
|
|
|
|
console.error('❌ 외부 호출 설정이 없습니다. relationships 구조:', relationships);
|
2025-09-29 12:17:10 +09:00
|
|
|
|
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,
|
|
|
|
|
|
},
|
2025-09-29 13:32:59 +09:00
|
|
|
|
templateData: restApiSettings.httpMethod !== 'GET' && requestBody ? JSON.parse(requestBody) : formData,
|
2025-09-29 12:17:10 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
2025-09-29 13:32:59 +09:00
|
|
|
|
console.log(`📥 데이터 매핑 설정 발견 - HTTP 메서드: ${restApiSettings.httpMethod}`);
|
|
|
|
|
|
console.log(`📥 매핑 설정:`, externalCallConfig.dataMappingConfig.inboundMapping);
|
|
|
|
|
|
console.log(`📥 응답 데이터:`, responseData);
|
|
|
|
|
|
|
2025-09-29 12:17:10 +09:00
|
|
|
|
await this.processInboundMapping(
|
|
|
|
|
|
externalCallConfig.dataMappingConfig.inboundMapping,
|
|
|
|
|
|
responseData,
|
|
|
|
|
|
context
|
|
|
|
|
|
);
|
2025-09-29 13:32:59 +09:00
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`ℹ️ 데이터 매핑 설정이 없습니다 - HTTP 메서드: ${restApiSettings.httpMethod}`);
|
2025-09-29 12:17:10 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 {
|
2025-09-29 13:32:59 +09:00
|
|
|
|
console.log(`💾 테이블 데이터 저장 시작: ${tableName}`, {
|
|
|
|
|
|
actionType,
|
|
|
|
|
|
data,
|
|
|
|
|
|
connection
|
2025-09-29 12:17:10 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-29 13:32:59 +09:00
|
|
|
|
// 데이터 저장 API 호출 (apiClient 사용)
|
|
|
|
|
|
const response = await apiClient.post('/dataflow/execute-data-action', {
|
|
|
|
|
|
tableName,
|
|
|
|
|
|
data,
|
|
|
|
|
|
actionType,
|
|
|
|
|
|
connection,
|
|
|
|
|
|
});
|
2025-09-29 12:17:10 +09:00
|
|
|
|
|
2025-09-29 13:32:59 +09:00
|
|
|
|
console.log(`✅ 테이블 데이터 저장 성공: ${tableName}`, response.data);
|
|
|
|
|
|
return response.data;
|
2025-09-29 12:17:10 +09:00
|
|
|
|
} catch (error) {
|
2025-09-29 13:32:59 +09:00
|
|
|
|
console.error('테이블 데이터 저장 오류:', error);
|
2025-09-29 12:17:10 +09:00
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 조건 평가
|
|
|
|
|
|
*/
|
|
|
|
|
|
private static evaluateConditions(
|
|
|
|
|
|
conditions: any[],
|
|
|
|
|
|
formData: Record<string, any>,
|
|
|
|
|
|
context: ButtonExecutionContext
|
|
|
|
|
|
): boolean {
|
2025-09-29 15:21:14 +09:00
|
|
|
|
console.log(`🔍 조건 평가 시작:`, {
|
|
|
|
|
|
conditions,
|
|
|
|
|
|
formDataKeys: Object.keys(formData),
|
|
|
|
|
|
formData,
|
|
|
|
|
|
contextData: context.contextData,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-29 12:17:10 +09:00
|
|
|
|
for (const condition of conditions) {
|
|
|
|
|
|
const fieldValue = formData[condition.field];
|
|
|
|
|
|
const conditionValue = condition.value;
|
|
|
|
|
|
const operator = condition.operator;
|
|
|
|
|
|
|
2025-09-29 15:21:14 +09:00
|
|
|
|
console.log(`🔍 개별 조건 검증:`, {
|
|
|
|
|
|
field: condition.field,
|
|
|
|
|
|
operator,
|
|
|
|
|
|
expectedValue: conditionValue,
|
|
|
|
|
|
actualValue: fieldValue,
|
|
|
|
|
|
formDataHasField: condition.field in formData,
|
|
|
|
|
|
allFormDataKeys: Object.keys(formData),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-29 12:17:10 +09:00
|
|
|
|
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})`);
|
2025-09-29 15:21:14 +09:00
|
|
|
|
console.log(`❌ 사용 가능한 필드들:`, Object.keys(formData));
|
|
|
|
|
|
console.log(`❌ 전체 formData:`, formData);
|
2025-09-29 12:17:10 +09:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ 모든 조건 만족`);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-29 13:32:59 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 다양한 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];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-29 12:17:10 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 인바운드 데이터 매핑 처리
|
|
|
|
|
|
*/
|
|
|
|
|
|
private static async processInboundMapping(
|
|
|
|
|
|
inboundMapping: any,
|
|
|
|
|
|
responseData: any,
|
|
|
|
|
|
context: ButtonExecutionContext
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log(`📥 인바운드 데이터 매핑 처리 시작`);
|
2025-09-29 13:32:59 +09:00
|
|
|
|
console.log(`📥 원본 응답 데이터:`, responseData);
|
2025-09-29 12:17:10 +09:00
|
|
|
|
|
|
|
|
|
|
const targetTable = inboundMapping.targetTable;
|
|
|
|
|
|
const fieldMappings = inboundMapping.fieldMappings || [];
|
|
|
|
|
|
const insertMode = inboundMapping.insertMode || 'insert';
|
|
|
|
|
|
|
2025-09-29 13:32:59 +09:00
|
|
|
|
console.log(`📥 매핑 설정:`, {
|
|
|
|
|
|
targetTable,
|
|
|
|
|
|
fieldMappings,
|
|
|
|
|
|
insertMode
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 응답 데이터에서 실제 데이터 추출 (다양한 구조 지원)
|
|
|
|
|
|
let actualData = this.extractActualData(responseData);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`📥 추출된 실제 데이터:`, actualData);
|
|
|
|
|
|
|
|
|
|
|
|
// 배열이 아닌 경우 배열로 변환
|
|
|
|
|
|
const dataArray = Array.isArray(actualData) ? actualData : [actualData];
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`📥 처리할 데이터 배열:`, dataArray);
|
|
|
|
|
|
|
|
|
|
|
|
if (dataArray.length === 0) {
|
|
|
|
|
|
console.log(`⚠️ 처리할 데이터가 없습니다`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-29 12:17:10 +09:00
|
|
|
|
|
|
|
|
|
|
for (const item of dataArray) {
|
|
|
|
|
|
const mappedData: Record<string, any> = {};
|
|
|
|
|
|
|
2025-09-29 13:32:59 +09:00
|
|
|
|
console.log(`📥 개별 아이템 처리:`, item);
|
|
|
|
|
|
|
2025-09-29 12:17:10 +09:00
|
|
|
|
// 필드 매핑 적용
|
|
|
|
|
|
for (const mapping of fieldMappings) {
|
|
|
|
|
|
const sourceValue = item[mapping.sourceField];
|
2025-09-29 13:32:59 +09:00
|
|
|
|
console.log(`📥 필드 매핑: ${mapping.sourceField} -> ${mapping.targetField} = ${sourceValue}`);
|
|
|
|
|
|
|
|
|
|
|
|
if (sourceValue !== undefined && sourceValue !== null) {
|
2025-09-29 12:17:10 +09:00
|
|
|
|
mappedData[mapping.targetField] = sourceValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`📋 매핑된 데이터:`, mappedData);
|
|
|
|
|
|
|
2025-09-29 13:32:59 +09:00
|
|
|
|
// 매핑된 데이터가 비어있지 않은 경우에만 저장
|
|
|
|
|
|
if (Object.keys(mappedData).length > 0) {
|
|
|
|
|
|
await this.saveDataToTable(targetTable, mappedData, insertMode);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`⚠️ 매핑된 데이터가 비어있어 저장을 건너뜁니다`);
|
|
|
|
|
|
}
|
2025-09-29 12:17:10 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 || "작업 중 오류가 발생했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|