1- import { Link , Outlet , useLocation } from "react-router" ;
2- import { Menu , UserRound , X } from "lucide-react" ;
1+ import { Link , Outlet , useLocation , useNavigate } from "react-router" ;
2+ import { Github , LogOut , Menu , Settings , UserRound , UsersRound , X } from "lucide-react" ;
33import { useEffect , useState } from "react" ;
44import { motion } from "motion/react" ;
55import { CodeDockWordmark } from "./CodeDockWordmark" ;
66import { CoffeeLogo } from "./CoffeeLogo" ;
77import { Footer } from "./Footer" ;
88import { LanguageToggleButton } from "./LanguageToggleButton" ;
99import { ThemeToggleButton } from "./ThemeToggleButton" ;
10+ import {
11+ DropdownMenu ,
12+ DropdownMenuContent ,
13+ DropdownMenuItem ,
14+ DropdownMenuLabel ,
15+ DropdownMenuSeparator ,
16+ DropdownMenuTrigger ,
17+ } from "./ui/dropdown-menu" ;
1018import { useTheme } from "../contexts/ThemeContext" ;
1119
1220const navItems = [
@@ -24,11 +32,15 @@ const currentUser = {
2432
2533export function Layout ( ) {
2634 const location = useLocation ( ) ;
35+ const navigate = useNavigate ( ) ;
2736 const [ mobileMenuOpen , setMobileMenuOpen ] = useState ( false ) ;
2837 const [ remoteNav , setRemoteNav ] = useState ( false ) ;
2938 const { colors } = useTheme ( ) ;
3039
3140 const isActive = ( path : string ) => location . pathname === path ;
41+ const handleLogout = ( ) => {
42+ navigate ( "/login" ) ;
43+ } ;
3244
3345 useEffect ( ( ) => {
3446 const handleScroll = ( ) => {
@@ -138,9 +150,12 @@ export function Layout() {
138150 >
139151 < ThemeToggleButton />
140152
153+ < AccountMenu variant = "full" onLogout = { handleLogout } />
154+ < AccountMenu variant = "icon" onLogout = { handleLogout } />
155+
141156 < Link
142157 to = "/profile"
143- className = "hidden h-12 items-center gap-3 rounded-2xl px-3 no-underline transition-all hover:scale-[1.02] xl:flex "
158+ className = "hidden"
144159 style = { {
145160 background : `linear-gradient(135deg, ${ colors . primary } , 0.12), rgba(234, 247, 255, 0.045))` ,
146161 border : `1px solid ${ colors . primary } , 0.22)` ,
@@ -165,7 +180,7 @@ export function Layout() {
165180
166181 < Link
167182 to = "/profile"
168- className = "grid h-10 w-10 place-items-center rounded-xl no-underline transition-all hover:scale-110 xl: hidden"
183+ className = "hidden"
169184 style = { {
170185 background : `${ colors . primary } , 0.10)` ,
171186 border : `1px solid ${ colors . primary } , 0.22)` ,
@@ -199,9 +214,10 @@ export function Layout() {
199214 transform : remoteNav ? "translateY(0) scale(1)" : "translateY(-8px) scale(0.96)" ,
200215 } }
201216 >
217+ < AccountMenu variant = "compact" onLogout = { handleLogout } tabIndex = { remoteNav ? 0 : - 1 } />
202218 < Link
203219 to = "/profile"
204- className = "hidden h-12 items-center gap-2 rounded-full px-3 no-underline lg:flex "
220+ className = "hidden"
205221 style = { {
206222 background : `linear-gradient(135deg, ${ colors . primary } , 0.12), rgba(234, 247, 255, 0.045))` ,
207223 border : `1px solid ${ colors . primary } , 0.22)` ,
@@ -240,6 +256,39 @@ export function Layout() {
240256 </ span >
241257 </ span >
242258 </ Link >
259+ < HeaderLink
260+ item = { { path : "/settings" , label : "계정 설정" } }
261+ active = { isActive ( "/settings" ) }
262+ onClick = { ( ) => setMobileMenuOpen ( false ) }
263+ />
264+ < HeaderLink
265+ item = { { path : "/profile" , label : "GitHub 연동 관리" } }
266+ active = { false }
267+ onClick = { ( ) => setMobileMenuOpen ( false ) }
268+ />
269+ < HeaderLink
270+ item = { { path : "/chat" , label : "워크스페이스 / 팀 관리" } }
271+ active = { isActive ( "/chat" ) }
272+ onClick = { ( ) => setMobileMenuOpen ( false ) }
273+ />
274+ < button
275+ type = "button"
276+ onClick = { ( ) => {
277+ setMobileMenuOpen ( false ) ;
278+ handleLogout ( ) ;
279+ } }
280+ className = "relative rounded-full px-4 py-2 text-left tracking-tight transition-colors"
281+ style = { {
282+ background : "transparent" ,
283+ border : "none" ,
284+ color : "#FF6B6B" ,
285+ fontSize : "14px" ,
286+ fontWeight : 800 ,
287+ cursor : "pointer" ,
288+ } }
289+ >
290+ 로그아웃
291+ </ button >
243292 { navItems . map ( ( item ) => (
244293 < HeaderLink
245294 key = { item . path }
@@ -261,6 +310,156 @@ export function Layout() {
261310 ) ;
262311}
263312
313+ interface AccountMenuProps {
314+ variant : "full" | "icon" | "compact" ;
315+ onLogout : ( ) => void ;
316+ tabIndex ?: number ;
317+ }
318+
319+ function AccountMenu ( { variant, onLogout, tabIndex } : AccountMenuProps ) {
320+ const { colors } = useTheme ( ) ;
321+ const isFull = variant === "full" ;
322+ const isCompact = variant === "compact" ;
323+
324+ return (
325+ < DropdownMenu >
326+ < DropdownMenuTrigger asChild >
327+ < button
328+ type = "button"
329+ className = {
330+ isFull
331+ ? "hidden h-12 items-center gap-3 rounded-2xl px-3 transition-all hover:scale-[1.02] xl:flex"
332+ : isCompact
333+ ? "hidden h-12 items-center gap-2 rounded-full px-3 transition-all hover:scale-[1.02] lg:flex"
334+ : "grid h-10 w-10 place-items-center rounded-xl transition-all hover:scale-110 xl:hidden"
335+ }
336+ style = {
337+ isFull
338+ ? {
339+ background : `linear-gradient(135deg, ${ colors . primary } , 0.12), rgba(234, 247, 255, 0.045))` ,
340+ border : `1px solid ${ colors . primary } , 0.22)` ,
341+ color : "var(--white)" ,
342+ boxShadow : `0 0 24px ${ colors . primary } , 0.10), inset 0 1px 0 rgba(255, 255, 255, 0.10)` ,
343+ backdropFilter : "blur(16px) saturate(180%)" ,
344+ cursor : "pointer" ,
345+ }
346+ : isCompact
347+ ? {
348+ background : `linear-gradient(135deg, ${ colors . primary } , 0.12), rgba(234, 247, 255, 0.045))` ,
349+ border : `1px solid ${ colors . primary } , 0.22)` ,
350+ color : "var(--white)" ,
351+ boxShadow : `0 18px 55px rgba(0, 0, 0, 0.38), 0 0 24px ${ colors . primary } , 0.10)` ,
352+ backdropFilter : "blur(22px) saturate(190%)" ,
353+ WebkitBackdropFilter : "blur(22px) saturate(190%)" ,
354+ cursor : "pointer" ,
355+ }
356+ : {
357+ background : `${ colors . primary } , 0.10)` ,
358+ border : `1px solid ${ colors . primary } , 0.22)` ,
359+ color : colors . primaryHex ,
360+ cursor : "pointer" ,
361+ }
362+ }
363+ aria-label = "계정 메뉴 열기"
364+ title = { `${ currentUser . name } - ${ currentUser . email } ` }
365+ tabIndex = { tabIndex }
366+ >
367+ { variant === "icon" ? (
368+ < UserRound size = { 19 } strokeWidth = { 2.4 } />
369+ ) : (
370+ < >
371+ < AccountAvatar />
372+ < span className = "grid min-w-0 leading-none" >
373+ < span
374+ className = {
375+ isCompact
376+ ? "max-w-[92px] truncate text-sm font-black tracking-tight"
377+ : "max-w-[112px] truncate text-sm font-black tracking-tight"
378+ }
379+ >
380+ { currentUser . name }
381+ </ span >
382+ { isFull && (
383+ < span
384+ className = "mt-1 max-w-[112px] truncate text-[11px] font-bold tracking-tight"
385+ style = { { color : "rgba(234, 247, 255, 0.62)" } }
386+ >
387+ { currentUser . workspace }
388+ </ span >
389+ ) }
390+ </ span >
391+ </ >
392+ ) }
393+ </ button >
394+ </ DropdownMenuTrigger >
395+ < DropdownMenuContent
396+ align = "end"
397+ sideOffset = { 10 }
398+ className = "w-64 rounded-2xl p-2"
399+ style = { {
400+ background : "rgba(5, 11, 20, 0.96)" ,
401+ border : `1px solid ${ colors . primary } , 0.22)` ,
402+ color : "var(--white)" ,
403+ boxShadow : `0 18px 55px rgba(0, 0, 0, 0.45), 0 0 28px ${ colors . primary } , 0.10)` ,
404+ backdropFilter : "blur(22px) saturate(180%)" ,
405+ } }
406+ >
407+ < DropdownMenuLabel className = "px-3 py-2" >
408+ < span className = "block truncate text-sm font-black tracking-tight" > { currentUser . name } </ span >
409+ < span className = "mt-1 block truncate text-xs font-bold tracking-tight" style = { { color : "var(--muted)" } } >
410+ { currentUser . email }
411+ </ span >
412+ </ DropdownMenuLabel >
413+ < DropdownMenuSeparator style = { { background : `${ colors . primary } , 0.14)` } } />
414+ < AccountMenuLink to = "/profile" icon = { UserRound } label = "프로필 보기" />
415+ < AccountMenuLink to = "/settings" icon = { Settings } label = "계정 설정" />
416+ < AccountMenuLink to = "/profile" icon = { Github } label = "GitHub 연동 관리" />
417+ < AccountMenuLink to = "/chat" icon = { UsersRound } label = "워크스페이스 / 팀 관리" />
418+ < DropdownMenuSeparator style = { { background : `${ colors . primary } , 0.14)` } } />
419+ < DropdownMenuItem
420+ onSelect = { onLogout }
421+ className = "rounded-xl px-3 py-2.5 tracking-tight"
422+ style = { {
423+ color : "#FF6B6B" ,
424+ cursor : "pointer" ,
425+ fontSize : "14px" ,
426+ fontWeight : 900 ,
427+ } }
428+ >
429+ < LogOut size = { 17 } strokeWidth = { 2.2 } />
430+ 로그아웃
431+ </ DropdownMenuItem >
432+ </ DropdownMenuContent >
433+ </ DropdownMenu >
434+ ) ;
435+ }
436+
437+ interface AccountMenuLinkProps {
438+ to : string ;
439+ icon : typeof UserRound ;
440+ label : string ;
441+ }
442+
443+ function AccountMenuLink ( { to, icon : Icon , label } : AccountMenuLinkProps ) {
444+ return (
445+ < DropdownMenuItem asChild className = "rounded-xl px-3 py-2.5 tracking-tight" >
446+ < Link
447+ to = { to }
448+ className = "flex items-center gap-2 no-underline"
449+ style = { {
450+ color : "var(--white)" ,
451+ cursor : "pointer" ,
452+ fontSize : "14px" ,
453+ fontWeight : 900 ,
454+ } }
455+ >
456+ < Icon size = { 17 } strokeWidth = { 2.2 } />
457+ { label }
458+ </ Link >
459+ </ DropdownMenuItem >
460+ ) ;
461+ }
462+
264463function AccountAvatar ( ) {
265464 const { colors } = useTheme ( ) ;
266465
0 commit comments