Skip to content

Commit f16c49f

Browse files
committed
feat: 마이페이지 구현
1 parent 8fc3944 commit f16c49f

11 files changed

Lines changed: 533 additions & 20 deletions

src/app/me/page.tsx

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,49 @@
1-
import LogoutButton from '@/components/mypage/logout-button';
2-
import Profile from '@/components/mypage/profile';
3-
import UserDetail from '@/components/mypage/user-detail';
1+
import ProfileSection from '@/components/mypage/profile-section';
2+
import PointSection from '@/components/mypage/point-section';
3+
import ReviewRequestCard from '@/components/mypage/review-request-card';
4+
import QuickMenuSection from '@/components/mypage/quick-menu-section';
5+
import MyShoppingSection from '@/components/mypage/my-shopping-section';
6+
import PaymentSection from '@/components/mypage/payment-section';
7+
import CustomerServiceSection from '@/components/mypage/customer-service-section';
8+
import SettingsSection from '@/components/mypage/settings-section';
9+
import MainNavBar from '@/components/layout/main-nav-bar';
410

5-
export default function Mypage() {
11+
export default function MyPage() {
612
return (
7-
<div>
8-
<Profile />
9-
<LogoutButton />
10-
<UserDetail />
11-
</div>
13+
<main className="mx-auto min-h-screen max-w-2xl bg-white pb-20">
14+
{/* 헤더 */}
15+
<header className="sticky top-0 z-20 flex h-14 items-center justify-center border-b border-gray-100 bg-white">
16+
<h1 className="text-lg font-bold">마이페이지</h1>
17+
</header>
18+
<div>
19+
{/* 프로필 섹션 */}
20+
<ProfileSection />
21+
22+
{/* 포인트 섹션 */}
23+
<PointSection />
24+
25+
{/* 리뷰 요청 카드 */}
26+
<ReviewRequestCard />
27+
28+
{/* 빠른 메뉴 */}
29+
<QuickMenuSection />
30+
31+
{/* 내 쇼핑 */}
32+
<MyShoppingSection />
33+
34+
{/* 결제 및 할인 */}
35+
<PaymentSection />
36+
37+
{/* 고객센터 */}
38+
<CustomerServiceSection />
39+
40+
{/* 설정 */}
41+
<SettingsSection />
42+
</div>
43+
44+
<div className="fixed bottom-0 w-full">
45+
<MainNavBar />
46+
</div>
47+
</main>
1248
);
1349
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import Link from 'next/link';
2+
import Image from 'next/image';
3+
4+
const customerServiceItems = [
5+
{
6+
icon: 'icons/many-questions.svg',
7+
label: '자주 묻는\n질문',
8+
href: '/faq',
9+
},
10+
{
11+
icon: 'icons/inquiry.svg',
12+
label: '문의하기',
13+
href: '/inquiry',
14+
},
15+
{
16+
icon: 'icons/shop.svg',
17+
label: '입점 문의',
18+
href: '/vendor-inquiry',
19+
},
20+
];
21+
22+
export default function CustomerServiceSection() {
23+
return (
24+
<div className="border-t border-gray-200">
25+
<h2 className="px-5 py-4 text-lg font-bold">고객센터</h2>
26+
<div className="mx-5 rounded-lg border border-gray-200 px-5 py-5">
27+
<div className="flex justify-center gap-8">
28+
{customerServiceItems.map((item) => (
29+
<Link
30+
key={item.label}
31+
href={item.href}
32+
className="flex flex-col items-center gap-2"
33+
>
34+
<div className="flex h-12 w-12 items-center justify-center">
35+
<Image
36+
src={`/${item.icon}`}
37+
alt={item.label}
38+
width={48}
39+
height={48}
40+
/>
41+
</div>
42+
<span className="text-center text-xs font-medium whitespace-pre-wrap text-gray-700">
43+
{item.label}
44+
</span>
45+
</Link>
46+
))}
47+
</div>
48+
</div>
49+
</div>
50+
);
51+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import Link from 'next/link';
2+
import Image from 'next/image';
3+
4+
const shoppingMenuItems = [
5+
{
6+
icon: 'icons/delivery-return.svg',
7+
label: '반품 내역',
8+
href: '/me/returns',
9+
},
10+
{
11+
icon: 'icons/current-view.svg',
12+
label: '최근 본 상품',
13+
href: '/me/recent',
14+
},
15+
];
16+
17+
export default function MyShoppingSection() {
18+
return (
19+
<div className="border-t border-gray-200">
20+
<h2 className="px-5 py-4 text-lg font-bold">내 쇼핑</h2>
21+
<div className="mx-5 rounded-lg border border-gray-200 px-5 py-5">
22+
<div className="flex justify-center gap-12">
23+
{shoppingMenuItems.map((item) => (
24+
<Link
25+
key={item.label}
26+
href={item.href}
27+
className="flex flex-col items-center gap-2"
28+
>
29+
<div className="flex h-12 w-12 items-center justify-center">
30+
<Image
31+
src={`/${item.icon}`}
32+
alt={item.label}
33+
className="h-full w-full"
34+
width={48}
35+
height={48}
36+
/>
37+
</div>
38+
<span className="text-xs font-medium text-gray-700">
39+
{item.label}
40+
</span>
41+
</Link>
42+
))}
43+
</div>
44+
</div>
45+
</div>
46+
);
47+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import Link from 'next/link';
2+
import Image from 'next/image';
3+
import { signOut } from 'next-auth/react';
4+
5+
interface MypageItem {
6+
icon: string;
7+
label: string;
8+
href?: string;
9+
onClick?: () => void;
10+
type?: 'link' | 'button';
11+
}
12+
13+
interface MypageSectionProps {
14+
title?: string;
15+
items: MypageItem[];
16+
outerDivClassName?: string;
17+
innerDivClassName?: string;
18+
gridClassName?: string;
19+
itemClassName?: string;
20+
iconContainerClassName?: string;
21+
labelClassName?: string;
22+
}
23+
24+
export default function MypageSection({
25+
title,
26+
items,
27+
outerDivClassName = 'border-t border-gray-200',
28+
innerDivClassName = 'mx-5 rounded-lg border border-gray-200 px-5 py-5',
29+
gridClassName = 'flex justify-center gap-8',
30+
itemClassName = 'flex flex-col items-center gap-2',
31+
iconContainerClassName = 'flex h-12 w-12 items-center justify-center',
32+
labelClassName = 'text-center text-xs font-medium whitespace-pre-wrap text-gray-700',
33+
}: MypageSectionProps) {
34+
const handleItemClick = async (item: MypageItem) => {
35+
if (item.type === 'button' && item.onClick) {
36+
item.onClick();
37+
} else if (item.type === 'button' && item.label === '로그아웃') {
38+
await signOut({ callbackUrl: '/' });
39+
}
40+
};
41+
42+
return (
43+
<div className={outerDivClassName}>
44+
{title && <h2 className="px-5 py-4 text-lg font-bold">{title}</h2>}
45+
<div className={innerDivClassName}>
46+
<div className={gridClassName}>
47+
{items.map((item) => {
48+
const content = (
49+
<>
50+
<div className={iconContainerClassName}>
51+
<Image
52+
src={item.icon}
53+
alt={item.label}
54+
className="h-full w-full"
55+
width={48}
56+
height={48}
57+
/>
58+
</div>
59+
<span className={labelClassName}>{item.label}</span>
60+
</>
61+
);
62+
63+
if (item.type === 'button') {
64+
return (
65+
<button
66+
key={item.label}
67+
onClick={() => handleItemClick(item)}
68+
className={itemClassName}
69+
>
70+
{content}
71+
</button>
72+
);
73+
}
74+
return (
75+
<Link
76+
key={item.label}
77+
href={item.href || '#'}
78+
className={itemClassName}
79+
>
80+
{content}
81+
</Link>
82+
);
83+
})}
84+
</div>
85+
</div>
86+
</div>
87+
);
88+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Link from 'next/link';
2+
import { CreditCard, Ticket, PiggyBank } from 'lucide-react';
3+
4+
const paymentMenuItems = [
5+
{
6+
icon: 'icons/payment.svg',
7+
label: '결제 수단',
8+
href: '/me/payment-methods',
9+
},
10+
{
11+
icon: 'icons/coupon.svg',
12+
label: '할인 쿠폰',
13+
href: '/me/coupons',
14+
},
15+
{
16+
icon: 'icons/mission.svg',
17+
label: '미션 후\n적립금',
18+
href: '/me/mission-points',
19+
},
20+
];
21+
22+
export default function PaymentSection() {
23+
return (
24+
<div className="border-t border-gray-200">
25+
<h2 className="px-5 py-4 text-lg font-bold">결제 및 할인</h2>
26+
<div className="mx-5 rounded-lg border border-gray-200 px-5 py-5">
27+
<div className="flex justify-center gap-8">
28+
{paymentMenuItems.map((item) => (
29+
<Link
30+
key={item.label}
31+
href={item.href}
32+
className="flex flex-col items-center gap-2"
33+
>
34+
<div className="flex h-12 w-12 items-center justify-center">
35+
<img
36+
src={`/${item.icon}`}
37+
alt={item.label}
38+
className="h-full w-full"
39+
/>
40+
</div>
41+
<span className="text-center text-xs font-medium whitespace-pre-wrap text-gray-700">
42+
{item.label}
43+
</span>
44+
</Link>
45+
))}
46+
</div>
47+
</div>
48+
</div>
49+
);
50+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { api } from '@/lib/api-client';
2+
import { UserInfoResDto } from '@/types/domain/user';
3+
4+
export default async function PointSection() {
5+
const user = await api.get<UserInfoResDto>('/users/me');
6+
7+
const formattedPoints = user.points.toLocaleString();
8+
9+
return (
10+
<div className="mx-5 rounded-lg border border-gray-200 px-5 py-4">
11+
<div className="text-center">
12+
<p className="mb-2 text-base font-medium text-gray-700">내 포인트</p>
13+
<p className="text-2xl font-bold">
14+
{formattedPoints}P <span className="text-xl">🪙</span>
15+
</p>
16+
</div>
17+
</div>
18+
);
19+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Link from 'next/link';
2+
import { auth } from '/auth';
3+
import { redirect } from 'next/navigation';
4+
import Profile from './profile';
5+
6+
export default async function ProfileSection() {
7+
const session = await auth();
8+
9+
if (!session) {
10+
redirect('/login');
11+
}
12+
13+
return (
14+
<div className="flex items-center gap-4 px-5 py-6">
15+
<div className="relative">
16+
<div className="flex h-16 w-16 items-center justify-center overflow-hidden rounded-full bg-gray-100">
17+
<Profile session={session} />
18+
</div>
19+
</div>
20+
21+
<div className="flex flex-col gap-2">
22+
<span className="text-xl font-semibold">
23+
{session.user.nickName || '사용자'}
24+
</span>
25+
<Link
26+
href="/me/edit"
27+
className="bg-ongil-mint text-ongil-teal inline-flex items-center justify-center rounded-full px-4 py-1.5 text-sm font-medium"
28+
>
29+
내 정보 수정
30+
</Link>
31+
</div>
32+
</div>
33+
);
34+
}

0 commit comments

Comments
 (0)