2025-10-09 15:39:46 +09:00
|
|
|
import { HeroBanner } from "@/app/components/HeroBanner";
|
2025-11-02 02:46:20 +09:00
|
|
|
import Link from "next/link";
|
2025-10-24 21:24:51 +09:00
|
|
|
import HorizontalCardScroller from "@/app/components/HorizontalCardScroller";
|
2025-10-31 20:05:09 +09:00
|
|
|
import { PostList } from "@/app/components/PostList";
|
2025-10-31 00:02:17 +09:00
|
|
|
import ProfileLabelIcon from "@/app/svgs/profilelableicon";
|
|
|
|
|
import SearchIcon from "@/app/svgs/SearchIcon";
|
2025-11-02 11:34:47 +09:00
|
|
|
import { RankIcon1st } from "@/app/components/RankIcon1st";
|
|
|
|
|
import { RankIcon2nd } from "@/app/components/RankIcon2nd";
|
|
|
|
|
import { RankIcon3rd } from "@/app/components/RankIcon3rd";
|
2025-11-02 12:07:11 +09:00
|
|
|
import { UserAvatar } from "@/app/components/UserAvatar";
|
|
|
|
|
import { ImagePlaceholderIcon } from "@/app/components/ImagePlaceholderIcon";
|
|
|
|
|
import { BoardPanelClient } from "@/app/components/BoardPanelClient";
|
2025-11-02 13:32:19 +09:00
|
|
|
import { GradeIcon, getGradeName } from "@/app/components/GradeIcon";
|
2025-10-31 20:05:09 +09:00
|
|
|
import prisma from "@/lib/prisma";
|
2025-11-02 13:32:19 +09:00
|
|
|
import { headers } from "next/headers";
|
2025-10-08 20:55:43 +09:00
|
|
|
|
2025-10-13 18:06:46 +09:00
|
|
|
export default async function Home({ searchParams }: { searchParams: Promise<{ sort?: "recent" | "popular" } | undefined> }) {
|
|
|
|
|
const sp = await searchParams;
|
|
|
|
|
const sort = sp?.sort ?? "recent";
|
2025-10-31 20:05:09 +09:00
|
|
|
|
2025-11-02 13:32:19 +09:00
|
|
|
// 로그인된 사용자 정보 가져오기 (기본값: 어드민)
|
|
|
|
|
let currentUser: {
|
|
|
|
|
userId: string;
|
|
|
|
|
nickname: string;
|
|
|
|
|
profileImage: string | null;
|
|
|
|
|
points: number;
|
|
|
|
|
level: number;
|
|
|
|
|
grade: number;
|
|
|
|
|
} | null = null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const h = await headers();
|
|
|
|
|
const cookieHeader = h.get("cookie") || "";
|
|
|
|
|
const uid = cookieHeader
|
|
|
|
|
.split(";")
|
|
|
|
|
.map((s) => s.trim())
|
|
|
|
|
.find((pair) => pair.startsWith("uid="))
|
|
|
|
|
?.split("=")[1];
|
|
|
|
|
|
|
|
|
|
if (uid) {
|
|
|
|
|
const user = await prisma.user.findUnique({
|
|
|
|
|
where: { userId: decodeURIComponent(uid) },
|
|
|
|
|
select: { userId: true, nickname: true, profileImage: true, points: true, level: true, grade: true },
|
|
|
|
|
});
|
|
|
|
|
if (user) currentUser = user;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// 에러 무시
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 로그인되지 않은 경우 어드민 사용자 가져오기
|
|
|
|
|
if (!currentUser) {
|
|
|
|
|
const admin = await prisma.user.findUnique({
|
|
|
|
|
where: { nickname: "admin" },
|
|
|
|
|
select: { userId: true, nickname: true, profileImage: true, points: true, level: true, grade: true },
|
|
|
|
|
});
|
|
|
|
|
if (admin) currentUser = admin;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-05 22:27:29 +09:00
|
|
|
// 내가 쓴 게시글/댓글 수
|
|
|
|
|
let myPostsCount = 0;
|
|
|
|
|
let myCommentsCount = 0;
|
|
|
|
|
if (currentUser) {
|
|
|
|
|
myPostsCount = await prisma.post.count({ where: { authorId: currentUser.userId, status: "published" } });
|
|
|
|
|
myCommentsCount = await prisma.comment.count({ where: { authorId: currentUser.userId } });
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-31 20:05:09 +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;
|
|
|
|
|
const showPartnerShops: boolean = parsed.showPartnerShops ?? true;
|
|
|
|
|
const visibleBoardIds: string[] = Array.isArray(parsed.visibleBoardIds) ? parsed.visibleBoardIds : [];
|
|
|
|
|
|
2025-11-02 07:01:42 +09:00
|
|
|
// 보드 메타데이터 (메인뷰 타입 포함)
|
2025-10-31 20:05:09 +09:00
|
|
|
const boardsMeta = visibleBoardIds.length
|
2025-11-02 07:01:42 +09:00
|
|
|
? await prisma.board.findMany({
|
|
|
|
|
where: { id: { in: visibleBoardIds } },
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
slug: true,
|
|
|
|
|
category: { select: { id: true, name: true } },
|
|
|
|
|
mainPageViewType: { select: { key: true } },
|
|
|
|
|
},
|
|
|
|
|
})
|
2025-10-31 20:05:09 +09:00
|
|
|
: [];
|
2025-11-02 07:01:42 +09:00
|
|
|
const idToMeta = new Map(
|
|
|
|
|
boardsMeta.map((b) => [
|
|
|
|
|
b.id,
|
|
|
|
|
{
|
|
|
|
|
id: b.id,
|
|
|
|
|
name: b.name,
|
|
|
|
|
slug: b.slug,
|
|
|
|
|
categoryId: b.category?.id ?? null,
|
|
|
|
|
categoryName: b.category?.name ?? "",
|
|
|
|
|
mainTypeKey: b.mainPageViewType?.key,
|
|
|
|
|
},
|
|
|
|
|
] as const)
|
|
|
|
|
);
|
2025-10-31 20:05:09 +09:00
|
|
|
const orderedBoards = visibleBoardIds
|
|
|
|
|
.map((id) => idToMeta.get(id))
|
2025-11-02 07:01:42 +09:00
|
|
|
.filter((v): v is any => Boolean(v));
|
2025-10-31 20:05:09 +09:00
|
|
|
const firstTwo = orderedBoards.slice(0, 2);
|
|
|
|
|
const restBoards = orderedBoards.slice(2);
|
|
|
|
|
|
2025-11-02 07:01:42 +09:00
|
|
|
function formatDateYmd(d: Date) {
|
|
|
|
|
const yyyy = d.getFullYear();
|
|
|
|
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
const dd = String(d.getDate()).padStart(2, "0");
|
|
|
|
|
return `${yyyy}.${mm}.${dd}`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-02 12:07:11 +09:00
|
|
|
// 게시판 패널 데이터 수집 함수 (모든 sibling boards 포함)
|
|
|
|
|
const prepareBoardPanelData = async (board: { id: string; name: string; slug: string; categoryId: string | null; categoryName: string; mainTypeKey?: string }) => {
|
2025-11-02 07:01:42 +09:00
|
|
|
// 같은 카테고리의 소분류(게시판) 탭 목록
|
|
|
|
|
const siblingBoards = board.categoryId
|
|
|
|
|
? await prisma.board.findMany({
|
|
|
|
|
where: { categoryId: board.categoryId },
|
2025-11-02 12:07:11 +09:00
|
|
|
select: { id: true, name: true, slug: true, mainPageViewType: { select: { key: true } } },
|
2025-11-02 07:01:42 +09:00
|
|
|
orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
|
|
|
|
|
})
|
|
|
|
|
: [];
|
2025-11-02 11:01:18 +09:00
|
|
|
|
2025-11-02 12:07:11 +09:00
|
|
|
const boardsData = [];
|
2025-11-02 11:01:18 +09:00
|
|
|
|
2025-11-02 12:07:11 +09:00
|
|
|
for (const sb of siblingBoards) {
|
|
|
|
|
const isTextMain = sb.mainPageViewType?.key === "main_text";
|
|
|
|
|
const isSpecialRank = sb.mainPageViewType?.key === "main_special_rank";
|
|
|
|
|
const isPreview = sb.mainPageViewType?.key === "main_preview";
|
2025-11-02 07:01:42 +09:00
|
|
|
|
2025-11-02 12:07:11 +09:00
|
|
|
const boardMeta = {
|
|
|
|
|
id: sb.id,
|
|
|
|
|
name: sb.name,
|
|
|
|
|
slug: sb.slug,
|
|
|
|
|
mainTypeKey: sb.mainPageViewType?.key,
|
|
|
|
|
};
|
2025-11-02 07:01:42 +09:00
|
|
|
|
2025-11-02 12:07:11 +09:00
|
|
|
let specialRankUsers;
|
|
|
|
|
let previewPosts;
|
|
|
|
|
let textPosts;
|
|
|
|
|
|
|
|
|
|
if (isSpecialRank) {
|
|
|
|
|
specialRankUsers = 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 12:07:11 +09:00
|
|
|
where: { status: "active" },
|
|
|
|
|
orderBy: { points: "desc" },
|
|
|
|
|
take: 3,
|
|
|
|
|
});
|
|
|
|
|
} else if (isPreview) {
|
|
|
|
|
previewPosts = await prisma.post.findMany({
|
|
|
|
|
where: { boardId: sb.id, status: "published" },
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
title: true,
|
|
|
|
|
createdAt: true,
|
2025-11-02 13:32:19 +09:00
|
|
|
content: true,
|
2025-11-02 12:07:11 +09:00
|
|
|
attachments: {
|
|
|
|
|
where: { type: "image" },
|
|
|
|
|
orderBy: { sortOrder: "asc" },
|
|
|
|
|
take: 1,
|
|
|
|
|
select: { url: true },
|
|
|
|
|
},
|
2025-11-02 15:13:03 +09:00
|
|
|
stat: { select: { commentsCount: true } },
|
2025-11-02 12:07:11 +09:00
|
|
|
},
|
|
|
|
|
orderBy: { createdAt: "desc" },
|
|
|
|
|
take: 3,
|
|
|
|
|
});
|
|
|
|
|
} else if (isTextMain) {
|
|
|
|
|
textPosts = await prisma.post.findMany({
|
|
|
|
|
where: { boardId: sb.id, status: "published" },
|
2025-11-02 15:13:03 +09:00
|
|
|
select: { id: true, title: true, createdAt: true, stat: { select: { recommendCount: true, commentsCount: true } } },
|
2025-11-02 12:07:11 +09:00
|
|
|
orderBy: { createdAt: "desc" },
|
2025-11-05 22:27:29 +09:00
|
|
|
take: 16,
|
2025-11-02 12:07:11 +09:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// 기본 타입은 PostList가 자체적으로 API를 호출하므로 데이터 미리 가져오지 않음
|
|
|
|
|
|
|
|
|
|
boardsData.push({
|
|
|
|
|
board: boardMeta,
|
|
|
|
|
categoryName: board.categoryName || board.name,
|
|
|
|
|
siblingBoards: siblingBoards.map(sb => ({
|
|
|
|
|
id: sb.id,
|
|
|
|
|
name: sb.name,
|
|
|
|
|
slug: sb.slug,
|
|
|
|
|
mainTypeKey: sb.mainPageViewType?.key,
|
|
|
|
|
})),
|
|
|
|
|
specialRankUsers,
|
|
|
|
|
previewPosts,
|
|
|
|
|
textPosts,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { initialBoardId: board.id, boardsData };
|
2025-11-02 07:01:42 +09:00
|
|
|
};
|
2025-10-31 20:05:09 +09:00
|
|
|
|
2025-10-08 20:55:43 +09:00
|
|
|
return (
|
2025-10-11 18:32:06 +09:00
|
|
|
<div className="space-y-8">
|
2025-10-31 20:05:09 +09:00
|
|
|
{/* 히어로 섹션: 상단 대형 비주얼 영역 (설정 온오프) */}
|
|
|
|
|
{showBanner && (
|
|
|
|
|
<section>
|
|
|
|
|
<HeroBanner />
|
|
|
|
|
</section>
|
|
|
|
|
)}
|
2025-10-11 18:32:06 +09:00
|
|
|
|
2025-11-02 02:46:20 +09:00
|
|
|
{/* 제휴 샾 가로 스크롤 (설정 온오프, DB에서 불러오기)
|
|
|
|
|
- 우선 partners 테이블(관리자 페이지 관리 대상) 사용
|
|
|
|
|
- 없으면 partner_shops로 대체 */}
|
|
|
|
|
{showPartnerShops && (async () => {
|
|
|
|
|
// 우선순위: partners(관리자 관리) → partner_shops(폴백)
|
|
|
|
|
let partners: any[] = [];
|
|
|
|
|
try {
|
|
|
|
|
partners = await (prisma as any).partner.findMany({ orderBy: [{ sortOrder: "asc" }, { createdAt: "desc" }], take: 10 });
|
|
|
|
|
} catch (_) {
|
|
|
|
|
partners = await prisma.partner.findMany({ orderBy: { createdAt: "desc" }, take: 10 });
|
|
|
|
|
}
|
|
|
|
|
const items = partners.map((p: any) => ({
|
|
|
|
|
id: p.id,
|
|
|
|
|
region: p.address ? String(p.address).split(" ")[0] : p.category,
|
|
|
|
|
name: p.name,
|
|
|
|
|
address: p.address || "",
|
|
|
|
|
image: p.imageUrl || "/sample.jpg",
|
|
|
|
|
}));
|
|
|
|
|
if (items.length > 0) return <HorizontalCardScroller items={items} />;
|
|
|
|
|
|
|
|
|
|
const shops = await (prisma as any).partnerShop.findMany({ where: { active: true }, orderBy: [{ sortOrder: "asc" }, { createdAt: "desc" }] });
|
|
|
|
|
const shopItems = shops.map((s: any) => ({ id: s.id, region: s.region, name: s.name, address: s.address, image: s.imageUrl }));
|
|
|
|
|
return <HorizontalCardScroller items={shopItems} />;
|
2025-10-24 21:24:51 +09:00
|
|
|
})()}
|
|
|
|
|
|
2025-10-31 20:05:09 +09:00
|
|
|
{/* 1행: 프로필 + 선택된 보드 2개 (최대 2개) */}
|
|
|
|
|
{(firstTwo.length > 0) && (
|
2025-11-02 11:01:18 +09:00
|
|
|
<section className="min-h-[395px] overflow-hidden">
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:flex xl:gap-[23px] gap-4 h-full min-h-0">
|
|
|
|
|
<div className="hidden xl:grid relative overflow-hidden rounded-xl bg-white px-[25px] py-[34px] grid-rows-[120px_120px_1fr] gap-y-[32px] h-[514px] w-[350px] shrink-0">
|
2025-10-31 20:05:09 +09:00
|
|
|
<div className="absolute inset-x-0 top-0 h-[56px] bg-[#d5d5d5] z-0" />
|
|
|
|
|
<div className="h-[120px] flex items-center justify-center relative z-10">
|
|
|
|
|
<div className="flex items-center justify-center gap-[8px]">
|
2025-11-02 12:07:11 +09:00
|
|
|
<UserAvatar
|
2025-11-02 13:32:19 +09:00
|
|
|
src={currentUser?.profileImage || null}
|
|
|
|
|
alt={currentUser?.nickname || "프로필"}
|
2025-11-02 12:07:11 +09:00
|
|
|
width={120}
|
|
|
|
|
height={120}
|
|
|
|
|
className="rounded-full"
|
|
|
|
|
/>
|
2025-11-02 13:32:19 +09:00
|
|
|
{currentUser && (
|
|
|
|
|
<div className="w-[62px] h-[62px] flex items-center justify-center">
|
|
|
|
|
<GradeIcon grade={currentUser.grade} width={62} height={62} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-10-30 20:47:34 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-31 20:05:09 +09:00
|
|
|
<div className="h-[120px] flex flex-col items-center relative z-10">
|
2025-11-02 13:32:19 +09:00
|
|
|
<div className="text-[18px] text-[#5c5c5c] font-[700] truncate text-center mb-[20px]">{currentUser?.nickname || "사용자"}</div>
|
2025-10-31 20:05:09 +09:00
|
|
|
<div className="w-[300px] pl-[67px] flex flex-col gap-[12px]">
|
|
|
|
|
<div className="grid grid-cols-[64px_auto] gap-x-[24px] items-center h-[16px]">
|
|
|
|
|
<div className="w-[64px] flex items-center">
|
|
|
|
|
<ProfileLabelIcon width={16} height={16} />
|
|
|
|
|
<span className="ml-[8px] text-[12px] text-[#8c8c8c] font-[700]">레벨</span>
|
|
|
|
|
</div>
|
2025-11-02 13:32:19 +09:00
|
|
|
<div className="text-[16px] text-[#5c5c5c] font-[700]">Lv. {currentUser?.level || 1}</div>
|
2025-10-30 20:47:34 +09:00
|
|
|
</div>
|
2025-10-31 20:05:09 +09:00
|
|
|
<div className="grid grid-cols-[64px_auto] gap-x-[24px] items-center h-[16px]">
|
|
|
|
|
<div className="w-[64px] flex items-center">
|
|
|
|
|
<ProfileLabelIcon width={16} height={16} />
|
|
|
|
|
<span className="ml-[8px] text-[12px] text-[#8c8c8c] font-[700]">등급</span>
|
|
|
|
|
</div>
|
2025-11-02 13:32:19 +09:00
|
|
|
<div className="text-[16px] text-[#5c5c5c] font-[700]">{getGradeName(currentUser?.grade || 0)}</div>
|
2025-10-30 20:47:34 +09:00
|
|
|
</div>
|
2025-10-31 20:05:09 +09:00
|
|
|
<div className="grid grid-cols-[64px_auto] gap-x-[24px] items-center h-[16px]">
|
|
|
|
|
<div className="w-[64px] flex items-center">
|
|
|
|
|
<ProfileLabelIcon width={16} height={16} />
|
|
|
|
|
<span className="ml-[8px] text-[12px] text-[#8c8c8c] font-[700]">포인트</span>
|
|
|
|
|
</div>
|
2025-11-02 13:32:19 +09:00
|
|
|
<div className="text-[16px] text-[#5c5c5c] font-[700]">{(currentUser?.points || 0).toLocaleString()}</div>
|
2025-10-30 20:47:34 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-31 20:05:09 +09:00
|
|
|
<div className="flex flex-col gap-[12px] relative z-10">
|
2025-11-05 22:27:29 +09:00
|
|
|
<Link href="/my-page" className="w-[300px] h-[32px] rounded-full bg-[#8c8c8c] hover:bg-[#5c5c5c] text-white text-[12px] font-[700] flex items-center px-[12px]">
|
|
|
|
|
<span className="flex items-center w-full pl-[88px]">
|
|
|
|
|
<span className="flex items-center gap-[8px]">
|
|
|
|
|
<SearchIcon width={16} height={16} />
|
|
|
|
|
<span>내 정보 페이지</span>
|
|
|
|
|
</span>
|
2025-10-31 20:05:09 +09:00
|
|
|
</span>
|
2025-11-02 13:32:19 +09:00
|
|
|
</Link>
|
2025-11-05 22:27:29 +09:00
|
|
|
<Link href="/my-page?tab=points" className="w-[300px] h-[32px] rounded-full bg-[#8c8c8c] hover:bg-[#5c5c5c] text-white text-[12px] font-[700] flex items-center px-[12px]">
|
|
|
|
|
<span className="flex items-center w-full pl-[88px]">
|
|
|
|
|
<span className="flex items-center gap-[8px]">
|
|
|
|
|
<SearchIcon width={16} height={16} />
|
|
|
|
|
<span>포인트 히스토리</span>
|
|
|
|
|
</span>
|
2025-10-31 20:05:09 +09:00
|
|
|
</span>
|
2025-11-02 15:13:03 +09:00
|
|
|
</Link>
|
2025-11-05 22:27:29 +09:00
|
|
|
<Link href={`/my-page?tab=posts`} className="w-[300px] h-[32px] rounded-full bg-[#8c8c8c] hover:bg-[#5c5c5c] text-white text-[12px] font-[700] flex items-center px-[12px]">
|
|
|
|
|
<span className="flex items-center w-full pl-[88px]">
|
|
|
|
|
<span className="flex items-center gap-[8px]">
|
|
|
|
|
<SearchIcon width={16} height={16} />
|
|
|
|
|
<span>내가 쓴 게시글</span>
|
|
|
|
|
</span>
|
|
|
|
|
<span className="ml-auto inline-flex items-center justify-center h-[20px] px-[8px] rounded-full bg-white text-[#5c5c5c] text-[12px] leading-[20px] shrink-0">{myPostsCount.toLocaleString()}개</span>
|
2025-10-31 20:05:09 +09:00
|
|
|
</span>
|
2025-11-02 15:13:03 +09:00
|
|
|
</Link>
|
2025-11-05 22:27:29 +09:00
|
|
|
<Link href={`/my-page?tab=comments`} className="w-[300px] h-[32px] rounded-full bg-[#8c8c8c] hover:bg-[#5c5c5c] text-white text-[12px] font-[700] flex items-center px-[12px]">
|
|
|
|
|
<span className="flex items-center w-full pl-[88px]">
|
|
|
|
|
<span className="flex items-center gap-[8px]">
|
|
|
|
|
<SearchIcon width={16} height={16} />
|
|
|
|
|
<span>내가 쓴 댓글</span>
|
|
|
|
|
</span>
|
|
|
|
|
<span className="ml-auto inline-flex items-center justify-center h-[20px] px-[8px] rounded-full bg-white text-[#5c5c5c] text-[12px] leading-[20px] shrink-0">{myCommentsCount.toLocaleString()}개</span>
|
2025-10-31 20:05:09 +09:00
|
|
|
</span>
|
2025-11-02 15:13:03 +09:00
|
|
|
</Link>
|
2025-10-31 20:05:09 +09:00
|
|
|
</div>
|
2025-10-24 21:24:51 +09:00
|
|
|
</div>
|
2025-11-02 12:07:11 +09:00
|
|
|
{(await Promise.all(firstTwo.map((b) => prepareBoardPanelData(b)))).map((panelData, idx) => (
|
2025-11-02 11:01:18 +09:00
|
|
|
<div key={firstTwo[idx].id} className="rounded-xl overflow-hidden xl:h-[514px] h-full min-h-0 flex flex-col flex-1">
|
2025-11-02 12:07:11 +09:00
|
|
|
<BoardPanelClient initialBoardId={panelData.initialBoardId} boardsData={panelData.boardsData} />
|
2025-10-31 20:05:09 +09:00
|
|
|
</div>
|
|
|
|
|
))}
|
2025-10-11 18:32:06 +09:00
|
|
|
</div>
|
2025-10-31 20:05:09 +09:00
|
|
|
</section>
|
|
|
|
|
)}
|
2025-10-11 18:32:06 +09:00
|
|
|
|
2025-10-31 20:05:09 +09:00
|
|
|
{/* 나머지 보드: 2개씩 다음 열로 렌더링 */}
|
|
|
|
|
{restBoards.length > 0 && (
|
|
|
|
|
<>
|
2025-11-02 07:01:42 +09:00
|
|
|
{Array.from({ length: Math.ceil(restBoards.length / 2) }).map(async (_, i) => {
|
2025-10-31 20:05:09 +09:00
|
|
|
const pair = restBoards.slice(i * 2, i * 2 + 2);
|
|
|
|
|
return (
|
2025-11-02 11:34:47 +09:00
|
|
|
<section key={`rest-${i}`} className="min-h-[395px] md:h-[540px] overflow-hidden">
|
2025-10-31 20:05:09 +09:00
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 h-full min-h-0">
|
2025-11-02 12:07:11 +09:00
|
|
|
{(await Promise.all(pair.map((b) => prepareBoardPanelData(b)))).map((panelData, idx) => (
|
2025-11-02 07:01:42 +09:00
|
|
|
<div key={pair[idx].id} className="rounded-xl overflow-hidden h-full min-h-0 flex flex-col">
|
2025-11-02 12:07:11 +09:00
|
|
|
<BoardPanelClient initialBoardId={panelData.initialBoardId} boardsData={panelData.boardsData} />
|
2025-10-31 20:05:09 +09:00
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2025-10-08 20:55:43 +09:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|