Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cd03daf
feat: Pagination 컴포넌트 구현
areumH Sep 11, 2026
be73a1c
docs: 버튼 제어 Pagination 문서를 Default로 수정
areumH Sep 11, 2026
65e86b6
feat: Pagination 비제어 방식 추가
areumH Sep 11, 2026
0bde812
refactor: Pagination ARIA 레이블 처리 통일
areumH Sep 11, 2026
aa19a09
feat: Pagination 커스텀 링크 컴포넌트 지원
areumH Sep 11, 2026
c6ccd34
refactor: Pagination 화살표 방향 조건 정리
areumH Sep 11, 2026
be86616
style: Pagination 배경 속성 표기 통일
areumH Sep 11, 2026
19a460a
refactor: Pagination 페이지 범위 유틸 정리
areumH Sep 11, 2026
6a933f0
fix: Pagination 긴 페이지 숫자 너비 대응
areumH Sep 12, 2026
f460a40
docs: Pagination 커스텀 링크 스토리 제거
areumH Sep 12, 2026
fa69132
style: Pagination 비활성 스타일 중복 제거
areumH Sep 12, 2026
f38b3c1
fix: Pagination 링크 컴포넌트 타입 제한
areumH Sep 12, 2026
413b1eb
docs: Pagination 스토리에 페이지 이동 레이블 적용
areumH Sep 15, 2026
31a49cf
fix: Pagination 페이지 보정 시 자동 콜백 제거
areumH Sep 15, 2026
2585f0f
docs: Pagination 표시 개수 스토리 레이블 줄바꿈 방지
areumH Sep 15, 2026
a456022
docs: Pagination 컴포넌트 추가 체인지셋 작성
areumH Sep 15, 2026
c169d6a
feat: Pagination 홀짝 구간 이동 규칙 통일
areumH Sep 15, 2026
22b3c0b
docs: Pagination 체인지셋에 홀짝 표시 동작 반영
areumH Sep 15, 2026
812d2e9
refactor: Pagination 표시 범위 계산을 전용 훅으로 통합
areumH Sep 16, 2026
5e78c17
fix: Pagination 단일 페이지 말줄임 방지
areumH Sep 16, 2026
89d074e
fix: Pagination 홀수 초기 표시 범위 보정
areumH Sep 16, 2026
4869fdc
docs: Pagination 체인지셋 표시 규칙 정리
areumH Sep 16, 2026
8fc06e3
refactor: Pagination 내부 타입 위치 정리
areumH Sep 16, 2026
659ee79
style: Pagination 유틸 불필요한 주석 제거
areumH Sep 16, 2026
d799c7a
Merge remote-tracking branch 'origin/dev' into feat/693-jds-pagination
areumH Sep 16, 2026
af87035
chore(vrt): CI에서 baseline 재생성
github-actions[bot] Sep 16, 2026
15614c7
refactor: Pagination 표시 항목 계산 의미 명확화
areumH Sep 23, 2026
81f09a6
Merge remote-tracking branch 'origin/dev' into feat/693-jds-pagination
areumH Sep 23, 2026
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
49 changes: 49 additions & 0 deletions .changeset/pagination-component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
"@jects/jds": minor
---

**Pagination**

`nav > ul > li` 구조로 페이지 번호, 말줄임, 이전과 다음 이동 요소를 표시하는 `Pagination` 컴포넌트를 추가합니다. 버튼의 제어, 비제어 방식과 `href` 기반 링크 방식을 지원합니다. `PaginationProps`, `PaginationButtonProps`, `PaginationLinkProps`, `PaginationVisiblePageCount` 타입을 함께 공개합니다.

| prop | 기본값 | 용도 |
| ------------------------------- | ------- | ---------------------------------------------------------------------------------------- |
| `totalPages` | 필수 | 전체 페이지 수, `0`이면 렌더링하지 않음 |
| `page` | 없음 | 제어 버튼 방식 또는 링크 방식의 현재 페이지 |
| `defaultPage` | `1` | 비제어 버튼 방식의 초기 페이지 |
| `onPageChange` | 없음 | 버튼 방식의 페이지 이동 콜백, 제어 방식에서는 필수 |
| `getPageHref` | 없음 | 링크 방식에서 페이지별 URL 생성 |
| `linkAs` | `a` | 링크 방식에서 사용할 `href`를 받는 라우팅 컴포넌트 |
| `visiblePageCount` | `7` | 이전과 다음을 제외한 최대 항목 수, 숫자와 말줄임 포함, `7`, `8`, `9`, `10`, `11` 중 선택 |
| `disabled` | `false` | 모든 페이지 이동 요소 비활성화 |
| `aria-label`, `aria-labelledby` | 없음 | 탐색 영역의 접근 가능한 이름 지정, 둘 중 하나 사용 |

전체 페이지 수가 표시 개수보다 크면 연속된 페이지 번호와 말줄임을 표시합니다. 양쪽에 말줄임이 있는 구간을 처음 표시할 때 홀수 표시 개수는 현재 페이지를 중앙에 배치하고, 짝수 표시 개수는 오른쪽에 페이지 번호를 하나 더 배치합니다. 말줄임은 두 페이지 이상이 생략될 때만 표시합니다.

