2025-09-18 10:05:50 +09:00
|
|
|
|
"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";
|
2025-09-19 12:19:34 +09:00
|
|
|
|
import {
|
|
|
|
|
|
optimizedButtonDataflowService,
|
|
|
|
|
|
OptimizedButtonDataflowService,
|
|
|
|
|
|
ExtendedControlContext,
|
|
|
|
|
|
} from "@/lib/services/optimizedButtonDataflowService";
|
2025-09-18 10:05:50 +09:00
|
|
|
|
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 {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log(`🔘 Button clicked: ${component.id} (${config?.actionType})`);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
|
2025-09-19 12:19:34 +09:00
|
|
|
|
// 🔥 확장된 컨텍스트 데이터 수집
|
2025-09-18 10:05:50 +09:00
|
|
|
|
const contextData = {
|
|
|
|
|
|
...formData,
|
|
|
|
|
|
buttonId: component.id,
|
|
|
|
|
|
componentData: component,
|
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
|
clickCount,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-19 12:19:34 +09:00
|
|
|
|
// 🔥 확장된 제어 컨텍스트 생성
|
|
|
|
|
|
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";
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🎯 OptimizedButtonComponent 실행:", {
|
|
|
|
|
|
// actionType: config?.actionType,
|
|
|
|
|
|
// isControlOnlyAction,
|
|
|
|
|
|
// enableDataflowControl: config?.enableDataflowControl,
|
|
|
|
|
|
// hasDataflowConfig: !!config?.dataflowConfig,
|
|
|
|
|
|
// selectedRows,
|
|
|
|
|
|
// selectedRowsData,
|
|
|
|
|
|
// });
|
2025-09-19 12:19:34 +09:00
|
|
|
|
|
2025-09-18 10:05:50 +09:00
|
|
|
|
if (config?.enableDataflowControl && config?.dataflowConfig) {
|
2025-09-19 12:19:34 +09:00
|
|
|
|
// 🔥 확장된 제어 검증 먼저 실행
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-18 10:05:50 +09:00
|
|
|
|
// 🔥 최적화된 버튼 실행 (즉시 응답)
|
|
|
|
|
|
await executeOptimizedButtonAction(contextData);
|
2025-09-19 12:19:34 +09:00
|
|
|
|
} else if (isControlOnlyAction) {
|
|
|
|
|
|
// 🔥 제어관리가 비활성화된 상태에서 제어 액션
|
|
|
|
|
|
toast.warning(
|
|
|
|
|
|
"제어관리를 먼저 활성화해주세요. 제어 액션을 사용하려면 버튼 설정에서 '제어관리 활성화'를 체크하고 조건을 설정해주세요.",
|
|
|
|
|
|
);
|
|
|
|
|
|
return;
|
2025-09-18 10:05:50 +09:00
|
|
|
|
} else {
|
2025-09-19 12:19:34 +09:00
|
|
|
|
// 🔥 기존 액션만 실행 (제어 액션 제외)
|
2025-09-18 10:05:50 +09:00
|
|
|
|
await executeOriginalAction(config?.actionType || "save", contextData);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("Button execution failed:", error);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
toast.error("버튼 실행 중 오류가 발생했습니다.");
|
|
|
|
|
|
setLastResult({ success: false, error: error.message });
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
const endTime = performance.now();
|
|
|
|
|
|
const totalTime = endTime - startTime;
|
|
|
|
|
|
setExecutionTime(totalTime);
|
|
|
|
|
|
setIsExecuting(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 성능 로깅
|
|
|
|
|
|
if (totalTime > 200) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.warn(`🐌 Slow button execution: ${totalTime.toFixed(2)}ms`);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
} else {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log(`⚡ Button execution: ${totalTime.toFixed(2)}ms`);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [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":
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("💾 Save action completed:", result);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
break;
|
|
|
|
|
|
case "delete":
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🗑️ Delete action completed:", result);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
break;
|
|
|
|
|
|
case "search":
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔍 Search action completed:", result);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
break;
|
|
|
|
|
|
case "add":
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("➕ Add action completed:", result);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
break;
|
|
|
|
|
|
case "edit":
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("✏️ Edit action completed:", result);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
break;
|
|
|
|
|
|
default:
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log(`✅ ${actionType} action completed:`, result);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🔥 성공 메시지 생성
|
|
|
|
|
|
*/
|
|
|
|
|
|
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;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("Background job failed:", status.result);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
toast.error("백그라운드 처리 중 오류가 발생했습니다.", { duration: 3000 });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 아직 진행 중이고 최대 횟수 미달 시 계속 polling
|
|
|
|
|
|
if (pollCount < maxPolls && (status.status === "pending" || status.status === "processing")) {
|
|
|
|
|
|
setTimeout(pollJobStatus, pollInterval);
|
|
|
|
|
|
} else if (pollCount >= maxPolls) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.warn(`Background job polling timeout: ${jobId}`);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
setBackgroundJobs((prev) => {
|
|
|
|
|
|
const newSet = new Set(prev);
|
|
|
|
|
|
newSet.delete(jobId);
|
|
|
|
|
|
return newSet;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("Failed to check job status:", error);
|
2025-09-18 10:05:50 +09:00
|
|
|
|
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> => {
|
2025-09-19 12:19:34 +09:00
|
|
|
|
// 🔥 제어 액션은 여기서 처리하지 않음 (이미 위에서 처리됨)
|
|
|
|
|
|
if (actionType === "control") {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.warn("제어 액션은 executeOriginalAction에서 처리되지 않아야 합니다.");
|
2025-09-19 12:19:34 +09:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-18 10:05:50 +09:00
|
|
|
|
// 간단한 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: "페이지 이동",
|
2025-09-19 12:19:34 +09:00
|
|
|
|
control: "제어",
|
2025-09-18 10:05:50 +09:00
|
|
|
|
};
|
|
|
|
|
|
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;
|