Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/api/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,34 @@ export function fetchDocuments(params: FetchDocumentsParams = {}): Promise<Docum
return apiFetch<DocumentPageResponse>(`/documents?${query.toString()}`)
}

const DOCUMENT_PAGE_SIZE = 100

export async function fetchAllDocuments(
params: Omit<FetchDocumentsParams, 'page' | 'size'> = {},
): Promise<DocumentPageResponse> {
const firstPage = await fetchDocuments({ ...params, page: 0, size: DOCUMENT_PAGE_SIZE })
const pageCount = Math.ceil(firstPage.total_elements / DOCUMENT_PAGE_SIZE)
if (pageCount <= 1) return firstPage

const remainingPages = await Promise.all(
Array.from({ length: pageCount - 1 }, (_, index) =>
fetchDocuments({ ...params, page: index + 1, size: DOCUMENT_PAGE_SIZE }),
),
)
const itemsById = new Map(
[firstPage, ...remainingPages]
.flatMap((page) => page.items)
.map((document) => [document.worker_document_id, document]),
)

return {
items: [...itemsById.values()],
page: 0,
size: itemsById.size,
total_elements: firstPage.total_elements,
}
}

export function fetchDocument(documentId: string): Promise<DocumentDetailResponse> {
return apiFetch<DocumentDetailResponse>(`/documents/${encodeURIComponent(documentId)}`)
}
Expand Down
59 changes: 51 additions & 8 deletions src/pages/DocumentListPage/DocumentListPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ function document(overrides: Partial<DocumentItemResponse>): DocumentItemRespons
}

const DOCUMENTS: DocumentItemResponse[] = [
document({ worker_document_id: 'D-1', display_name: '수라즈C', document_type: 'ARC', submission_status: 'MISSING' }),
document({
worker_document_id: 'D-1',
display_name: '수라즈C',
document_type: 'ARC',
submission_status: 'MISSING',
}),
document({
worker_document_id: 'D-2',
display_name: '쩐티B',
Expand Down Expand Up @@ -56,12 +61,24 @@ const DOCUMENTS: DocumentItemResponse[] = [
]

function jsonResponse(body: unknown, init: ResponseInit = {}) {
return new Response(JSON.stringify(body), { status: 200, headers: { 'Content-Type': 'application/json' }, ...init })
return new Response(JSON.stringify(body), {
status: 200,
headers: { 'Content-Type': 'application/json' },
...init,
})
}

function errorResponse(status: number, code: string, message: string) {
return jsonResponse(
{ timestamp: '2026-07-27T01:23:45Z', status, code, message, path: '/api/v1/documents', request_id: 'req-1', field_errors: [] },
{
timestamp: '2026-07-27T01:23:45Z',
status,
code,
message,
path: '/api/v1/documents',
request_id: 'req-1',
field_errors: [],
},
{ status },
)
}
Expand Down Expand Up @@ -146,7 +163,9 @@ describe('DocumentListPage', () => {

await waitFor(() => expect(vi.mocked(fetch)).toHaveBeenCalledTimes(2))
expect(String(vi.mocked(fetch).mock.calls[1][0])).not.toContain('workerId=')
expect(screen.queryByText('업무함에서 선택한 근로자의 문서만 표시합니다.')).not.toBeInTheDocument()
expect(
screen.queryByText('업무함에서 선택한 근로자의 문서만 표시합니다.'),
).not.toBeInTheDocument()
})

it('filters documents by tab', async () => {
Expand Down Expand Up @@ -240,13 +259,37 @@ describe('DocumentListPage', () => {
expect(await screen.findByText('등록된 서류가 없습니다')).toBeInTheDocument()
})

it('shows a cap notice when the server has more documents than the fetched page', async () => {
vi.mocked(fetch).mockResolvedValueOnce(
jsonResponse({ items: DOCUMENTS, page: 0, size: 100, total_elements: 150 }),
it('loads every document page before computing the list and metrics', async () => {
const paginatedDocuments = Array.from({ length: 102 }, (_, index) =>
document({
worker_document_id: `D-PAGE-${index + 1}`,
display_name: `페이지 근로자 ${index + 1}`,
submission_status: 'VERIFIED',
}),
)
vi.mocked(fetch)
.mockResolvedValueOnce(
jsonResponse({
items: paginatedDocuments.slice(0, 100),
page: 0,
size: 100,
total_elements: 102,
}),
)
.mockResolvedValueOnce(
jsonResponse({
items: paginatedDocuments.slice(100),
page: 1,
size: 100,
total_elements: 102,
}),
)
renderPage()

expect(await screen.findByText(/전체 150건 중 4건만 불러왔습니다/)).toBeInTheDocument()
expect(await screen.findByRole('tab', { name: '전체 102' })).toBeInTheDocument()
expect(screen.getByText('페이지 근로자 102')).toBeInTheDocument()
expect(vi.mocked(fetch)).toHaveBeenCalledTimes(2)
expect(String(vi.mocked(fetch).mock.calls[1][0])).toContain('page=1')
})

it('opens the HWP/HWPX upload modal', async () => {
Expand Down
11 changes: 2 additions & 9 deletions src/pages/DocumentListPage/DocumentListPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useMemo, useState } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { fetchDocuments, type DocumentItemResponse } from '../../api/documents'
import { fetchAllDocuments, type DocumentItemResponse } from '../../api/documents'
import { getErrorMessage } from '../../api/errors'
import { EmptyState } from '../../components/ui/EmptyState/EmptyState'
import { ListRow } from '../../components/ui/ListRow/ListRow'
Expand Down Expand Up @@ -50,7 +50,7 @@ export function DocumentListPage() {
const debouncedQuery = useDebouncedValue(query)

const { status, data, error, refetch } = useApiQuery(
useCallback(() => fetchDocuments({ workerId: workerId ?? undefined, size: 100 }), [workerId]),
useCallback(() => fetchAllDocuments({ workerId: workerId ?? undefined }), [workerId]),
useCallback((page: { items: unknown[] }) => page.items.length === 0, []),
)
const documents = useMemo(() => data?.items ?? [], [data])
Expand Down Expand Up @@ -200,13 +200,6 @@ export function DocumentListPage() {
<span />
</div>

{data && data.total_elements > data.items.length && (
<p className={styles.capNotice}>
전체 {data.total_elements}건 중 {data.items.length}건만 불러왔습니다. 찾는 서류가 안
보이면 검색어를 바꿔보세요.
</p>
)}

{visibleDocuments.length === 0 ? (
<div className={styles.stateWrap}>
<EmptyState
Expand Down