diff options
| author | joonhoekim <26rote@gmail.com> | 2025-07-18 03:58:34 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-07-18 03:58:34 +0000 |
| commit | 3e59693e017742d971f490eb7c58870cb745a98d (patch) | |
| tree | f7f846613e40e4f058de70afca5809b8e6bd0e2d /components/knox/approval/ApprovalList.tsx | |
| parent | 2ef02e27dbe639876fa3b90c30307dda183545ec (diff) | |
(김준회) 결재 모듈 개발
Diffstat (limited to 'components/knox/approval/ApprovalList.tsx')
| -rw-r--r-- | components/knox/approval/ApprovalList.tsx | 322 |
1 files changed, 322 insertions, 0 deletions
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<ListItem[]>([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState<string | null>(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<string, string> = { + '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<string, string> = { + 'SUBMIT': '상신', + 'APPROVE': '승인', + 'REJECT': '반려', + 'CANCEL': '취소', + 'DELEGATE': '위임' + }; + return actionMap[actionType] || actionType; + }; + + const handleItemClick = (apInfId: string) => { + onItemClick?.(apInfId); + }; + + // 컴포넌트 마운트 시 데이터 로드 + useEffect(() => { + fetchData(); + }, [type, useFakeData, systemId]); + + return ( + <Card className="w-full max-w-6xl"> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <List className="w-5 h-5" /> + {type === 'submission' ? '상신함' : '결재 이력'} + </CardTitle> + <CardDescription> + {type === 'submission' + ? '상신한 결재 목록을 확인합니다.' + : '결재 처리 이력을 확인합니다.' + } {useFakeData && '(테스트 모드)'} + </CardDescription> + </CardHeader> + + <CardContent className="space-y-4"> + {/* 새로고침 버튼 */} + <div className="flex justify-between items-center"> + <div className="text-sm text-gray-500"> + 총 {listData.length}건 + </div> + <Button + onClick={fetchData} + disabled={isLoading} + variant="outline" + size="sm" + > + {isLoading ? ( + <> + <Loader2 className="w-4 h-4 mr-2 animate-spin" /> + 조회 중... + </> + ) : ( + <> + <RefreshCw className="w-4 h-4 mr-2" /> + 새로고침 + </> + )} + </Button> + </div> + + {/* 에러 메시지 */} + {error && ( + <div className="p-4 bg-red-50 border border-red-200 rounded-lg"> + <div className="flex items-center gap-2 text-red-700"> + <AlertCircle className="w-4 h-4" /> + <span className="font-medium">오류</span> + </div> + <p className="text-sm text-red-600 mt-1">{error}</p> + </div> + )} + + {/* 목록 테이블 */} + <div className="border rounded-lg"> + <Table> + <TableHeader> + <TableRow> + <TableHead>결재 ID</TableHead> + <TableHead>제목</TableHead> + <TableHead>상신일시</TableHead> + <TableHead>상태</TableHead> + {type === 'submission' && ( + <> + <TableHead>긴급</TableHead> + <TableHead>보안등급</TableHead> + </> + )} + {type === 'history' && ( + <> + <TableHead>처리일시</TableHead> + <TableHead>처리자</TableHead> + <TableHead>처리유형</TableHead> + </> + )} + <TableHead>작업</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {listData.length === 0 ? ( + <TableRow> + <TableCell + colSpan={type === 'submission' ? 7 : 8} + className="text-center py-8 text-gray-500" + > + {isLoading ? '데이터를 불러오는 중...' : '데이터가 없습니다.'} + </TableCell> + </TableRow> + ) : ( + listData.map((item) => ( + <TableRow key={item.apInfId} className="hover:bg-gray-50"> + <TableCell className="font-mono text-sm"> + {item.apInfId} + </TableCell> + <TableCell className="font-medium"> + {item.subject} + </TableCell> + <TableCell> + {formatDate(item.sbmDt)} + </TableCell> + <TableCell> + <Badge variant={getStatusBadgeVariant(item.status)}> + {getStatusText(item.status)} + </Badge> + </TableCell> + + {type === 'submission' && ( + <> + <TableCell> + {item.urgYn === 'Y' ? ( + <Badge variant="destructive" className="text-xs"> + 긴급 + </Badge> + ) : ( + <Badge variant="outline" className="text-xs"> + 일반 + </Badge> + )} + </TableCell> + <TableCell> + <Badge + variant={getSecurityTypeBadgeVariant(item.docSecuType || 'PERSONAL')} + className="text-xs" + > + {getSecurityTypeText(item.docSecuType || 'PERSONAL')} + </Badge> + </TableCell> + </> + )} + + {type === 'history' && ( + <> + <TableCell> + {item.actionDt ? formatDate(item.actionDt) : '-'} + </TableCell> + <TableCell> + {item.userId || '-'} + </TableCell> + <TableCell> + {item.actionType ? ( + <Badge variant="outline" className="text-xs"> + {getActionTypeText(item.actionType)} + </Badge> + ) : '-'} + </TableCell> + </> + )} + + <TableCell> + <Button + variant="ghost" + size="sm" + onClick={() => handleItemClick(item.apInfId)} + > + <Eye className="w-4 h-4 mr-2" /> + 상세 + </Button> + </TableCell> + </TableRow> + )) + )} + </TableBody> + </Table> + </div> + + {/* 페이지네이션 영역 (향후 구현 예정) */} + {listData.length > 0 && ( + <div className="flex justify-center pt-4"> + <div className="text-sm text-gray-500"> + 페이지네이션 기능은 향후 구현 예정입니다. + </div> + </div> + )} + </CardContent> + </Card> + ); +}
\ No newline at end of file |
