ERP-node/frontend/lib/meta-components/Action/ActionRenderer.tsx

371 lines
9.6 KiB
TypeScript
Raw Normal View History

2026-03-01 03:39:00 +09:00
/**
* 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<string, any>;
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 (
<>
<Button
id={id}
variant={getButtonVariant()}
onClick={handleClick}
disabled={disabled || loading || isDesignMode}
className={cn("h-8 text-xs sm:h-10 sm:text-sm", className)}
>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{config.label}
</Button>
{/* 확인 대화상자 */}
{config.confirmDialog && (
<Dialog open={showConfirm} onOpenChange={setShowConfirm}>
<DialogContent className="max-w-[95vw] sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">
{config.confirmDialog.title || "확인"}
</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
{config.confirmDialog.message || "이 작업을 수행하시겠습니까?"}
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => setShowConfirm(false)}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
<Button
variant={config.buttonType === "destructive" ? "destructive" : "default"}
onClick={executeAction}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</>
);
}