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),
|
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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 개인화 위젯(최근 본 글/알림 요약)
|
||||||
|
|||||||
Reference in New Issue
Block a user