diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index 6bc3f87d..146beb3e 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -26,7 +26,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
- "react-icons": "^5.3.0"
+ "react-icons": "^5.3.0",
"zod": "^3.23.8"
},
"devDependencies": {
diff --git a/apps/frontend/src/feature/Pagination/usePagination.tsx b/apps/frontend/src/feature/Pagination/usePagination.tsx
new file mode 100644
index 00000000..1be14e19
--- /dev/null
+++ b/apps/frontend/src/feature/Pagination/usePagination.tsx
@@ -0,0 +1,57 @@
+import { useState } from 'react';
+
+interface UsePaginationProps {
+ totalPages: number;
+ initialPage?: number;
+ onChangePage?: (page: number) => void;
+}
+
+export function usePagination({ totalPages, initialPage = 1, onChangePage }: UsePaginationProps) {
+ const [currentPage, setCurrentPage] = useState(initialPage);
+
+ const onClickPage = (page: number) => {
+ setCurrentPage(page);
+ onChangePage?.(page);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ };
+
+ const onClickPrevious = () => {
+ if (currentPage > 1) onClickPage(currentPage - 1);
+ };
+
+ const onClickNext = () => {
+ if (currentPage < totalPages) onClickPage(currentPage + 1);
+ };
+
+ // "첫 페이지 ... 현재 페이지와 앞뒤 1페이지 ... 마지막 페이지 "로 구성되도록 구현함
+ const getPaginationItems = () => {
+ const items: (number | 'ellipsis')[] = [];
+ const showStartEllipsis = currentPage > 4;
+ const showEndEllipsis = currentPage < totalPages - 3;
+
+ items.push(1);
+
+ if (showStartEllipsis) items.push('ellipsis');
+
+ const startPage = showStartEllipsis ? Math.max(2, currentPage - 1) : 2;
+ const endPage = showEndEllipsis ? Math.min(totalPages - 1, currentPage + 1) : totalPages - 1;
+
+ for (let page = startPage; page <= endPage; page++) {
+ items.push(page);
+ }
+
+ if (showEndEllipsis) items.push('ellipsis');
+
+ if (totalPages > 1) items.push(totalPages);
+
+ return items;
+ };
+
+ return {
+ currentPage,
+ onClickPage,
+ onClickPrevious,
+ onClickNext,
+ getPaginationItems
+ };
+}
diff --git a/apps/frontend/src/widget/LotusList/LotusCardList.tsx b/apps/frontend/src/widget/LotusList/LotusCardList.tsx
index 2cd0d0b0..20afcb46 100644
--- a/apps/frontend/src/widget/LotusList/LotusCardList.tsx
+++ b/apps/frontend/src/widget/LotusList/LotusCardList.tsx
@@ -1,5 +1,6 @@
import { Lotus } from '@/feature/Lotus';
import { LotusType } from '@/feature/Lotus/type';
+import { Pagination } from '@/widget/Pagination';
const LotusDummyData: LotusType = {
link: 'https://example.com',
@@ -14,29 +15,33 @@ const LotusDummyData: LotusType = {
}
};
+// TODO: 나중에 Props로 size 받아서 재사용하기
export function LotusCardList() {
return (
-
- {new Array(10).fill(0).map((_, index) => (
-
-
-
-
-
-
-
-
- {LotusDummyData?.tags?.length ? (
- <>
-
-
- >
- ) : (
- <>>
- )}
-
-
- ))}
-
+ <>
+
+ {new Array(10).fill(0).map((_, index) => (
+
+
+
+
+
+
+
+
+ {LotusDummyData?.tags?.length ? (
+ <>
+
+
+ >
+ ) : (
+ <>>
+ )}
+
+
+ ))}
+
+ console.log(page)} />
+ >
);
}
diff --git a/apps/frontend/src/widget/Pagination.tsx b/apps/frontend/src/widget/Pagination.tsx
new file mode 100644
index 00000000..7bbd470e
--- /dev/null
+++ b/apps/frontend/src/widget/Pagination.tsx
@@ -0,0 +1,48 @@
+import {
+ Pagination as PaginationBox,
+ PaginationContent,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious
+} from '@froxy/design/components';
+import { usePagination } from '@/feature/Pagination/usePagination';
+
+interface PaginationProps {
+ totalPages: number;
+ initialPage?: number;
+ onChangePage?: (page: number) => void;
+}
+
+export function Pagination({ totalPages, initialPage = 1, onChangePage }: PaginationProps) {
+ const { currentPage, onClickPage, onClickPrevious, onClickNext, getPaginationItems } = usePagination({
+ totalPages,
+ initialPage,
+ onChangePage
+ });
+
+ return (
+
+
+
+
+
+ {getPaginationItems().map((item, index) => (
+
+ {typeof item === 'number' ? (
+ item !== currentPage && onClickPage(item)} isActive={item === currentPage}>
+ {item}
+
+ ) : (
+
+ )}
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/packages/design/src/components/index.tsx b/packages/design/src/components/index.tsx
index 86afb753..fac30e49 100644
--- a/packages/design/src/components/index.tsx
+++ b/packages/design/src/components/index.tsx
@@ -2,6 +2,7 @@ export * from './ui/badge';
export * from './ui/button';
export * from './ui/carousel';
export * from './ui/input';
+export * from './ui/pagination';
export * from './ui/typography';
export * from './ui/tabs';
export * from './Slot';
diff --git a/packages/design/src/components/ui/pagination.tsx b/packages/design/src/components/ui/pagination.tsx
new file mode 100644
index 00000000..5be67417
--- /dev/null
+++ b/packages/design/src/components/ui/pagination.tsx
@@ -0,0 +1,80 @@
+import * as React from 'react';
+import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
+import { ButtonProps, buttonVariants } from './button';
+import { cn } from '@/lib/utils';
+
+const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
+
+);
+Pagination.displayName = 'Pagination';
+
+const PaginationContent = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+PaginationContent.displayName = 'PaginationContent';
+
+const PaginationItem = React.forwardRef>(({ className, ...props }, ref) => (
+
+));
+PaginationItem.displayName = 'PaginationItem';
+
+type PaginationLinkProps = {
+ isActive?: boolean;
+} & Pick &
+ React.ComponentProps<'a'>;
+
+const PaginationLink = ({ className, isActive, size = 'icon', ...props }: PaginationLinkProps) => (
+
+);
+PaginationLink.displayName = 'PaginationLink';
+
+const PaginationPrevious = ({ className, ...props }: React.ComponentProps) => (
+
+
+ Previous
+
+);
+PaginationPrevious.displayName = 'PaginationPrevious';
+
+const PaginationNext = ({ className, ...props }: React.ComponentProps) => (
+
+ Next
+
+
+);
+PaginationNext.displayName = 'PaginationNext';
+
+const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (
+
+
+ More pages
+
+);
+PaginationEllipsis.displayName = 'PaginationEllipsis';
+
+export {
+ Pagination,
+ PaginationContent,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious
+};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6864d789..c9c4ddd3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -132,21 +132,33 @@ importers:
'@froxy/react-markdown':
specifier: workspace:^
version: link:../../packages/react-markdown
+ '@hookform/resolvers':
+ specifier: ^3.9.1
+ version: 3.9.1(react-hook-form@7.53.2(react@18.3.1))
'@tanstack/react-query':
specifier: ^5.59.19
version: 5.59.19(react@18.3.1)
'@tanstack/react-router':
specifier: ^1.78.3
version: 1.78.3(@tanstack/router-generator@1.78.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ framer-motion:
+ specifier: ^11.11.11
+ version: 11.11.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react:
specifier: ^18.3.1
version: 18.3.1
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
+ react-hook-form:
+ specifier: ^7.53.2
+ version: 7.53.2(react@18.3.1)
react-icons:
specifier: ^5.3.0
version: 5.3.0(react@18.3.1)
+ zod:
+ specifier: ^3.23.8
+ version: 3.23.8
devDependencies:
'@eslint/js':
specifier: ^9.13.0
@@ -1022,6 +1034,11 @@ packages:
resolution: {integrity: sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@hookform/resolvers@3.9.1':
+ resolution: {integrity: sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==}
+ peerDependencies:
+ react-hook-form: ^7.0.0
+
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@@ -3295,6 +3312,20 @@ packages:
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
+ framer-motion@11.11.13:
+ resolution: {integrity: sha512-aoEA83gsqRRsnh4TN7S9YNcKVLrg+GtPNnxNMd9bGn23+pLmuKGQeccPnqffEKzlkgmy2MkMo0jRkK41S2LzWw==}
+ peerDependencies:
+ '@emotion/is-prop-valid': '*'
+ react: ^18.0.0
+ react-dom: ^18.0.0
+ peerDependenciesMeta:
+ '@emotion/is-prop-valid':
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+
fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
@@ -4773,6 +4804,12 @@ packages:
peerDependencies:
react: ^18.3.1
+ react-hook-form@7.53.2:
+ resolution: {integrity: sha512-YVel6fW5sOeedd1524pltpHX+jgU2u3DSDtXEaBORNdqiNrsX/nUI/iGXONegttg0mJVnfrIkiV0cmTU6Oo2xw==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+
react-icons@5.3.0:
resolution: {integrity: sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==}
peerDependencies:
@@ -5126,9 +5163,6 @@ packages:
stringify-entities@4.0.4:
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
- strip-ansi-cjs@8.0.0:
- resolution: {integrity: sha512-32gkt3BeWEaDWScWe6w75HZX3dgTwM6SCzYZOJqnQWwGIugHHj5+u/TSzZUbcEsg8v0dpVljHNHcUJ+1dmVRqw==}
-
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@@ -6463,6 +6497,10 @@ snapshots:
dependencies:
levn: 0.4.1
+ '@hookform/resolvers@3.9.1(react-hook-form@7.53.2(react@18.3.1))':
+ dependencies:
+ react-hook-form: 7.53.2(react@18.3.1)
+
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6':
@@ -9255,6 +9293,13 @@ snapshots:
fraction.js@4.3.7: {}
+ framer-motion@11.11.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ tslib: 2.8.1
+ optionalDependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
fresh@0.5.2: {}
fs-constants@1.0.0: {}
@@ -11116,6 +11161,10 @@ snapshots:
react: 18.3.1
scheduler: 0.23.2
+ react-hook-form@7.53.2(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+
react-icons@5.3.0(react@18.3.1):
dependencies:
react: 18.3.1
@@ -11562,8 +11611,6 @@ snapshots:
character-entities-html4: 2.1.0
character-entities-legacy: 3.0.0
- strip-ansi-cjs@8.0.0: {}
-
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1