summaryrefslogtreecommitdiff
path: root/components/knox/approval/ApprovalDetail.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/knox/approval/ApprovalDetail.tsx')
-rw-r--r--components/knox/approval/ApprovalDetail.tsx362
1 files changed, 362 insertions, 0 deletions
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<ApprovalDetailData | null>(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState<string | null>(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<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 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 (
+ <Card className="w-full max-w-6xl">
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <FileText className="w-5 h-5" />
+ 결재 상세 조회
+ </CardTitle>
+ <CardDescription>
+ 결재 ID를 입력하여 상세 정보를 조회합니다. {useFakeData && '(테스트 모드)'}
+ </CardDescription>
+ </CardHeader>
+
+ <CardContent className="space-y-6">
+ {/* 검색 영역 */}
+ <div className="flex items-center gap-3">
+ <div className="flex-1">
+ <Label htmlFor="apInfId">결재 ID</Label>
+ <Input
+ id="apInfId"
+ placeholder="결재 ID를 입력하세요"
+ value={apInfId}
+ onChange={(e) => setApInfId(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && fetchApprovalDetail(apInfId)}
+ />
+ </div>
+ <Button
+ onClick={() => fetchApprovalDetail(apInfId)}
+ disabled={isLoading}
+ >
+ {isLoading ? (
+ <>
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
+ 조회 중...
+ </>
+ ) : (
+ <>
+ <Search 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>
+ )}
+
+ {/* 결재 상세 정보 */}
+ {approvalData && (
+ <div className="space-y-6">
+ {/* 기본 정보 */}
+ <div className="space-y-4">
+ <h3 className="text-lg font-semibold flex items-center gap-2">
+ <FileText className="w-5 h-5" />
+ 기본 정보
+ </h3>
+
+ <div className="grid grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg">
+ <div>
+ <Label className="text-sm font-medium text-gray-600">결재 ID</Label>
+ <p className="text-sm font-mono mt-1">{approvalData.detail.apInfId}</p>
+ </div>
+ <div>
+ <Label className="text-sm font-medium text-gray-600">시스템 ID</Label>
+ <p className="text-sm mt-1">{approvalData.detail.systemId}</p>
+ </div>
+ <div>
+ <Label className="text-sm font-medium text-gray-600">제목</Label>
+ <p className="text-sm mt-1 font-medium">{approvalData.detail.subject}</p>
+ </div>
+ <div>
+ <Label className="text-sm font-medium text-gray-600">상신일시</Label>
+ <p className="text-sm mt-1 flex items-center gap-2">
+ <Clock className="w-4 h-4" />
+ {formatDate(approvalData.detail.sbmDt)}
+ </p>
+ </div>
+ <div>
+ <Label className="text-sm font-medium text-gray-600">상태</Label>
+ <div className="mt-1">
+ <Badge variant={getStatusBadgeVariant(approvalData.detail.status)}>
+ {getStatusText(approvalData.detail.status)}
+ </Badge>
+ </div>
+ </div>
+ <div>
+ <Label className="text-sm font-medium text-gray-600">보안 등급</Label>
+ <div className="mt-1">
+ <Badge variant={getSecurityTypeBadgeVariant(approvalData.detail.docSecuType)}>
+ {getSecurityTypeText(approvalData.detail.docSecuType)}
+ </Badge>
+ </div>
+ </div>
+ <div>
+ <Label className="text-sm font-medium text-gray-600">긴급 여부</Label>
+ <div className="mt-1">
+ <Badge variant={approvalData.detail.urgYn === 'Y' ? 'destructive' : 'outline'}>
+ {approvalData.detail.urgYn === 'Y' ? '긴급' : '일반'}
+ </Badge>
+ </div>
+ </div>
+ <div>
+ <Label className="text-sm font-medium text-gray-600">언어</Label>
+ <p className="text-sm mt-1">{approvalData.detail.sbmLang}</p>
+ </div>
+ </div>
+ </div>
+
+ <Separator />
+
+ {/* 결재 내용 */}
+ <div className="space-y-4">
+ <h3 className="text-lg font-semibold">결재 내용</h3>
+
+ <div className="p-4 bg-gray-50 rounded-lg">
+ <div className="mb-2">
+ <Label className="text-sm font-medium text-gray-600">내용 형식</Label>
+ <Badge variant="outline" className="ml-2">
+ {approvalData.content.contentsType}
+ </Badge>
+ </div>
+
+ <div className="mt-4 p-4 bg-white rounded border">
+ <pre className="whitespace-pre-wrap text-sm">
+ {approvalData.content.contents}
+ </pre>
+ </div>
+ </div>
+ </div>
+
+ <Separator />
+
+ {/* 결재 경로 */}
+ <div className="space-y-4">
+ <h3 className="text-lg font-semibold flex items-center gap-2">
+ <User className="w-5 h-5" />
+ 결재 경로
+ </h3>
+
+ <div className="space-y-3">
+ {approvalData.detail.aplns.map((apln: ApprovalLine, index: number) => (
+ <div key={index} className="flex items-center gap-4 p-4 border rounded-lg">
+ <Badge variant="outline" className="min-w-[40px] text-center">
+ {apln.seq}
+ </Badge>
+
+ <div className="flex-1 grid grid-cols-4 gap-4">
+ <div>
+ <Label className="text-xs font-medium text-gray-600">사용자 ID</Label>
+ <p className="text-sm mt-1">{apln.userId || apln.epId || '-'}</p>
+ </div>
+ <div>
+ <Label className="text-xs font-medium text-gray-600">이메일</Label>
+ <p className="text-sm mt-1">{apln.emailAddress || '-'}</p>
+ </div>
+ <div>
+ <Label className="text-xs font-medium text-gray-600">역할</Label>
+ <div className="mt-1">
+ <Badge variant="secondary">
+ {getRoleText(apln.role)}
+ </Badge>
+ </div>
+ </div>
+ <div>
+ <Label className="text-xs font-medium text-gray-600">상태</Label>
+ <div className="mt-1">
+ <Badge variant={apln.aplnStatsCode === '1' ? 'default' :
+ apln.aplnStatsCode === '2' ? 'destructive' : 'outline'}>
+ {getApprovalStatusText(apln.aplnStatsCode)}
+ </Badge>
+ </div>
+ </div>
+ </div>
+
+ <div className="flex flex-col gap-1 text-xs">
+ {apln.arbPmtYn === 'Y' && (
+ <Badge variant="outline" className="text-xs">전결권한</Badge>
+ )}
+ {apln.contentsMdfyPmtYn === 'Y' && (
+ <Badge variant="outline" className="text-xs">본문수정</Badge>
+ )}
+ {apln.aplnMdfyPmtYn === 'Y' && (
+ <Badge variant="outline" className="text-xs">경로변경</Badge>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+
+ {/* 첨부파일 */}
+ {approvalData.detail.attachments && approvalData.detail.attachments.length > 0 && (
+ <>
+ <Separator />
+ <div className="space-y-4">
+ <h3 className="text-lg font-semibold">첨부파일</h3>
+
+ <div className="space-y-2">
+ {approvalData.detail.attachments.map((attachment: any, index: number) => (
+ <div key={index} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
+ <FileText className="w-4 h-4 text-gray-500" />
+ <div className="flex-1">
+ <p className="text-sm font-medium">{attachment.fileName || `첨부파일 ${index + 1}`}</p>
+ <p className="text-xs text-gray-500">{attachment.fileSize || '크기 정보 없음'}</p>
+ </div>
+ <Button variant="outline" size="sm">
+ 다운로드
+ </Button>
+ </div>
+ ))}
+ </div>
+ </div>
+ </>
+ )}
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ );
+} \ No newline at end of file