From 3364d50d48bb208cb43abcbfd62f7df73b5730c0 Mon Sep 17 00:00:00 2001 From: heejin Date: Sat, 2 Nov 2024 00:38:40 +0900 Subject: [PATCH 01/11] refactor: add import type --- pages/boards/components/BestBoardCard.tsx | 2 +- pages/boards/components/BestBoards.tsx | 2 +- pages/boards/index.tsx | 2 +- src/apis/boardsApi.ts | 6 +++++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pages/boards/components/BestBoardCard.tsx b/pages/boards/components/BestBoardCard.tsx index 555d7496..6e20e8d9 100644 --- a/pages/boards/components/BestBoardCard.tsx +++ b/pages/boards/components/BestBoardCard.tsx @@ -2,7 +2,7 @@ import Image from 'next/image'; import styles from './BestBoardCard.module.css'; import medalSvg from '@/src/assets/ic_medal.svg'; import heardSvg from '@/src/assets/ic_heart.svg'; -import { Board } from '@/src/apis/boardTypes'; +import type { Board } from '@/src/apis/boardTypes'; interface BestBoardCardProps extends Pick< diff --git a/pages/boards/components/BestBoards.tsx b/pages/boards/components/BestBoards.tsx index 5a81735e..3f3833af 100644 --- a/pages/boards/components/BestBoards.tsx +++ b/pages/boards/components/BestBoards.tsx @@ -1,4 +1,4 @@ -import { Board } from '@/src/apis/boardTypes'; +import type { Board } from '@/src/apis/boardTypes'; import BestBoardCard from './BestBoardCard'; import styles from './BestBoards.module.css'; diff --git a/pages/boards/index.tsx b/pages/boards/index.tsx index 5e584f2c..d337186c 100644 --- a/pages/boards/index.tsx +++ b/pages/boards/index.tsx @@ -1,6 +1,6 @@ import { GetStaticProps } from 'next'; import BestBoards from './components/BestBoards'; -import { Board } from '@/src/apis/boardTypes'; +import type { Board } from '@/src/apis/boardTypes'; import { getBoards } from '@/src/apis/boardsApi'; import Boards from './components/Boards'; diff --git a/src/apis/boardsApi.ts b/src/apis/boardsApi.ts index d0b14090..3784eb64 100644 --- a/src/apis/boardsApi.ts +++ b/src/apis/boardsApi.ts @@ -1,4 +1,8 @@ -import { Board, GetBoardsResponse, GetBoardsRequestParams } from './boardTypes'; +import type { + Board, + GetBoardsResponse, + GetBoardsRequestParams, +} from './boardTypes'; const BASE_URL = 'https://panda-market-api.vercel.app'; From 657ccd650b7798b5f3455ac0bba46d42f7f955b3 Mon Sep 17 00:00:00 2001 From: heejin Date: Sat, 2 Nov 2024 00:45:50 +0900 Subject: [PATCH 02/11] refactor: code review --- src/components/Button.tsx | 3 +-- src/components/GlobalLayout.module.css | 19 +++++++++++++++++++ src/components/GlobalLayout.tsx | 2 +- src/components/Header.module.css | 4 ++++ src/components/Header.tsx | 19 ++++++++++++++++--- styles/globals.css | 19 ------------------- 6 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 56cd4dd6..682bfaca 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -3,12 +3,11 @@ import styles from './Button.module.css'; interface PrimaryButtonProps extends ButtonHTMLAttributes { children: React.ReactNode; - className?: string; } export default function Button({ children, - className, + className = '', ...props }: PrimaryButtonProps) { return ( diff --git a/src/components/GlobalLayout.module.css b/src/components/GlobalLayout.module.css index 2ccbc164..5e2bfc9a 100644 --- a/src/components/GlobalLayout.module.css +++ b/src/components/GlobalLayout.module.css @@ -2,3 +2,22 @@ margin-top: 75px; width: 100%; } + +.maxContainer { + max-width: var(--size-max-width); + margin: auto; +} + +/* tablet */ +@media screen and (max-width: 1199px) { + .maxContainer { + max-width: 760px; + } +} + +/* mobile */ +@media screen and (max-width: 767px) { + .maxContainer { + max-width: 360px; + } +} diff --git a/src/components/GlobalLayout.tsx b/src/components/GlobalLayout.tsx index f4ce9201..6af4010e 100644 --- a/src/components/GlobalLayout.tsx +++ b/src/components/GlobalLayout.tsx @@ -7,7 +7,7 @@ export default function GlobalLayout({ children }: { children: ReactNode }) { <>
-
{children}
+
{children}
); diff --git a/src/components/Header.module.css b/src/components/Header.module.css index 49aa3e69..8bd7f1d8 100644 --- a/src/components/Header.module.css +++ b/src/components/Header.module.css @@ -6,6 +6,10 @@ border-bottom: 1px solid #dfdfdf; } +.active { + color: var(--Primary-100); +} + .headerContainer { padding: 10px 0; display: flex; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 1f5362ba..ccd1e14b 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -2,19 +2,32 @@ import Link from 'next/link'; import styles from './Header.module.css'; import Image from 'next/image'; import avatarSvg from '@/src/assets/avatar.svg'; +import { usePathname } from 'next/navigation'; export default function Header() { + const pathname = usePathname(); + return (
-
+
avatar diff --git a/styles/globals.css b/styles/globals.css index 14b366ae..4e36ccef 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -29,22 +29,3 @@ header { footer { background-color: var(--Secondary-900); } - -.max-container { - max-width: var(--size-max-width); - margin: auto; -} - -/* tablet */ -@media screen and (max-width: 1199px) { - .max-container { - max-width: 760px; - } -} - -/* mobile */ -@media screen and (max-width: 767px) { - .max-container { - max-width: 360px; - } -} From 31fc75989b4c9e8e3f7336abda0a4966aeedb571 Mon Sep 17 00:00:00 2001 From: heejin Date: Sat, 2 Nov 2024 13:11:28 +0900 Subject: [PATCH 03/11] feat: add select box on board filter --- pages/boards/[id].tsx | 9 +++++ pages/boards/components/Boards.module.css | 19 +++++++++++ pages/boards/components/Boards.tsx | 6 ++++ pages/boards/index.tsx | 2 +- src/assets/ic_arrow_down.svg | 5 +++ src/hooks/useApi.tsx | 41 +++++++++++++++++++++++ 6 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 pages/boards/[id].tsx create mode 100644 src/assets/ic_arrow_down.svg create mode 100644 src/hooks/useApi.tsx diff --git a/pages/boards/[id].tsx b/pages/boards/[id].tsx new file mode 100644 index 00000000..908547fa --- /dev/null +++ b/pages/boards/[id].tsx @@ -0,0 +1,9 @@ +import { useRouter } from 'next/router'; +import styles from './[id].module.css'; + +export default function BoardDetail() { + const router = useRouter(); + const { id } = router.query; + + return
{id}
; +} diff --git a/pages/boards/components/Boards.module.css b/pages/boards/components/Boards.module.css index fdc84945..2a734619 100644 --- a/pages/boards/components/Boards.module.css +++ b/pages/boards/components/Boards.module.css @@ -11,10 +11,16 @@ font-weight: 700; } +.filter { + display: flex; + gap: 20px; +} + .searchBar { padding: 9px 20px 9px 16px; border-radius: 12px; background: var(--Secondary-100, #f3f4f6); + flex: 1; display: flex; align-items: center; @@ -31,6 +37,19 @@ color: var(--Secondary-400, #9ca3af); } +.options { + height: 100%; + padding: 12px 20px; + padding-right: 50px; + + border-radius: 12px; + border: 1px solid var(--Secondary-200, #e5e7eb); + + appearance: none; + background: url('../../../src/assets/ic_arrow_down.svg') no-repeat right 10px + center; +} + /* tablet */ @media screen and (max-width: 1199px) { .boardsHeader { diff --git a/pages/boards/components/Boards.tsx b/pages/boards/components/Boards.tsx index 1d4d8495..0f7e4c7b 100644 --- a/pages/boards/components/Boards.tsx +++ b/pages/boards/components/Boards.tsx @@ -20,6 +20,12 @@ export default function Boards() { placeholder="검색할 상품을 입력해주세요" />
+
+ +
); diff --git a/pages/boards/index.tsx b/pages/boards/index.tsx index d337186c..83a248b7 100644 --- a/pages/boards/index.tsx +++ b/pages/boards/index.tsx @@ -1,6 +1,6 @@ import { GetStaticProps } from 'next'; import BestBoards from './components/BestBoards'; -import type { Board } from '@/src/apis/boardTypes'; +import type { Board, GetBoardsRequestParams } from '@/src/apis/boardTypes'; import { getBoards } from '@/src/apis/boardsApi'; import Boards from './components/Boards'; diff --git a/src/assets/ic_arrow_down.svg b/src/assets/ic_arrow_down.svg new file mode 100644 index 00000000..ad19ce55 --- /dev/null +++ b/src/assets/ic_arrow_down.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/hooks/useApi.tsx b/src/hooks/useApi.tsx new file mode 100644 index 00000000..0c6f3d2f --- /dev/null +++ b/src/hooks/useApi.tsx @@ -0,0 +1,41 @@ +import { useEffect, useState, useCallback } from 'react'; + +type ApiResponse = { + data: T | null; + isLoading: boolean; + error: string | null; + makeRequest: (params: P) => Promise; +}; + +export function useApi( + fetchFunction: (params: P) => Promise, + params: P +): ApiResponse { + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const wrappedFunction = useCallback( + async (params: P) => { + setIsLoading(true); + setError(null); + + try { + const result = await fetchFunction(params); + setData(result); + } catch (err) { + console.error(err); + setError('데이터 로딩 실패'); + } finally { + setIsLoading(false); + } + }, + [fetchFunction] + ); + + useEffect(() => { + wrappedFunction(params); + }, [params, wrappedFunction]); + + return { data, isLoading, error, makeRequest: wrappedFunction }; +} From 71f6017aee787fd030e2c524982d311382564335 Mon Sep 17 00:00:00 2001 From: heejin Date: Sat, 2 Nov 2024 17:35:09 +0900 Subject: [PATCH 04/11] feat: add board card css --- pages/boards/components/BestBoardCard.tsx | 11 ++- pages/boards/components/BoardCard.module.css | 74 ++++++++++++++++++++ pages/boards/components/BoardCard.tsx | 44 ++++++++++++ pages/boards/components/Boards.module.css | 8 +++ pages/boards/components/Boards.tsx | 56 +++++++++++++-- pages/boards/index.tsx | 4 +- src/apis/boardTypes.ts | 2 +- src/apis/boardsApi.ts | 6 +- src/hooks/useApi.tsx | 2 +- 9 files changed, 190 insertions(+), 17 deletions(-) create mode 100644 pages/boards/components/BoardCard.module.css create mode 100644 pages/boards/components/BoardCard.tsx diff --git a/pages/boards/components/BestBoardCard.tsx b/pages/boards/components/BestBoardCard.tsx index 6e20e8d9..1253117e 100644 --- a/pages/boards/components/BestBoardCard.tsx +++ b/pages/boards/components/BestBoardCard.tsx @@ -1,7 +1,7 @@ import Image from 'next/image'; import styles from './BestBoardCard.module.css'; import medalSvg from '@/src/assets/ic_medal.svg'; -import heardSvg from '@/src/assets/ic_heart.svg'; +import heartSvg from '@/src/assets/ic_heart.svg'; import type { Board } from '@/src/apis/boardTypes'; interface BestBoardCardProps @@ -26,13 +26,18 @@ export default function BestBoardCard({

{title}

- medal + 게시판 첨부이미지
{writer.nickname}
- heardIcon + heartIcon {likeCount}
{new Date(createdAt).toLocaleDateString()} diff --git a/pages/boards/components/BoardCard.module.css b/pages/boards/components/BoardCard.module.css new file mode 100644 index 00000000..dbb8235e --- /dev/null +++ b/pages/boards/components/BoardCard.module.css @@ -0,0 +1,74 @@ +.boardCard { + background: #fcfcfc; + padding: 20px; + position: relative; + display: flex; + flex-direction: column; + gap: 16px; +} + +.boardCard::after { + content: ''; + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: 1px; + background-color: var(--Secondary-200); +} + +.contentContainer { + display: flex; + gap: 8px; + min-height: 72px; +} + +.title { + color: var(--Secondary-800); + font-size: 20px; + font-weight: 600; + + flex: 1; +} + +.imageWrapper { + position: relative; + width: 72px; + height: 72px; + + border-radius: 6px; + border: 1px solid var(--Secondary-200, #e5e7eb); + background: #fff; +} + +.additionalInfo { + display: flex; + justify-content: space-between; +} + +.infoWrapper { + display: flex; + align-items: center; + gap: 8px; +} + +.nickname { + color: var(--Secondary-600, #4b5563); + font-size: 14px; + font-weight: 400; +} + +.date { + color: var(--Secondary-400, #9ca3af); + font-size: 14px; + font-weight: 400; +} + +.likeCountWrapper { + display: flex; + align-items: center; + gap: 8px; + color: var(--Secondary-500, #6b7280); + font-size: 16px; + font-weight: 400; +} diff --git a/pages/boards/components/BoardCard.tsx b/pages/boards/components/BoardCard.tsx new file mode 100644 index 00000000..9ff1f2d9 --- /dev/null +++ b/pages/boards/components/BoardCard.tsx @@ -0,0 +1,44 @@ +import Image from 'next/image'; +import styles from './BoardCard.module.css'; +import type { Board } from '@/src/apis/boardTypes'; +import heartSvg from '@/src/assets/ic_heart.svg'; +import avatarSvg from '@/src/assets/avatar.svg'; + +export default function BoardCard({ + title, + image, + writer, + createdAt, + likeCount, +}: Board) { + return ( +
+
+

{title}

+ {image && ( +
+ 게시판 첨부이미지 +
+ )} +
+
+
+ avatar + {writer.nickname} + + {new Date(createdAt).toLocaleDateString()} + +
+
+ heardIcon + {likeCount} +
+
+
+ ); +} diff --git a/pages/boards/components/Boards.module.css b/pages/boards/components/Boards.module.css index 2a734619..18733bfe 100644 --- a/pages/boards/components/Boards.module.css +++ b/pages/boards/components/Boards.module.css @@ -50,6 +50,14 @@ center; } +.boardsContainer { + margin-top: 24px; + display: flex; + flex-direction: column; + justify-content: center; + gap: 24px; +} + /* tablet */ @media screen and (max-width: 1199px) { .boardsHeader { diff --git a/pages/boards/components/Boards.tsx b/pages/boards/components/Boards.tsx index 0f7e4c7b..98877b22 100644 --- a/pages/boards/components/Boards.tsx +++ b/pages/boards/components/Boards.tsx @@ -2,17 +2,50 @@ import Image from 'next/image'; import Button from '@/src/components/Button'; import styles from './Boards.module.css'; import searchSvg from '@/src/assets/ic_search.svg'; +import { getBoards } from '@/src/apis/boardsApi'; +import type { + Board, + GetBoardsResponse, + GetBoardsRequestParams, +} from '@/src/apis/boardTypes'; +import { useApi } from '@/src/hooks/useApi'; +import { useEffect, useState } from 'react'; +import BoardCard from './BoardCard'; + +const PAGE_SIZE = 10; export default function Boards() { + const [orderBy, setOrderBy] = useState('recent'); + + const requestParams: GetBoardsRequestParams = { + page: 1, + pageSize: PAGE_SIZE, + orderBy: orderBy, + }; + + const { data, isLoading, error, makeRequest } = useApi< + GetBoardsResponse, + GetBoardsRequestParams + >(getBoards, requestParams); + + const handelOptionChange = (e: React.ChangeEvent) => { + const newOrder = e.target.value; + setOrderBy(newOrder); + makeRequest({ ...requestParams, orderBy: newOrder }); + }; + + const boards = data?.list || []; + console.log(boards); + return (
-
+

게시글

-
-
+ +
searchIcon
-
-
+ +
+ {isLoading ? ( +
Loading
+ ) : error ? ( +
{error}
+ ) : ( + boards.map((board) => ) + )} +
); } diff --git a/pages/boards/index.tsx b/pages/boards/index.tsx index 83a248b7..599e6e54 100644 --- a/pages/boards/index.tsx +++ b/pages/boards/index.tsx @@ -1,6 +1,6 @@ import { GetStaticProps } from 'next'; import BestBoards from './components/BestBoards'; -import type { Board, GetBoardsRequestParams } from '@/src/apis/boardTypes'; +import type { Board } from '@/src/apis/boardTypes'; import { getBoards } from '@/src/apis/boardsApi'; import Boards from './components/Boards'; @@ -28,6 +28,6 @@ export const getStaticProps: GetStaticProps = async () => { props: { boards: list || [], }, - revalidate: 600, // Re-generate the page every 600 seconds (ISR) + revalidate: 600, }; }; diff --git a/src/apis/boardTypes.ts b/src/apis/boardTypes.ts index e44f3fdf..babe3662 100644 --- a/src/apis/boardTypes.ts +++ b/src/apis/boardTypes.ts @@ -22,6 +22,6 @@ export interface GetBoardsResponse { export interface GetBoardsRequestParams { page?: number; pageSize?: number; - orderBy?: 'recent' | 'like'; + orderBy?: string; keyword?: string; } diff --git a/src/apis/boardsApi.ts b/src/apis/boardsApi.ts index 3784eb64..65cbfb9b 100644 --- a/src/apis/boardsApi.ts +++ b/src/apis/boardsApi.ts @@ -1,8 +1,4 @@ -import type { - Board, - GetBoardsResponse, - GetBoardsRequestParams, -} from './boardTypes'; +import type { GetBoardsResponse, GetBoardsRequestParams } from './boardTypes'; const BASE_URL = 'https://panda-market-api.vercel.app'; diff --git a/src/hooks/useApi.tsx b/src/hooks/useApi.tsx index 0c6f3d2f..e683bc07 100644 --- a/src/hooks/useApi.tsx +++ b/src/hooks/useApi.tsx @@ -35,7 +35,7 @@ export function useApi( useEffect(() => { wrappedFunction(params); - }, [params, wrappedFunction]); + }, [JSON.stringify(params), wrappedFunction]); return { data, isLoading, error, makeRequest: wrappedFunction }; } From 66547ee10502e44446c1818d27ea5fd0566b87ab Mon Sep 17 00:00:00 2001 From: heejin Date: Sat, 2 Nov 2024 17:58:58 +0900 Subject: [PATCH 05/11] feat: add infinite scroll --- pages/boards/components/Boards.tsx | 77 ++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/pages/boards/components/Boards.tsx b/pages/boards/components/Boards.tsx index 98877b22..e0234abd 100644 --- a/pages/boards/components/Boards.tsx +++ b/pages/boards/components/Boards.tsx @@ -9,34 +9,68 @@ import type { GetBoardsRequestParams, } from '@/src/apis/boardTypes'; import { useApi } from '@/src/hooks/useApi'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import BoardCard from './BoardCard'; +import Link from 'next/link'; const PAGE_SIZE = 10; export default function Boards() { const [orderBy, setOrderBy] = useState('recent'); - - const requestParams: GetBoardsRequestParams = { - page: 1, - pageSize: PAGE_SIZE, - orderBy: orderBy, - }; + const [page, setPage] = useState(1); + const [boards, setBoards] = useState([]); + const [isFetching, setIsFetching] = useState(false); + const [hasMore, setHasMore] = useState(true); + const observerRef = useRef(null); const { data, isLoading, error, makeRequest } = useApi< GetBoardsResponse, GetBoardsRequestParams - >(getBoards, requestParams); + >(getBoards, { + page, + pageSize: PAGE_SIZE, + orderBy, + }); + + useEffect(() => { + if (data) { + setBoards((prevBoards) => [...prevBoards, ...data.list]); + setIsFetching(false); - const handelOptionChange = (e: React.ChangeEvent) => { + if (data.list.length < PAGE_SIZE) { + setHasMore(false); + } + } + }, [data]); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && !isFetching && hasMore) { + setIsFetching(true); + setPage((prevPage) => prevPage + 1); + makeRequest({ page: page + 1, pageSize: PAGE_SIZE, orderBy }); + } + }, + { threshold: 0.5 } + ); + + if (observerRef.current) observer.observe(observerRef.current); + + return () => { + if (observerRef.current) observer.unobserve(observerRef.current); + }; + }, [isFetching, page, orderBy, hasMore]); + + const handleOptionChange = (e: React.ChangeEvent) => { const newOrder = e.target.value; setOrderBy(newOrder); - makeRequest({ ...requestParams, orderBy: newOrder }); + setHasMore(true); + setBoards([]); + setPage(1); + makeRequest({ page: 1, pageSize: PAGE_SIZE, orderBy: newOrder }); }; - const boards = data?.list || []; - console.log(boards); - return (
@@ -57,7 +91,7 @@ export default function Boards() {