summaryrefslogtreecommitdiff
path: root/components/knox/approval/ApprovalList.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/knox/approval/ApprovalList.tsx')
-rw-r--r--components/knox/approval/ApprovalList.tsx322
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