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

456 lines
14 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 } 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,
};
if (config?.enableDataflowControl && config?.dataflowConfig) {
// 🔥 최적화된 버튼 실행 (즉시 응답)
await executeOptimizedButtonAction(contextData);
} 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> => {
// 간단한 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: "페이지 이동",
};
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;