Skip to content

Commit 81b2332

Browse files
authored
[#99] ✨ shared 컴포넌트 Pagination 개발 (#133)
* [#99] ✨ add Pagination component * [#99] ✨ add usePagination hook * [#99] 💄 update chevron-right svg with currentColor attribute * [#99] ♻️ refactor button from using label and icons props to children props * [#99] ♻️ reflect changes in Clickable on module content component * [#99] ✅ add Pagination stories * [#99] ✨ add extended twMerge due to the issue with merging custom classes * [#99] ♻️ apply extended twMerge in clickable to test if it works * [#99] ♻️ separate resuable interface (UsePagination) * [#99] ♻️ replace clsx with twMergeEx in Pagination component * [#99] ♻️ add conditional className handling function in Pagination component' * [#99] 🚚 rename UsePaginationReturn into PaginationState
1 parent b032f29 commit 81b2332

File tree

10 files changed

+219
-15
lines changed

10 files changed

+219
-15
lines changed
Lines changed: 1 addition & 1 deletion
Loading

src/components/auth/SignUpSuccessModalContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const SignUpSuccessModalContent = (): JSX.Element => {
2222
lastLabel={'에서 가능합니다.'}
2323
to={'/'}
2424
/>
25-
<ModalContent.Link href={'/'} label={'로그인 바로가기'} />
25+
<ModalContent.Link href={'/'}>로그인 바로가기</ModalContent.Link>
2626
</ModalContent>
2727
)
2828
}

src/components/common/button/Clickable.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1+
import { twMergeEx } from '@/lib/twMerge'
12
import clsx from 'clsx'
23

34
export interface ClickableProps {
4-
label?: string
5+
children?: React.ReactNode
56
variant?: Variant
67
size?: Size
78
borderColor?: BorderColor
89
backgroundColor?: BackgroundColor
910
textColor?: TextColor
10-
startIcon?: React.ReactElement
11-
endIcon?: React.ReactElement
1211
fullWidth?: boolean
1312
leftAlign?: boolean
1413
disabled?: boolean
@@ -19,7 +18,14 @@ type Variant = 'contained' | 'outlined' | 'text'
1918
type Size = 'sm' | 'md' | 'lg' | 'xl'
2019
type BorderColor = 'blue' | 'gray'
2120
type BackgroundColor = 'blue' | 'white' | 'gray' | 'transparentBlue'
22-
type TextColor = 'blue' | 'white' | 'black' | 'gray400' | 'gray500' | 'gray600'
21+
type TextColor =
22+
| 'blue'
23+
| 'white'
24+
| 'black'
25+
| 'gray400'
26+
| 'gray500'
27+
| 'gray600'
28+
| 'gray800'
2329

2430
const baseStyle =
2531
'flex items-center justify-center gap-4 rounded-8 text-body1 font-medium'
@@ -60,17 +66,16 @@ const styleByTextColor: Record<TextColor, string> = {
6066
gray400: 'text-gray-400',
6167
gray500: 'text-gray-500',
6268
gray600: 'text-gray-600',
69+
gray800: 'text-gray-800',
6370
}
6471

6572
export const Clickable = ({
66-
label = '',
73+
children,
6774
variant = 'contained',
6875
size = 'md',
6976
borderColor,
7077
backgroundColor,
7178
textColor,
72-
startIcon,
73-
endIcon,
7479
fullWidth = false,
7580
leftAlign = false,
7681
disabled = false,
@@ -82,26 +87,24 @@ export const Clickable = ({
8287
: ''
8388
const textColorClass = textColor ? styleByTextColor[textColor] : ''
8489

85-
const clickableStyle = clsx(
90+
const clickableStyle = twMergeEx(
8691
baseStyle,
8792
styleByVariant[variant],
8893
styleBySize[size],
8994
textColorClass,
90-
{
95+
clsx({
9196
[borderColorClass]: variant === 'outlined',
9297
[backgroundColorClass]: variant !== 'text',
9398
[disabledStyle]: disabled,
9499
'w-full': fullWidth,
95100
'justify-start': leftAlign,
96-
},
101+
}),
97102
className
98103
)
99104

100105
return (
101106
<span className={clickableStyle} aria-disabled={disabled}>
102-
{startIcon}
103-
{label}
104-
{endIcon}
107+
{children}
105108
</span>
106109
)
107110
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { IcChevronLeft, IcChevronRight } from '@/assets/IconList'
2+
import { twMergeEx } from '@/lib/twMerge'
3+
import type { PaginationState } from '@/types/hooks'
4+
5+
import { Button } from '@/components/common/button'
6+
7+
const baseStyle =
8+
'flex h-24 w-24 items-center justify-center bg-common-white p-0 text-body3 text-gray-600'
9+
const defaultPageButtonClass = twMergeEx(baseStyle, 'hover:bg-gray-100')
10+
const currentPageButtonClass = twMergeEx(
11+
baseStyle,
12+
'bg-primary-normal text-common-white'
13+
)
14+
15+
const getPageButtonClass = (page: number, currentPage: number): string =>
16+
currentPage === page ? currentPageButtonClass : defaultPageButtonClass
17+
18+
export const Pagination = ({
19+
currentPage,
20+
pageButtons,
21+
hasNextPageGroup,
22+
hasPreviousPageGroup,
23+
goToPage,
24+
goToNextPageGroup,
25+
goToPreviousPageGroup,
26+
}: PaginationState): JSX.Element => {
27+
return (
28+
<div className='flex items-center gap-20'>
29+
<Button
30+
variant='text'
31+
onClick={goToPreviousPageGroup}
32+
className={defaultPageButtonClass}
33+
disabled={!hasPreviousPageGroup}
34+
>
35+
<IcChevronLeft />
36+
</Button>
37+
{pageButtons.map(page => (
38+
<Button
39+
variant='text'
40+
key={page}
41+
onClick={() => goToPage(page)}
42+
className={getPageButtonClass(page, currentPage)}
43+
aria-label={`${page}번 페이지로 이동`}
44+
aria-current={currentPage === page ? 'page' : undefined}
45+
>
46+
{page}
47+
</Button>
48+
))}
49+
<Button
50+
variant='text'
51+
onClick={goToNextPageGroup}
52+
className={defaultPageButtonClass}
53+
disabled={!hasNextPageGroup}
54+
>
55+
<IcChevronRight />
56+
</Button>
57+
</div>
58+
)
59+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Pagination } from './Pagination'
2+
3+
export { Pagination }

src/hooks/usePagination.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { useState } from 'react'
2+
3+
import type { PaginationState } from '@/types/hooks'
4+
5+
interface UsePaginationProps {
6+
totalItems: number // 전체 아이템 수
7+
itemsPerPage: number // 페이지당 아이템 수
8+
buttonsPerPage?: number // 한 번에 보여줄 페이지네이션 버튼 수 (기본값: 10)
9+
}
10+
11+
export function usePagination({
12+
totalItems,
13+
itemsPerPage,
14+
buttonsPerPage = 10,
15+
}: UsePaginationProps): PaginationState {
16+
if (totalItems <= 0 || itemsPerPage <= 0 || buttonsPerPage <= 0) {
17+
throw new Error('0보다 같거나 작은 페이지를 인자로 전달할 수 없습니다.')
18+
}
19+
20+
const totalPages = Math.ceil(totalItems / itemsPerPage) // 총 페이지 수
21+
const totalGroups = Math.ceil(totalPages / buttonsPerPage) // 총 그룹 수
22+
const [currentPage, setCurrentPage] = useState(1) // 현재 페이지
23+
const [currentGroupIndex, setCurrentGroupIndex] = useState(0) // 현재 페이지 그룹 인덱스
24+
25+
const firstPageInGroup = currentGroupIndex * buttonsPerPage + 1
26+
const lastPageInGroup = Math.min(
27+
firstPageInGroup + buttonsPerPage - 1,
28+
totalPages
29+
)
30+
31+
// 현재 그룹에 표시될 페이지 번호 계산
32+
const pageButtons = Array.from(
33+
{ length: lastPageInGroup - firstPageInGroup + 1 },
34+
(_, idx) => firstPageInGroup + idx
35+
)
36+
37+
const hasNextPageGroup = currentGroupIndex < totalGroups - 1
38+
const hasPreviousPageGroup = currentGroupIndex > 0
39+
40+
const goToPage = (page: number) => {
41+
if (page < 1 || page > totalPages) {
42+
console.warn('Invalid page number')
43+
return
44+
}
45+
setCurrentPage(page)
46+
}
47+
48+
const goToNextPageGroup = () => {
49+
if (hasNextPageGroup) {
50+
setCurrentGroupIndex(prev => prev + 1)
51+
setCurrentPage((currentGroupIndex + 1) * buttonsPerPage + 1)
52+
}
53+
}
54+
55+
const goToPreviousPageGroup = () => {
56+
if (hasPreviousPageGroup) {
57+
setCurrentGroupIndex(prev => prev - 1)
58+
setCurrentPage((currentGroupIndex - 1) * buttonsPerPage + buttonsPerPage)
59+
}
60+
}
61+
62+
return {
63+
currentPage,
64+
pageButtons,
65+
hasNextPageGroup,
66+
hasPreviousPageGroup,
67+
goToPage,
68+
goToNextPageGroup,
69+
goToPreviousPageGroup,
70+
}
71+
}

src/lib/twMerge/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { extendTailwindMerge } from 'tailwind-merge'
2+
3+
export const twMergeEx = extendTailwindMerge({
4+
extend: {
5+
classGroups: {
6+
'font-size': [
7+
{
8+
text: [
9+
'heading1',
10+
'heading2',
11+
'heading3',
12+
'heading4',
13+
'heading5',
14+
'title1',
15+
'title2',
16+
'body1',
17+
'body2',
18+
'body3',
19+
'caption1',
20+
'caption2',
21+
'inherit',
22+
],
23+
},
24+
],
25+
},
26+
},
27+
})
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Meta, StoryObj } from '@storybook/react'
2+
3+
import { Pagination } from '@/components/shared/pagination'
4+
5+
import { usePagination } from '@/hooks/usePagination'
6+
7+
export default {
8+
title: 'Shared/Pagination/Pagination',
9+
component: Pagination,
10+
argTypes: {
11+
currentPage: { control: { type: 'number', min: 1 }, defaultValue: 1 },
12+
totalPages: { control: { type: 'number', min: 1 }, defaultValue: 5 },
13+
hasNextPage: { control: 'boolean', defaultValue: true },
14+
hasPreviousPage: { control: 'boolean', defaultValue: false },
15+
},
16+
} as Meta
17+
18+
export const Default: StoryObj = {
19+
args: {
20+
currentPage: 1,
21+
pageButtons: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
22+
totalPages: 5,
23+
hasNextPageGroup: true,
24+
hasPreviousPageGroup: true,
25+
goToPage: (page: number) => alert(`Go to page: ${page}`),
26+
goToNextPageGroup: () => alert('Next page'),
27+
goToPreviousPageGroup: () => alert('Previous page'),
28+
},
29+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export interface PaginationState {
2+
currentPage: number // 현재 페이지
3+
pageButtons: number[] // 현재 페이지 그룹의 버튼 목록
4+
hasNextPageGroup: boolean // 다음 페이지 그룹 존재 여부
5+
hasPreviousPageGroup: boolean // 이전 페이지 그룹 존재 여부
6+
goToPage: (page: number) => void // 특정 페이지로 이동
7+
goToNextPageGroup: () => void // 다음 페이지 그룹으로 이동
8+
goToPreviousPageGroup: () => void // 이전 페이지 그룹으로 이동
9+
}

src/types/hooks/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { PaginationState } from './PaginationState.types'
2+
3+
export type { PaginationState }

0 commit comments

Comments
 (0)