ERP-node/frontend/components/screen/OptimizedButtonComponent.tsx

517 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useCallback, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { toast } from "react-hot-toast";
import { Loader2, CheckCircle2, AlertCircle, Clock } from "lucide-react";
import { ComponentData, ButtonActionType } from "@/types/screen";
import {
optimizedButtonDataflowService,
OptimizedButtonDataflowService,
ExtendedControlContext,
} from "@/lib/services/optimizedButtonDataflowService";
import { dataflowJobQueue } from "@/lib/services/dataflowJobQueue";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
interface OptimizedButtonProps {
component: ComponentData;
onDataflowComplete?: (result: any) => void;
onActionComplete?: (result: any) => void;
formData?: Record<string, any>;
companyCode?: string;
disabled?: boolean;
}
/**
* 🔥 성능 최적화된 버튼 컴포넌트
*
* 핵심 기능:
* 1. 즉시 응답 (0-100ms)
* 2. 백그라운드 제어관리 처리
* 3. 실시간 상태 추적
* 4. 디바운싱으로 중복 클릭 방지
* 5. 시각적 피드백
*/
export const OptimizedButtonComponent: React.FC<OptimizedButtonProps> = ({
component,
onDataflowComplete,
onActionComplete,
formData = {},
companyCode = "DEFAULT",
disabled = false,
}) => {
// 🔥 상태 관리
const [isExecuting, setIsExecuting] = useState(false);
const [executionTime, setExecutionTime] = useState<number | null>(null);
const [backgroundJobs, setBackgroundJobs] = useState<Set<string>>(new Set());
const [lastResult, setLastResult] = useState<any>(null);
const [clickCount, setClickCount] = useState(0);
const config = component.webTypeConfig;
const buttonLabel = component.label || "버튼";
// 🔥 디바운싱된 클릭 핸들러 (300ms)
const handleClick = useCallback(async () => {
if (isExecuting || disabled) return;
// 클릭 카운트 증가 (통계용)
setClickCount((prev) => prev + 1);
setIsExecuting(true);
const startTime = performance.now();
try {
console.log(`🔘 Button clicked: ${component.id} (${config?.actionType})`);
// 🔥 확장된 컨텍스트 데이터 수집
const contextData = {
...formData,
buttonId: component.id,
componentData: component,
timestamp: new Date().toISOString(),
clickCount,
};
// 🔥 확장된 제어 컨텍스트 생성
const extendedContext = {
formData,
selectedRows: selectedRows || [],
selectedRowsData: selectedRowsData || [],
controlDataSource: config?.dataflowConfig?.controlDataSource || "form",
buttonId: component.id,
componentData: component,
timestamp: new Date().toISOString(),
clickCount,
};
// 🔥 제어 전용 액션인지 확인
const isControlOnlyAction = config?.actionType === "control";
console.log("🎯 OptimizedButtonComponent 실행:", {
actionType: config?.actionType,
isControlOnlyAction,
enableDataflowControl: config?.enableDataflowControl,
hasDataflowConfig: !!config?.dataflowConfig,
selectedRows,
selectedRowsData,
});
if (config?.enableDataflowControl && config?.dataflowConfig) {
// 🔥 확장된 제어 검증 먼저 실행
const validationResult = await OptimizedButtonDataflowService.executeExtendedValidation(
config.dataflowConfig,
extendedContext as ExtendedControlContext,
);
if (!validationResult.success) {
toast.error(validationResult.message || "제어 조건을 만족하지 않습니다.");
return;
}
// 🔥 제어 전용 액션이면 여기서 종료
if (isControlOnlyAction) {
toast.success("제어 조건을 만족합니다.");
if (onActionComplete) {
onActionComplete({ success: true, message: "제어 조건 통과" });
}
return;
}
// 🔥 최적화된 버튼 실행 (즉시 응답)
await executeOptimizedButtonAction(contextData);
} else if (isControlOnlyAction) {
// 🔥 제어관리가 비활성화된 상태에서 제어 액션
toast.warning(
"제어관리를 먼저 활성화해주세요. 제어 액션을 사용하려면 버튼 설정에서 '제어관리 활성화'를 체크하고 조건을 설정해주세요.",
);
return;
} else {
// 🔥 기존 액션만 실행 (제어 액션 제외)
await executeOriginalAction(config?.actionType || "save", contextData);
}
} catch (error) {
console.error("Button execution failed:", error);
toast.error("버튼 실행 중 오류가 발생했습니다.");
setLastResult({ success: false, error: error.message });
} finally {
const endTime = performance.now();
const totalTime = endTime - startTime;
setExecutionTime(totalTime);
setIsExecuting(false);
// 성능 로깅
if (totalTime > 200) {
console.warn(`🐌 Slow button execution: ${totalTime.toFixed(2)}ms`);
} else {
console.log(`⚡ Button execution: ${totalTime.toFixed(2)}ms`);
}
}
}, [isExecuting, disabled, component.id, config?.actionType, config?.enableDataflowControl, formData, clickCount]);
/**
* 🔥 최적화된 버튼 액션 실행
*/
const executeOptimizedButtonAction = async (contextData: Record<string, any>) => {
const actionType = config?.actionType as ButtonActionType;
if (!actionType) {
throw new Error("액션 타입이 설정되지 않았습니다.");
}
// 🔥 API 호출 (즉시 응답)
const result = await optimizedButtonDataflowService.executeButtonWithDataflow(
component.id,
actionType,
config,
contextData,
companyCode,
);
const { jobId, immediateResult, isBackground, timing } = result;
// 🔥 즉시 결과 처리
if (immediateResult) {
handleImmediateResult(actionType, immediateResult);
setLastResult(immediateResult);
// 사용자에게 즉시 피드백
const message = getSuccessMessage(actionType, timing);
if (immediateResult.success) {
toast.success(message);
} else {
toast.error(immediateResult.message || "처리 중 오류가 발생했습니다.");
}
// 콜백 호출
if (onActionComplete) {
onActionComplete(immediateResult);
}
}
// 🔥 백그라운드 작업 추적
if (isBackground && jobId && jobId !== "immediate") {
setBackgroundJobs((prev) => new Set([...prev, jobId]));
// 백그라운드 작업 완료 대기 (선택적)
if (timing === "before") {
// before 타이밍은 결과를 기다려야 함
await waitForBackgroundJob(jobId);
} else {
// after/replace 타이밍은 백그라운드에서 조용히 처리
trackBackgroundJob(jobId);
}
}
};
/**
* 🔥 즉시 결과 처리
*/
const handleImmediateResult = (actionType: ButtonActionType, result: any) => {
if (!result.success) return;
switch (actionType) {
case "save":
console.log("💾 Save action completed:", result);
break;
case "delete":
console.log("🗑️ Delete action completed:", result);
break;
case "search":
console.log("🔍 Search action completed:", result);
break;
case "add":
console.log(" Add action completed:", result);
break;
case "edit":
console.log("✏️ Edit action completed:", result);
break;
default:
console.log(`${actionType} action completed:`, result);
}
};
/**
* 🔥 성공 메시지 생성
*/
const getSuccessMessage = (actionType: ButtonActionType, timing?: string): string => {
const actionName = getActionDisplayName(actionType);
switch (timing) {
case "before":
return `${actionName} 작업을 처리 중입니다...`;
case "after":
return `${actionName}이 완료되었습니다.`;
case "replace":
return `사용자 정의 작업을 처리 중입니다...`;
default:
return `${actionName}이 완료되었습니다.`;
}
};
/**
* 🔥 백그라운드 작업 추적 (polling 방식)
*/
const trackBackgroundJob = (jobId: string) => {
const pollInterval = 1000; // 1초
let pollCount = 0;
const maxPolls = 60; // 최대 1분
const pollJobStatus = async () => {
pollCount++;
try {
const status = optimizedButtonDataflowService.getJobStatus(jobId);
if (status.status === "completed") {
setBackgroundJobs((prev) => {
const newSet = new Set(prev);
newSet.delete(jobId);
return newSet;
});
// 백그라운드 작업 완료 알림 (조용하게)
if (status.result?.executedActions > 0) {
toast.success(`추가 처리가 완료되었습니다. (${status.result.executedActions}개 액션)`, { duration: 2000 });
}
if (onDataflowComplete) {
onDataflowComplete(status.result);
}
return;
}
if (status.status === "failed") {
setBackgroundJobs((prev) => {
const newSet = new Set(prev);
newSet.delete(jobId);
return newSet;
});
console.error("Background job failed:", status.result);
toast.error("백그라운드 처리 중 오류가 발생했습니다.", { duration: 3000 });
return;
}
// 아직 진행 중이고 최대 횟수 미달 시 계속 polling
if (pollCount < maxPolls && (status.status === "pending" || status.status === "processing")) {
setTimeout(pollJobStatus, pollInterval);
} else if (pollCount >= maxPolls) {
console.warn(`Background job polling timeout: ${jobId}`);
setBackgroundJobs((prev) => {
const newSet = new Set(prev);
newSet.delete(jobId);
return newSet;
});
}
} catch (error) {
console.error("Failed to check job status:", error);
setBackgroundJobs((prev) => {
const newSet = new Set(prev);
newSet.delete(jobId);
return newSet;
});
}
};
// 첫 polling 시작
setTimeout(pollJobStatus, 500);
};
/**
* 🔥 백그라운드 작업 완료 대기 (before 타이밍용)
*/
const waitForBackgroundJob = async (jobId: string): Promise<void> => {
return new Promise((resolve, reject) => {
const maxWaitTime = 30000; // 최대 30초 대기
const pollInterval = 500; // 0.5초
let elapsedTime = 0;
const checkStatus = async () => {
try {
const status = optimizedButtonDataflowService.getJobStatus(jobId);
if (status.status === "completed") {
setBackgroundJobs((prev) => {
const newSet = new Set(prev);
newSet.delete(jobId);
return newSet;
});
toast.success("모든 처리가 완료되었습니다.");
if (onDataflowComplete) {
onDataflowComplete(status.result);
}
resolve();
return;
}
if (status.status === "failed") {
setBackgroundJobs((prev) => {
const newSet = new Set(prev);
newSet.delete(jobId);
return newSet;
});
toast.error("처리 중 오류가 발생했습니다.");
reject(new Error(status.result?.error || "Unknown error"));
return;
}
// 시간 체크
elapsedTime += pollInterval;
if (elapsedTime >= maxWaitTime) {
reject(new Error("Processing timeout"));
return;
}
// 계속 대기
setTimeout(checkStatus, pollInterval);
} catch (error) {
reject(error);
}
};
checkStatus();
});
};
/**
* 🔥 기존 액션 실행 (제어관리 없음)
*/
const executeOriginalAction = async (
actionType: ButtonActionType,
contextData: Record<string, any>,
): Promise<any> => {
// 🔥 제어 액션은 여기서 처리하지 않음 (이미 위에서 처리됨)
if (actionType === "control") {
console.warn("제어 액션은 executeOriginalAction에서 처리되지 않아야 합니다.");
return;
}
// 간단한 mock 처리 (실제로는 API 호출)
await new Promise((resolve) => setTimeout(resolve, 100)); // 100ms 시뮬레이션
const result = {
success: true,
message: `${getActionDisplayName(actionType)}이 완료되었습니다.`,
actionType,
timestamp: new Date().toISOString(),
};
setLastResult(result);
toast.success(result.message);
if (onActionComplete) {
onActionComplete(result);
}
return result;
};
/**
* 액션 타입별 표시명
*/
const getActionDisplayName = (actionType: ButtonActionType): string => {
const displayNames: Record<ButtonActionType, string> = {
save: "저장",
delete: "삭제",
edit: "수정",
add: "추가",
search: "검색",
reset: "초기화",
submit: "제출",
close: "닫기",
popup: "팝업",
modal: "모달",
newWindow: "새 창",
navigate: "페이지 이동",
control: "제어",
};
return displayNames[actionType] || actionType;
};
/**
* 버튼 상태에 따른 아이콘
*/
const getStatusIcon = () => {
if (isExecuting) {
return <Loader2 className="h-4 w-4 animate-spin" />;
}
if (lastResult?.success === false) {
return <AlertCircle className="h-4 w-4 text-red-500" />;
}
if (lastResult?.success === true) {
return <CheckCircle2 className="h-4 w-4 text-green-500" />;
}
return null;
};
/**
* 백그라운드 작업 상태 표시
*/
const renderBackgroundStatus = () => {
if (backgroundJobs.size === 0) return null;
return (
<div className="absolute -top-1 -right-1">
<Badge variant="secondary" className="h-5 px-1 text-xs">
<Clock className="mr-1 h-3 w-3" />
{backgroundJobs.size}
</Badge>
</div>
);
};
return (
<div className="relative">
<Button
onClick={handleClick}
disabled={isExecuting || disabled}
variant={config?.variant || "default"}
className={cn(
"transition-all duration-200",
isExecuting && "cursor-wait opacity-75",
backgroundJobs.size > 0 && "border-blue-200 bg-blue-50",
config?.backgroundColor && { backgroundColor: config.backgroundColor },
config?.textColor && { color: config.textColor },
config?.borderColor && { borderColor: config.borderColor },
)}
style={{
backgroundColor: config?.backgroundColor,
color: config?.textColor,
borderColor: config?.borderColor,
}}
>
{/* 메인 버튼 내용 */}
<div className="flex items-center space-x-2">
{getStatusIcon()}
<span>{isExecuting ? "처리 중..." : buttonLabel}</span>
</div>
{/* 개발 모드에서 성능 정보 표시 */}
{process.env.NODE_ENV === "development" && executionTime && (
<span className="ml-2 text-xs opacity-60">{executionTime.toFixed(0)}ms</span>
)}
</Button>
{/* 백그라운드 작업 상태 표시 */}
{renderBackgroundStatus()}
{/* 제어관리 활성화 표시 */}
{config?.enableDataflowControl && (
<div className="absolute -right-1 -bottom-1">
<Badge variant="outline" className="h-4 bg-white px-1 text-xs">
🔧
</Badge>
</div>
)}
</div>
);
};
export default OptimizedButtonComponent;