6.2 최근/인기 글 리스트 및 무한스크롤 연동 o

This commit is contained in:
koreacomp5
2025-10-09 16:31:46 +09:00
parent b1557851ab
commit cdafc2908f
4 changed files with 78 additions and 5 deletions

View File

@@ -37,6 +37,7 @@ const listQuerySchema = z.object({
pageSize: z.coerce.number().min(1).max(100).default(10), pageSize: z.coerce.number().min(1).max(100).default(10),
boardId: z.string().optional(), boardId: z.string().optional(),
q: z.string().optional(), q: z.string().optional(),
sort: z.enum(["recent", "popular"]).default("recent").optional(),
}); });
export async function GET(req: Request) { export async function GET(req: Request) {
@@ -45,7 +46,7 @@ export async function GET(req: Request) {
if (!parsed.success) { if (!parsed.success) {
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); 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 = { const where = {
...(boardId ? { boardId } : {}), ...(boardId ? { boardId } : {}),
...(q ...(q
@@ -62,7 +63,10 @@ export async function GET(req: Request) {
prisma.post.count({ where }), prisma.post.count({ where }),
prisma.post.findMany({ prisma.post.findMany({
where, where,
orderBy: [{ isPinned: "desc" }, { createdAt: "desc" }], orderBy:
sort === "popular"
? [{ isPinned: "desc" }, { stat: { recommendCount: "desc" } }, { createdAt: "desc" }]
: [{ isPinned: "desc" }, { createdAt: "desc" }],
skip: (page - 1) * pageSize, skip: (page - 1) * pageSize,
take: pageSize, take: pageSize,
select: { select: {
@@ -72,11 +76,12 @@ export async function GET(req: Request) {
boardId: true, boardId: true,
isPinned: true, isPinned: true,
status: 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 });
} }

View File

@@ -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<Resp>(getKey, fetcher);
const items = data?.flatMap((d) => d.items) ?? [];
const canLoadMore = (data?.at(-1)?.items.length ?? 0) === pageSize;
return (
<div>
<div style={{ display: "flex", gap: 8, marginBottom: 8 }}>
<span>:</span>
<a href={`/?sort=recent`} style={{ textDecoration: sort === "recent" ? "underline" : "none" }}></a>
<a href={`/?sort=popular`} style={{ textDecoration: sort === "popular" ? "underline" : "none" }}></a>
</div>
<ul style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{items.map((p) => (
<li key={p.id} style={{ padding: 12, border: "1px solid #eee", borderRadius: 8 }}>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<strong>{p.title}</strong>
<span style={{ opacity: 0.7 }}>{new Date(p.createdAt).toLocaleString()}</span>
</div>
<div style={{ fontSize: 12, opacity: 0.8 }}>
{p.stat?.recommendCount ?? 0} · {p.stat?.views ?? 0} · {p.stat?.commentsCount ?? 0}
</div>
</li>
))}
</ul>
<div style={{ marginTop: 12 }}>
<button disabled={!canLoadMore || isLoading} onClick={() => setSize(size + 1)}>
{isLoading ? "로딩 중..." : canLoadMore ? "더 보기" : "끝"}
</button>
</div>
</div>
);
}

View File

@@ -1,12 +1,15 @@
import Image from "next/image"; import Image from "next/image";
import { QuickActions } from "@/app/components/QuickActions"; import { QuickActions } from "@/app/components/QuickActions";
import { HeroBanner } from "@/app/components/HeroBanner"; 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 ( return (
<div className=""> <div className="">
<HeroBanner /> <HeroBanner />
<QuickActions /> <QuickActions />
<PostList sort={sort} />
</div> </div>
); );
} }

View File

@@ -34,7 +34,7 @@
[메인 화면] [메인 화면]
6.1 Hero/공지 배너 컴포넌트(자동/수동 슬라이드) o 6.1 Hero/공지 배너 컴포넌트(자동/수동 슬라이드) o
6.2 최근/인기 글 리스트 및 무한스크롤 연동 6.2 최근/인기 글 리스트 및 무한스크롤 연동 o
6.3 권한 기반 빠른 액션 노출 제어 6.3 권한 기반 빠른 액션 노출 제어
6.4 검색 바 및 결과 페이지 라우팅 6.4 검색 바 및 결과 페이지 라우팅
6.5 개인화 위젯(최근 본 글/알림 요약) 6.5 개인화 위젯(최근 본 글/알림 요약)