-
Notifications
You must be signed in to change notification settings - Fork 0
feat(document): 합성 문서 유형과 원본 미리보기 지원 #334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,15 @@ | ||
| import { apiFetch } from './client' | ||
|
|
||
| // fowoco/server DocumentController 기준 (#57 통합 문서함·파일 업로드·문서 준비도 구현). | ||
| export type DocumentType = 'PASSPORT_COPY' | 'ARC' | 'CONTRACT' | 'PERMIT' | ||
| export type SubmissionStatus = 'MISSING' | 'SUBMITTED' | 'VERIFIED' | ||
| export type DocumentType = | ||
| | 'PASSPORT_COPY' | ||
| | 'ARC' | ||
| | 'CONTRACT' | ||
| | 'PERMIT' | ||
| | 'EMPLOYMENT_EXTENSION_APPLICATION' | ||
| | 'INTEGRATED_APPLICATION' | ||
| | 'RESIDENCE_PROOF' | ||
| export type SubmissionStatus = 'DRAFT' | 'MISSING' | 'SUBMITTED' | 'VERIFIED' | ||
|
|
||
| export interface DocumentItemResponse { | ||
| worker_document_id: string | ||
|
|
@@ -21,6 +28,15 @@ export interface DocumentPageResponse { | |
| total_elements: number | ||
| } | ||
|
|
||
| export interface DocumentDetailResponse extends DocumentItemResponse { | ||
| task_id: string | null | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 확인이용 |
||
| version: number | ||
| file_name: string | null | ||
| file_mime_type: string | null | ||
| file_size: number | null | ||
| file_scan_status: 'NOT_SCANNED' | 'CLEAN' | 'INFECTED' | null | ||
| } | ||
|
|
||
| export interface FetchDocumentsParams { | ||
| workerId?: string | ||
| documentType?: DocumentType | ||
|
|
@@ -43,6 +59,10 @@ export function fetchDocuments(params: FetchDocumentsParams = {}): Promise<Docum | |
| return apiFetch<DocumentPageResponse>(`/documents?${query.toString()}`) | ||
| } | ||
|
|
||
| export function fetchDocument(documentId: string): Promise<DocumentDetailResponse> { | ||
| return apiFetch<DocumentDetailResponse>(`/documents/${encodeURIComponent(documentId)}`) | ||
| } | ||
|
|
||
| export interface DocumentReadinessResponse { | ||
| required: DocumentType[] | ||
| available: DocumentType[] | ||
|
|
@@ -53,7 +73,9 @@ export interface DocumentReadinessResponse { | |
|
|
||
| // Task 생성 시점 체크리스트 snapshot 기준이라 Workflow Catalog를 실시간으로 다시 읽지 않는다 (#176). | ||
| export function fetchDocumentReadiness(taskId: string): Promise<DocumentReadinessResponse> { | ||
| return apiFetch<DocumentReadinessResponse>(`/tasks/${encodeURIComponent(taskId)}/document-readiness`) | ||
| return apiFetch<DocumentReadinessResponse>( | ||
| `/tasks/${encodeURIComponent(taskId)}/document-readiness`, | ||
| ) | ||
| } | ||
|
|
||
| export interface DocumentRequestUpsertBody { | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react' | |
| import userEvent from '@testing-library/user-event' | ||
| import { MemoryRouter, Route, Routes } from 'react-router-dom' | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' | ||
| import type { DocumentItemResponse, DocumentPageResponse } from '../../api/documents' | ||
| import type { DocumentDetailResponse, DocumentItemResponse } from '../../api/documents' | ||
| import type { DocumentOcrRunResponse } from '../../api/documentOcr' | ||
| import { DocumentDetailPage } from './DocumentDetailPage' | ||
|
|
||
|
|
@@ -19,6 +19,19 @@ function document(overrides: Partial<DocumentItemResponse>): DocumentItemRespons | |
| } | ||
| } | ||
|
|
||
| function detail(overrides: Partial<DocumentDetailResponse>): DocumentDetailResponse { | ||
| return { | ||
| ...document(overrides), | ||
| task_id: null, | ||
| version: 0, | ||
| file_name: null, | ||
| file_mime_type: null, | ||
| file_size: null, | ||
| file_scan_status: null, | ||
| ...overrides, | ||
| } | ||
| } | ||
|
|
||
| const DOCUMENTS: DocumentItemResponse[] = [ | ||
| document({ worker_document_id: 'D-1', worker_id: 'W-1', display_name: '응웬반A' }), | ||
| document({ | ||
|
|
@@ -98,10 +111,6 @@ function errorResponse(status: number, code: string, message: string) { | |
| ) | ||
| } | ||
|
|
||
| function pageResponse(items: DocumentItemResponse[]): DocumentPageResponse { | ||
| return { items, page: 0, size: 100, total_elements: items.length } | ||
| } | ||
|
|
||
| function renderPage(documentId: string) { | ||
| render( | ||
| <MemoryRouter initialEntries={[`/documents/${documentId}`]}> | ||
|
|
@@ -126,7 +135,7 @@ afterEach(() => { | |
|
|
||
| describe('DocumentDetailPage', () => { | ||
| it('renders the document type and worker name', async () => { | ||
| vi.mocked(fetch).mockResolvedValueOnce(jsonResponse(pageResponse(DOCUMENTS))) | ||
| vi.mocked(fetch).mockResolvedValueOnce(jsonResponse(detail(DOCUMENTS[0]))) | ||
| renderPage('D-1') | ||
|
|
||
| expect(await screen.findByRole('heading', { name: '외국인등록증' })).toBeInTheDocument() | ||
|
|
@@ -135,7 +144,7 @@ describe('DocumentDetailPage', () => { | |
|
|
||
| it('navigates to the related worker', async () => { | ||
| const user = userEvent.setup() | ||
| vi.mocked(fetch).mockResolvedValueOnce(jsonResponse(pageResponse(DOCUMENTS))) | ||
| vi.mocked(fetch).mockResolvedValueOnce(jsonResponse(detail(DOCUMENTS[0]))) | ||
| renderPage('D-1') | ||
|
|
||
| await user.click(await screen.findByRole('button', { name: '응웬반A 정보 →' })) | ||
|
|
@@ -145,16 +154,18 @@ describe('DocumentDetailPage', () => { | |
|
|
||
| it('downloads the attached original file through the authenticated file API', async () => { | ||
| const user = userEvent.setup() | ||
| const fileDocuments = [ | ||
| document({ | ||
| worker_document_id: 'D-1', | ||
| display_name: '응웬반A', | ||
| submission_status: 'SUBMITTED', | ||
| file_id: 'file-1', | ||
| }), | ||
| ] | ||
| const fileDocument = detail({ | ||
| worker_document_id: 'D-1', | ||
| display_name: '응웬반A', | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 하나만.? |
||
| submission_status: 'SUBMITTED', | ||
| file_id: 'file-1', | ||
| file_name: 'arc.pdf', | ||
| file_mime_type: 'application/pdf', | ||
| file_size: 3, | ||
| file_scan_status: 'NOT_SCANNED', | ||
| }) | ||
| vi.mocked(fetch) | ||
| .mockResolvedValueOnce(jsonResponse(pageResponse(fileDocuments))) | ||
| .mockResolvedValueOnce(jsonResponse(fileDocument)) | ||
| .mockResolvedValueOnce( | ||
| errorResponse(404, 'DOCUMENT_OCR_RUN_NOT_FOUND', 'OCR 실행 이력을 찾을 수 없습니다.'), | ||
| ) | ||
|
|
@@ -180,14 +191,14 @@ describe('DocumentDetailPage', () => { | |
| }) | ||
|
|
||
| it('shows an empty state when the documentId does not match any document', async () => { | ||
| vi.mocked(fetch).mockResolvedValueOnce(jsonResponse(pageResponse(DOCUMENTS))) | ||
| vi.mocked(fetch).mockResolvedValueOnce(errorResponse(404, 'DOCUMENT_NOT_FOUND', 'not found')) | ||
| renderPage('does-not-exist') | ||
|
|
||
| expect(await screen.findByText('서류를 찾을 수 없습니다')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| it('does not offer OCR review when no file is connected', async () => { | ||
| vi.mocked(fetch).mockResolvedValueOnce(jsonResponse(pageResponse(DOCUMENTS))) | ||
| vi.mocked(fetch).mockResolvedValueOnce(jsonResponse(detail(DOCUMENTS[0]))) | ||
| renderPage('D-1') | ||
| await screen.findByRole('heading', { name: '외국인등록증' }) | ||
|
|
||
|
|
@@ -199,16 +210,18 @@ describe('DocumentDetailPage', () => { | |
| it('runs OCR, polls until ready, submits only HR corrections, and marks review complete', async () => { | ||
| vi.useFakeTimers({ shouldAdvanceTime: true }) | ||
| const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }) | ||
| const fileDocuments = [ | ||
| document({ | ||
| worker_document_id: 'D-1', | ||
| display_name: '응웬반A', | ||
| submission_status: 'SUBMITTED', | ||
| file_id: 'file-1', | ||
| }), | ||
| ] | ||
| const fileDocument = detail({ | ||
| worker_document_id: 'D-1', | ||
| display_name: '응웬반A', | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 동일하게 하나만? |
||
| submission_status: 'SUBMITTED', | ||
| file_id: 'file-1', | ||
| file_name: 'arc.pdf', | ||
| file_mime_type: 'application/pdf', | ||
| file_size: 3, | ||
| file_scan_status: 'NOT_SCANNED', | ||
| }) | ||
| vi.mocked(fetch) | ||
| .mockResolvedValueOnce(jsonResponse(pageResponse(fileDocuments))) | ||
| .mockResolvedValueOnce(jsonResponse(fileDocument)) | ||
| .mockResolvedValueOnce( | ||
| errorResponse(404, 'DOCUMENT_OCR_RUN_NOT_FOUND', 'OCR 실행 이력을 찾을 수 없습니다.'), | ||
| ) | ||
|
|
@@ -252,12 +265,56 @@ describe('DocumentDetailPage', () => { | |
| }) | ||
| }) | ||
|
|
||
| it('previews an authenticated image and revokes the object URL on unmount', async () => { | ||
| const imageDocument = detail({ | ||
| worker_document_id: 'D-1', | ||
| submission_status: 'SUBMITTED', | ||
| file_id: 'file-1', | ||
| file_name: '외국인등록증_앞면.png', | ||
| file_mime_type: 'image/png', | ||
| file_size: 3, | ||
| file_scan_status: 'NOT_SCANNED', | ||
| }) | ||
| vi.mocked(fetch).mockImplementation((url) => { | ||
| if (String(url).includes('/files/file-1/content')) { | ||
| return Promise.resolve(new Response(new Blob(['png'], { type: 'image/png' }))) | ||
| } | ||
| if (String(url).includes('/ocr-runs/latest')) { | ||
| return Promise.resolve(errorResponse(404, 'DOCUMENT_OCR_RUN_NOT_FOUND', 'not found')) | ||
| } | ||
| return Promise.resolve(jsonResponse(imageDocument)) | ||
| }) | ||
| const createObjectUrl = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:preview-1') | ||
| const revokeObjectUrl = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {}) | ||
| const rendered = render( | ||
| <MemoryRouter initialEntries={['/documents/D-1']}> | ||
| <Routes> | ||
| <Route path="/documents/:documentId" element={<DocumentDetailPage />} /> | ||
| </Routes> | ||
| </MemoryRouter>, | ||
| ) | ||
|
|
||
| expect( | ||
| await screen.findByRole('img', { name: '외국인등록증 합성 원본 미리보기' }), | ||
| ).toHaveAttribute('src', 'blob:preview-1') | ||
| expect(createObjectUrl).toHaveBeenCalledTimes(1) | ||
|
|
||
| rendered.unmount() | ||
| expect(revokeObjectUrl).toHaveBeenCalledWith('blob:preview-1') | ||
| }) | ||
|
|
||
| it('shows a preparing message when the OCR feature returns 503', async () => { | ||
| const fileDocuments = [ | ||
| document({ worker_document_id: 'D-1', submission_status: 'SUBMITTED', file_id: 'file-1' }), | ||
| ] | ||
| const fileDocument = detail({ | ||
| worker_document_id: 'D-1', | ||
| submission_status: 'SUBMITTED', | ||
| file_id: 'file-1', | ||
| file_name: 'arc.pdf', | ||
| file_mime_type: 'application/pdf', | ||
| file_size: 3, | ||
| file_scan_status: 'NOT_SCANNED', | ||
| }) | ||
| vi.mocked(fetch) | ||
| .mockResolvedValueOnce(jsonResponse(pageResponse(fileDocuments))) | ||
| .mockResolvedValueOnce(jsonResponse(fileDocument)) | ||
| .mockResolvedValueOnce( | ||
| errorResponse(503, 'DOCUMENT_OCR_DISABLED', 'OCR 기능이 아직 활성화되지 않았습니다.'), | ||
| ) | ||
|
|
@@ -269,11 +326,17 @@ describe('DocumentDetailPage', () => { | |
|
|
||
| it('requires a reason and submits no corrected fields when OCR is rejected', async () => { | ||
| const user = userEvent.setup() | ||
| const fileDocuments = [ | ||
| document({ worker_document_id: 'D-1', submission_status: 'SUBMITTED', file_id: 'file-1' }), | ||
| ] | ||
| const fileDocument = detail({ | ||
| worker_document_id: 'D-1', | ||
| submission_status: 'SUBMITTED', | ||
| file_id: 'file-1', | ||
| file_name: 'arc.pdf', | ||
| file_mime_type: 'application/pdf', | ||
| file_size: 3, | ||
| file_scan_status: 'NOT_SCANNED', | ||
| }) | ||
| vi.mocked(fetch) | ||
| .mockResolvedValueOnce(jsonResponse(pageResponse(fileDocuments))) | ||
| .mockResolvedValueOnce(jsonResponse(fileDocument)) | ||
| .mockResolvedValueOnce(jsonResponse(OCR_READY)) | ||
| .mockResolvedValueOnce( | ||
| jsonResponse( | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
200 확인이용