66 * 로그인 상태: 사용자 이름 + 프로필 이미지 (클릭 시 로그아웃/회원탈퇴 드롭다운)
77 */
88import { useState } from 'react' ;
9- import { useNavigate } from 'react-router-dom' ;
9+ import { useLocation , useNavigate } from 'react-router-dom' ;
1010
1111import { useQueryClient } from '@tanstack/react-query' ;
1212
1313import { apiClient } from '@/api/client' ;
1414import LoginIcon from '@/assets/icons/icon-login.svg?react' ;
1515import LogoutIcon from '@/assets/icons/icon-logout.svg?react' ;
16- import { Dropdown } from '@/components/common/Dropdown ' ;
16+ import { Popover } from '@/components/common/Popover ' ;
1717import { UserAvatar } from '@/components/common/UserAvatar' ;
1818import { useAuthStore } from '@/stores/authStore' ;
1919import { useHomeStore } from '@/stores/homeStore' ;
20+ import { useThemeStore } from '@/stores/themeStore' ;
2021import { isAnonymousEmail } from '@/utils/auth' ;
2122import { showToast } from '@/utils/toast' ;
2223import { getUserDisplayName } from '@/utils/user' ;
@@ -27,18 +28,23 @@ import { WithdrawConfirmModal } from './WithdrawConfirmModal';
2728export function LoginButton ( ) {
2829 const queryClient = useQueryClient ( ) ;
2930 const navigate = useNavigate ( ) ;
31+ const { pathname } = useLocation ( ) ;
3032 const accessToken = useAuthStore ( ( s ) => s . accessToken ) ;
3133 const user = useAuthStore ( ( s ) => s . user ) ;
3234 const openLoginModal = useAuthStore ( ( s ) => s . openLoginModal ) ;
3335 const logout = useAuthStore ( ( s ) => s . logout ) ;
3436 const resetHome = useHomeStore ( ( s ) => s . reset ) ;
37+ const resolvedTheme = useThemeStore ( ( s ) => s . resolvedTheme ) ;
38+ const setTheme = useThemeStore ( ( s ) => s . setTheme ) ;
3539
3640 const [ isWithdrawModalOpen , setIsWithdrawModalOpen ] = useState ( false ) ;
3741 const [ isWithdrawing , setIsWithdrawing ] = useState ( false ) ;
3842
3943 const isGuest = ! accessToken ;
4044 const isAnon = accessToken && isAnonymousEmail ( user ?. email ) ;
4145 const isSocial = accessToken && user ?. email && ! isAnonymousEmail ( user . email ) ;
46+ const isSlideRoute = / \/ s l i d e \/ ? $ / . test ( pathname ) ;
47+ const isDark = resolvedTheme === 'dark' ;
4248
4349 const handleLogout = ( ) => {
4450 logout ( ) ;
@@ -50,7 +56,14 @@ export function LoginButton() {
5056 } ;
5157 // 로그인 전 (게스트)
5258 if ( isGuest ) {
53- return < HeaderButton text = "로그인" icon = { < LoginIcon /> } onClick = { openLoginModal } /> ;
59+ return (
60+ < HeaderButton
61+ text = "로그인"
62+ icon = { < LoginIcon /> }
63+ onClick = { openLoginModal }
64+ iconOnlyOnMobile = { isSlideRoute }
65+ />
66+ ) ;
5467 }
5568
5669 // 익명 사용자
@@ -60,7 +73,14 @@ export function LoginButton() {
6073
6174 // 소셜이 아닌데 여기까지 왔다면(비정상 상태) 방어
6275 if ( ! isSocial ) {
63- return < HeaderButton text = "로그인" icon = { < LoginIcon /> } onClick = { openLoginModal } /> ;
76+ return (
77+ < HeaderButton
78+ text = "로그인"
79+ icon = { < LoginIcon /> }
80+ onClick = { openLoginModal }
81+ iconOnlyOnMobile = { isSlideRoute }
82+ />
83+ ) ;
6484 }
6585
6686 const handleWithdraw = async ( ) => {
@@ -81,40 +101,101 @@ export function LoginButton() {
81101
82102 return (
83103 < >
84- < Dropdown
104+ < Popover
85105 key = { `${ accessToken ?? 'guest' } -${ user ?. id ?? 'nouser' } ` }
86106 position = "bottom"
87107 align = "end"
88108 ariaLabel = "사용자 메뉴"
109+ className = "mt-2 w-80 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-[0_0.5rem_1.25rem_rgba(0,0,0,0.08)]"
89110 trigger = {
90111 < button
91112 type = "button"
92- className = "flex cursor-pointer items-center gap-2 text-body-s-bold text-gray-800 transition-colors hover:text -gray-600 "
113+ className = "flex cursor-pointer items-center gap-2 rounded-full px-2 py-1 text-body-s-bold text-gray-800 transition-colors hover:bg -gray-100 "
93114 >
94- { displayName }
115+ < span
116+ className = { isSlideRoute ? 'hidden md:inline max-w-24 truncate' : 'max-w-24 truncate' }
117+ >
118+ { displayName }
119+ </ span >
95120 < UserAvatar src = { user . profileImage } alt = { displayName } size = { 24 } />
96121 </ button >
97122 }
98- items = { [
99- {
100- id : 'logout' ,
101- label : (
102- < span className = "flex items-center gap-1" >
103- 로그아웃
104- < LogoutIcon className = "size-6" />
105- </ span >
106- ) ,
107- onClick : handleLogout ,
108- variant : 'danger' ,
109- } ,
110- {
111- id : 'withdraw' ,
112- label : '회원 탈퇴' ,
113- onClick : ( ) => setIsWithdrawModalOpen ( true ) ,
114- variant : 'danger' ,
115- } ,
116- ] }
117- />
123+ >
124+ { ( { close } ) => (
125+ < div className = "p-3" >
126+ < div className = "rounded-lg px-3 py-3" >
127+ < p className = "text-caption-bold text-gray-600" > 내 계정</ p >
128+ < div className = "mt-2 flex items-center gap-3" >
129+ < UserAvatar src = { user . profileImage } alt = { displayName } size = { 42 } />
130+ < div className = "min-w-0" >
131+ < p className = "truncate text-body-m-bold text-gray-800" > { displayName } </ p >
132+ < p className = "truncate text-caption text-gray-600" > { user . email } </ p >
133+ </ div >
134+ </ div >
135+ </ div >
136+
137+ < div className = "mt-2 border-t border-gray-200 pt-2" >
138+ < button
139+ type = "button"
140+ className = "flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-left transition-colors hover:bg-gray-100"
141+ onClick = { ( ) => setTheme ( isDark ? 'light' : 'dark' ) }
142+ >
143+ < div >
144+ < p className = "text-body-s-bold text-gray-800" > 테마</ p >
145+ < p className = "text-caption text-gray-600" >
146+ { isDark ? '다크 모드 사용 중' : '라이트 모드 사용 중' }
147+ </ p >
148+ </ div >
149+ < span
150+ className = { `relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
151+ isDark ? 'bg-main' : 'bg-gray-400'
152+ } `}
153+ aria-hidden = "true"
154+ >
155+ < span
156+ className = { `inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
157+ isDark ? 'translate-x-4' : 'translate-x-1'
158+ } `}
159+ />
160+ </ span >
161+ </ button >
162+ </ div >
163+
164+ < div className = "mt-1 border-t border-gray-200 pt-2" >
165+ < button
166+ type = "button"
167+ className = "flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-left text-body-s text-gray-800 transition-colors hover:bg-gray-100"
168+ onClick = { ( ) => {
169+ close ( ) ;
170+ handleLogout ( ) ;
171+ } }
172+ >
173+ < div >
174+ < p className = "text-body-s-bold" > 로그아웃</ p >
175+ < p className = "text-caption text-gray-600" > 현재 계정에서 로그아웃합니다.</ p >
176+ </ div >
177+ < LogoutIcon className = "size-5 text-gray-400" />
178+ </ button >
179+ </ div >
180+
181+ < div className = "mt-1 border-t border-gray-200 pt-2" >
182+ < button
183+ type = "button"
184+ className = "w-full rounded-lg px-3 py-2 text-left transition-colors hover:bg-gray-100"
185+ onClick = { ( ) => {
186+ close ( ) ;
187+ setIsWithdrawModalOpen ( true ) ;
188+ } }
189+ >
190+ < p className = "text-caption-bold text-error" > 회원 탈퇴</ p >
191+ < p className = "text-caption text-gray-600" >
192+ 계정과 데이터가 삭제되며 되돌릴 수 없습니다.
193+ </ p >
194+ </ button >
195+ </ div >
196+ </ div >
197+ ) }
198+ </ Popover >
118199
119200 < WithdrawConfirmModal
120201 isOpen = { isWithdrawModalOpen }
0 commit comments