146 lines
7.6 KiB
TypeScript
146 lines
7.6 KiB
TypeScript
import { PostList } from "@/app/components/PostList";
|
|
import { HeroBanner } from "@/app/components/HeroBanner";
|
|
import Link from "next/link";
|
|
import { BoardToolbar } from "@/app/components/BoardToolbar";
|
|
import { headers } from "next/headers";
|
|
import prisma from "@/lib/prisma";
|
|
import { UserAvatar } from "@/app/components/UserAvatar";
|
|
import { RankIcon1st } from "@/app/components/RankIcon1st";
|
|
import { RankIcon2nd } from "@/app/components/RankIcon2nd";
|
|
import { RankIcon3rd } from "@/app/components/RankIcon3rd";
|
|
import { GradeIcon } from "@/app/components/GradeIcon";
|
|
|
|
// Next 15: params/searchParams가 Promise가 될 수 있어 안전 언랩 처리합니다.
|
|
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;
|
|
const idOrSlug = p.id as string;
|
|
const sort = (sp?.sort as "recent" | "popular" | undefined) ?? "recent";
|
|
// 보드 slug 조회 (새 글 페이지 프리셋 전달)
|
|
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" });
|
|
const { boards } = await res.json();
|
|
const board = (boards || []).find((b: any) => b.slug === idOrSlug || b.id === idOrSlug);
|
|
const id = board?.id as string;
|
|
const siblingBoards = (boards || []).filter((b: any) => b.category?.id && b.category.id === board?.category?.id);
|
|
const categoryName = board?.category?.name ?? "";
|
|
// 메인배너 표시 설정
|
|
const SETTINGS_KEY = "mainpage_settings" as const;
|
|
const settingRow = await prisma.setting.findUnique({ where: { key: SETTINGS_KEY } });
|
|
const parsed = settingRow ? JSON.parse(settingRow.value as string) : {};
|
|
const showBanner: boolean = parsed.showBanner ?? true;
|
|
|
|
// 리스트 뷰 타입 확인 (특수랭킹일 경우 게시글 대신 랭킹 노출)
|
|
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; profileImage: string | null; grade: number }[] = [];
|
|
if (isSpecialRanking) {
|
|
const topUsers = await prisma.user.findMany({
|
|
select: { userId: true, nickname: true, points: true, profileImage: true, grade: true },
|
|
where: { status: "active" },
|
|
orderBy: { points: "desc" },
|
|
take: 100,
|
|
});
|
|
rankingItems = topUsers.map((u) => ({ userId: u.userId, nickname: u.nickname, points: u.points, profileImage: u.profileImage, grade: u.grade }));
|
|
}
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* 상단 배너 (서브카테고리 표시) */}
|
|
{showBanner ? (
|
|
<section>
|
|
<HeroBanner
|
|
subItems={siblingBoards.map((b: any) => ({ id: b.id, name: b.name, href: `/boards/${b.slug}` }))}
|
|
activeSubId={id}
|
|
/>
|
|
</section>
|
|
) : (
|
|
<section>
|
|
<div className="h-[58px] bg-black rounded-xl flex items-center justify-center px-2">
|
|
<div className="flex flex-wrap items-center gap-[8px]">
|
|
{siblingBoards.map((b: any) => (
|
|
<Link
|
|
key={b.id}
|
|
href={`/boards/${b.slug}`}
|
|
className={
|
|
b.id === id
|
|
? "px-3 h-[28px] rounded-full bg-[#F94B37] text-white text-[12px] leading-[28px] whitespace-nowrap"
|
|
: "px-3 h-[28px] rounded-full bg-transparent text-white/85 hover:text-white border border-white/30 text-[12px] leading-[28px] whitespace-nowrap"
|
|
}
|
|
>
|
|
{b.name}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* 검색/필터 툴바 + 리스트 */}
|
|
<section>
|
|
{!isSpecialRanking && <BoardToolbar boardId={board?.slug} />}
|
|
<div className="p-0">
|
|
{isSpecialRanking ? (
|
|
<div className="w-full">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-[32px]">
|
|
{rankingItems.map((i, idx) => {
|
|
const rank = idx + 1;
|
|
return (
|
|
<div key={i.userId} className="border-t border-[#d5d5d5]">
|
|
<div className="flex gap-[16px] items-center p-[16px]">
|
|
<div className="flex items-center gap-[8px] shrink-0">
|
|
{(rank === 1 || rank === 2 || rank === 3) && (
|
|
<div className="relative w-[20px] h-[20px] shrink-0">
|
|
{rank === 1 && <RankIcon1st />}
|
|
{rank === 2 && <RankIcon2nd />}
|
|
{rank === 3 && <RankIcon3rd />}
|
|
</div>
|
|
)}
|
|
<div className="bg-white border border-[#d5d5d5] px-[16px] py-[5px] rounded-[12px] text-[14px] text-[#5c5c5c] shrink-0">
|
|
{rank}위
|
|
</div>
|
|
<div className="flex items-center gap-[10px] shrink-0 pl-0 pr-[15px] py-0">
|
|
<UserAvatar src={i.profileImage} alt={i.nickname || "프로필"} width={36} height={36} className="rounded-full" />
|
|
<span className="text-[16px] text-[#5c5c5c] leading-[16px] tracking-[-0.28px] whitespace-nowrap">
|
|
{i.nickname || "회원"}
|
|
</span>
|
|
<GradeIcon grade={i.grade} width={20} height={20} />
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-[13px] shrink-0 ml-auto">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none" className="shrink-0">
|
|
<path d="M8.17377 0.0481988C8.1834 0.0326271 8.19596 0.0201252 8.21037 0.0117499C8.22478 0.00337451 8.24062 -0.000628204 8.25656 7.99922e-05C8.2725 0.000788189 8.28806 0.0061865 8.30194 0.0158187C8.31582 0.0254509 8.3276 0.0390338 8.33629 0.0554196L11.3323 5.55973C11.3575 5.60512 11.4038 5.62472 11.4468 5.60718L15.8683 3.80198C15.9441 3.77104 16.0174 3.85666 15.9963 3.95053L14.2584 16H1.74226L0.00344328 3.95156C-0.00125237 3.93019 -0.0011435 3.90765 0.00375838 3.88635C0.00866025 3.86505 0.0181727 3.84576 0.0312889 3.83054C0.0444051 3.81532 0.0606369 3.80472 0.0782661 3.79988C0.0958953 3.79503 0.114266 3.79612 0.131434 3.80302L4.55889 5.61131C4.59846 5.62678 4.64309 5.61131 4.66835 5.57005L8.17461 0.0481988H8.17377Z" fill="#FFB625"/>
|
|
</svg>
|
|
<span className="text-[16px] font-semibold text-[#5c5c5c] leading-[22px]">{i.points.toLocaleString()}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
{rankingItems.length === 0 && (
|
|
<div className="px-4 py-10 text-center text-neutral-500">랭킹 데이터가 없습니다.</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<PostList
|
|
boardId={id}
|
|
sort={sort}
|
|
variant="board"
|
|
newPostHref={`/posts/new?boardId=${id}${board?.slug ? `&boardSlug=${board.slug}` : ""}`}
|
|
/>
|
|
)}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|