ERP-node/frontend/lib/services/optimizedButtonDataflowServ...

778 lines
25 KiB
TypeScript
Raw Normal View History

2025-09-18 10:05:50 +09:00
/**
* 🔥 최적화: 버튼
*
* +
* .
*/
import {
ButtonActionType,
ExtendedButtonTypeConfig,
2025-09-18 10:05:50 +09:00
ButtonDataflowConfig,
DataflowExecutionResult,
DataflowCondition,
} from "@/types";
2025-09-18 10:05:50 +09:00
import { dataflowConfigCache } from "./dataflowCache";
import { dataflowJobQueue, JobPriority } from "./dataflowJobQueue";
import { apiClient } from "@/lib/api/client";
export interface OptimizedExecutionResult {
jobId: string;
immediateResult?: any;
isBackground?: boolean;
timing?: "before" | "after" | "replace";
}
export interface QuickValidationResult {
success: boolean;
message?: string;
canExecuteImmediately: boolean;
actions?: any[]; // 조건 만족 시 실행할 액션들
}
/**
*
*/
export type ControlDataSource = "form" | "table-selection" | "both";
/**
*
*/
export interface ExtendedControlContext {
// 기존 폼 데이터
formData: Record<string, any>;
// 테이블 선택 데이터
selectedRows?: any[];
selectedRowsData?: any[];
// 제어 데이터 소스 타입
controlDataSource: ControlDataSource;
// 기타 컨텍스트
buttonId: string;
componentData?: any;
timestamp: string;
clickCount?: number;
2025-09-18 10:05:50 +09:00
}
/**
* 🔥
*
* :
* 1. (0-100ms)
* 2.
* 3.
* 4.
*/
export class OptimizedButtonDataflowService {
/**
* 🔥 엔트리포인트: 즉시 +
*/
static async executeButtonWithDataflow(
buttonId: string,
actionType: ButtonActionType,
buttonConfig: ExtendedExtendedButtonTypeConfig,
2025-09-18 10:05:50 +09:00
contextData: Record<string, any>,
companyCode: string,
): Promise<OptimizedExecutionResult> {
const { enableDataflowControl, dataflowTiming } = buttonConfig;
// 🔥 제어관리가 비활성화된 경우: 즉시 실행
if (!enableDataflowControl) {
const result = await this.executeOriginalAction(actionType, buttonConfig, contextData);
return {
jobId: "immediate",
immediateResult: result,
timing: undefined,
};
}
// 🔥 타이밍별 즉시 응답 전략
switch (dataflowTiming) {
case "before":
return await this.executeBeforeTiming(buttonId, actionType, buttonConfig, contextData, companyCode);
case "after":
return await this.executeAfterTiming(buttonId, actionType, buttonConfig, contextData, companyCode);
case "replace":
return await this.executeReplaceTiming(buttonId, actionType, buttonConfig, contextData, companyCode);
default:
// 기본값은 after
return await this.executeAfterTiming(buttonId, actionType, buttonConfig, contextData, companyCode);
}
}
/**
* 🔥 After 타이밍: 즉시 +
*
*
* - (50-200ms)
* -
*/
private static async executeAfterTiming(
buttonId: string,
actionType: ButtonActionType,
buttonConfig: ExtendedButtonTypeConfig,
2025-09-18 10:05:50 +09:00
contextData: Record<string, any>,
companyCode: string,
): Promise<OptimizedExecutionResult> {
// 🔥 Step 1: 기존 액션 즉시 실행
const immediateResult = await this.executeOriginalAction(actionType, buttonConfig, contextData);
// 🔥 Step 2: 제어관리는 백그라운드에서 실행
const enrichedContext = {
...contextData,
originalActionResult: immediateResult,
};
const jobId = dataflowJobQueue.enqueue(
buttonId,
actionType,
buttonConfig,
enrichedContext,
companyCode,
"normal", // 일반 우선순위
);
return {
jobId,
immediateResult,
isBackground: true,
timing: "after",
};
}
/**
* 🔥 Before 타이밍: 빠른 +
*
*
* - 검증: 즉시
* - 검증: 백그라운드
*/
private static async executeBeforeTiming(
buttonId: string,
actionType: ButtonActionType,
buttonConfig: ExtendedButtonTypeConfig,
2025-09-18 10:05:50 +09:00
contextData: Record<string, any>,
companyCode: string,
): Promise<OptimizedExecutionResult> {
// 🔥 설정 캐시에서 빠르게 로드
const dataflowConfig = buttonConfig.dataflowConfig || (await dataflowConfigCache.getConfig(buttonId));
if (!dataflowConfig) {
// 설정이 없으면 기존 액션만 실행
const result = await this.executeOriginalAction(actionType, buttonConfig, contextData);
return { jobId: "immediate", immediateResult: result, timing: "before" };
}
// 간단한 검증인지 판단
const isSimpleValidation = await this.isSimpleValidationOnly(dataflowConfig);
if (isSimpleValidation) {
// 🔥 간단한 검증: 메모리에서 즉시 처리 (1-10ms)
const validationResult = await this.executeQuickValidation(dataflowConfig, contextData);
if (!validationResult.success) {
return {
jobId: "validation_failed",
immediateResult: {
success: false,
message: validationResult.message,
},
timing: "before",
};
}
// 검증 통과 시 기존 액션 실행
const actionResult = await this.executeOriginalAction(actionType, buttonConfig, contextData);
return {
jobId: "immediate",
immediateResult: actionResult,
timing: "before",
};
} else {
// 🔥 복잡한 검증: 사용자에게 알림 후 백그라운드 처리
const jobId = dataflowJobQueue.enqueue(
buttonId,
actionType,
buttonConfig,
contextData,
companyCode,
"high", // 높은 우선순위 (사용자 대기 중)
);
return {
jobId,
immediateResult: {
success: true,
message: "검증 중입니다. 잠시만 기다려주세요.",
processing: true,
},
isBackground: true,
timing: "before",
};
}
}
/**
* 🔥 Replace 타이밍: 제어관리로
*
*
* - 제어: 즉시
* - 제어: 백그라운드
*/
private static async executeReplaceTiming(
buttonId: string,
actionType: ButtonActionType,
buttonConfig: ExtendedButtonTypeConfig,
2025-09-18 10:05:50 +09:00
contextData: Record<string, any>,
companyCode: string,
): Promise<OptimizedExecutionResult> {
const dataflowConfig = buttonConfig.dataflowConfig || (await dataflowConfigCache.getConfig(buttonId));
if (!dataflowConfig) {
throw new Error("Replace 모드이지만 제어관리 설정이 없습니다.");
}
// 간단한 제어관리인지 판단
const isSimpleControl = this.isSimpleControl(dataflowConfig);
if (isSimpleControl) {
// 🔥 간단한 제어: 즉시 실행
try {
const result = await this.executeSimpleDataflow(dataflowConfig, contextData, companyCode);
return {
jobId: "immediate",
immediateResult: result,
timing: "replace",
};
} catch (error) {
return {
jobId: "immediate",
immediateResult: {
success: false,
message: "제어관리 실행 중 오류가 발생했습니다.",
error: error.message,
},
timing: "replace",
};
}
} else {
// 🔥 복잡한 제어: 백그라운드 실행
const jobId = dataflowJobQueue.enqueue(buttonId, actionType, buttonConfig, contextData, companyCode, "normal");
return {
jobId,
immediateResult: {
success: true,
message: "사용자 정의 작업을 처리 중입니다...",
processing: true,
},
isBackground: true,
timing: "replace",
};
}
}
/**
* 🔥
*
* :
* - 5
* -
* -
*/
private static async isSimpleValidationOnly(config: ButtonDataflowConfig): Promise<boolean> {
if (config.controlMode !== "advanced") {
return true; // 간편 모드는 일단 간단하다고 가정
}
const conditions = config.directControl?.conditions || [];
return (
conditions.length <= 5 &&
conditions.every((c) => c.type === "condition" && ["=", "!=", ">", "<", ">=", "<="].includes(c.operator || ""))
);
}
/**
* 🔥
*/
private static isSimpleControl(config: ButtonDataflowConfig): boolean {
if (config.controlMode === "simple") {
return true; // 간편 모드는 대부분 간단
}
const actions = config.directControl?.actions || [];
const conditions = config.directControl?.conditions || [];
// 액션 3개 이하, 조건 5개 이하면 간단한 제어로 판단
return actions.length <= 3 && conditions.length <= 5;
}
/**
* 🔥 ( ) -
2025-09-18 10:05:50 +09:00
*/
private static async executeQuickValidation(
config: ButtonDataflowConfig,
data: Record<string, any>,
): Promise<QuickValidationResult> {
if (config.controlMode === "simple") {
// 간편 모드는 일단 통과 (실제 검증은 백그라운드에서)
return {
success: true,
canExecuteImmediately: true,
};
}
const conditions = config.directControl?.conditions || [];
for (const condition of conditions) {
if (condition.type === "condition") {
const fieldValue = data[condition.field!];
const isValid = this.evaluateSimpleCondition(fieldValue, condition.operator!, condition.value);
if (!isValid) {
return {
success: false,
message: `조건 불만족: ${condition.field} ${condition.operator} ${condition.value}`,
canExecuteImmediately: true,
};
}
}
}
return {
success: true,
canExecuteImmediately: true,
};
}
/**
* 🔥 ( + )
*/
static async executeExtendedValidation(
config: ButtonDataflowConfig,
context: ExtendedControlContext,
): Promise<QuickValidationResult> {
console.log("🔍 executeExtendedValidation 시작:", {
controlMode: config.controlMode,
controlDataSource: context.controlDataSource,
selectedRowsData: context.selectedRowsData,
formData: context.formData,
directControlConditions: config.directControl?.conditions,
selectedDiagramId: config.selectedDiagramId,
selectedRelationshipId: config.selectedRelationshipId,
fullConfig: config,
});
// 🔥 simple 모드에서도 조건 검증을 수행해야 함
// if (config.controlMode === "simple") {
// return {
// success: true,
// canExecuteImmediately: true,
// };
// }
let conditions = config.directControl?.conditions || [];
// 🔥 관계도 방식인 경우 실제 API에서 조건 가져오기
if (conditions.length === 0 && config.selectedDiagramId && config.selectedRelationshipId) {
console.log("🔍 관계도 방식에서 실제 조건 로딩:", {
selectedDiagramId: config.selectedDiagramId,
selectedRelationshipId: config.selectedRelationshipId,
});
try {
// 관계도에서 실제 조건을 가져오는 API 호출
const response = await apiClient.get(
`/test-button-dataflow/diagrams/${config.selectedDiagramId}/relationships/${config.selectedRelationshipId}/preview`,
);
if (response.data.success && response.data.data) {
const previewData = response.data.data;
// control.conditions에서 실제 조건 추출
if (previewData.control && previewData.control.conditions) {
conditions = previewData.control.conditions.filter((cond: any) => cond.type === "condition");
console.log("✅ 관계도에서 control 조건 로딩 성공:", {
totalConditions: previewData.control.conditions.length,
filteredConditions: conditions.length,
conditions,
});
} else {
console.warn("⚠️ 관계도 control에 조건이 설정되지 않았습니다:", previewData);
}
// 🔧 control.conditions가 비어있으면 plan.actions[].conditions에서 조건 추출
if (conditions.length === 0 && previewData.plan && previewData.plan.actions) {
console.log("🔍 control 조건이 없어서 액션별 조건 확인:", previewData.plan.actions);
previewData.plan.actions.forEach((action: any, index: number) => {
if (action.conditions && Array.isArray(action.conditions) && action.conditions.length > 0) {
conditions.push(...action.conditions);
console.log(
`✅ 액션 ${index + 1}(${action.name})에서 조건 ${action.conditions.length}개 로딩:`,
action.conditions,
);
}
});
if (conditions.length > 0) {
console.log("✅ 총 액션별 조건 로딩 성공:", {
totalConditions: conditions.length,
conditions: conditions,
});
}
}
// plan.actions에서 실제 액션도 저장 (조건 만족 시 실행용)
if (previewData.plan && previewData.plan.actions) {
// config에 액션 정보 임시 저장
(config as any)._relationshipActions = previewData.plan.actions;
console.log("✅ 관계도에서 실제 액션 로딩 성공:", {
totalActions: previewData.plan.actions.length,
actions: previewData.plan.actions,
});
} else {
console.warn("⚠️ 관계도에 액션이 설정되지 않았습니다:", {
hasPlan: !!previewData.plan,
planActions: previewData.plan?.actions,
fullPreviewData: previewData,
});
}
} else {
console.warn("⚠️ 관계도 API 응답이 올바르지 않습니다:", response.data);
}
} catch (error) {
console.error("❌ 관계도에서 조건 로딩 실패:", error);
// API 실패 시 사용자에게 명확한 안내
return {
success: false,
message: "관계도에서 조건을 불러올 수 없습니다. 관계도 설정을 확인해주세요.",
canExecuteImmediately: true,
};
}
}
// 🔥 여전히 조건이 없으면 경고
if (conditions.length === 0) {
console.warn("⚠️ 제어 조건이 설정되지 않았습니다.");
return {
success: false,
message: "제어 조건이 설정되지 않았습니다. 관계도에서 관계를 선택하거나 직접 조건을 추가해주세요.",
canExecuteImmediately: true,
};
}
console.log("🔍 조건 검증 시작:", conditions);
for (const condition of conditions) {
if (condition.type === "condition") {
let dataToCheck: Record<string, any> = {};
// 제어 데이터 소스에 따라 검증할 데이터 결정
console.log("🔍 제어 데이터 소스 확인:", {
controlDataSource: context.controlDataSource,
hasFormData: Object.keys(context.formData).length > 0,
hasSelectedRowsData: context.selectedRowsData && context.selectedRowsData.length > 0,
selectedRowsData: context.selectedRowsData,
});
switch (context.controlDataSource) {
case "form":
dataToCheck = context.formData;
console.log("📝 폼 데이터 사용:", dataToCheck);
break;
case "table-selection":
// 선택된 첫 번째 행의 데이터 사용 (다중 선택 시)
if (context.selectedRowsData && context.selectedRowsData.length > 0) {
dataToCheck = context.selectedRowsData[0];
console.log("📋 테이블 선택 데이터 사용:", dataToCheck);
} else {
console.warn("⚠️ 테이블에서 항목이 선택되지 않음");
return {
success: false,
message: "테이블에서 항목을 선택해주세요.",
canExecuteImmediately: true,
};
}
break;
case "both":
// 폼 데이터와 선택된 데이터 모두 병합
dataToCheck = {
...context.formData,
...(context.selectedRowsData?.[0] || {}),
};
console.log("🔄 폼 + 테이블 데이터 병합 사용:", dataToCheck);
break;
default:
// 기본값이 없는 경우 자동 판단
if (context.selectedRowsData && context.selectedRowsData.length > 0) {
dataToCheck = context.selectedRowsData[0];
console.log("🔄 자동 판단: 테이블 선택 데이터 사용:", dataToCheck);
} else {
dataToCheck = context.formData;
console.log("🔄 자동 판단: 폼 데이터 사용:", dataToCheck);
}
break;
}
const fieldValue = dataToCheck[condition.field!];
const isValid = this.evaluateSimpleCondition(fieldValue, condition.operator!, condition.value);
console.log("🔍 조건 검증 결과:", {
field: condition.field,
operator: condition.operator,
expectedValue: condition.value,
actualValue: fieldValue,
isValid,
dataToCheck,
});
if (!isValid) {
2025-10-24 14:11:12 +09:00
const sourceLabel = getDataSourceLabel(context.controlDataSource);
const actualValueMsg = fieldValue !== undefined ? ` (실제값: ${fieldValue})` : " (값 없음)";
return {
success: false,
message: `${sourceLabel} 조건 불만족: ${condition.field} ${condition.operator} ${condition.value}${actualValueMsg}`,
canExecuteImmediately: true,
};
}
}
}
// 🔥 모든 조건을 만족했으므로 액션 정보도 함께 반환
const relationshipActions = (config as any)._relationshipActions || [];
return {
success: true,
canExecuteImmediately: true,
actions: relationshipActions,
};
}
2025-09-18 10:05:50 +09:00
/**
* 🔥 ( )
*/
private static evaluateSimpleCondition(fieldValue: any, operator: string, conditionValue: any): boolean {
switch (operator) {
case "=":
return fieldValue === conditionValue;
case "!=":
return fieldValue !== conditionValue;
case ">":
return Number(fieldValue) > Number(conditionValue);
case "<":
return Number(fieldValue) < Number(conditionValue);
case ">=":
return Number(fieldValue) >= Number(conditionValue);
case "<=":
return Number(fieldValue) <= Number(conditionValue);
case "LIKE":
return String(fieldValue).toLowerCase().includes(String(conditionValue).toLowerCase());
default:
return true;
}
}
/**
* 🔥
*/
private static async executeSimpleDataflow(
config: ButtonDataflowConfig,
contextData: Record<string, any>,
companyCode: string,
): Promise<DataflowExecutionResult> {
try {
const response = await apiClient.post("/api/button-dataflow/execute-simple", {
config,
contextData,
companyCode,
});
if (response.data.success) {
return response.data.data as DataflowExecutionResult;
} else {
throw new Error(response.data.message || "Simple dataflow execution failed");
}
} catch (error) {
console.error("Simple dataflow execution failed:", error);
throw error;
}
}
/**
* 🔥 ()
*/
private static async executeOriginalAction(
actionType: ButtonActionType,
buttonConfig: ExtendedButtonTypeConfig,
2025-09-18 10:05:50 +09:00
contextData: Record<string, any>,
): Promise<any> {
const startTime = performance.now();
try {
// 액션별 분기 처리
switch (actionType) {
case "save":
return await this.executeSaveAction(buttonConfig, contextData);
case "delete":
return await this.executeDeleteAction(buttonConfig, contextData);
case "search":
return await this.executeSearchAction(buttonConfig, contextData);
case "edit":
return await this.executeEditAction(buttonConfig, contextData);
case "add":
return await this.executeAddAction(buttonConfig, contextData);
case "reset":
return await this.executeResetAction(buttonConfig, contextData);
case "submit":
return await this.executeSubmitAction(buttonConfig, contextData);
case "close":
return await this.executeCloseAction(buttonConfig, contextData);
case "popup":
return await this.executePopupAction(buttonConfig, contextData);
case "navigate":
return await this.executeNavigateAction(buttonConfig, contextData);
default:
return {
success: true,
message: `${actionType} 액션이 실행되었습니다.`,
};
}
} catch (error) {
console.error(`Action execution failed: ${actionType}`, error);
return {
success: false,
message: `${actionType} 액션 실행 중 오류가 발생했습니다.`,
error: error.message,
};
} finally {
const executionTime = performance.now() - startTime;
if (executionTime > 200) {
console.warn(`🐌 Slow action: ${actionType} took ${executionTime.toFixed(2)}ms`);
} else {
console.log(`${actionType} completed in ${executionTime.toFixed(2)}ms`);
}
}
}
/**
*
*/
private static async executeSaveAction(config: ExtendedButtonTypeConfig, data: Record<string, any>) {
2025-09-18 10:05:50 +09:00
// TODO: 실제 저장 로직 구현
return { success: true, message: "저장되었습니다." };
}
private static async executeDeleteAction(config: ExtendedButtonTypeConfig, data: Record<string, any>) {
2025-09-18 10:05:50 +09:00
// TODO: 실제 삭제 로직 구현
return { success: true, message: "삭제되었습니다." };
}
private static async executeSearchAction(config: ExtendedButtonTypeConfig, data: Record<string, any>) {
2025-09-18 10:05:50 +09:00
// TODO: 실제 검색 로직 구현
return { success: true, message: "검색되었습니다.", data: [] };
}
private static async executeEditAction(config: ExtendedButtonTypeConfig, data: Record<string, any>) {
2025-09-18 10:05:50 +09:00
return { success: true, message: "수정 모드로 전환되었습니다." };
}
private static async executeAddAction(config: ExtendedButtonTypeConfig, data: Record<string, any>) {
2025-09-18 10:05:50 +09:00
return { success: true, message: "추가 모드로 전환되었습니다." };
}
private static async executeResetAction(config: ExtendedButtonTypeConfig, data: Record<string, any>) {
2025-09-18 10:05:50 +09:00
return { success: true, message: "초기화되었습니다." };
}
private static async executeSubmitAction(config: ExtendedButtonTypeConfig, data: Record<string, any>) {
2025-09-18 10:05:50 +09:00
return { success: true, message: "제출되었습니다." };
}
private static async executeCloseAction(config: ExtendedButtonTypeConfig, data: Record<string, any>) {
2025-09-18 10:05:50 +09:00
return { success: true, message: "닫기 액션이 실행되었습니다." };
}
private static async executePopupAction(config: ExtendedButtonTypeConfig, data: Record<string, any>) {
2025-09-18 10:05:50 +09:00
return {
success: true,
message: "팝업이 열렸습니다.",
popupUrl: config.navigateUrl,
popupScreenId: config.popupScreenId,
};
}
private static async executeNavigateAction(config: ExtendedButtonTypeConfig, data: Record<string, any>) {
2025-09-18 10:05:50 +09:00
return {
success: true,
message: "페이지 이동이 실행되었습니다.",
navigateUrl: config.navigateUrl,
navigateTarget: config.navigateTarget,
};
}
/**
* 🔥
*/
static getJobStatus(jobId: string): { status: string; result?: any; progress?: number } {
try {
return dataflowJobQueue.getJobStatus(jobId);
} catch (error) {
return { status: "not_found" };
}
}
/**
* 🔥
*/
static getPerformanceMetrics(): {
cache: any;
queue: any;
} {
return {
cache: dataflowConfigCache.getMetrics(),
queue: dataflowJobQueue.getMetrics(),
};
}
}
2025-10-24 14:11:12 +09:00
/**
*
*/
function getDataSourceLabel(dataSource: string | undefined): string {
switch (dataSource) {
case "form":
return "폼";
case "table-selection":
return "테이블 선택 항목";
case "table-all":
return "테이블 전체";
case "flow-selection":
return "플로우 선택 항목";
case "flow-step-all":
return "플로우 스텝 전체";
case "both":
return "폼 + 테이블 선택";
case "all-sources":
return "모든 소스";
default:
return "데이터";
}
}
2025-09-18 10:05:50 +09:00
// 🔥 전역 접근을 위한 싱글톤 서비스
export const optimizedButtonDataflowService = OptimizedButtonDataflowService;