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

701 lines
23 KiB
TypeScript
Raw Permalink Normal View History

2025-09-18 10:05:50 +09:00
"use client";
import React, { useState, useCallback, useEffect, useMemo } from "react";
2025-09-18 10:05:50 +09:00
import { Button } from "@/components/ui/button";
import { toast } from "react-hot-toast";
import { Loader2, CheckCircle2, AlertCircle, Clock, Workflow } from "lucide-react";
2025-09-18 10:05:50 +09:00
import { ComponentData, ButtonActionType } from "@/types/screen";
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";
feat: 노드 기반 데이터 플로우 시스템 구현 - 노드 에디터 UI 구현 (React Flow 기반) - TableSource, DataTransform, INSERT, UPDATE, DELETE, UPSERT 노드 - 드래그앤드롭 노드 추가 및 연결 - 속성 패널을 통한 노드 설정 - 실시간 필드 라벨 표시 (column_labels 테이블 연동) - 데이터 변환 노드 (DataTransform) 기능 - EXPLODE: 구분자로 1개 행 → 여러 행 확장 - UPPERCASE, LOWERCASE, TRIM, CONCAT, SPLIT, REPLACE 등 12가지 변환 타입 - In-place 변환 지원 (타겟 필드 생략 시 소스 필드 덮어쓰기) - 변환된 필드가 하위 액션 노드에 자동 전달 - 노드 플로우 실행 엔진 - 위상 정렬을 통한 노드 실행 순서 결정 - 레벨별 병렬 실행 (Promise.allSettled) - 부분 실패 허용 (한 노드 실패 시 연결된 하위 노드만 스킵) - 트랜잭션 기반 안전한 데이터 처리 - UPSERT 액션 로직 구현 - DB 제약 조건 없이 SELECT → UPDATE or INSERT 방식 - 복합 충돌 키 지원 (예: sales_no + product_name) - 파라미터 인덱스 정확한 매핑 - 데이터 소스 자동 감지 - 테이블 선택 데이터 (selectedRowsData) 자동 주입 - 폼 입력 데이터 (formData) 자동 주입 - TableSource 노드가 외부 데이터 우선 사용 - 버튼 컴포넌트 통합 - 기존 관계 실행 + 새 노드 플로우 실행 하이브리드 지원 - 노드 플로우 선택 UI 추가 - API 클라이언트 통합 (Axios) - 개발 문서 작성 - 노드 기반 제어 시스템 개선 계획 - 노드 연결 규칙 설계 - 노드 실행 엔진 설계 - 노드 구조 개선안 - 버튼 통합 분석
2025-10-02 16:22:29 +09:00
import { executeButtonWithFlow, handleFlowExecutionResult } from "@/lib/utils/nodeFlowButtonExecutor";
import { useCurrentFlowStep } from "@/stores/flowStepStore";
2025-09-18 10:05:50 +09:00
interface OptimizedButtonProps {
component: ComponentData;
onDataflowComplete?: (result: any) => void;
onActionComplete?: (result: any) => void;
formData?: Record<string, any>;
companyCode?: string;
disabled?: boolean;
selectedRows?: any[];
selectedRowsData?: any[];
flowSelectedData?: any[];
flowSelectedStepId?: number | null;
2025-10-24 14:11:12 +09:00
// 🆕 테이블 전체 데이터 (table-all 모드용)
tableAllData?: any[];
// 🆕 플로우 스텝 전체 데이터 (flow-step-all 모드용)
flowStepAllData?: any[];
// 🆕 테이블 전체 데이터 로드 콜백 (필요 시 부모에서 제공)
onRequestTableAllData?: () => Promise<any[]>;
// 🆕 플로우 스텝 전체 데이터 로드 콜백 (필요 시 부모에서 제공)
onRequestFlowStepAllData?: (stepId: number) => Promise<any[]>;
2025-09-18 10:05:50 +09:00
}
/**
* 🔥
*
* :
* 1. (0-100ms)
* 2.
* 3.
* 4.
* 5.
*/
export const OptimizedButtonComponent: React.FC<OptimizedButtonProps> = ({
component,
onDataflowComplete,
onActionComplete,
formData = {},
companyCode = "DEFAULT",
disabled = false,
selectedRows = [],
selectedRowsData = [],
flowSelectedData = [],
flowSelectedStepId = null,
2025-10-24 14:11:12 +09:00
tableAllData = [],
flowStepAllData = [],
onRequestTableAllData,
onRequestFlowStepAllData,
2025-09-18 10:05:50 +09:00
}) => {
// 🔥 상태 관리
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 || "버튼";
const flowConfig = config?.flowVisibilityConfig;
// 🆕 현재 플로우 단계 구독
const currentStep = useCurrentFlowStep(flowConfig?.targetFlowComponentId);
// 🆕 버튼 표시 여부 계산
const shouldShowButton = useMemo(() => {
// 플로우 제어 비활성화 시 항상 표시
if (!flowConfig?.enabled) {
return true;
}
// 플로우 단계가 선택되지 않은 경우 처리
if (currentStep === null) {
// 🔧 화이트리스트 모드일 때는 단계 미선택 시 숨김
if (flowConfig.mode === "whitelist") {
console.log("🔍 [OptimizedButton] 화이트리스트 모드 + 단계 미선택 → 숨김");
return false;
}
// 블랙리스트나 all 모드는 표시
return true;
}
const { mode, visibleSteps = [], hiddenSteps = [] } = flowConfig;
let result = true;
if (mode === "whitelist") {
result = visibleSteps.includes(currentStep);
} else if (mode === "blacklist") {
result = !hiddenSteps.includes(currentStep);
} else if (mode === "all") {
result = true;
}
// 항상 로그 출력 (개발 모드뿐만 아니라)
console.log("🔍 [OptimizedButton] 표시 체크:", {
buttonId: component.id,
buttonLabel,
flowComponentId: flowConfig.targetFlowComponentId,
currentStep,
mode,
visibleSteps,
hiddenSteps,
result: result ? "표시 ✅" : "숨김 ❌",
});
return result;
}, [flowConfig, currentStep, component.id, buttonLabel]);
2025-09-18 10:05:50 +09:00
// 🔥 디바운싱된 클릭 핸들러 (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})`);
2025-09-18 10:05:50 +09:00
// 🔥 확장된 컨텍스트 데이터 수집
2025-09-18 10:05:50 +09:00
const contextData = {
...formData,
buttonId: component.id,
componentData: component,
timestamp: new Date().toISOString(),
clickCount,
};
// 🔥 확장된 제어 컨텍스트 생성
const extendedContext = {
formData,
selectedRows: selectedRows || [],
selectedRowsData: selectedRowsData || [],
flowSelectedData: flowSelectedData || [],
flowSelectedStepId: flowSelectedStepId,
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,
// });
2025-09-18 10:05:50 +09:00
if (config?.enableDataflowControl && config?.dataflowConfig) {
feat: 노드 기반 데이터 플로우 시스템 구현 - 노드 에디터 UI 구현 (React Flow 기반) - TableSource, DataTransform, INSERT, UPDATE, DELETE, UPSERT 노드 - 드래그앤드롭 노드 추가 및 연결 - 속성 패널을 통한 노드 설정 - 실시간 필드 라벨 표시 (column_labels 테이블 연동) - 데이터 변환 노드 (DataTransform) 기능 - EXPLODE: 구분자로 1개 행 → 여러 행 확장 - UPPERCASE, LOWERCASE, TRIM, CONCAT, SPLIT, REPLACE 등 12가지 변환 타입 - In-place 변환 지원 (타겟 필드 생략 시 소스 필드 덮어쓰기) - 변환된 필드가 하위 액션 노드에 자동 전달 - 노드 플로우 실행 엔진 - 위상 정렬을 통한 노드 실행 순서 결정 - 레벨별 병렬 실행 (Promise.allSettled) - 부분 실패 허용 (한 노드 실패 시 연결된 하위 노드만 스킵) - 트랜잭션 기반 안전한 데이터 처리 - UPSERT 액션 로직 구현 - DB 제약 조건 없이 SELECT → UPDATE or INSERT 방식 - 복합 충돌 키 지원 (예: sales_no + product_name) - 파라미터 인덱스 정확한 매핑 - 데이터 소스 자동 감지 - 테이블 선택 데이터 (selectedRowsData) 자동 주입 - 폼 입력 데이터 (formData) 자동 주입 - TableSource 노드가 외부 데이터 우선 사용 - 버튼 컴포넌트 통합 - 기존 관계 실행 + 새 노드 플로우 실행 하이브리드 지원 - 노드 플로우 선택 UI 추가 - API 클라이언트 통합 (Axios) - 개발 문서 작성 - 노드 기반 제어 시스템 개선 계획 - 노드 연결 규칙 설계 - 노드 실행 엔진 설계 - 노드 구조 개선안 - 버튼 통합 분석
2025-10-02 16:22:29 +09:00
// 🆕 노드 플로우 방식 실행
if (config.dataflowConfig.controlMode === "flow" && config.dataflowConfig.flowConfig) {
console.log("🔄 노드 플로우 방식 실행:", config.dataflowConfig.flowConfig);
2025-10-24 14:11:12 +09:00
console.log("📊 전달될 데이터 확인:", {
controlDataSource: config.dataflowConfig.controlDataSource,
formDataKeys: Object.keys(formData),
selectedRowsDataLength: selectedRowsData.length,
flowSelectedDataLength: flowSelectedData.length,
flowSelectedStepId,
});
// 🆕 데이터 소스에 따라 추가 데이터 로드
let preparedTableAllData = tableAllData;
let preparedFlowStepAllData = flowStepAllData;
const dataSource = config.dataflowConfig.controlDataSource;
// table-all 모드일 때 데이터 로드
if (dataSource === "table-all" || dataSource === "all-sources") {
if (tableAllData.length === 0 && onRequestTableAllData) {
console.log("📊 테이블 전체 데이터 로드 중...");
try {
preparedTableAllData = await onRequestTableAllData();
console.log(`✅ 테이블 전체 데이터 ${preparedTableAllData.length}건 로드 완료`);
} catch (error) {
console.error("❌ 테이블 전체 데이터 로드 실패:", error);
toast.error("테이블 전체 데이터를 불러오지 못했습니다");
}
}
}
// flow-step-all 모드일 때 데이터 로드
if ((dataSource === "flow-step-all" || dataSource === "all-sources") && flowSelectedStepId) {
if (flowStepAllData.length === 0 && onRequestFlowStepAllData) {
console.log(`📊 플로우 스텝 ${flowSelectedStepId} 전체 데이터 로드 중...`);
try {
preparedFlowStepAllData = await onRequestFlowStepAllData(flowSelectedStepId);
console.log(`✅ 플로우 스텝 전체 데이터 ${preparedFlowStepAllData.length}건 로드 완료`);
} catch (error) {
console.error("❌ 플로우 스텝 전체 데이터 로드 실패:", error);
toast.error("플로우 스텝 전체 데이터를 불러오지 못했습니다");
}
}
}
feat: 노드 기반 데이터 플로우 시스템 구현 - 노드 에디터 UI 구현 (React Flow 기반) - TableSource, DataTransform, INSERT, UPDATE, DELETE, UPSERT 노드 - 드래그앤드롭 노드 추가 및 연결 - 속성 패널을 통한 노드 설정 - 실시간 필드 라벨 표시 (column_labels 테이블 연동) - 데이터 변환 노드 (DataTransform) 기능 - EXPLODE: 구분자로 1개 행 → 여러 행 확장 - UPPERCASE, LOWERCASE, TRIM, CONCAT, SPLIT, REPLACE 등 12가지 변환 타입 - In-place 변환 지원 (타겟 필드 생략 시 소스 필드 덮어쓰기) - 변환된 필드가 하위 액션 노드에 자동 전달 - 노드 플로우 실행 엔진 - 위상 정렬을 통한 노드 실행 순서 결정 - 레벨별 병렬 실행 (Promise.allSettled) - 부분 실패 허용 (한 노드 실패 시 연결된 하위 노드만 스킵) - 트랜잭션 기반 안전한 데이터 처리 - UPSERT 액션 로직 구현 - DB 제약 조건 없이 SELECT → UPDATE or INSERT 방식 - 복합 충돌 키 지원 (예: sales_no + product_name) - 파라미터 인덱스 정확한 매핑 - 데이터 소스 자동 감지 - 테이블 선택 데이터 (selectedRowsData) 자동 주입 - 폼 입력 데이터 (formData) 자동 주입 - TableSource 노드가 외부 데이터 우선 사용 - 버튼 컴포넌트 통합 - 기존 관계 실행 + 새 노드 플로우 실행 하이브리드 지원 - 노드 플로우 선택 UI 추가 - API 클라이언트 통합 (Axios) - 개발 문서 작성 - 노드 기반 제어 시스템 개선 계획 - 노드 연결 규칙 설계 - 노드 실행 엔진 설계 - 노드 구조 개선안 - 버튼 통합 분석
2025-10-02 16:22:29 +09:00
const flowResult = await executeButtonWithFlow(
config.dataflowConfig.flowConfig,
{
buttonId: component.id,
screenId: component.screenId,
companyCode,
userId: contextData.userId,
formData,
selectedRows: selectedRows || [],
selectedRowsData: selectedRowsData || [],
2025-10-24 14:11:12 +09:00
flowSelectedData: flowSelectedData || [],
flowStepId: flowSelectedStepId || undefined,
feat: 노드 기반 데이터 플로우 시스템 구현 - 노드 에디터 UI 구현 (React Flow 기반) - TableSource, DataTransform, INSERT, UPDATE, DELETE, UPSERT 노드 - 드래그앤드롭 노드 추가 및 연결 - 속성 패널을 통한 노드 설정 - 실시간 필드 라벨 표시 (column_labels 테이블 연동) - 데이터 변환 노드 (DataTransform) 기능 - EXPLODE: 구분자로 1개 행 → 여러 행 확장 - UPPERCASE, LOWERCASE, TRIM, CONCAT, SPLIT, REPLACE 등 12가지 변환 타입 - In-place 변환 지원 (타겟 필드 생략 시 소스 필드 덮어쓰기) - 변환된 필드가 하위 액션 노드에 자동 전달 - 노드 플로우 실행 엔진 - 위상 정렬을 통한 노드 실행 순서 결정 - 레벨별 병렬 실행 (Promise.allSettled) - 부분 실패 허용 (한 노드 실패 시 연결된 하위 노드만 스킵) - 트랜잭션 기반 안전한 데이터 처리 - UPSERT 액션 로직 구현 - DB 제약 조건 없이 SELECT → UPDATE or INSERT 방식 - 복합 충돌 키 지원 (예: sales_no + product_name) - 파라미터 인덱스 정확한 매핑 - 데이터 소스 자동 감지 - 테이블 선택 데이터 (selectedRowsData) 자동 주입 - 폼 입력 데이터 (formData) 자동 주입 - TableSource 노드가 외부 데이터 우선 사용 - 버튼 컴포넌트 통합 - 기존 관계 실행 + 새 노드 플로우 실행 하이브리드 지원 - 노드 플로우 선택 UI 추가 - API 클라이언트 통합 (Axios) - 개발 문서 작성 - 노드 기반 제어 시스템 개선 계획 - 노드 연결 규칙 설계 - 노드 실행 엔진 설계 - 노드 구조 개선안 - 버튼 통합 분석
2025-10-02 16:22:29 +09:00
controlDataSource: config.dataflowConfig.controlDataSource,
2025-10-24 14:11:12 +09:00
// 🆕 확장된 데이터 소스
tableAllData: preparedTableAllData,
flowStepAllData: preparedFlowStepAllData,
feat: 노드 기반 데이터 플로우 시스템 구현 - 노드 에디터 UI 구현 (React Flow 기반) - TableSource, DataTransform, INSERT, UPDATE, DELETE, UPSERT 노드 - 드래그앤드롭 노드 추가 및 연결 - 속성 패널을 통한 노드 설정 - 실시간 필드 라벨 표시 (column_labels 테이블 연동) - 데이터 변환 노드 (DataTransform) 기능 - EXPLODE: 구분자로 1개 행 → 여러 행 확장 - UPPERCASE, LOWERCASE, TRIM, CONCAT, SPLIT, REPLACE 등 12가지 변환 타입 - In-place 변환 지원 (타겟 필드 생략 시 소스 필드 덮어쓰기) - 변환된 필드가 하위 액션 노드에 자동 전달 - 노드 플로우 실행 엔진 - 위상 정렬을 통한 노드 실행 순서 결정 - 레벨별 병렬 실행 (Promise.allSettled) - 부분 실패 허용 (한 노드 실패 시 연결된 하위 노드만 스킵) - 트랜잭션 기반 안전한 데이터 처리 - UPSERT 액션 로직 구현 - DB 제약 조건 없이 SELECT → UPDATE or INSERT 방식 - 복합 충돌 키 지원 (예: sales_no + product_name) - 파라미터 인덱스 정확한 매핑 - 데이터 소스 자동 감지 - 테이블 선택 데이터 (selectedRowsData) 자동 주입 - 폼 입력 데이터 (formData) 자동 주입 - TableSource 노드가 외부 데이터 우선 사용 - 버튼 컴포넌트 통합 - 기존 관계 실행 + 새 노드 플로우 실행 하이브리드 지원 - 노드 플로우 선택 UI 추가 - API 클라이언트 통합 (Axios) - 개발 문서 작성 - 노드 기반 제어 시스템 개선 계획 - 노드 연결 규칙 설계 - 노드 실행 엔진 설계 - 노드 구조 개선안 - 버튼 통합 분석
2025-10-02 16:22:29 +09:00
},
// 원래 액션 (timing이 before나 after일 때 실행)
async () => {
if (!isControlOnlyAction) {
await executeOriginalAction(config?.actionType || "save", contextData);
}
},
);
handleFlowExecutionResult(flowResult, {
buttonId: component.id,
formData,
onRefresh: onDataflowComplete,
});
if (onActionComplete) {
onActionComplete(flowResult);
}
return;
}
// 🔥 기존 관계 방식 실행
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);
} else if (isControlOnlyAction) {
// 🔥 제어관리가 비활성화된 상태에서 제어 액션
toast.warning(
"제어관리를 먼저 활성화해주세요. 제어 액션을 사용하려면 버튼 설정에서 '제어관리 활성화'를 체크하고 조건을 설정해주세요.",
);
return;
2025-09-18 10:05:50 +09:00
} else {
// 🔥 기존 액션만 실행 (제어 액션 제외)
2025-09-18 10:05:50 +09:00
await executeOriginalAction(config?.actionType || "save", contextData);
}
} catch (error) {
// 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) {
// console.warn(`🐌 Slow button execution: ${totalTime.toFixed(2)}ms`);
2025-09-18 10:05:50 +09:00
} else {
// 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":
// console.log("💾 Save action completed:", result);
2025-09-18 10:05:50 +09:00
break;
case "delete":
// console.log("🗑️ Delete action completed:", result);
2025-09-18 10:05:50 +09:00
break;
case "search":
// console.log("🔍 Search action completed:", result);
2025-09-18 10:05:50 +09:00
break;
case "add":
// console.log(" Add action completed:", result);
2025-09-18 10:05:50 +09:00
break;
case "edit":
// console.log("✏️ Edit action completed:", result);
2025-09-18 10:05:50 +09:00
break;
default:
// 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;
});
// 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) {
// 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) {
// 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> => {
// 🔥 제어 액션은 여기서 처리하지 않음 (이미 위에서 처리됨)
if (actionType === "control") {
// console.warn("제어 액션은 executeOriginalAction에서 처리되지 않아야 합니다.");
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: "페이지 이동",
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>
);
};
// 🆕 플로우 단계별 표시 제어
if (!shouldShowButton) {
// 레이아웃 동작에 따라 다르게 처리
if (flowConfig?.layoutBehavior === "preserve-position") {
// 위치 유지 (빈 공간, display: none)
return <div style={{ display: "none" }} />;
} else {
// 완전히 렌더링하지 않음 (auto-compact, 빈 공간 제거)
return null;
}
}
// 색상이 설정되어 있으면 variant 스타일을 무시하고 직접 스타일 적용
const hasCustomColors = config?.backgroundColor || config?.textColor;
2025-09-18 10:05:50 +09:00
return (
<div className="relative">
<Button
onClick={handleClick}
disabled={isExecuting || disabled}
// 색상이 설정되어 있으면 variant를 적용하지 않아서 Tailwind 색상 클래스가 덮어씌우지 않도록 함
variant={hasCustomColors ? undefined : (config?.variant || "default")}
2025-09-18 10:05:50 +09:00
className={cn(
"transition-all duration-200",
isExecuting && "cursor-wait opacity-75",
backgroundJobs.size > 0 && "border-primary/20 bg-accent",
// 커스텀 색상이 없을 때만 기본 스타일 적용
!hasCustomColors && "bg-primary text-primary-foreground hover:bg-primary/90",
2025-09-18 10:05:50 +09:00
)}
style={{
// 커스텀 색상이 있을 때만 인라인 스타일 적용
...(config?.backgroundColor && { backgroundColor: config.backgroundColor }),
...(config?.textColor && { color: config.textColor }),
...(config?.borderColor && { borderColor: config.borderColor }),
2025-09-18 10:05:50 +09:00
}}
>
{/* 메인 버튼 내용 */}
<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()}
{/* 🆕 플로우 제어 활성화 표시 */}
{flowConfig?.enabled && (
<div className="absolute -right-1 -top-1">
<Badge variant="outline" className="h-4 bg-white px-1 text-xs" title="플로우 단계별 표시 제어 활성화">
<Workflow className="h-3 w-3" />
</Badge>
</div>
)}
2025-09-18 10:05:50 +09:00
{/* 제어관리 활성화 표시 */}
{config?.enableDataflowControl && (
<div className="absolute -right-1 -bottom-1">
<Badge variant="outline" className="h-4 bg-white px-1 text-xs" title="제어관리 활성화">
2025-09-18 10:05:50 +09:00
🔧
</Badge>
</div>
)}
</div>
);
};
export default OptimizedButtonComponent;