feat: Basic 레이아웃 셸과 사이드바 상태 훅 추가
Co-Authored-By: Claude Opus 5
@ae211640b924dec47a6c59212d50257cecef3bfc
--- app/(protected)/(basic)/_components/admin-header.tsx
+++ app/(protected)/(basic)/_components/admin-header.tsx
... | ... | @@ -1,11 +1,39 @@ |
| 1 | 1 |
import { logout } from '@/app/(protected)/_actions';
|
| 2 | 2 |
|
| 3 |
-export function AdminHeader() {
|
|
| 3 |
+interface AdminHeaderProps {
|
|
| 4 |
+ onOpenSidebar: () => void; |
|
| 5 |
+ onToggleSidebarCollapse: () => void; |
|
| 6 |
+} |
|
| 7 |
+ |
|
| 8 |
+export function AdminHeader({
|
|
| 9 |
+ onOpenSidebar, |
|
| 10 |
+ onToggleSidebarCollapse, |
|
| 11 |
+}: AdminHeaderProps) {
|
|
| 4 | 12 |
return ( |
| 5 | 13 |
<header className="flex items-center justify-between border-b border-foreground/15 bg-background px-6 py-4"> |
| 6 |
- <span className="text-base font-semibold text-foreground"> |
|
| 7 |
- 에듀페이 관리자 |
|
| 8 |
- </span> |
|
| 14 |
+ <div className="flex items-center gap-3"> |
|
| 15 |
+ {/* 모바일 전용 — 사이드바를 오버레이로 연다 */}
|
|
| 16 |
+ <button |
|
| 17 |
+ type="button" |
|
| 18 |
+ aria-label="사이드바 열기" |
|
| 19 |
+ onClick={onOpenSidebar}
|
|
| 20 |
+ className="border border-foreground/25 px-2 py-1 text-sm text-foreground md:hidden" |
|
| 21 |
+ > |
|
| 22 |
+ 메뉴 |
|
| 23 |
+ </button> |
|
| 24 |
+ {/* 데스크톱 전용 — 사이드바를 접거나 펼친다 */}
|
|
| 25 |
+ <button |
|
| 26 |
+ type="button" |
|
| 27 |
+ aria-label="사이드바 접기/펼치기" |
|
| 28 |
+ onClick={onToggleSidebarCollapse}
|
|
| 29 |
+ className="hidden border border-foreground/25 px-2 py-1 text-sm text-foreground md:inline-flex" |
|
| 30 |
+ > |
|
| 31 |
+ 메뉴 |
|
| 32 |
+ </button> |
|
| 33 |
+ <span className="text-base font-semibold text-foreground"> |
|
| 34 |
+ 에듀페이 관리자 |
|
| 35 |
+ </span> |
|
| 36 |
+ </div> |
|
| 9 | 37 |
<form action={logout}>
|
| 10 | 38 |
<button |
| 11 | 39 |
type="submit" |
+++ app/(protected)/(basic)/_components/admin-shell.tsx
... | ... | @@ -0,0 +1,39 @@ |
| 1 | +'use client'; | |
| 2 | + | |
| 3 | +import { useSidebar } from '@/app/(protected)/(basic)/_hooks/use-sidebar'; | |
| 4 | +import { AdminFooter } from './admin-footer'; | |
| 5 | +import { AdminHeader } from './admin-header'; | |
| 6 | +import { AdminSidebar } from './admin-sidebar'; | |
| 7 | + | |
| 8 | +interface AdminShellProps { | |
| 9 | + children: React.ReactNode; | |
| 10 | +} | |
| 11 | + | |
| 12 | +/** | |
| 13 | + * Basic 레이아웃의 상호작용 경계 — 헤더·사이드바·풋터를 조립하고 사이드바 상태(useSidebar)를 | |
| 14 | + * 소유한다. page 콘텐츠(children)는 상위 Server Component가 렌더한 결과를 props로 그대로 | |
| 15 | + * 통과시켜 RSC를 유지한다 — 이 컴포넌트 자신은 children의 내용을 알거나 가공하지 않는다. | |
| 16 | + */ | |
| 17 | +export function AdminShell({ children }: AdminShellProps) { | |
| 18 | + const sidebar = useSidebar(); | |
| 19 | + | |
| 20 | + return ( | |
| 21 | + <div className="flex min-h-screen flex-col"> | |
| 22 | + <AdminHeader | |
| 23 | + onOpenSidebar={sidebar.openMobile} | |
| 24 | + onToggleSidebarCollapse={sidebar.toggleDesktopCollapse} | |
| 25 | + /> | |
| 26 | + <div className="flex flex-1"> | |
| 27 | + <AdminSidebar | |
| 28 | + isMobileOpen={sidebar.isMobileOpen} | |
| 29 | + isDesktopCollapsed={sidebar.isDesktopCollapsed} | |
| 30 | + onCloseMobile={sidebar.closeMobile} | |
| 31 | + /> | |
| 32 | + <div className="flex flex-1 flex-col"> | |
| 33 | + <main className="flex flex-1 flex-col">{children}</main> | |
| 34 | + <AdminFooter /> | |
| 35 | + </div> | |
| 36 | + </div> | |
| 37 | + </div> | |
| 38 | + ); | |
| 39 | +} |
--- app/(protected)/(basic)/layout.tsx
+++ app/(protected)/(basic)/layout.tsx
... | ... | @@ -1,16 +1,12 @@ |
| 1 |
-import { AdminHeader } from './_components/admin-header';
|
|
| 1 |
+import { AdminShell } from './_components/admin-shell';
|
|
| 2 | 2 |
|
| 3 |
-// Basic 레이아웃 — 헤더가 있는 일반 관리자 화면 진입점. 보안 강제는 상위 `(protected)/layout.tsx`가 |
|
| 4 |
-// 담당하므로 여기서는 크롬 조립만 책임진다. |
|
| 3 |
+// Basic 레이아웃 — 헤더·사이드바·풋터가 있는 일반 관리자 화면 진입점. 보안 강제는 상위 |
|
| 4 |
+// `(protected)/layout.tsx`가 담당하므로 여기서는 크롬 조립(AdminShell)만 책임진다. page |
|
| 5 |
+// 콘텐츠는 Server Component로 렌더된 그대로 AdminShell의 children으로 통과한다. |
|
| 5 | 6 |
export default function Layout({
|
| 6 | 7 |
children, |
| 7 | 8 |
}: Readonly<{
|
| 8 | 9 |
children: React.ReactNode; |
| 9 | 10 |
}>) {
|
| 10 |
- return ( |
|
| 11 |
- <div className="flex min-h-screen flex-col"> |
|
| 12 |
- <AdminHeader /> |
|
| 13 |
- <main className="flex flex-1 flex-col">{children}</main>
|
|
| 14 |
- </div> |
|
| 15 |
- ); |
|
| 11 |
+ return <AdminShell>{children}</AdminShell>;
|
|
| 16 | 12 |
} |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?