From 89af35093519429f1acb5fcb336c0a30d9931f1e Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Tue, 3 Mar 2026 22:00:52 +0900 Subject: [PATCH] [agent-pipeline] pipe-20260303124213-d7zo round-4 --- frontend/app/(main)/approval/page.tsx | 426 ++++++++++++++++++ frontend/app/(main)/layout.tsx | 2 + .../approval/ApprovalGlobalListener.tsx | 52 +++ .../approval/ApprovalRequestModal.tsx | 420 +++++++++++++++++ .../config-panels/ButtonConfigPanel.tsx | 95 ++++ frontend/types/v2-core.ts | 5 +- 6 files changed, 999 insertions(+), 1 deletion(-) create mode 100644 frontend/app/(main)/approval/page.tsx create mode 100644 frontend/components/approval/ApprovalGlobalListener.tsx create mode 100644 frontend/components/approval/ApprovalRequestModal.tsx diff --git a/frontend/app/(main)/approval/page.tsx b/frontend/app/(main)/approval/page.tsx new file mode 100644 index 00000000..26af713c --- /dev/null +++ b/frontend/app/(main)/approval/page.tsx @@ -0,0 +1,426 @@ +"use client"; + +import React, { useState, useEffect, useCallback } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Loader2, CheckCircle2, XCircle, Clock, FileCheck2 } from "lucide-react"; +import { + getApprovalRequests, + getApprovalRequest, + getMyPendingApprovals, + processApprovalLine, + cancelApprovalRequest, + type ApprovalRequest, + type ApprovalLine, +} from "@/lib/api/approval"; + +// 상태 배지 색상 +const statusConfig: Record = { + requested: { label: "요청됨", variant: "secondary" }, + in_progress: { label: "진행 중", variant: "default" }, + approved: { label: "승인됨", variant: "outline" }, + rejected: { label: "반려됨", variant: "destructive" }, + cancelled: { label: "취소됨", variant: "secondary" }, +}; + +const lineStatusConfig: Record = { + waiting: { label: "대기", icon: }, + pending: { label: "진행 중", icon: }, + approved: { label: "승인", icon: }, + rejected: { label: "반려", icon: }, + skipped: { label: "건너뜀", icon: }, +}; + +// 결재 상세 모달 +interface ApprovalDetailModalProps { + request: ApprovalRequest | null; + open: boolean; + onClose: () => void; + onRefresh: () => void; + pendingLineId?: number; // 내가 처리해야 할 결재 라인 ID +} + +function ApprovalDetailModal({ request, open, onClose, onRefresh, pendingLineId }: ApprovalDetailModalProps) { + const [comment, setComment] = useState(""); + const [isProcessing, setIsProcessing] = useState(false); + const [isCancelling, setIsCancelling] = useState(false); + + useEffect(() => { + if (!open) setComment(""); + }, [open]); + + const handleProcess = async (action: "approved" | "rejected") => { + if (!pendingLineId) return; + setIsProcessing(true); + const res = await processApprovalLine(pendingLineId, { action, comment: comment.trim() || undefined }); + setIsProcessing(false); + if (res.success) { + onRefresh(); + onClose(); + } + }; + + const handleCancel = async () => { + if (!request) return; + setIsCancelling(true); + const res = await cancelApprovalRequest(request.request_id); + setIsCancelling(false); + if (res.success) { + onRefresh(); + onClose(); + } + }; + + if (!request) return null; + + const statusInfo = statusConfig[request.status] || { label: request.status, variant: "secondary" as const }; + + return ( + + + + + + {request.title} + + + + {statusInfo.label} + + 요청자: {request.requester_name || request.requester_id} + {request.requester_dept ? ` (${request.requester_dept})` : ""} + + + +
+ {/* 결재 사유 */} + {request.description && ( +
+

결재 사유

+

{request.description}

+
+ )} + + {/* 결재선 */} +
+

결재선

+
+ {(request.lines || []).map((line) => { + const lineStatus = lineStatusConfig[line.status] || { label: line.status, icon: null }; + return ( +
+
+ {lineStatus.icon} +
+

+ {line.approver_label || `${line.step_order}차 결재`} — {line.approver_name || line.approver_id} +

+ {line.approver_position && ( +

{line.approver_position}

+ )} + {line.comment && ( +

+ 의견: {line.comment} +

+ )} +
+
+ {lineStatus.label} +
+ ); + })} +
+
+ + {/* 승인/반려 입력 (대기 상태일 때만) */} + {pendingLineId && ( +
+

결재 의견 (선택사항)

+