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/ApprovalDetail.tsx | 362 ++++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 components/knox/approval/ApprovalDetail.tsx (limited to 'components/knox/approval/ApprovalDetail.tsx') diff --git a/components/knox/approval/ApprovalDetail.tsx b/components/knox/approval/ApprovalDetail.tsx new file mode 100644 index 00000000..034bde7d --- /dev/null +++ b/components/knox/approval/ApprovalDetail.tsx @@ -0,0 +1,362 @@ +'use client' + +import { useState, useEffect } 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 { toast } from 'sonner'; +import { Loader2, Search, FileText, Clock, User, AlertCircle } from 'lucide-react'; + +// API 함수 및 타입 +import { getApprovalDetail, getApprovalContent } from '@/lib/knox-api/approval/approval'; +import type { ApprovalDetailResponse, ApprovalContentResponse, ApprovalLine } from '@/lib/knox-api/approval/approval'; + +// Mock 데이터 +import { mockApprovalAPI, getStatusText, getRoleText, getApprovalStatusText } from './mocks/approval-mock'; + +interface ApprovalDetailProps { + useFakeData?: boolean; + systemId?: string; + initialApInfId?: string; +} + +interface ApprovalDetailData { + detail: ApprovalDetailResponse['data']; + content: ApprovalContentResponse['data']; +} + +export default function ApprovalDetail({ + useFakeData = false, + systemId = 'EVCP_SYSTEM', + initialApInfId = '' +}: ApprovalDetailProps) { + const [apInfId, setApInfId] = useState(initialApInfId); + const [approvalData, setApprovalData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchApprovalDetail = async (id: string) => { + if (!id.trim()) { + toast.error('결재 ID를 입력해주세요.'); + return; + } + + setIsLoading(true); + setError(null); + setApprovalData(null); + + try { + const [detailResponse, contentResponse] = await Promise.all([ + useFakeData + ? mockApprovalAPI.getApprovalDetail(id) + : getApprovalDetail(id, systemId), + useFakeData + ? mockApprovalAPI.getApprovalContent(id) + : getApprovalContent(id, systemId) + ]); + + if (detailResponse.result === 'SUCCESS' && contentResponse.result === 'SUCCESS') { + setApprovalData({ + detail: detailResponse.data, + content: contentResponse.data + }); + } else { + setError('결재 정보를 가져오는데 실패했습니다.'); + toast.error('결재 정보를 가져오는데 실패했습니다.'); + } + } catch (err) { + console.error('결재 상세 조회 오류:', err); + setError('결재 정보를 가져오는 중 오류가 발생했습니다.'); + toast.error('결재 정보를 가져오는 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + }; + + const formatDate = (dateString: string) => { + if (!dateString || dateString.length < 14) return dateString; + // YYYYMMDDHHMMSS 형식을 YYYY-MM-DD HH:MM:SS로 변환 + 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 getSecurityTypeText = (type: string) => { + const typeMap: Record = { + 'PERSONAL': '개인', + 'CONFIDENTIAL': '기밀', + 'CONFIDENTIAL_STRICT': '극기밀' + }; + return typeMap[type] || type; + }; + + const getSecurityTypeBadgeVariant = (type: string) => { + switch (type) { + case 'PERSONAL': + return 'default'; + case 'CONFIDENTIAL': + return 'secondary'; + case 'CONFIDENTIAL_STRICT': + return 'destructive'; + default: + return 'outline'; + } + }; + + 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'; + } + }; + + // 초기 로딩 (initialApInfId가 있는 경우) + useEffect(() => { + if (initialApInfId) { + fetchApprovalDetail(initialApInfId); + } + }, [initialApInfId, useFakeData, systemId]); + + return ( + + + + + 결재 상세 조회 + + + 결재 ID를 입력하여 상세 정보를 조회합니다. {useFakeData && '(테스트 모드)'} + + + + + {/* 검색 영역 */} +
+
+ + setApInfId(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && fetchApprovalDetail(apInfId)} + /> +
+ +
+ + {/* 에러 메시지 */} + {error && ( +
+
+ + 오류 +
+

{error}

+
+ )} + + {/* 결재 상세 정보 */} + {approvalData && ( +
+ {/* 기본 정보 */} +
+

+ + 기본 정보 +

+ +
+
+ +

{approvalData.detail.apInfId}

+
+
+ +

{approvalData.detail.systemId}

+
+
+ +

{approvalData.detail.subject}

+
+
+ +

+ + {formatDate(approvalData.detail.sbmDt)} +

+
+
+ +
+ + {getStatusText(approvalData.detail.status)} + +
+
+
+ +
+ + {getSecurityTypeText(approvalData.detail.docSecuType)} + +
+
+
+ +
+ + {approvalData.detail.urgYn === 'Y' ? '긴급' : '일반'} + +
+
+
+ +

{approvalData.detail.sbmLang}

+
+
+
+ + + + {/* 결재 내용 */} +
+

결재 내용

+ +
+
+ + + {approvalData.content.contentsType} + +
+ +
+
+                    {approvalData.content.contents}
+                  
+
+
+
+ + + + {/* 결재 경로 */} +
+

+ + 결재 경로 +

+ +
+ {approvalData.detail.aplns.map((apln: ApprovalLine, index: number) => ( +
+ + {apln.seq} + + +
+
+ +

{apln.userId || apln.epId || '-'}

+
+
+ +

{apln.emailAddress || '-'}

+
+
+ +
+ + {getRoleText(apln.role)} + +
+
+
+ +
+ + {getApprovalStatusText(apln.aplnStatsCode)} + +
+
+
+ +
+ {apln.arbPmtYn === 'Y' && ( + 전결권한 + )} + {apln.contentsMdfyPmtYn === 'Y' && ( + 본문수정 + )} + {apln.aplnMdfyPmtYn === 'Y' && ( + 경로변경 + )} +
+
+ ))} +
+
+ + {/* 첨부파일 */} + {approvalData.detail.attachments && approvalData.detail.attachments.length > 0 && ( + <> + +
+

첨부파일

+ +
+ {approvalData.detail.attachments.map((attachment: any, index: number) => ( +
+ +
+

{attachment.fileName || `첨부파일 ${index + 1}`}

+

{attachment.fileSize || '크기 정보 없음'}

+
+ +
+ ))} +
+
+ + )} +
+ )} +
+
+ ); +} \ No newline at end of file -- cgit v1.2.3