From cdafc2908f25b21b716b3d5ff88db3e103e07e5a Mon Sep 17 00:00:00 2001 From: koreacomp5 Date: Thu, 9 Oct 2025 16:31:46 +0900 Subject: [PATCH] =?UTF-8?q?6.2=20=EC=B5=9C=EA=B7=BC/=EC=9D=B8=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=80=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=B0=8F=20=EB=AC=B4?= =?UTF-8?q?=ED=95=9C=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=97=B0=EB=8F=99=20o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/posts/route.ts | 11 ++++-- src/app/components/PostList.tsx | 65 +++++++++++++++++++++++++++++++++ src/app/page.tsx | 5 ++- todolist.txt | 2 +- 4 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 src/app/components/PostList.tsx diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts index c481c6b..4c21d6f 100644 --- a/src/app/api/posts/route.ts +++ b/src/app/api/posts/route.ts @@ -37,6 +37,7 @@ const listQuerySchema = z.object({ pageSize: z.coerce.number().min(1).max(100).default(10), boardId: z.string().optional(), q: z.string().optional(), + sort: z.enum(["recent", "popular"]).default("recent").optional(), }); export async function GET(req: Request) { @@ -45,7 +46,7 @@ export async function GET(req: Request) { if (!parsed.success) { return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); } - const { page, pageSize, boardId, q } = parsed.data; + const { page, pageSize, boardId, q, sort = "recent" } = parsed.data; const where = { ...(boardId ? { boardId } : {}), ...(q @@ -62,7 +63,10 @@ export async function GET(req: Request) { prisma.post.count({ where }), prisma.post.findMany({ where, - orderBy: [{ isPinned: "desc" }, { createdAt: "desc" }], + orderBy: + sort === "popular" + ? [{ isPinned: "desc" }, { stat: { recommendCount: "desc" } }, { createdAt: "desc" }] + : [{ isPinned: "desc" }, { createdAt: "desc" }], skip: (page - 1) * pageSize, take: pageSize, select: { @@ -72,11 +76,12 @@ export async function GET(req: Request) { boardId: true, isPinned: true, status: true, + stat: { select: { recommendCount: true, views: true, commentsCount: true } }, }, }), ]); - return NextResponse.json({ total, page, pageSize, items }); + return NextResponse.json({ total, page, pageSize, items, sort }); } diff --git a/src/app/components/PostList.tsx b/src/app/components/PostList.tsx new file mode 100644 index 0000000..673d022 --- /dev/null +++ b/src/app/components/PostList.tsx @@ -0,0 +1,65 @@ +"use client"; +import useSWRInfinite from "swr/infinite"; + +type Item = { + id: string; + title: string; + createdAt: string; + isPinned: boolean; + status: string; + stat?: { recommendCount: number; views: number; commentsCount: number } | null; +}; + +type Resp = { + total: number; + page: number; + pageSize: number; + items: Item[]; + sort: "recent" | "popular"; +}; + +const fetcher = (url: string) => fetch(url).then((r) => r.json()); + +export function PostList({ boardId, sort = "recent" }: { boardId?: string; sort?: "recent" | "popular" }) { + const pageSize = 10; + const getKey = (index: number, prev: Resp | null) => { + if (prev && prev.items.length === 0) return null; + const page = index + 1; + const sp = new URLSearchParams({ page: String(page), pageSize: String(pageSize), sort }); + if (boardId) sp.set("boardId", boardId); + return `/api/posts?${sp.toString()}`; + }; + const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher); + const items = data?.flatMap((d) => d.items) ?? []; + const canLoadMore = (data?.at(-1)?.items.length ?? 0) === pageSize; + + return ( +
+
+ 정렬: + 최신 + 인기 +
+
    + {items.map((p) => ( +
  • +
    + {p.title} + {new Date(p.createdAt).toLocaleString()} +
    +
    + 추천 {p.stat?.recommendCount ?? 0} · 조회 {p.stat?.views ?? 0} · 댓글 {p.stat?.commentsCount ?? 0} +
    +
  • + ))} +
+
+ +
+
+ ); +} + + diff --git a/src/app/page.tsx b/src/app/page.tsx index 87bd0de..415ef83 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,12 +1,15 @@ import Image from "next/image"; import { QuickActions } from "@/app/components/QuickActions"; import { HeroBanner } from "@/app/components/HeroBanner"; +import { PostList } from "@/app/components/PostList"; -export default function Home() { +export default function Home({ searchParams }: { searchParams?: { sort?: "recent" | "popular" } }) { + const sort = searchParams?.sort ?? "recent"; return (
+
); } diff --git a/todolist.txt b/todolist.txt index 0feaded..82198e8 100644 --- a/todolist.txt +++ b/todolist.txt @@ -34,7 +34,7 @@ [메인 화면] 6.1 Hero/공지 배너 컴포넌트(자동/수동 슬라이드) o -6.2 최근/인기 글 리스트 및 무한스크롤 연동 +6.2 최근/인기 글 리스트 및 무한스크롤 연동 o 6.3 권한 기반 빠른 액션 노출 제어 6.4 검색 바 및 결과 페이지 라우팅 6.5 개인화 위젯(최근 본 글/알림 요약)