From cfecb3d834754b91976446b7df3a2a930f9131dd Mon Sep 17 00:00:00 2001 From: ParkWonYeop Date: Sat, 27 Dec 2025 22:17:44 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/posts.ts | 9 +- src/app/category/[id]/page.tsx | 123 +++++++++++------ src/app/page.tsx | 214 ++++++++++++++++++----------- src/components/layout/Sidebar.tsx | 52 +++++-- src/components/post/PostSearch.tsx | 69 ++++++++++ 5 files changed, 327 insertions(+), 140 deletions(-) create mode 100644 src/components/post/PostSearch.tsx diff --git a/src/api/posts.ts b/src/api/posts.ts index 5fc2bd0..da783d2 100644 --- a/src/api/posts.ts +++ b/src/api/posts.ts @@ -8,7 +8,7 @@ export const getPosts = async (params?: { keyword?: string; category?: string; tag?: string; - sort?: string; // ๐Ÿ†• ์ •๋ ฌ ์˜ต์…˜ ์ถ”๊ฐ€ (์˜ˆ: 'viewCount,desc') + sort?: string; }) => { const response = await http.get>('/api/posts', { params: { @@ -20,12 +20,13 @@ export const getPosts = async (params?: { return response.data.data; }; -// 2. ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ -export const getPostsByCategory = async (categoryName: string, page = 0, size = 10) => { +// 2. ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ (๐Ÿ› ๏ธ keyword ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€) +export const getPostsByCategory = async (categoryName: string, page = 0, size = 10, keyword?: string) => { return getPosts({ page, size, - category: categoryName + category: categoryName, + keyword // ๐Ÿ†• ๊ฒ€์ƒ‰์–ด ์ „๋‹ฌ }); }; diff --git a/src/app/category/[id]/page.tsx b/src/app/category/[id]/page.tsx index 11cb109..5d09b59 100644 --- a/src/app/category/[id]/page.tsx +++ b/src/app/category/[id]/page.tsx @@ -2,11 +2,11 @@ import { use, useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { getPostsByCategory } from '@/api/posts'; +import { getPostsByCategory } from '@/api/posts'; // ๐Ÿ› ๏ธ ์ˆ˜์ •๋œ API ์‚ฌ์šฉ import PostCard from '@/components/post/PostCard'; -import PostListItem from '@/components/post/PostListItem'; -import { Loader2, ChevronLeft, ChevronRight, LayoutGrid, List } from 'lucide-react'; -import { notFound } from 'next/navigation'; +import PostListItem from '@/components/post/PostListItem'; +import PostSearch from '@/components/post/PostSearch'; // ๐Ÿ†• ๊ฒ€์ƒ‰ ์ปดํฌ๋„ŒํŠธ +import { Loader2, ChevronLeft, ChevronRight, LayoutGrid, List, Search as SearchIcon } from 'lucide-react'; import { clsx } from 'clsx'; export default function CategoryPage({ params }: { params: Promise<{ id: string }> }) { @@ -14,39 +14,45 @@ export default function CategoryPage({ params }: { params: Promise<{ id: string const categoryName = decodeURIComponent(id); const apiCategoryName = categoryName === 'uncategorized' ? '๋ฏธ๋ถ„๋ฅ˜' : categoryName; - // ๐Ÿ“ข ๊ณต์ง€ ์นดํ…Œ๊ณ ๋ฆฌ์ธ์ง€ ํŒ๋ณ„ const isNoticeCategory = apiCategoryName === '๊ณต์ง€' || apiCategoryName.toLowerCase() === 'notice'; const [page, setPage] = useState(0); - - // ๊ณต์ง€์‚ฌํ•ญ์ด๋ฉด ๊ธฐ๋ณธ๊ฐ’์„ 'list'๋กœ ์„ค์ • + const [keyword, setKeyword] = useState(''); // ๐Ÿ†• ๊ฒ€์ƒ‰์–ด ์ƒํƒœ const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const PAGE_SIZE = 10; useEffect(() => { - // ๐Ÿ“ข ๊ณต์ง€ ์นดํ…Œ๊ณ ๋ฆฌ๋Š” ๊ฐ•์ œ๋กœ ๋ฆฌ์ŠคํŠธ ๋ทฐ ์ ์šฉ if (isNoticeCategory) { setViewMode('list'); return; } - - // ๊ทธ ์™ธ์—๋Š” ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์„ค์ • ๋”ฐ๋ฆ„ const savedMode = localStorage.getItem('postViewMode') as 'grid' | 'list'; if (savedMode) setViewMode(savedMode); }, [isNoticeCategory]); const handleViewModeChange = (mode: 'grid' | 'list') => { setViewMode(mode); - // ๊ณต์ง€ ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ์•„๋‹ ๋•Œ๋งŒ ์‚ฌ์šฉ์ž ์„ค์ •์„ ์ €์žฅ (๊ณต์ง€๋Š” ๋ทฐ ๊ฐ•์ œ์ด๋ฏ€๋กœ ์ €์žฅ ์•ˆ ํ•จ) if (!isNoticeCategory) { localStorage.setItem('postViewMode', mode); } }; + // ๐Ÿ†• ๊ฒ€์ƒ‰์–ด๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ํŽ˜์ด์ง€๋ฅผ 0์œผ๋กœ ์ดˆ๊ธฐํ™” + const handleSearch = (newKeyword: string) => { + setKeyword(newKeyword); + setPage(0); + }; + const { data: postsData, isLoading, error, isPlaceholderData } = useQuery({ - queryKey: ['posts', 'category', apiCategoryName, page], - queryFn: () => getPostsByCategory(apiCategoryName, page, PAGE_SIZE), - placeholderData: (previousData) => previousData, + queryKey: ['posts', 'category', apiCategoryName, page, keyword], // ๐Ÿ”‘ ์ฟผ๋ฆฌ ํ‚ค์— keyword ์ถ”๊ฐ€ + queryFn: () => getPostsByCategory(apiCategoryName, page, PAGE_SIZE, keyword), // ๐Ÿ†• ๊ฒ€์ƒ‰์–ด ์ „๋‹ฌ + // ๐Ÿ› ๏ธ ์ˆ˜์ •๋จ: ๊ฒ€์ƒ‰์–ด๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์ด์ „ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ์ง€ ์•Š๊ณ  ๋กœ๋”ฉ ์ƒํƒœ๋กœ ์ „ํ™˜ + // (ํŽ˜์ด์ง€ ์ด๋™ ์‹œ์—๋Š” ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด ์œ ์ง€) + placeholderData: (previousData, previousQuery) => { + const prevKeyword = previousQuery?.queryKey[4]; + if (prevKeyword !== keyword) return undefined; + return previousData; + }, }); if (isLoading || (postsData === undefined && !error)) { @@ -68,49 +74,80 @@ export default function CategoryPage({ params }: { params: Promise<{ id: string const posts = postsData?.content || []; + // ๐Ÿ› ๏ธ ๋ฐฑ์—”๋“œ PagedModel ๊ตฌ์กฐ ๋Œ€์‘ (page ํ•„๋“œ ๋‚ด๋ถ€์— ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Œ) + // postsData๊ฐ€ any๋กœ ์บ์ŠคํŒ…๋˜์–ด ์•ˆ์ „ํ•˜๊ฒŒ ์ ‘๊ทผ + const pagingData = (postsData as any)?.page || postsData; + const totalElements = pagingData?.totalElements ?? 0; + const totalPages = pagingData?.totalPages ?? 0; + // page.number๊ฐ€ ์กด์žฌํ•˜๋ฉด ๊ณ„์‚ฐํ•ด์„œ isLast ํŒ๋‹จ, ์•„๋‹ˆ๋ฉด ๊ธฐ์กด last ํ•„๋“œ ์‚ฌ์šฉ + const isLast = pagingData?.number !== undefined + ? (pagingData.number + 1 >= pagingData.totalPages) + : (postsData?.last ?? true); + const handlePrevPage = () => setPage((old) => Math.max(old - 1, 0)); const handleNextPage = () => { - if (!postsData?.last) { + if (!isLast) { setPage((old) => old + 1); } }; return (
- {/* ํ—ค๋” ์˜์—ญ ์ˆ˜์ • */} -
-

+ {/* ํ—ค๋” ์˜์—ญ */} +
+

{apiCategoryName} ๊ธ€ ๋ชฉ๋ก

- {/* ๋ทฐ ๋ชจ๋“œ ๋ฒ„ํŠผ */} -
- - +
+ {/* ๐Ÿ” ์นดํ…Œ๊ณ ๋ฆฌ ๋‚ด ๊ฒ€์ƒ‰๋ฐ” */} + + + {/* ๋ทฐ ๋ชจ๋“œ ๋ฒ„ํŠผ */} +
+ + +
+ {/* ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์•ˆ๋‚ด (๊ฒ€์ƒ‰ ์ค‘์ผ ๋•Œ๋งŒ ํ‘œ์‹œ) */} + {keyword && ( +
+ + + "{keyword}" ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ: {totalElements}๊ฑด + +
+ )} + {posts.length === 0 ? (
-

์•„์ง ์ž‘์„ฑ๋œ ๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.

+

+ {keyword ? `"${keyword}"์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.` : '์•„์ง ์ž‘์„ฑ๋œ ๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.'} +

) : ( <> @@ -139,12 +176,12 @@ export default function CategoryPage({ params }: { params: Promise<{ id: string - Page {page + 1} {postsData && postsData.totalPages > 0 && `/ ${postsData.totalPages}`} + Page {page + 1} {totalPages > 0 && `/ ${totalPages}`} + )} +
+

+ ); +} \ No newline at end of file