표시된 구간의 안쪽 페이지를 선택하면 구간을 유지하고, 양끝 페이지를 선택하면 표시 구간을 이동합니다. `page`나 `totalPages` 변경으로 현재 페이지가 표시 가능한 범위로 보정되어도 `onPageChange`를 호출하지 않습니다. `totalPages` 또는 `visiblePageCount`가 바뀌면 현재 페이지에 맞춰 표시 구간을 다시 배치합니다. 링크 방식에서 URL을 현재 페이지의 기준으로 쓰려면 소비처가 URL에서 페이지 값을 읽어 `page`로 전달합니다.

```tsx
import { useState } from "react";
import { Pagination } from "@jects/jds";

function ButtonPagination() {
const [page, setPage] = useState(1);

return <Pagination aria-label='페이지 이동' page={page} totalPages={10} onPageChange={setPage} />;
}

function UncontrolledPagination() {
return <Pagination aria-label='페이지 이동' defaultPage={1} totalPages={10} />;
}

function LinkPagination({ page }: { page: number }) {
return (
<Pagination
aria-label='페이지 이동'
page={page}
totalPages={10}
getPageHref={targetPage => `?page=${targetPage}`}
/>
);
}
```
147 changes: 147 additions & 0 deletions packages/jds/src/components/Pagination/Pagination.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { FlexColumn, Label } from "@storybook-utils/layout";
import { useState } from "react";

import { Pagination } from "./Pagination";

const meta = {
title: "Components/Pagination",
component: Pagination,
parameters: {
layout: "centered",
docs: {
description: {
component:
"연관된 콘텐츠 뷰를 여러 페이지 단위로 더 작게 나눠 탐색할 수 있도록 돕는 컴포넌트입니다.",
},
},
},
args: {
"aria-label": "페이지 이동",
defaultPage: 5,
totalPages: 10,
visiblePageCount: 7,
disabled: false,
},
argTypes: {
page: {
control: { type: "number", min: 1, step: 1 },
description: "현재 페이지입니다. 1부터 시작합니다.",
},
defaultPage: {
control: { type: "number", min: 1, step: 1 },
description: "비제어 방식에서 사용할 초기 페이지입니다.",
},
totalPages: {
control: { type: "number", min: 1, step: 1 },
description: "전체 페이지 수입니다.",
},
visiblePageCount: {
control: "radio",
options: [7, 8, 9, 10, 11],
description: "이전, 다음 버튼을 제외하고 말줄임을 포함해 표시할 최대 항목 수입니다.",
},
disabled: {
control: "boolean",
description: "모든 페이지 이동 요소의 비활성 여부입니다.",
},
getPageHref: {
control: false,
description: "각 페이지의 URL을 반환합니다. 제공하면 링크로 렌더링합니다.",
},
linkAs: {
control: false,
description: "링크 모드에서 사용할 라우팅 컴포넌트입니다. 기본값은 a 요소입니다.",
},
onPageChange: {
control: false,
description: "페이지 이동 요소를 눌렀을 때 호출됩니다.",
},
},
} satisfies Meta<typeof Pagination>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
render: args => <Pagination key={args.defaultPage} {...args} />,
parameters: {
docs: {
description: {
story: "defaultPage를 사용해 내부에서 현재 페이지를 관리하는 비제어 방식입니다.",
},
},
},
};

export const Controlled: Story = {
render: function Render() {
const [page, setPage] = useState(5);

return (
<Pagination aria-label='페이지 이동' page={page} totalPages={10} onPageChange={setPage} />
);
},
parameters: {
docs: {
description: {
story: "page와 onPageChange를 사용해 외부에서 현재 페이지를 관리하는 제어 방식입니다.",
},
},
},
};

export const LinkNavigation: Story = {
args: {
page: 5,
defaultPage: undefined,
getPageHref: page => `?page=${page}`,
},
parameters: {
docs: {
description: {
story: "getPageHref를 제공하면 모든 페이지 이동 요소를 링크로 렌더링합니다.",
},
},
},
};

export const BoundaryStates: Story = {
render: () => (
<FlexColumn gap='20px'>
<FlexColumn gap='8px'>
<Label>첫 페이지</Label>
<Pagination aria-label='페이지 이동' defaultPage={1} totalPages={10} />
</FlexColumn>
<FlexColumn gap='8px'>
<Label>마지막 페이지</Label>
<Pagination aria-label='페이지 이동' defaultPage={10} totalPages={10} />
</FlexColumn>
</FlexColumn>
),
};

export const VisiblePageCounts: Story = {
render: () => (
<FlexColumn gap='20px'>
{([7, 8, 9, 10, 11] as const).map(visiblePageCount => (
<FlexColumn gap='8px' key={visiblePageCount}>
<Label style={{ width: "max-content" }}>visiblePageCount: {visiblePageCount}</Label>
<Pagination
aria-label='페이지 이동'
defaultPage={10}
totalPages={20}
visiblePageCount={visiblePageCount}
/>
</FlexColumn>
))}
</FlexColumn>
),
};

export const Disabled: Story = {
args: {
disabled: true,
},
};
Loading
Loading