diff options
| -rw-r--r-- | app/[lng]/admin/approval-test/page.tsx | 6 | ||||
| -rw-r--r-- | components/knox/approval/ApprovalDetail.tsx | 54 | ||||
| -rw-r--r-- | components/knox/approval/ApprovalSubmit.tsx | 46 |
3 files changed, 94 insertions, 12 deletions
diff --git a/app/[lng]/admin/approval-test/page.tsx b/app/[lng]/admin/approval-test/page.tsx index de0e2b5a..f044d87d 100644 --- a/app/[lng]/admin/approval-test/page.tsx +++ b/app/[lng]/admin/approval-test/page.tsx @@ -3,7 +3,7 @@ import ApprovalManager from '@/components/knox/approval/ApprovalManager'; export const metadata: Metadata = { title: 'Knox 결재 시스템 테스트 | Admin', - description: 'Knox API를 사용한 결재 시스템의 모든 기능을 테스트할 수 있는 페이지입니다.', + description: 'Knox API를 사용한 결재 시스템 기능 테스트용', }; export default function ApprovalTestPage() { @@ -14,9 +14,9 @@ export default function ApprovalTestPage() { <div className="space-y-2"> <h1 className="text-3xl font-bold tracking-tight">Knox 결재 시스템 테스트</h1> <p className="text-muted-foreground"> - Knox API를 사용한 결재 시스템의 모든 기능을 테스트할 수 있습니다. + Knox API를 사용한 결재 시스템 컴포넌트입니다. <br /> - 테스트 모드가 기본적으로 활성화되어 있으며, 실제 API 대신 가짜 데이터를 사용합니다. + 테스트 모드가 기본적으로 활성화되어 있으며, 테스트 모드에서는 실제 API 대신 모킹 데이터를 사용 </p> </div> diff --git a/components/knox/approval/ApprovalDetail.tsx b/components/knox/approval/ApprovalDetail.tsx index 034bde7d..6db43cbe 100644 --- a/components/knox/approval/ApprovalDetail.tsx +++ b/components/knox/approval/ApprovalDetail.tsx @@ -28,6 +28,15 @@ interface ApprovalDetailData { content: ApprovalContentResponse['data']; } +// 첨부파일 타입 (가이드에 명확히 정의되어 있지 않아 필드 일부 추정) +interface ApprovalAttachment { + fileName?: string; + fileSize?: string; + downloadUrl?: string; + fileId?: string; + [key: string]: unknown; // 기타 필드 허용 +} + export default function ApprovalDetail({ useFakeData = false, systemId = 'EVCP_SYSTEM', @@ -126,6 +135,43 @@ export default function ApprovalDetail({ } }; + // 첨부파일 다운로드 헬퍼 + const handleDownload = async (attachment: ApprovalAttachment) => { + try { + // 1) downloadUrl 이 이미 포함된 경우 + if (attachment.downloadUrl) { + window.open(attachment.downloadUrl, '_blank'); + return; + } + + // 2) fileId + 별도 엔드포인트 조합 (가이드에 명시되지 않았으므로 best-effort 처리) + if (attachment.fileId) { + const url = `${process.env.KNOX_API_BASE_URL}/approval/api/v2.0/attachments/${attachment.fileId}`; + const resp = await fetch(url, { + method: 'GET', + headers: { + 'System-ID': systemId, + }, + }); + if (!resp.ok) throw new Error('다운로드 실패'); + + // blob 생성 후 브라우저 다운로드 + const blob = await resp.blob(); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = attachment.fileName || 'attachment'; + link.click(); + URL.revokeObjectURL(link.href); + return; + } + + toast.error('다운로드 URL 정보를 찾을 수 없습니다.'); + } catch (err) { + console.error('첨부파일 다운로드 오류:', err); + toast.error('첨부파일 다운로드 중 오류가 발생했습니다.'); + } + }; + // 초기 로딩 (initialApInfId가 있는 경우) useEffect(() => { if (initialApInfId) { @@ -338,14 +384,18 @@ export default function ApprovalDetail({ <h3 className="text-lg font-semibold">첨부파일</h3> <div className="space-y-2"> - {approvalData.detail.attachments.map((attachment: any, index: number) => ( + {approvalData.detail.attachments.map((attachment: ApprovalAttachment, 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 + variant="outline" + size="sm" + onClick={() => handleDownload(attachment)} + > 다운로드 </Button> </div> diff --git a/components/knox/approval/ApprovalSubmit.tsx b/components/knox/approval/ApprovalSubmit.tsx index 37779ce3..526a87f3 100644 --- a/components/knox/approval/ApprovalSubmit.tsx +++ b/components/knox/approval/ApprovalSubmit.tsx @@ -17,7 +17,7 @@ import { toast } from 'sonner'; import { Loader2, Plus, Trash2, FileText, AlertCircle } from 'lucide-react'; // API 함수 및 타입 -import { submitApproval, createSubmitApprovalRequest, createApprovalLine } from '@/lib/knox-api/approval/approval'; +import { submitApproval, submitSecurityApproval, createSubmitApprovalRequest, createApprovalLine } from '@/lib/knox-api/approval/approval'; import type { ApprovalLine, SubmitApprovalRequest } from '@/lib/knox-api/approval/approval'; // Mock 데이터 @@ -40,7 +40,9 @@ const formSchema = z.object({ role: z.enum(['0', '1', '2', '3', '4', '7', '9']), seq: z.string(), opinion: z.string().optional() - })).min(1, '최소 1개의 결재 경로가 필요합니다') + })).min(1, '최소 1개의 결재 경로가 필요합니다'), + // 첨부파일 (선택) + attachments: z.any().optional() }); type FormData = z.infer<typeof formSchema>; @@ -80,7 +82,8 @@ export default function ApprovalSubmit({ seq: '1', opinion: '' } - ] + ], + attachments: undefined } }); @@ -137,6 +140,8 @@ export default function ApprovalSubmit({ ); // 상신 요청 생성 + const attachmentsArray = data.attachments ? Array.from(data.attachments as FileList) : undefined; + const submitRequest: SubmitApprovalRequest = useFakeData ? { ...data, @@ -144,7 +149,8 @@ export default function ApprovalSubmit({ importantYn: data.importantYn ? 'Y' : 'N', sbmDt: new Date().toISOString().replace(/-|:|T/g, '').slice(0, 14), apInfId: 'test-ap-inf-id-' + Date.now(), - aplns: approvalLines + aplns: approvalLines, + attachments: attachmentsArray } : await createSubmitApprovalRequest( data.contents, @@ -158,14 +164,19 @@ export default function ApprovalSubmit({ notifyOption: data.notifyOption, docMngSaveCode: data.docMngSaveCode, sbmLang: data.sbmLang, - timeZone: data.timeZone + timeZone: data.timeZone, + attachments: attachmentsArray } ); - // API 호출 + // API 호출 (보안 등급에 따라 분기) + const isSecure = data.docSecuType === 'CONFIDENTIAL' || data.docSecuType === 'CONFIDENTIAL_STRICT'; + const response = useFakeData ? await mockApprovalAPI.submitApproval(submitRequest) - : await submitApproval(submitRequest, systemId); + : isSecure + ? await submitSecurityApproval(submitRequest, systemId) + : await submitApproval(submitRequest, systemId); if (response.result === 'SUCCESS') { setSubmitResult({ apInfId: response.data.apInfId }); @@ -333,6 +344,27 @@ export default function ApprovalSubmit({ )} /> </div> + + {/* 첨부 파일 */} + <FormField + control={form.control} + name="attachments" + render={({ field }) => ( + <FormItem> + <FormLabel>첨부 파일</FormLabel> + <FormControl> + <Input + type="file" + multiple + onChange={(e) => field.onChange(e.target.files)} + /> + </FormControl> + <FormDescription>필요 시 파일을 선택하세요. (다중 선택 가능)</FormDescription> + <FormMessage /> + </FormItem> + )} + /> + </div> <Separator /> |
