@@ -11,6 +11,8 @@ import {
1111 Monitor ,
1212 Settings ,
1313 LogOut ,
14+ Menu ,
15+ X ,
1416} from 'lucide-react' ;
1517import useAuthRedirect from '@/hooks/useAuthRedirect' ;
1618import {
@@ -44,6 +46,7 @@ export default function AuthLayout({ children }: AuthLayoutProps) {
4446 const [ role , setRole ] = useState < Role | null > ( null ) ;
4547 const [ userInfo , setUserInfo ] = useState < UserInfo | null > ( null ) ;
4648 const [ showAccessDenied , setShowAccessDenied ] = useState ( false ) ;
49+ const [ sidebarOpen , setSidebarOpen ] = useState ( false ) ;
4750
4851 useEffect ( ( ) => {
4952 setRole ( getRoleFromToken ( ) ) ;
@@ -68,74 +71,115 @@ export default function AuthLayout({ children }: AuthLayoutProps) {
6871 [ role ] ,
6972 ) ;
7073
74+ const sidebarContent = (
75+ < >
76+ { /* Logo */ }
77+ < div className = "flex h-16 items-center justify-between border-b border-[var(--border)] px-6" >
78+ < Link href = "/rental" className = "flex items-center gap-2" >
79+ < span className = "text-xl font-bold text-[var(--primary)]" > 빌릴게</ span >
80+ < span className = "text-sm text-[var(--sidebar-muted)]" > 관리자 시스템</ span >
81+ </ Link >
82+ < button
83+ onClick = { ( ) => setSidebarOpen ( false ) }
84+ className = "rounded-md p-1 text-[var(--foreground-muted)] hover:bg-[var(--sidebar-hover)] md:hidden cursor-pointer"
85+ >
86+ < X className = "h-5 w-5" />
87+ </ button >
88+ </ div >
89+
90+ { /* Navigation */ }
91+ < nav className = "flex-1 overflow-y-auto py-4" >
92+ < ul className = "space-y-1 px-3" >
93+ { navItems . map ( ( item ) => {
94+ const isActive = pathname === item . href ;
95+ const permitted = hasPermission ( item . href , role ) ;
96+ return (
97+ < li key = { item . href } >
98+ < Link
99+ href = { item . href }
100+ onClick = { ( e ) => {
101+ handleNavClick ( e , item . href ) ;
102+ setSidebarOpen ( false ) ;
103+ } }
104+ className = { `flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${
105+ ! permitted
106+ ? 'cursor-not-allowed text-[var(--foreground-subtle)] opacity-50'
107+ : isActive
108+ ? 'bg-[var(--sidebar-active)] text-[var(--primary)]'
109+ : 'text-[var(--foreground-muted)] hover:bg-[var(--sidebar-hover)] hover:text-[var(--foreground)]'
110+ } `}
111+ >
112+ < item . icon className = "h-5 w-5" />
113+ { item . name }
114+ </ Link >
115+ </ li >
116+ ) ;
117+ } ) }
118+ </ ul >
119+ </ nav >
120+
121+ { /* User Info & Logout */ }
122+ < div className = "border-t border-[var(--border)] p-3" >
123+ { userInfo && (
124+ < div className = "mb-2 rounded-lg bg-[var(--sidebar-hover)] px-3 py-2.5 flex-col" >
125+ < div className = "flex flex-row justify-between" >
126+ < p className = "text-sm font-medium text-[var(--foreground)]" > { userInfo . name } </ p >
127+ < span className = "inline-block rounded-full bg-[var(--primary)] px-2 py-0.5 text-xs font-medium text-white" >
128+ { ROLE_LABEL [ userInfo . role ] }
129+ </ span >
130+ </ div >
131+ < p className = "text-xs text-[var(--foreground-muted)]" > { userInfo . studentId } </ p >
132+ </ div >
133+ ) }
134+ < button
135+ onClick = { ( ) => {
136+ localStorage . removeItem ( 'token' ) ;
137+ router . push ( '/login' ) ;
138+ } }
139+ className = "flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-[var(--foreground-muted)] hover:bg-[var(--sidebar-hover)] hover:text-[var(--error)] cursor-pointer"
140+ >
141+ < LogOut className = "h-5 w-5" />
142+ 로그아웃
143+ </ button >
144+ </ div >
145+ </ >
146+ ) ;
147+
71148 return (
72149 < div className = "flex min-h-screen bg-[var(--background)]" >
150+ { /* Mobile Header */ }
151+ < header className = "fixed left-0 right-0 top-0 z-30 flex h-14 items-center border-b border-[var(--border)] bg-[var(--sidebar)] px-4 md:hidden" >
152+ < button
153+ onClick = { ( ) => setSidebarOpen ( true ) }
154+ className = "rounded-md p-1.5 text-[var(--foreground-muted)] hover:bg-[var(--sidebar-hover)] cursor-pointer"
155+ >
156+ < Menu className = "h-5 w-5" />
157+ </ button >
158+ < Link href = "/rental" className = "ml-3 flex items-center gap-2" >
159+ < span className = "text-lg font-bold text-[var(--primary)]" > 빌릴게</ span >
160+ < span className = "text-xs text-[var(--sidebar-muted)]" > 관리자 시스템</ span >
161+ </ Link >
162+ </ header >
163+
164+ { /* Mobile Sidebar Overlay */ }
165+ { sidebarOpen && (
166+ < div
167+ className = "fixed inset-0 z-40 bg-black/40 md:hidden"
168+ onClick = { ( ) => setSidebarOpen ( false ) }
169+ />
170+ ) }
171+
73172 { /* Sidebar */ }
74- < aside className = "fixed left-0 top-0 z-20 flex h-full w-64 flex-col border-r border-[var(--border)] bg-[var(--sidebar)]" >
75- { /* Logo */ }
76- < div className = "flex h-16 items-center border-b border-[var(--border)] px-6" >
77- < Link href = "/rental" className = "flex items-center gap-2" >
78- < span className = "text-xl font-bold text-[var(--primary)]" > 빌릴게</ span >
79- < span className = "text-sm text-[var(--sidebar-muted)]" > 관리자 시스템</ span >
80- </ Link >
81- </ div >
82-
83- { /* Navigation */ }
84- < nav className = "flex-1 overflow-y-auto py-4" >
85- < ul className = "space-y-1 px-3" >
86- { navItems . map ( ( item ) => {
87- const isActive = pathname === item . href ;
88- const permitted = hasPermission ( item . href , role ) ;
89- return (
90- < li key = { item . href } >
91- < Link
92- href = { item . href }
93- onClick = { ( e ) => handleNavClick ( e , item . href ) }
94- className = { `flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${
95- ! permitted
96- ? 'cursor-not-allowed text-[var(--foreground-subtle)] opacity-50'
97- : isActive
98- ? 'bg-[var(--sidebar-active)] text-[var(--primary)]'
99- : 'text-[var(--foreground-muted)] hover:bg-[var(--sidebar-hover)] hover:text-[var(--foreground)]'
100- } `}
101- >
102- < item . icon className = "h-5 w-5" />
103- { item . name }
104- </ Link >
105- </ li >
106- ) ;
107- } ) }
108- </ ul >
109- </ nav >
110-
111- { /* User Info & Logout */ }
112- < div className = "border-t border-[var(--border)] p-3" >
113- { userInfo && (
114- < div className = "mb-2 rounded-lg bg-[var(--sidebar-hover)] px-3 py-2.5 flex-col" >
115- < div className = "flex flex-row justify-between" >
116- < p className = "text-sm font-medium text-[var(--foreground)]" > { userInfo . name } </ p >
117- < span className = "inline-block rounded-full bg-[var(--primary)] px-2 py-0.5 text-xs font-medium text-white" >
118- { ROLE_LABEL [ userInfo . role ] }
119- </ span >
120- </ div >
121- < p className = "text-xs text-[var(--foreground-muted)]" > { userInfo . studentId } </ p >
122- </ div >
123- ) }
124- < button
125- onClick = { ( ) => {
126- localStorage . removeItem ( 'token' ) ;
127- router . push ( '/login' ) ;
128- } }
129- className = "flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-[var(--foreground-muted)] hover:bg-[var(--sidebar-hover)] hover:text-[var(--error)] cursor-pointer"
130- >
131- < LogOut className = "h-5 w-5" />
132- 로그아웃
133- </ button >
134- </ div >
173+ < aside
174+ className = { `fixed left-0 top-0 z-50 flex h-full w-64 flex-col border-r border-[var(--border)] bg-[var(--sidebar)] transition-transform duration-200 md:z-20 md:translate-x-0 ${
175+ sidebarOpen ? 'translate-x-0' : '-translate-x-full'
176+ } `}
177+ >
178+ { sidebarContent }
135179 </ aside >
136180
137181 { /* Main Content */ }
138- < main className = "ml-64 flex-1 p-8" > { children } </ main >
182+ < main className = "min-w-0 flex-1 pt-14 p-4 md:ml-64 md:pt-0 md: p-8" > { children } </ main >
139183
140184 { /* Access Denied Modal */ }
141185 < AccessDeniedModal isOpen = { showAccessDenied } onClose = { ( ) => setShowAccessDenied ( false ) } />
0 commit comments