"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; companyCode?: string; disabled?: boolean; } /** * ๐Ÿ”ฅ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋œ ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ * * ํ•ต์‹ฌ ๊ธฐ๋Šฅ: * 1. ์ฆ‰์‹œ ์‘๋‹ต (0-100ms) * 2. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ œ์–ด๊ด€๋ฆฌ ์ฒ˜๋ฆฌ * 3. ์‹ค์‹œ๊ฐ„ ์ƒํƒœ ์ถ”์  * 4. ๋””๋ฐ”์šด์‹ฑ์œผ๋กœ ์ค‘๋ณต ํด๋ฆญ ๋ฐฉ์ง€ * 5. ์‹œ๊ฐ์  ํ”ผ๋“œ๋ฐฑ */ export const OptimizedButtonComponent: React.FC = ({ component, onDataflowComplete, onActionComplete, formData = {}, companyCode = "DEFAULT", disabled = false, }) => { // ๐Ÿ”ฅ ์ƒํƒœ ๊ด€๋ฆฌ const [isExecuting, setIsExecuting] = useState(false); const [executionTime, setExecutionTime] = useState(null); const [backgroundJobs, setBackgroundJobs] = useState>(new Set()); const [lastResult, setLastResult] = useState(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) => { 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 => { 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, ): Promise => { // ๐Ÿ”ฅ ์ œ์–ด ์•ก์…˜์€ ์—ฌ๊ธฐ์„œ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Œ (์ด๋ฏธ ์œ„์—์„œ ์ฒ˜๋ฆฌ๋จ) 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 = { save: "์ €์žฅ", delete: "์‚ญ์ œ", edit: "์ˆ˜์ •", add: "์ถ”๊ฐ€", search: "๊ฒ€์ƒ‰", reset: "์ดˆ๊ธฐํ™”", submit: "์ œ์ถœ", close: "๋‹ซ๊ธฐ", popup: "ํŒ์—…", modal: "๋ชจ๋‹ฌ", newWindow: "์ƒˆ ์ฐฝ", navigate: "ํŽ˜์ด์ง€ ์ด๋™", control: "์ œ์–ด", }; return displayNames[actionType] || actionType; }; /** * ๋ฒ„ํŠผ ์ƒํƒœ์— ๋”ฐ๋ฅธ ์•„์ด์ฝ˜ */ const getStatusIcon = () => { if (isExecuting) { return ; } if (lastResult?.success === false) { return ; } if (lastResult?.success === true) { return ; } return null; }; /** * ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์ƒํƒœ ํ‘œ์‹œ */ const renderBackgroundStatus = () => { if (backgroundJobs.size === 0) return null; return (
{backgroundJobs.size}
); }; return (
{/* ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… ์ƒํƒœ ํ‘œ์‹œ */} {renderBackgroundStatus()} {/* ์ œ์–ด๊ด€๋ฆฌ ํ™œ์„ฑํ™” ํ‘œ์‹œ */} {config?.enableDataflowControl && (
๐Ÿ”ง
)}
); }; export default OptimizedButtonComponent;