/** * V3 Action 메타 컴포넌트 렌더러 * - 버튼 + CRUD 액션 실행 * - config.steps 순차 처리 * - 자체적으로 완전히 동작하는 액션 시스템 */ "use client"; import React, { useState } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { ActionComponentConfig } from "@/lib/api/metaComponent"; import { tableTypeApi } from "@/lib/api/screen"; import { v2EventBus } from "@/lib/v2-core/events/EventBus"; import { V2_EVENTS } from "@/lib/v2-core/events/types"; import { apiClient } from "@/lib/api/client"; import { cn } from "@/lib/utils"; import { Loader2 } from "lucide-react"; import { toast } from "sonner"; import { useRouter } from "next/navigation"; interface ActionRendererProps { id: string; config: ActionComponentConfig; // 데이터 formData?: Record; selectedRowsData?: any[]; // 컨텍스트 tableName?: string; companyCode?: string; screenId?: number; userId?: string; // 콜백 onRefresh?: () => void; // UI isDesignMode?: boolean; disabled?: boolean; className?: string; } export function ActionRenderer({ id, config, formData = {}, selectedRowsData = [], tableName, companyCode, screenId, userId, onRefresh, isDesignMode = false, disabled = false, className, }: ActionRendererProps) { const [loading, setLoading] = useState(false); const [showConfirm, setShowConfirm] = useState(false); const router = useRouter(); // 버튼 클릭 핸들러 const handleClick = async () => { // 디자인 모드에서는 실행 안 함 if (isDesignMode) { return; } // 활성화 조건 체크 if (config.enableCondition) { const isEnabled = evaluateCondition(config.enableCondition); if (!isEnabled) { toast.warning("이 작업을 실행할 수 없습니다."); return; } } // 확인 대화상자가 있으면 먼저 표시 if (config.confirmDialog) { setShowConfirm(true); return; } // 확인 없이 바로 실행 await executeAction(); }; // 조건 평가 (간단한 버전) const evaluateCondition = (condition: any): boolean => { // TODO: 복잡한 조건 평가 로직 구현 // 현재는 항상 true 반환 return true; }; // 액션 실행 const executeAction = async () => { setShowConfirm(false); setLoading(true); try { // config.steps 순차 실행 if (config.steps && config.steps.length > 0) { for (const step of config.steps) { await executeStep(step); } } else { toast.info("실행할 액션이 없습니다."); } } catch (error: any) { console.error("Action 실행 실패:", error); toast.error(error.message || "액션 실행에 실패했습니다."); } finally { setLoading(false); } }; // 개별 스텝 실행 const executeStep = async (step: any) => { switch (step.type) { case "save": await executeSaveStep(step); break; case "delete": await executeDeleteStep(step); break; case "refresh": executeRefreshStep(step); break; case "toast": executeToastStep(step); break; case "api": await executeApiStep(step); break; case "navigate": executeNavigateStep(step); break; default: console.warn(`알 수 없는 스텝 타입: ${step.type}`); } }; // Save 스텝 실행 const executeSaveStep = async (step: any) => { const targetTable = step.target || tableName; if (!targetTable) { throw new Error("저장할 테이블명이 없습니다."); } // formData에서 데이터 수집 const dataToSave = { ...formData }; // company_code 자동 추가 if (companyCode && !dataToSave.company_code) { dataToSave.company_code = companyCode; } // 저장 모드 판단: 새로 생성 vs 수정 const isEdit = step.mode === "edit" || (dataToSave && Object.keys(dataToSave).length > 0 && dataToSave.id); if (isEdit) { // 수정 모드 await tableTypeApi.editTableData(targetTable, dataToSave, dataToSave); toast.success("수정 완료"); } else { // 생성 모드 await tableTypeApi.addTableData(targetTable, dataToSave); toast.success("저장 완료"); } // 테이블 새로고침 이벤트 발행 v2EventBus.emitSync(V2_EVENTS.TABLE_REFRESH, { tableName: targetTable, target: "single", }); // onRefresh 콜백 호출 onRefresh?.(); }; // Delete 스텝 실행 const executeDeleteStep = async (step: any) => { const targetTable = step.target || tableName; if (!targetTable) { throw new Error("삭제할 테이블명이 없습니다."); } // 선택된 행 확인 if (!selectedRowsData || selectedRowsData.length === 0) { toast.warning("삭제할 데이터를 선택해주세요."); return; } // 선택된 행 삭제 await tableTypeApi.deleteTableData(targetTable, selectedRowsData); toast.success(`${selectedRowsData.length}개 항목이 삭제되었습니다.`); // 테이블 새로고침 이벤트 발행 v2EventBus.emitSync(V2_EVENTS.TABLE_REFRESH, { tableName: targetTable, target: "single", }); // onRefresh 콜백 호출 onRefresh?.(); }; // Refresh 스텝 실행 const executeRefreshStep = (step: any) => { const targetTable = step.target || tableName; if (targetTable) { // 특정 테이블 새로고침 v2EventBus.emitSync(V2_EVENTS.TABLE_REFRESH, { tableName: targetTable, target: "single", }); } else { // 전체 새로고침 v2EventBus.emitSync(V2_EVENTS.TABLE_REFRESH, { target: "all", }); } // onRefresh 콜백 호출 onRefresh?.(); toast.success("새로고침 완료"); }; // Toast 스텝 실행 const executeToastStep = (step: any) => { const variant = step.variant || "info"; const message = step.message || "알림"; switch (variant) { case "success": toast.success(message); break; case "error": toast.error(message); break; case "warning": toast.warning(message); break; case "info": default: toast.info(message); break; } }; // API 스텝 실행 const executeApiStep = async (step: any) => { const method = (step.method || "GET").toLowerCase(); const endpoint = step.endpoint; const body = step.body || {}; if (!endpoint) { throw new Error("API 엔드포인트가 없습니다."); } let response; switch (method) { case "get": response = await apiClient.get(endpoint); break; case "post": response = await apiClient.post(endpoint, body); break; case "put": response = await apiClient.put(endpoint, body); break; case "delete": response = await apiClient.delete(endpoint, { data: body }); break; default: throw new Error(`지원하지 않는 HTTP 메서드: ${method}`); } // 성공 메시지 if (step.successMessage) { toast.success(step.successMessage); } return response; }; // Navigate 스텝 실행 const executeNavigateStep = (step: any) => { const path = step.path; if (!path) { console.warn("Navigate 스텝에 path가 없습니다."); return; } // Next.js router로 화면 이동 router.push(path); }; // 버튼 variant 매핑 const getButtonVariant = () => { switch (config.buttonType) { case "primary": return "default"; case "secondary": return "secondary"; case "destructive": return "destructive"; case "ghost": return "ghost"; case "outline": return "outline"; default: return "default"; } }; return ( <> {/* 확인 대화상자 */} {config.confirmDialog && ( {config.confirmDialog.title || "확인"} {config.confirmDialog.message || "이 작업을 수행하시겠습니까?"} )} ); }