6.2 최근/인기 글 리스트 및 무한스크롤 연동 o
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
|
||||
65
src/app/components/PostList.tsx
Normal file
65
src/app/components/PostList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="">
|
||||
<HeroBanner />
|
||||
<QuickActions />
|
||||
<PostList sort={sort} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user