fix(admin_basic,basic): 상한 총 건수 목록의 페이지 번호 목록 복원

총 건수를 상한까지만 세는 목록(last_page: null)에서 Pagination 이 번호 목록을
통째로 비우고 현재 페이지 숫자 하나만 남겼다. 합의되고 CHANGELOG 에 약속된
동작은 "마지막 페이지 점프만 감춤" 이었으므로, 1..현재 페이지와 (다음이 있으면)
현재+1 까지는 그대로 그리고 그 너머만 계속됨 표시로 둔다. 활동 로그처럼
10,000건을 넘는 화면에서 앞 페이지로 직접 이동할 방법이 없던 것을 되돌린다.

두 템플릿 컴포넌트 회귀 테스트(작성 시 6/7 fail 확인)와 활동 로그 E2E 를 추가하고,
pagination.md 의 화면 규약 서술을 실제 동작으로 정정했다.
This commit is contained in:
HeuJung
2026-09-04 18:53:31 +09:00
parent 9d3b15e300
commit 3e41dc6075
14 changed files with 356 additions and 43 deletions
+2 -1
View File
@@ -87,6 +87,7 @@ FULLTEXT(`MATCH ... AGAINST`) 검색에서 이 차이가 크게 드러난다.
| 다음 페이지 이동 | **그대로 가능** — `per_page + 1` 조회로 정확히 판정 |
| 이전 페이지 이동 | 그대로 가능 |
| 마지막 페이지 점프 | **감춤** — 총 건수를 알아야 계산되는 유일한 값 |
| 페이지 번호 목록 | 그대로 — 1 부터 현재 페이지까지와, 다음이 있으면 현재+1 을 그린다. 그 너머는 존재 여부를 모르므로 계속됨 표시(`...`)만 둔다 |
마지막 페이지 점프를 감추는 것은 기능 축소가 아니라 **계산 불가 사실의 정직한 표시**다.
그 버튼은 대량 매칭에서 초대형 OFFSET 을 실행하므로, 눌러도 정상 응답이 오지 않는 경우가 많다.
@@ -318,7 +319,7 @@ HookManager::addFilter('core.pagination.filter_result_cap', function (int $cap,
| Prop | 의미 |
|---|---|
| `totalPages` | 마지막 페이지. **`null` 이면 페이지 번호 목록과 마지막 페이지 점프가 사라진다** |
| `totalPages` | 마지막 페이지. **`null` 이면 마지막 페이지 점프만 사라진다** — 번호 목록은 1..현재(+다음이 있으면 현재+1)까지 그대로 그린다. 번호 목록까지 비우면 앞 페이지로 직접 뛰어갈 방법이 없어진다 |
| `hasMorePages` | 다음 페이지 존재 여부. 총 건수를 몰라도 정확하다 |
| `showFirst` / `showLast` | 첫/마지막 버튼을 따로 제어 (미지정 시 `showFirstLast` 를 따름) |
@@ -122,7 +122,7 @@ admin/`)이 이 템플릿의 베이스(`_admin_base`)를 extends 하고 이 템
| 종류 | 개수 | 위치 |
|---|---|---|
| PHPUnit | 0개 | — |
| Vitest | 206개 | `vitest.config.ts` |
| Vitest | 207개 | `vitest.config.ts` |
| Playwright | 9개 | `tests/Playwright` |
| 시나리오 매니페스트 | 2개 | `tests/scenarios` |
@@ -26,6 +26,7 @@
### Fixed
- 기록이 아주 많아 총 건수를 정확히 세지 못한 목록(활동 로그 등)에서 페이지 번호가 전부 사라지고 현재 페이지 숫자 하나만 남던 문제를 수정했습니다. 이제 1 부터 현재 페이지까지의 번호와 다음 페이지 번호가 그대로 표시되어 앞 페이지로 바로 이동할 수 있으며, 감춰지는 것은 마지막 페이지로 뛰는 버튼뿐입니다.
- 설정 파일의 자산 항목이 실제 파일 위치와 달랐던 것을 정정했습니다. 존재하지 않는 글꼴·이미지 폴더 항목은 제거했습니다.
- 환경설정 > 일반의 「자산 URL 방식 자동 감지」가 감지 결과를 선택 항목과 저장 내용에 반영합니다. 종전에는 버튼을 눌러도 선택이 그대로였고 진행·결과 안내도 표시되지 않았습니다.
- 대시보드 알림 중 일부 경고가 일반 안내와 같은 회색으로 표시되어 경고로 보이지 않던 문제를 수정했습니다. 이제 알림의 심각도에 따라 색이 정해집니다.
File diff suppressed because one or more lines are too long
@@ -6,8 +6,8 @@ export interface PaginationProps {
* 마지막 페이지 번호
*
* 총 건수가 상한을 넘겨 정확히 세지 않은 목록은 마지막 페이지를 계산할 수 없다.
* 그 경우 서버가 `null` 을 보내며, 페이지 번호 목록과 마지막 페이지 점프가 사라지고
* 이전/다음 이동만 남는다. 0 이나 1 로 채우면 화면이 "1페이지뿐" 이라고 잘못 말하게 된다.
* 그 경우 서버가 `null` 을 보내며, 마지막 페이지 점프만 사라진다. 1 부터 현재 페이지까지와
* (`hasMorePages` 가 참이면) 다음 페이지 번호는 그대로 그린다. 0 이나 1 로 채우면 화면이 "1페이지뿐" 이라고 잘못 말하게 된다.
*/
totalPages: number | null;
onPageChange: (page: number) => void;
@@ -14,8 +14,8 @@ export interface PaginationProps {
* 마지막 페이지 번호
*
* 총 건수가 상한을 넘겨 정확히 세지 않은 목록은 마지막 페이지를 계산할 수 없다.
* 그 경우 서버가 `null` 을 보내며, 페이지 번호 목록과 마지막 페이지 점프가 사라지고
* 이전/다음 이동만 남는다. 0 이나 1 로 채우면 화면이 "1페이지뿐" 이라고 잘못 말하게 된다.
* 그 경우 서버가 `null` 을 보내며, 마지막 페이지 점프만 사라진다. 1 부터 현재 페이지까지와
* (`hasMorePages` 가 참이면) 다음 페이지 번호는 그대로 그린다. 0 이나 1 로 채우면 화면이 "1페이지뿐" 이라고 잘못 말하게 된다.
*/
totalPages: number | null;
onPageChange: (page: number) => void;
@@ -91,8 +91,8 @@ export const Pagination: React.FC<PaginationProps> = ({
editorAttrs,
}) => {
// 총 건수를 정확히 세지 못한 목록은 마지막 페이지를 계산할 수 없다.
// 이때는 페이지 번호 목록과 마지막 페이지 점프를 감추고 이전/다음 이동만 남긴다 —
// 끝까지 넘겨 보는 것은 그대로 가능하다.
// 계산 불가한 값은 마지막 페이지 번호 하나뿐이므로 그 점프만 감춘다. 1 부터 현재
// 페이지까지는 확실히 존재하고, 다음 페이지 존재 여부는 서버가 `hasMorePages` 로 알린다.
const isBounded = totalPages !== null && totalPages !== undefined;
const resolvedTotalPages = isBounded ? (totalPages as number) : 0;
@@ -112,7 +112,21 @@ export const Pagination: React.FC<PaginationProps> = ({
const pages: (number | string)[] = [];
if (!isBounded) {
// 마지막 페이지를 모르면 번호 목록을 만들 수 없다 (현재 페이지만 별도로 표시)
// 마지막 페이지를 모르면 아는 범위(1..현재, 다음이 있으면 현재+1)만 그린다.
// 현재+1 너머는 존재 여부를 모르므로 번호 대신 계속됨 표시만 둔다.
const halfVisible = Math.floor(maxVisiblePages / 2);
const startPage = currentPage <= maxVisiblePages ? 1 : currentPage - halfVisible;
if (startPage > 1) {
pages.push(1);
if (startPage > 2) pages.push('...');
}
for (let i = startPage; i <= currentPage; i++) {
pages.push(i);
}
if (hasMorePages) {
pages.push(currentPage + 1);
pages.push('...');
}
return pages;
}
@@ -160,7 +174,7 @@ export const Pagination: React.FC<PaginationProps> = ({
}
return pages;
}, [currentPage, resolvedTotalPages, isBounded, maxVisiblePages]);
}, [currentPage, resolvedTotalPages, isBounded, maxVisiblePages, hasMorePages]);
const handlePageClick = (page: number) => {
if (page < 1 || page === currentPage) return;
@@ -210,13 +224,8 @@ export const Pagination: React.FC<PaginationProps> = ({
{prevText || <>&#8249;</>}
</Button>
{/* Page Numbers — 마지막 페이지를 모르면 현재 페이지만 표시 */}
{/* Page Numbers — 마지막 페이지를 모르면 아는 범위까지만 표시 */}
<Div className="pagination">
{!isBounded && (
<Span className="px-3 py-1 text-sm text-gray-700 dark:text-gray-300">
{currentPage}
</Span>
)}
{pageNumbers.map((page, index) => {
if (page === '...') {
return (
@@ -0,0 +1,110 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Pagination } from '../Pagination';
/**
* 마지막 페이지를 모르는 목록의 페이지 번호 목록 회귀
*
* 총 건수가 상한을 넘겨 `last_page` 가 `null` 이면 계산할 수 없는 값은 마지막 페이지
* 번호 하나뿐이다. 1 부터 현재 페이지까지는 확실히 존재하고, 다음 페이지 존재 여부는
* 서버가 `has_more_pages` 로 알린다. 그런데 종전 구현은 번호 목록을 통째로 비우고
* 현재 페이지 숫자 하나만 남겨, 운영자가 앞 페이지로 직접 뛰어갈 방법을 잃었다
* (활동 로그 화면 실측: "총 10000건 이상" 인데 페이저에 "1" 하나).
*
* 합의된 동작(CHANGELOG 7.0.10): 마지막 페이지로 바로 뛰는 버튼만 감춘다.
*/
describe('Pagination — last_page 를 모를 때의 페이지 번호 목록', () => {
// 접근성 이름은 「N페이지」 라벨이므로 번호 버튼은 표시 텍스트로 찾는다
const pageButton = (n: number) =>
screen.queryAllByRole('button').find((b) => b.textContent?.trim() === String(n)) ?? null;
it('1 부터 현재 페이지까지의 번호가 그대로 그려진다', () => {
render(
<Pagination currentPage={5} totalPages={null} hasMorePages={true} onPageChange={vi.fn()} />
);
for (let n = 1; n <= 5; n++) {
expect(pageButton(n)).toBeInTheDocument();
}
expect(pageButton(5)).toHaveAttribute('aria-current', 'page');
});
it('다음 페이지가 있으면 그 번호 하나와 계속됨 표시가 붙고, 그 너머는 그리지 않는다', () => {
render(
<Pagination currentPage={5} totalPages={null} hasMorePages={true} onPageChange={vi.fn()} />
);
expect(pageButton(6)).toBeInTheDocument();
expect(pageButton(7)).not.toBeInTheDocument();
expect(screen.getByText('...')).toBeInTheDocument();
});
it('다음 페이지가 없으면 현재 페이지가 끝이다 — 계속됨 표시도 없다', () => {
render(
<Pagination currentPage={3} totalPages={null} hasMorePages={false} onPageChange={vi.fn()} />
);
expect(pageButton(3)).toBeInTheDocument();
expect(pageButton(4)).not.toBeInTheDocument();
expect(screen.queryByText('...')).not.toBeInTheDocument();
});
it('첫 페이지에서도 번호가 버튼으로 그려진다', () => {
render(
<Pagination currentPage={1} totalPages={null} hasMorePages={true} onPageChange={vi.fn()} />
);
expect(pageButton(1)).toBeInTheDocument();
expect(pageButton(2)).toBeInTheDocument();
});
it('깊은 페이지에서는 앞쪽을 접고 현재 페이지 주변만 남긴다', () => {
render(
<Pagination
currentPage={12}
totalPages={null}
hasMorePages={true}
maxVisiblePages={5}
onPageChange={vi.fn()}
/>
);
expect(pageButton(1)).toBeInTheDocument();
expect(pageButton(2)).not.toBeInTheDocument();
expect(pageButton(10)).toBeInTheDocument();
expect(pageButton(11)).toBeInTheDocument();
expect(pageButton(12)).toBeInTheDocument();
expect(pageButton(13)).toBeInTheDocument();
expect(pageButton(14)).not.toBeInTheDocument();
expect(screen.getAllByText('...')).toHaveLength(2);
});
it('앞 페이지 번호와 다음 페이지 번호를 클릭하면 그 페이지로 이동한다', () => {
const onPageChange = vi.fn();
render(
<Pagination currentPage={5} totalPages={null} hasMorePages={true} onPageChange={onPageChange} />
);
fireEvent.click(pageButton(3)!);
expect(onPageChange).toHaveBeenCalledWith(3);
fireEvent.click(pageButton(6)!);
expect(onPageChange).toHaveBeenCalledWith(6);
});
it('마지막 페이지 점프 버튼만 감춰진다', () => {
render(
<Pagination
currentPage={5}
totalPages={null}
hasMorePages={true}
showFirstLast={true}
onPageChange={vi.fn()}
/>
);
expect(screen.queryByLabelText('마지막 페이지')).not.toBeInTheDocument();
expect(screen.getByLabelText('이전 페이지')).toBeInTheDocument();
expect(screen.getByLabelText('다음 페이지')).toBeInTheDocument();
});
});
+1 -1
View File
@@ -158,7 +158,7 @@ API 까지만 소유하고, 그 API 를 소비해 실제로 그리는 것은 이
| 종류 | 개수 | 위치 |
|---|---|---|
| PHPUnit | 0개 | — |
| Vitest | 142개 | `vitest.config.ts` |
| Vitest | 143개 | `vitest.config.ts` |
| Playwright | 8개 | `tests/Playwright` |
| 시나리오 매니페스트 | 3개 | `tests/scenarios` |
@@ -17,6 +17,7 @@
### Fixed
- 총 건수를 정확히 세지 못한 목록(대량 검색 결과, 게시글이 아주 많은 게시판 등)에서 페이지 번호가 전부 사라지고 현재 페이지 숫자 하나만 남던 문제를 수정했습니다. 이제 1 부터 현재 페이지까지의 번호와 다음 페이지 번호가 그대로 표시되며, 감춰지는 것은 마지막 페이지로 뛰는 버튼뿐입니다.
- 게시글·페이지 본문을 표시할 때 쓰는 HTML 정화 라이브러리가 구버전에 머물러 있던 문제를 고쳤습니다. 관리자 템플릿과 동일한 최신 버전으로 맞췄습니다. (#126 @jiwonpapa 님께서 제보해주셨습니다.)
- 통화 표시·선호 통화 저장 관련 화면 동작 함수 4종이 실제 호출 규약과 다른 형태로 작성돼 있어, 호출되면 값이 전달되지 않고 상태가 잘못 기록되던 문제를 고쳤습니다.
- 주소 검색을 불러오지 못한 상태에서 주문서의 우편번호·주소를 직접 입력해도 값이 주문에 반영되지 않아 결제 버튼이 계속 눌리지 않던 문제를 고쳤습니다. 이제 직접 입력한 주소로 주문을 끝까지 진행할 수 있습니다.
File diff suppressed because one or more lines are too long
@@ -6,8 +6,8 @@ export interface PaginationProps {
* 마지막 페이지 번호
*
* 총 건수가 상한을 넘겨 정확히 세지 않은 목록은 마지막 페이지를 계산할 수 없다.
* 그 경우 서버가 `null` 을 보내며, 페이지 번호 목록과 마지막 페이지 점프가 사라지고
* 이전/다음 이동만 남는다. 0 이나 1 로 채우면 화면이 "1페이지뿐" 이라고 잘못 말하게 된다.
* 그 경우 서버가 `null` 을 보내며, 마지막 페이지 점프만 사라진다. 1 부터 현재 페이지까지와
* (`hasMorePages` 가 참이면) 다음 페이지 번호는 그대로 그린다. 0 이나 1 로 채우면 화면이 "1페이지뿐" 이라고 잘못 말하게 된다.
*/
totalPages: number | null;
onPageChange: (page: number) => void;
@@ -14,8 +14,8 @@ export interface PaginationProps {
* 마지막 페이지 번호
*
* 총 건수가 상한을 넘겨 정확히 세지 않은 목록은 마지막 페이지를 계산할 수 없다.
* 그 경우 서버가 `null` 을 보내며, 페이지 번호 목록과 마지막 페이지 점프가 사라지고
* 이전/다음 이동만 남는다. 0 이나 1 로 채우면 화면이 "1페이지뿐" 이라고 잘못 말하게 된다.
* 그 경우 서버가 `null` 을 보내며, 마지막 페이지 점프만 사라진다. 1 부터 현재 페이지까지와
* (`hasMorePages` 가 참이면) 다음 페이지 번호는 그대로 그린다. 0 이나 1 로 채우면 화면이 "1페이지뿐" 이라고 잘못 말하게 된다.
*/
totalPages: number | null;
onPageChange: (page: number) => void;
@@ -93,8 +93,8 @@ export const Pagination: React.FC<PaginationProps> = ({
editorAttrs,
}) => {
// 총 건수를 정확히 세지 못한 목록은 마지막 페이지를 계산할 수 없다.
// 이때는 페이지 번호 목록과 마지막 페이지 점프를 감추고 이전/다음 이동만 남긴다 —
// 끝까지 넘겨 보는 것은 그대로 가능하다.
// 계산 불가한 값은 마지막 페이지 번호 하나뿐이므로 그 점프만 감춘다. 1 부터 현재
// 페이지까지는 확실히 존재하고, 다음 페이지 존재 여부는 서버가 `hasMorePages` 로 알린다.
const isBounded = totalPages !== null && totalPages !== undefined;
const resolvedTotalPages = isBounded ? (totalPages as number) : 0;
@@ -114,7 +114,21 @@ export const Pagination: React.FC<PaginationProps> = ({
const pages: (number | string)[] = [];
if (!isBounded) {
// 마지막 페이지를 모르면 번호 목록을 만들 수 없다 (현재 페이지만 별도로 표시)
// 마지막 페이지를 모르면 아는 범위(1..현재, 다음이 있으면 현재+1)만 그린다.
// 현재+1 너머는 존재 여부를 모르므로 번호 대신 계속됨 표시만 둔다.
const halfVisible = Math.floor(maxVisiblePages / 2);
const startPage = currentPage <= maxVisiblePages ? 1 : currentPage - halfVisible;
if (startPage > 1) {
pages.push(1);
if (startPage > 2) pages.push('...');
}
for (let i = startPage; i <= currentPage; i++) {
pages.push(i);
}
if (hasMorePages) {
pages.push(currentPage + 1);
pages.push('...');
}
return pages;
}
@@ -162,7 +176,7 @@ export const Pagination: React.FC<PaginationProps> = ({
}
return pages;
}, [currentPage, resolvedTotalPages, isBounded, maxVisiblePages]);
}, [currentPage, resolvedTotalPages, isBounded, maxVisiblePages, hasMorePages]);
const handlePageClick = (page: number) => {
if (page < 1 || page === currentPage) return;
@@ -212,13 +226,8 @@ export const Pagination: React.FC<PaginationProps> = ({
{prevText || <>&#8249;</>}
</Button>
{/* Page Numbers — 마지막 페이지를 모르면 현재 페이지만 표시 */}
{/* Page Numbers — 마지막 페이지를 모르면 아는 범위까지만 표시 */}
<Div className="flex items-center gap-1">
{!isBounded && (
<Span className="px-3 py-1 text-sm text-gray-700 dark:text-gray-300">
{currentPage}
</Span>
)}
{pageNumbers.map((page, index) => {
if (page === '...') {
return (
@@ -0,0 +1,110 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Pagination } from '../Pagination';
/**
* 마지막 페이지를 모르는 목록의 페이지 번호 목록 회귀
*
* 총 건수가 상한을 넘겨 `last_page` 가 `null` 이면 계산할 수 없는 값은 마지막 페이지
* 번호 하나뿐이다. 1 부터 현재 페이지까지는 확실히 존재하고, 다음 페이지 존재 여부는
* 서버가 `has_more_pages` 로 알린다. 그런데 종전 구현은 번호 목록을 통째로 비우고
* 현재 페이지 숫자 하나만 남겨, 운영자가 앞 페이지로 직접 뛰어갈 방법을 잃었다
* (활동 로그 화면 실측: "총 10000건 이상" 인데 페이저에 "1" 하나).
*
* 합의된 동작(CHANGELOG 7.0.10): 마지막 페이지로 바로 뛰는 버튼만 감춘다.
*/
describe('Pagination — last_page 를 모를 때의 페이지 번호 목록', () => {
// 접근성 이름은 「N페이지」 라벨이므로 번호 버튼은 표시 텍스트로 찾는다
const pageButton = (n: number) =>
screen.queryAllByRole('button').find((b) => b.textContent?.trim() === String(n)) ?? null;
it('1 부터 현재 페이지까지의 번호가 그대로 그려진다', () => {
render(
<Pagination currentPage={5} totalPages={null} hasMorePages={true} onPageChange={vi.fn()} />
);
for (let n = 1; n <= 5; n++) {
expect(pageButton(n)).toBeInTheDocument();
}
expect(pageButton(5)).toHaveAttribute('aria-current', 'page');
});
it('다음 페이지가 있으면 그 번호 하나와 계속됨 표시가 붙고, 그 너머는 그리지 않는다', () => {
render(
<Pagination currentPage={5} totalPages={null} hasMorePages={true} onPageChange={vi.fn()} />
);
expect(pageButton(6)).toBeInTheDocument();
expect(pageButton(7)).not.toBeInTheDocument();
expect(screen.getByText('...')).toBeInTheDocument();
});
it('다음 페이지가 없으면 현재 페이지가 끝이다 — 계속됨 표시도 없다', () => {
render(
<Pagination currentPage={3} totalPages={null} hasMorePages={false} onPageChange={vi.fn()} />
);
expect(pageButton(3)).toBeInTheDocument();
expect(pageButton(4)).not.toBeInTheDocument();
expect(screen.queryByText('...')).not.toBeInTheDocument();
});
it('첫 페이지에서도 번호가 버튼으로 그려진다', () => {
render(
<Pagination currentPage={1} totalPages={null} hasMorePages={true} onPageChange={vi.fn()} />
);
expect(pageButton(1)).toBeInTheDocument();
expect(pageButton(2)).toBeInTheDocument();
});
it('깊은 페이지에서는 앞쪽을 접고 현재 페이지 주변만 남긴다', () => {
render(
<Pagination
currentPage={12}
totalPages={null}
hasMorePages={true}
maxVisiblePages={5}
onPageChange={vi.fn()}
/>
);
expect(pageButton(1)).toBeInTheDocument();
expect(pageButton(2)).not.toBeInTheDocument();
expect(pageButton(10)).toBeInTheDocument();
expect(pageButton(11)).toBeInTheDocument();
expect(pageButton(12)).toBeInTheDocument();
expect(pageButton(13)).toBeInTheDocument();
expect(pageButton(14)).not.toBeInTheDocument();
expect(screen.getAllByText('...')).toHaveLength(2);
});
it('앞 페이지 번호와 다음 페이지 번호를 클릭하면 그 페이지로 이동한다', () => {
const onPageChange = vi.fn();
render(
<Pagination currentPage={5} totalPages={null} hasMorePages={true} onPageChange={onPageChange} />
);
fireEvent.click(pageButton(3)!);
expect(onPageChange).toHaveBeenCalledWith(3);
fireEvent.click(pageButton(6)!);
expect(onPageChange).toHaveBeenCalledWith(6);
});
it('마지막 페이지 점프 버튼만 감춰진다', () => {
render(
<Pagination
currentPage={5}
totalPages={null}
hasMorePages={true}
showFirstLast={true}
onPageChange={vi.fn()}
/>
);
expect(screen.queryByLabelText('마지막 페이지')).not.toBeInTheDocument();
expect(screen.getByLabelText('이전 페이지')).toBeInTheDocument();
expect(screen.getByLabelText('다음 페이지')).toBeInTheDocument();
});
});
@@ -0,0 +1,72 @@
/**
* E2E: 총 건수를 정확히 세지 못한 목록의 페이지 번호 목록
*
* @scenario activity_log_unbounded_pager_numbers, activity_log_unbounded_pager_no_last
* @effects page_numbers_visible, last_page_jump_hidden
*
* 총 건수 집계 상한(관리자 > 환경설정 > 고급)을 활동 로그 건수보다 작게 낮춰
* `last_page: null` 응답을 실제 서버에서 만들고, 활동 로그 화면의 페이저가
* 1. 1 페이지 번호 버튼과 다음 페이지(2) 번호 버튼을 그리고
* 2. 마지막 페이지 점프 버튼은 그리지 않으며
* 3. 번호 버튼을 눌러 그 페이지로 이동하는지
* 를 확인한다. 종전에는 현재 페이지 숫자 하나만 남아 앞 페이지로 뛸 방법이 없었다.
* 테스트가 끝나면 상한을 원래 값으로 되돌린다.
*/
import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth';
const SETTINGS_URL = '/api/admin/settings';
async function readResultCap(page: import('@playwright/test').Page, token: string): Promise<number> {
const res = await page.request.get(SETTINGS_URL, { headers: { Authorization: `Bearer ${token}` } });
expect(res.ok()).toBeTruthy();
const body = await res.json();
return Number(body?.data?.advanced?.pagination_result_cap ?? 10000);
}
async function writeResultCap(page: import('@playwright/test').Page, token: string, cap: number): Promise<void> {
const res = await page.request.post(SETTINGS_URL, {
headers: { Authorization: `Bearer ${token}` },
data: { advanced: { pagination_result_cap: cap } },
});
expect(res.ok(), `설정 저장 실패: ${res.status()}`).toBeTruthy();
}
// @scenario link=activity_log_unbounded_pager_numbers, permitted=na
// @effects page_numbers_visible, last_page_jump_hidden
test('#519 후속 - 총 건수를 모르는 활동 로그에서도 페이지 번호가 그려지고 마지막 페이지 버튼만 감춰진다', async ({ page }) => {
const token = issueToken('core.settings.update');
await authenticatePage(page, token);
const originalCap = await readResultCap(page, token);
await writeResultCap(page, token, 5);
try {
await page.goto('/admin/activity-logs?per_page=5');
await page.waitForLoadState('domcontentloaded', { timeout: 30_000 });
const list = await page.request.get('/api/admin/activity-logs?per_page=5', {
headers: { Authorization: `Bearer ${token}` },
});
const listBody = await list.json();
const meta = listBody?.data?.pagination ?? listBody?.meta;
test.skip(!meta || meta.last_page !== null, '활동 로그가 상한(5건) 이하라 last_page 가 계산된다 — 이 시나리오의 전제가 성립하지 않는다');
// 페이저 루트는 Div(role=navigation) 이라 태그가 아니라 역할·이름으로 찾는다
const nav = page.getByRole('navigation', { name: '페이지네이션' }).first();
await expect(nav).toBeVisible({ timeout: 15_000 });
const pageButton = (n: number) => nav.locator('button', { hasText: new RegExp(`^${n}$`) });
await expect(pageButton(1)).toBeVisible();
await expect(pageButton(1)).toHaveAttribute('aria-current', 'page');
await expect(pageButton(2)).toBeVisible();
await expect(nav.locator('button[aria-label="마지막 페이지"]')).toHaveCount(0);
await expect(nav.locator('button[aria-label="다음 페이지"]')).toBeEnabled();
await pageButton(2).click();
await expect(pageButton(2)).toHaveAttribute('aria-current', 'page', { timeout: 15_000 });
await expect(pageButton(1)).toBeVisible();
await expect(pageButton(3)).toBeVisible();
} finally {
await writeResultCap(page, token, originalCap);
}
});