Files
msgapp/src/app/boards/[id]/page.tsx

147 lines
7.7 KiB
TypeScript
Raw Normal View History

import { PostList } from "@/app/components/PostList";
2025-10-24 21:24:51 +09:00
import { HeroBanner } from "@/app/components/HeroBanner";
2025-11-02 15:13:03 +09:00
import Link from "next/link";
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-11-02 13:32:19 +09:00
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;
2025-11-02 04:39:28 +09:00
const idOrSlug = p.id as string;
2025-11-07 23:41:52 +09:00
const sort = (sp?.sort as "recent" | "popular" | "views" | "likes" | "comments" | undefined) ?? "recent";
// 보드 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" });
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 15:13:03 +09:00
// 메인배너 표시 설정
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;
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";
2025-11-02 13:32:19 +09:00
let rankingItems: { userId: string; nickname: string; points: number; profileImage: string | null; grade: number }[] = [];
2025-11-02 04:39:23 +09:00
if (isSpecialRanking) {
2025-11-02 04:59:09 +09:00
const topUsers = await prisma.user.findMany({
2025-11-02 13:32:19 +09:00
select: { userId: true, nickname: true, points: true, profileImage: true, grade: true },
2025-11-02 04:59:09 +09:00
where: { status: "active" },
orderBy: { points: "desc" },
take: 100,
});
2025-11-02 13:32:19 +09:00
rankingItems = topUsers.map((u) => ({ userId: u.userId, nickname: u.nickname, points: u.points, profileImage: u.profileImage, grade: u.grade }));
2025-11-02 04:39:23 +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-11-02 15:13:03 +09:00
{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
2025-11-07 23:41:52 +09:00
? "px-3 h-[28px] mt-[11px] rounded-full bg-[#F94B37] text-white text-[12px] font-[700] leading-[28px] whitespace-nowrap"
: "px-3 h-[28px] rounded-full bg-transparent text-white/85 hover:text-white border border-white/30 text-[12px] font-[700] leading-[28px] whitespace-nowrap"
2025-11-02 15:13:03 +09:00
}
>
{b.name}
</Link>
))}
</div>
</div>
</section>
)}
2025-10-24 21:24:51 +09:00
2025-11-01 23:16:22 +09:00
{/* 검색/필터 툴바 + 리스트 */}
2025-11-08 01:21:44 +09:00
<section className="px-[0px] md:px-[30px] ">
2025-11-02 04:59:09 +09:00
{!isSpecialRanking && <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 ? (
2025-11-02 13:32:19 +09:00
<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>
2025-11-02 04:39:23 +09:00
</div>
2025-11-02 13:32:19 +09:00
);
})}
</div>
{rankingItems.length === 0 && (
<div className="px-4 py-10 text-center text-neutral-500"> .</div>
)}
2025-11-02 04:39:23 +09:00
</div>
) : (
<PostList
boardId={id}
sort={sort}
variant="board"
2025-11-08 01:21:44 +09:00
titleHoverOrange
2025-11-02 04:39:23 +09:00
newPostHref={`/posts/new?boardId=${id}${board?.slug ? `&boardSlug=${board.slug}` : ""}`}
/>
)}
2025-10-24 21:24:51 +09:00
</div>
</section>
</div>
);
}