227 lines
9.9 KiB
TypeScript
227 lines
9.9 KiB
TypeScript
|
|
import { headers } from "next/headers";
|
||
|
|
import prisma from "@/lib/prisma";
|
||
|
|
import Link from "next/link";
|
||
|
|
import { UserAvatar } from "@/app/components/UserAvatar";
|
||
|
|
import { GradeIcon, getGradeName } from "@/app/components/GradeIcon";
|
||
|
|
import { HeroBanner } from "@/app/components/HeroBanner";
|
||
|
|
import { PostList } from "@/app/components/PostList";
|
||
|
|
import ProfileLabelIcon from "@/app/svgs/profilelableicon";
|
||
|
|
export default async function MyPage({ searchParams }: { searchParams: Promise<{ tab?: string; page?: string; sort?: string; q?: string }> }) {
|
||
|
|
const sp = await searchParams;
|
||
|
|
const activeTab = sp?.tab || "posts";
|
||
|
|
const page = parseInt(sp?.page || "1", 10);
|
||
|
|
const sort = sp?.sort || "recent";
|
||
|
|
const q = sp?.q || "";
|
||
|
|
|
||
|
|
// 현재 로그인한 사용자 정보 가져오기
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!currentUser) {
|
||
|
|
return <div className="p-4">로그인이 필요합니다.</div>;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 통계 정보 가져오기
|
||
|
|
const [postsCount, commentsCount, receivedMessagesCount, sentMessagesCount] = await Promise.all([
|
||
|
|
prisma.post.count({ where: { authorId: currentUser.userId } }),
|
||
|
|
prisma.comment.count({ where: { authorId: currentUser.userId } }),
|
||
|
|
prisma.message.count({ where: { receiverId: currentUser.userId } }),
|
||
|
|
prisma.message.count({ where: { senderId: currentUser.userId } }),
|
||
|
|
]);
|
||
|
|
|
||
|
|
// 등급 업그레이드에 필요한 포인트 계산 (예시: 다음 등급까지)
|
||
|
|
const nextGradePoints = (() => {
|
||
|
|
// 등급별 포인트 기준을 여기에 설정 (임시로 2M)
|
||
|
|
const gradeThresholds = [0, 200000, 400000, 800000, 1600000, 3200000, 6400000, 10000000];
|
||
|
|
const currentGradeIndex = Math.min(7, Math.max(0, currentUser.grade));
|
||
|
|
return gradeThresholds[currentGradeIndex + 1] || 2000000;
|
||
|
|
})();
|
||
|
|
|
||
|
|
const tabs = [
|
||
|
|
{ key: "posts", label: "내가 쓴 게시글", count: postsCount },
|
||
|
|
{ key: "comments", label: "내가 쓴 댓글", count: commentsCount },
|
||
|
|
{ key: "messages-received", label: "받은 쪽지함", count: receivedMessagesCount },
|
||
|
|
{ key: "messages-sent", label: "보낸 쪽지함", count: sentMessagesCount },
|
||
|
|
];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
{/* 히어로 배너 */}
|
||
|
|
<section>
|
||
|
|
<HeroBanner />
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* 프로필 섹션 */}
|
||
|
|
<section className="bg-white rounded-[24px] px-[108px] py-[23px]">
|
||
|
|
<div className="flex gap-[64px] items-center justify-center">
|
||
|
|
{/* 좌측: 프로필 이미지, 닉네임, 레벨/등급/포인트 */}
|
||
|
|
<div className="flex flex-col gap-[16px] items-center">
|
||
|
|
<div className="flex flex-col gap-[16px] items-center">
|
||
|
|
{/* 프로필 이미지 */}
|
||
|
|
<div className="relative w-[161px] h-[162px]">
|
||
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
||
|
|
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
|
|
<circle cx="80" cy="80" r="79" stroke="#8c8c8c" strokeWidth="2" fill="none" />
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
<div className="absolute inset-[10px] flex items-center justify-center">
|
||
|
|
<UserAvatar
|
||
|
|
src={currentUser.profileImage || null}
|
||
|
|
alt={currentUser.nickname || "프로필"}
|
||
|
|
width={140}
|
||
|
|
height={140}
|
||
|
|
className="rounded-full"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{/* 닉네임 */}
|
||
|
|
<div className="flex items-center gap-[4px]">
|
||
|
|
<span className="text-[20px] font-bold text-[#5c5c5c] truncate max-w-[200px]">{currentUser.nickname || "사용자"}</span>
|
||
|
|
<button className="w-[20px] h-[20px] shrink-0">
|
||
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
|
|
<path d="M11.7188 3.125L13.5938 4.99999L5.3125 13.2812L3.4375 11.4062L11.7188 3.125ZM14.0625 1.5625L15.3125 2.8125C15.625 3.12499 15.625 3.62499 15.3125 3.93749L13.4375 5.81249L11.5625 3.93749L13.4375 2.06249C13.75 1.74999 14.25 1.74999 14.5625 2.06249L14.0625 1.5625ZM3.125 14.375L9.0625 12.1875L7.1875 10.3125L3.125 14.375Z" fill="#707070"/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{/* 레벨/등급/포인트 정보 - 가로 배치 */}
|
||
|
|
<div className="flex gap-[16px] items-center justify-center">
|
||
|
|
<div className="flex items-center gap-[4px] min-w-0">
|
||
|
|
<ProfileLabelIcon width={14} height={14} className="shrink-0" />
|
||
|
|
<span className="text-[11px] text-[#8c8c8c] font-[700] shrink-0">레벨</span>
|
||
|
|
<span className="text-[14px] text-[#5c5c5c] font-[700] min-w-0 truncate">Lv.{currentUser.level || 1}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center gap-[4px] min-w-0">
|
||
|
|
<ProfileLabelIcon width={14} height={14} className="shrink-0" />
|
||
|
|
<span className="text-[11px] text-[#8c8c8c] font-[700] shrink-0">등급</span>
|
||
|
|
<span className="text-[14px] text-[#5c5c5c] font-[700] min-w-0 truncate">{getGradeName(currentUser.grade)}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center gap-[4px] min-w-0">
|
||
|
|
<ProfileLabelIcon width={14} height={14} className="shrink-0" />
|
||
|
|
<span className="text-[11px] text-[#8c8c8c] font-[700] shrink-0">포인트</span>
|
||
|
|
<span className="text-[14px] text-[#5c5c5c] font-[700] min-w-0 truncate">{currentUser.points.toLocaleString()}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 구분선 */}
|
||
|
|
<div className="w-[8px] h-[162px] bg-[#d5d5d5] shrink-0"></div>
|
||
|
|
|
||
|
|
{/* 우측: 등급 배지 및 포인트 진행 상황 */}
|
||
|
|
<div className="flex flex-col items-center justify-center w-[161px] shrink-0">
|
||
|
|
<div className="w-[125px] h-[125px] flex items-center justify-center mb-[16px]">
|
||
|
|
<GradeIcon grade={currentUser.grade} width={125} height={125} />
|
||
|
|
</div>
|
||
|
|
<div className="flex flex-col gap-[16px] items-center">
|
||
|
|
<div className="text-[20px] font-bold text-[#5c5c5c]">{getGradeName(currentUser.grade)}</div>
|
||
|
|
<div className="flex items-start gap-[8px] text-[20px] font-bold">
|
||
|
|
<span className="text-[#5c5c5c] truncate">{(currentUser.points / 1000000).toFixed(1)}M</span>
|
||
|
|
<span className="text-[#8c8c8c] shrink-0">/ {(nextGradePoints / 1000000).toFixed(1)}M</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* 탭 버튼 */}
|
||
|
|
<section className="h-[67px]">
|
||
|
|
<div className="bg-white rounded-[24px] p-[10px] flex gap-[10px] items-center">
|
||
|
|
{tabs.map((tab) => (
|
||
|
|
<Link
|
||
|
|
key={tab.key}
|
||
|
|
href={`/my-page?tab=${tab.key}`}
|
||
|
|
className={`flex-1 h-[46px] rounded-[16px] flex items-center justify-center gap-[8px] ${
|
||
|
|
activeTab === tab.key
|
||
|
|
? "bg-[#5c5c5c] text-white"
|
||
|
|
: "bg-transparent text-[#5c5c5c]"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<span className={`text-[16px] ${activeTab === tab.key ? "font-medium" : "font-normal"}`}>
|
||
|
|
{tab.label}
|
||
|
|
</span>
|
||
|
|
<div
|
||
|
|
className={`h-[20px] px-[8px] py-[4px] rounded-[14px] ${
|
||
|
|
activeTab === tab.key
|
||
|
|
? "bg-white"
|
||
|
|
: "border border-[#707070]"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<span
|
||
|
|
className={`text-[10px] font-semibold ${
|
||
|
|
activeTab === tab.key ? "text-[#707070]" : "text-[#707070]"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{tab.count.toLocaleString()}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</Link>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* 컨텐츠 영역 */}
|
||
|
|
<section>
|
||
|
|
{activeTab === "posts" && (
|
||
|
|
<PostList
|
||
|
|
authorId={currentUser.userId}
|
||
|
|
sort={sort as "recent" | "popular"}
|
||
|
|
q={q}
|
||
|
|
variant="board"
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
{activeTab === "comments" && (
|
||
|
|
<div className="bg-white rounded-[24px] p-4">
|
||
|
|
<div className="text-center text-neutral-500 py-10">댓글 기능은 준비 중입니다.</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{activeTab === "messages-received" && (
|
||
|
|
<div className="bg-white rounded-[24px] p-4">
|
||
|
|
<div className="text-center text-neutral-500 py-10">받은 쪽지함 기능은 준비 중입니다.</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{activeTab === "messages-sent" && (
|
||
|
|
<div className="bg-white rounded-[24px] p-4">
|
||
|
|
<div className="text-center text-neutral-500 py-10">보낸 쪽지함 기능은 준비 중입니다.</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|