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/ApprovalList.tsx | 322 ++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 components/knox/approval/ApprovalList.tsx (limited to 'components/knox/approval/ApprovalList.tsx') diff --git a/components/knox/approval/ApprovalList.tsx b/components/knox/approval/ApprovalList.tsx new file mode 100644 index 00000000..7f80e74a --- /dev/null +++ b/components/knox/approval/ApprovalList.tsx @@ -0,0 +1,322 @@ +'use client' + +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { toast } from 'sonner'; +import { Loader2, List, Eye, RefreshCw, AlertCircle } from 'lucide-react'; + +// API 함수 및 타입 +import { getSubmissionList, getApprovalHistory } from '@/lib/knox-api/approval/approval'; +import type { SubmissionListResponse, ApprovalHistoryResponse } from '@/lib/knox-api/approval/approval'; + +// Mock 데이터 +import { mockApprovalAPI, getStatusText } from './mocks/approval-mock'; + +interface ApprovalListProps { + useFakeData?: boolean; + systemId?: string; + type?: 'submission' | 'history'; + onItemClick?: (apInfId: string) => void; +} + +type ListItem = { + apInfId: string; + subject: string; + sbmDt: string; + status: string; + urgYn?: string; + docSecuType?: string; + actionType?: string; + actionDt?: string; + userId?: string; +}; + +export default function ApprovalList({ + useFakeData = false, + systemId = 'EVCP_SYSTEM', + type = 'submission', + onItemClick +}: ApprovalListProps) { + const [listData, setListData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchData = async () => { + setIsLoading(true); + setError(null); + + try { + let response: SubmissionListResponse | ApprovalHistoryResponse; + + if (type === 'submission') { + response = useFakeData + ? await mockApprovalAPI.getSubmissionList() + : await getSubmissionList(systemId); + } else { + response = useFakeData + ? await mockApprovalAPI.getApprovalHistory() + : await getApprovalHistory(systemId); + } + + if (response.result === 'SUCCESS') { + setListData(response.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; + 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); + + return `${year}-${month}-${day} ${hour}:${minute}`; + }; + + 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 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 getActionTypeText = (actionType: string) => { + const actionMap: Record = { + 'SUBMIT': '상신', + 'APPROVE': '승인', + 'REJECT': '반려', + 'CANCEL': '취소', + 'DELEGATE': '위임' + }; + return actionMap[actionType] || actionType; + }; + + const handleItemClick = (apInfId: string) => { + onItemClick?.(apInfId); + }; + + // 컴포넌트 마운트 시 데이터 로드 + useEffect(() => { + fetchData(); + }, [type, useFakeData, systemId]); + + return ( + + + + + {type === 'submission' ? '상신함' : '결재 이력'} + + + {type === 'submission' + ? '상신한 결재 목록을 확인합니다.' + : '결재 처리 이력을 확인합니다.' + } {useFakeData && '(테스트 모드)'} + + + + + {/* 새로고침 버튼 */} +
+
+ 총 {listData.length}건 +
+ +
+ + {/* 에러 메시지 */} + {error && ( +
+
+ + 오류 +
+

{error}

+
+ )} + + {/* 목록 테이블 */} +
+ + + + 결재 ID + 제목 + 상신일시 + 상태 + {type === 'submission' && ( + <> + 긴급 + 보안등급 + + )} + {type === 'history' && ( + <> + 처리일시 + 처리자 + 처리유형 + + )} + 작업 + + + + {listData.length === 0 ? ( + + + {isLoading ? '데이터를 불러오는 중...' : '데이터가 없습니다.'} + + + ) : ( + listData.map((item) => ( + + + {item.apInfId} + + + {item.subject} + + + {formatDate(item.sbmDt)} + + + + {getStatusText(item.status)} + + + + {type === 'submission' && ( + <> + + {item.urgYn === 'Y' ? ( + + 긴급 + + ) : ( + + 일반 + + )} + + + + {getSecurityTypeText(item.docSecuType || 'PERSONAL')} + + + + )} + + {type === 'history' && ( + <> + + {item.actionDt ? formatDate(item.actionDt) : '-'} + + + {item.userId || '-'} + + + {item.actionType ? ( + + {getActionTypeText(item.actionType)} + + ) : '-'} + + + )} + + + + + + )) + )} + +
+
+ + {/* 페이지네이션 영역 (향후 구현 예정) */} + {listData.length > 0 && ( +
+
+ 페이지네이션 기능은 향후 구현 예정입니다. +
+
+ )} +
+
+ ); +} \ No newline at end of file -- cgit v1.2.3