2025-10-09 17:11:50 +09:00
|
|
|
import { PostList } from "@/app/components/PostList";
|
2025-10-24 21:24:51 +09:00
|
|
|
import { HeroBanner } from "@/app/components/HeroBanner";
|
2025-11-01 23:16:22 +09:00
|
|
|
import { BoardToolbar } from "@/app/components/BoardToolbar";
|
2025-10-10 14:39:22 +09:00
|
|
|
import { headers } from "next/headers";
|
2025-11-02 04:39:23 +09:00
|
|
|
import prisma from "@/lib/prisma";
|
2025-10-09 17:11:50 +09:00
|
|
|
|
2025-10-10 16:07:56 +09:00
|
|
|
// Next 15: params/searchParams가 Promise가 될 수 있어 안전 언랩 처리합니다.
|
2025-10-10 14:46:59 +09:00
|
|
|
export default async function BoardDetail({ params, searchParams }: { params: any; searchParams: any }) {
|
|
|
|
|
const p = params?.then ? await params : params;
|
|
|
|
|
const sp = searchParams?.then ? await searchParams : searchParams;
|
2025-11-02 04:39:28 +09:00
|
|
|
const idOrSlug = p.id as string;
|
2025-10-10 14:46:59 +09:00
|
|
|
const sort = (sp?.sort as "recent" | "popular" | undefined) ?? "recent";
|
2025-11-02 04:39:23 +09:00
|
|
|
const period = (sp?.period as string | undefined) ?? "monthly";
|
2025-10-09 18:14:22 +09:00
|
|
|
// 보드 slug 조회 (새 글 페이지 프리셋 전달)
|
2025-10-10 14:39:22 +09:00
|
|
|
const h = await headers();
|
|
|
|
|
const host = h.get("host") ?? "localhost:3000";
|
|
|
|
|
const proto = h.get("x-forwarded-proto") ?? "http";
|
|
|
|
|
const base = process.env.NEXT_PUBLIC_BASE_URL || `${proto}://${host}`;
|
|
|
|
|
const res = await fetch(new URL("/api/boards", base).toString(), { cache: "no-store" });
|
2025-10-09 18:14:22 +09:00
|
|
|
const { boards } = await res.json();
|
2025-11-02 04:39:28 +09:00
|
|
|
const board = (boards || []).find((b: any) => b.slug === idOrSlug || b.id === idOrSlug);
|
|
|
|
|
const id = board?.id as string;
|
2025-10-24 21:24:51 +09:00
|
|
|
const siblingBoards = (boards || []).filter((b: any) => b.category?.id && b.category.id === board?.category?.id);
|
|
|
|
|
const categoryName = board?.category?.name ?? "";
|
2025-11-02 04:39:23 +09:00
|
|
|
|
|
|
|
|
// 리스트 뷰 타입 확인 (특수랭킹일 경우 게시글 대신 랭킹 노출)
|
|
|
|
|
const boardView = await prisma.board.findUnique({
|
|
|
|
|
where: { id },
|
|
|
|
|
select: { listViewType: { select: { key: true } } },
|
|
|
|
|
});
|
|
|
|
|
const isSpecialRanking = boardView?.listViewType?.key === "list_special_rank";
|
|
|
|
|
|
|
|
|
|
let rankingItems: { userId: string; nickname: string; points: number }[] = [];
|
|
|
|
|
if (isSpecialRanking) {
|
|
|
|
|
const rankingUrl = new URL(`/api/ranking?period=${encodeURIComponent(period)}`, base).toString();
|
|
|
|
|
const rankingRes = await fetch(rankingUrl, { cache: "no-store" });
|
|
|
|
|
const rankingData = await rankingRes.json().catch(() => ({ items: [] }));
|
|
|
|
|
rankingItems = rankingData?.items ?? [];
|
|
|
|
|
}
|
2025-10-09 17:11:50 +09:00
|
|
|
return (
|
2025-10-24 21:24:51 +09:00
|
|
|
<div className="space-y-6">
|
2025-11-01 23:16:22 +09:00
|
|
|
{/* 상단 배너 (서브카테고리 표시) */}
|
2025-10-24 21:24:51 +09:00
|
|
|
<section>
|
2025-11-01 23:16:22 +09:00
|
|
|
<HeroBanner
|
2025-11-02 04:39:28 +09:00
|
|
|
subItems={siblingBoards.map((b: any) => ({ id: b.id, name: b.name, href: `/boards/${b.slug}` }))}
|
2025-11-01 23:16:22 +09:00
|
|
|
activeSubId={id}
|
|
|
|
|
/>
|
2025-10-24 21:24:51 +09:00
|
|
|
</section>
|
|
|
|
|
|
2025-11-01 23:16:22 +09:00
|
|
|
{/* 검색/필터 툴바 + 리스트 */}
|
|
|
|
|
<section>
|
2025-11-02 04:39:28 +09:00
|
|
|
<BoardToolbar boardId={board?.slug} />
|
2025-10-24 21:24:51 +09:00
|
|
|
<div className="p-0">
|
2025-11-02 04:39:23 +09:00
|
|
|
{isSpecialRanking ? (
|
|
|
|
|
<div className="rounded-xl border border-neutral-200 overflow-hidden">
|
|
|
|
|
<div className="px-4 py-3 border-b border-neutral-200 flex items-center justify-between bg-[#f6f4f4]">
|
|
|
|
|
<h2 className="text-sm text-neutral-700">포인트 랭킹 ({period})</h2>
|
|
|
|
|
<div className="text-xs text-neutral-500 flex gap-2">
|
|
|
|
|
<a href={`?period=daily`} className={`px-2 py-0.5 rounded ${period === "daily" ? "bg-neutral-900 text-white" : "bg-white border border-neutral-300"}`}>일간</a>
|
|
|
|
|
<a href={`?period=weekly`} className={`px-2 py-0.5 rounded ${period === "weekly" ? "bg-neutral-900 text-white" : "bg-white border border-neutral-300"}`}>주간</a>
|
|
|
|
|
<a href={`?period=monthly`} className={`px-2 py-0.5 rounded ${period === "monthly" ? "bg-neutral-900 text-white" : "bg-white border border-neutral-300"}`}>월간</a>
|
|
|
|
|
<a href={`?period=all`} className={`px-2 py-0.5 rounded ${period === "all" ? "bg-neutral-900 text-white" : "bg-white border border-neutral-300"}`}>전체</a>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<ol className="divide-y divide-neutral-200">
|
|
|
|
|
{rankingItems.map((i, idx) => (
|
|
|
|
|
<li key={i.userId} className="px-4 py-3 flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-3 min-w-0">
|
|
|
|
|
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-neutral-900 text-white text-xs">{idx + 1}</span>
|
|
|
|
|
<span className="truncate text-neutral-900 font-medium">{i.nickname || "회원"}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="shrink-0 text-sm text-neutral-700">{i.points}점</div>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
{rankingItems.length === 0 && (
|
|
|
|
|
<li className="px-4 py-10 text-center text-neutral-500">랭킹 데이터가 없습니다.</li>
|
|
|
|
|
)}
|
|
|
|
|
</ol>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<PostList
|
|
|
|
|
boardId={id}
|
|
|
|
|
sort={sort}
|
|
|
|
|
variant="board"
|
|
|
|
|
newPostHref={`/posts/new?boardId=${id}${board?.slug ? `&boardSlug=${board.slug}` : ""}`}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2025-10-24 21:24:51 +09:00
|
|
|
</div>
|
|
|
|
|
</section>
|
2025-10-09 17:11:50 +09:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|