Skip to content

Commit 120f16b

Browse files
committed
feat: 프로필 드롭다운 컴포넌트화 및 헤더 내부에 조립
1 parent abc038a commit 120f16b

File tree

3 files changed

+92
-60
lines changed

3 files changed

+92
-60
lines changed

src/components/Header.tsx

Lines changed: 10 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,19 @@
11
'use client';
22

33
import Link from 'next/link';
4-
import Image from 'next/image';
54
import IconLogo from '@assets/svg/logo';
65
import IconBell from '@assets/svg/bell';
76
import useUserStore from '@/stores/authStore';
8-
import { useEffect, useRef, useState } from 'react';
97
import { useRouter } from 'next/navigation';
10-
import ProfileDefaultIcon from '@assets/svg/profile-default';
8+
import ProfileDropdown from '@/components/ProfileDropdown';
119

1210
export default function Header() {
1311
const router = useRouter();
1412
const user = useUserStore((state) => state.user);
1513
const setUser = useUserStore((state) => state.setUser);
1614
const isLoggedIn = !!user;
1715

18-
const [isOpen, setIsOpen] = useState(false);
19-
const dropdownRef = useRef<HTMLDivElement>(null);
20-
21-
// 바깥 클릭 시 드롭다운 닫기
22-
useEffect(() => {
23-
const handleClickOutside = (e: MouseEvent) => {
24-
if (
25-
dropdownRef.current &&
26-
!dropdownRef.current.contains(e.target as Node)
27-
) {
28-
setIsOpen(false);
29-
}
30-
};
31-
document.addEventListener('mousedown', handleClickOutside);
32-
return () => document.removeEventListener('mousedown', handleClickOutside);
33-
}, []);
34-
35-
// 로그아웃 처리 후 홈 이동
16+
// 로그아웃 처리
3617
const handleLogout = () => {
3718
setUser(null);
3819
router.push('/');
@@ -53,51 +34,20 @@ export default function Header() {
5334
<div className='text-md relative flex items-center gap-24 text-black'>
5435
{isLoggedIn ? (
5536
<>
37+
{/* 알림 아이콘 */}
5638
<button aria-label='알림' className='hover:text-primary'>
5739
<IconBell />
5840
</button>
5941

42+
{/* 구분선 */}
6043
<div className='mx-12 h-22 w-px bg-gray-300' />
6144

62-
{/* 프로필 + 드롭다운 */}
63-
<div
64-
className='flex items-center gap-8 cursor-pointer'
65-
onClick={() => setIsOpen((prev) => !prev)}
66-
ref={dropdownRef}
67-
>
68-
{user.profileImageUrl ? (
69-
<Image
70-
src={user.profileImageUrl}
71-
alt='프로필 이미지'
72-
width={32}
73-
height={32}
74-
className='rounded-full border border-gray-300'
75-
/>
76-
) : (
77-
<div className='size-32 rounded-full border border-gray-300 overflow-hidden'>
78-
<ProfileDefaultIcon size={32} />
79-
</div>
80-
)}
81-
<span>{user.nickname || '사용자'}</span>
82-
83-
{/* 드롭다운 메뉴 */}
84-
{isOpen && (
85-
<div className='absolute top-50 right-0 z-50 mt-20 w-120 rounded-md border border-gray-200 bg-white shadow-md'>
86-
<Link
87-
href='/mypage'
88-
className='block w-full px-16 py-12 text-left hover:bg-gray-50 cursor-pointer'
89-
>
90-
마이페이지
91-
</Link>
92-
<button
93-
onClick={handleLogout}
94-
className='w-full px-16 py-12 text-left hover:bg-gray-50 cursor-pointer'
95-
>
96-
로그아웃
97-
</button>
98-
</div>
99-
)}
100-
</div>
45+
{/* 프로필 드롭다운 */}
46+
<ProfileDropdown
47+
nickname={user.nickname}
48+
profileImageUrl={user.profileImageUrl}
49+
onLogout={handleLogout}
50+
/>
10151
</>
10252
) : (
10353
<>

src/components/ProfileDropdown.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use client';
2+
3+
import Image from 'next/image';
4+
import Link from 'next/link';
5+
import { useEffect, useRef, useState } from 'react';
6+
import { usePathname } from 'next/navigation';
7+
import ProfileDefaultIcon from '@assets/svg/profile-default';
8+
9+
import { ProfileDropdownProps } from '@/types/profileDropdownTypes';
10+
11+
export default function ProfileDropdown({ nickname, profileImageUrl, onLogout }: ProfileDropdownProps) {
12+
const [isOpen, setIsOpen] = useState(false);
13+
const dropdownRef = useRef<HTMLDivElement>(null);
14+
const pathname = usePathname();
15+
16+
// 외부 클릭 시 드롭다운 닫기
17+
useEffect(() => {
18+
const handleClickOutside = (e: MouseEvent) => {
19+
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
20+
setIsOpen(false);
21+
}
22+
};
23+
document.addEventListener('mousedown', handleClickOutside);
24+
return () => document.removeEventListener('mousedown', handleClickOutside);
25+
}, []);
26+
27+
// 경로 변경 시 드롭다운 닫기
28+
useEffect(() => {
29+
setIsOpen(false);
30+
}, [pathname]);
31+
32+
return (
33+
<div className='relative' ref={dropdownRef}>
34+
{/* 프로필 영역 (드롭다운 토글) */}
35+
<div
36+
className='flex items-center gap-8 cursor-pointer'
37+
onClick={() => setIsOpen((prev) => !prev)}
38+
>
39+
{profileImageUrl ? (
40+
<Image
41+
src={profileImageUrl}
42+
alt='프로필 이미지'
43+
width={32}
44+
height={32}
45+
className='rounded-full border border-gray-300'
46+
/>
47+
) : (
48+
<div className='size-32 rounded-full border border-gray-300 overflow-hidden'>
49+
<ProfileDefaultIcon size={32} />
50+
</div>
51+
)}
52+
<span>{nickname || '사용자'}</span>
53+
</div>
54+
55+
{/* 드롭다운 메뉴 */}
56+
{isOpen && (
57+
<div
58+
className='absolute top-50 right-0 z-50 mt-12 w-140 rounded-md border border-gray-200 bg-white shadow-md'
59+
onClick={(e) => e.stopPropagation()}
60+
>
61+
<Link
62+
href='/mypage'
63+
className='block w-full px-16 py-12 text-left hover:bg-gray-50'
64+
>
65+
마이페이지
66+
</Link>
67+
<button
68+
onClick={onLogout}
69+
className='w-full px-16 py-12 text-left hover:bg-gray-50'
70+
>
71+
로그아웃
72+
</button>
73+
</div>
74+
)}
75+
</div>
76+
);
77+
}

src/types/profileDropdownTypes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface ProfileDropdownProps {
2+
nickname: string;
3+
profileImageUrl: string | null;
4+
onLogout: () => void;
5+
}

0 commit comments

Comments
 (0)