From 3e59693e017742d971f490eb7c58870cb745a98d Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Fri, 18 Jul 2025 03:58:34 +0000 Subject: (김준회) 결재 모듈 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/knox/approval/ApprovalCancel.tsx | 341 ++++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 components/knox/approval/ApprovalCancel.tsx (limited to 'components/knox/approval/ApprovalCancel.tsx') diff --git a/components/knox/approval/ApprovalCancel.tsx b/components/knox/approval/ApprovalCancel.tsx new file mode 100644 index 00000000..d077bfc6 --- /dev/null +++ b/components/knox/approval/ApprovalCancel.tsx @@ -0,0 +1,341 @@ +'use client' + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; +import { toast } from 'sonner'; +import { Loader2, XCircle, AlertTriangle, CheckCircle } from 'lucide-react'; + +// API 함수 및 타입 +import { cancelApproval, getApprovalDetail } from '@/lib/knox-api/approval/approval'; +import type { ApprovalDetailResponse } from '@/lib/knox-api/approval/approval'; + +// Mock 데이터 +import { mockApprovalAPI, getStatusText } from './mocks/approval-mock'; + +interface ApprovalCancelProps { + useFakeData?: boolean; + systemId?: string; + initialApInfId?: string; + onCancelSuccess?: (apInfId: string) => void; +} + +export default function ApprovalCancel({ + useFakeData = false, + systemId = 'EVCP_SYSTEM', + initialApInfId = '', + onCancelSuccess +}: ApprovalCancelProps) { + const [apInfId, setApInfId] = useState(initialApInfId); + const [approvalDetail, setApprovalDetail] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isCancelling, setIsCancelling] = useState(false); + const [error, setError] = useState(null); + const [cancelResult, setCancelResult] = useState<{ apInfId: string } | null>(null); + + const fetchApprovalDetail = async () => { + if (!apInfId.trim()) { + toast.error('결재 ID를 입력해주세요.'); + return; + } + + setIsLoading(true); + setError(null); + setApprovalDetail(null); + setCancelResult(null); + + try { + const response = useFakeData + ? await mockApprovalAPI.getApprovalDetail(apInfId) + : await getApprovalDetail(apInfId, systemId); + + if (response.result === 'SUCCESS') { + setApprovalDetail(response.data); + } else { + setError('결재 정보를 가져오는데 실패했습니다.'); + toast.error('결재 정보를 가져오는데 실패했습니다.'); + } + } catch (err) { + console.error('결재 상세 조회 오류:', err); + setError('결재 정보를 가져오는 중 오류가 발생했습니다.'); + toast.error('결재 정보를 가져오는 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + }; + + const handleCancelApproval = async () => { + if (!approvalDetail) return; + + setIsCancelling(true); + + try { + const response = useFakeData + ? await mockApprovalAPI.cancelApproval(approvalDetail.apInfId) + : await cancelApproval(approvalDetail.apInfId, systemId); + + if (response.result === 'SUCCESS') { + setCancelResult({ apInfId: response.data.apInfId }); + toast.success('결재가 성공적으로 취소되었습니다.'); + onCancelSuccess?.(response.data.apInfId); + + // 상태 업데이트 + setApprovalDetail({ + ...approvalDetail, + status: '4' // 상신취소 + }); + } else { + toast.error('결재 취소에 실패했습니다.'); + } + } catch (err) { + console.error('결재 취소 오류:', err); + toast.error('결재 취소 중 오류가 발생했습니다.'); + } finally { + setIsCancelling(false); + } + }; + + const formatDate = (dateString: string) => { + if (!dateString || dateString.length < 14) return dateString; + const year = dateString.substring(0, 4); + const month = dateString.substring(4, 6); + const day = dateString.substring(6, 8); + const hour = dateString.substring(8, 10); + const minute = dateString.substring(10, 12); + const second = dateString.substring(12, 14); + + return `${year}-${month}-${day} ${hour}:${minute}:${second}`; + }; + + const getStatusBadgeVariant = (status: string) => { + switch (status) { + case '2': // 완결 + return 'default'; + case '1': // 진행중 + return 'secondary'; + case '3': // 반려 + return 'destructive'; + case '4': // 상신취소 + return 'outline'; + default: + return 'outline'; + } + }; + + const canCancelApproval = (status: string) => { + // 진행중(1), 보류(0) 상태에서만 취소 가능 + return ['0', '1'].includes(status); + }; + + const getCancelabilityMessage = (status: string) => { + if (canCancelApproval(status)) { + return '이 결재는 취소할 수 있습니다.'; + } + + switch (status) { + case '2': + return '완결된 결재는 취소할 수 없습니다.'; + case '3': + return '반려된 결재는 취소할 수 없습니다.'; + case '4': + return '이미 취소된 결재입니다.'; + case '5': + return '전결 처리된 결재는 취소할 수 없습니다.'; + case '6': + return '후완결된 결재는 취소할 수 없습니다.'; + default: + return '현재 상태에서는 취소할 수 없습니다.'; + } + }; + + return ( + + + + + 결재 취소 + + + 결재 ID를 입력하여 상신을 취소합니다. {useFakeData && '(테스트 모드)'} + + + + + {/* 검색 영역 */} +
+
+ + setApInfId(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && fetchApprovalDetail()} + /> +
+ +
+ + {/* 취소 완료 메시지 */} + {cancelResult && ( +
+
+ + 취소 완료 +
+

+ 결재 ID: {cancelResult.apInfId}가 성공적으로 취소되었습니다. +

+
+ )} + + {/* 에러 메시지 */} + {error && ( +
+
+ + 오류 +
+

{error}

+
+ )} + + {/* 결재 정보 */} + {approvalDetail && ( +
+
+

결재 정보

+ +
+
+ +

{approvalDetail.apInfId}

+
+
+ +

{approvalDetail.subject}

+
+
+ +

{formatDate(approvalDetail.sbmDt)}

+
+
+ +
+ + {getStatusText(approvalDetail.status)} + +
+
+
+
+ + + + {/* 취소 가능 여부 */} +
+

취소 가능 여부

+ +
+
+ {canCancelApproval(approvalDetail.status) ? ( + + ) : ( + + )} + + {canCancelApproval(approvalDetail.status) ? '취소 가능' : '취소 불가'} + +
+

+ {getCancelabilityMessage(approvalDetail.status)} +

+
+
+ + {/* 취소 버튼 */} + {canCancelApproval(approvalDetail.status) && ( + <> + + +
+ + + + + + + 결재 취소 확인 + + 정말로 이 결재를 취소하시겠습니까? +
+
+ 결재 ID: {approvalDetail.apInfId} +
+ 제목: {approvalDetail.subject} +
+
+ 이 작업은 되돌릴 수 없습니다. +
+
+ + 취소 + + 결재 취소 + + +
+
+
+ + )} +
+ )} +
+
+ ); +} \ No newline at end of file -- cgit v1.2.3