임동욱 임동욱 07-27
refactor: 레이아웃 크롬을 공용 UI 컴포넌트로 교체
Co-Authored-By: Claude Opus 5 
@72f3c1354382f0b74698195aa59c8045ebe885a0
app/(protected)/(basic)/_components/admin-header.tsx
--- app/(protected)/(basic)/_components/admin-header.tsx
+++ app/(protected)/(basic)/_components/admin-header.tsx
@@ -1,39 +1,46 @@
 import { logout } from '@/app/(protected)/_actions';
+import { Header } from '@/components/ui/header';
 
 interface AdminHeaderProps {
   onOpenSidebar: () => void;
   onToggleSidebarCollapse: () => void;
 }
 
+/**
+ * 헤더 배치 — 비주얼 셸(Header)은 design 공용 컴포넌트를 그대로 쓰고, 사이드바 토글 버튼(공용
+ * 컴포넌트가 제공하지 않는 상호작용)만 `leading` 슬롯에 주입한다. 로그아웃은 `children`(우측
+ * 슬롯)에 주입한다.
+ */
 export function AdminHeader({
   onOpenSidebar,
   onToggleSidebarCollapse,
 }: AdminHeaderProps) {
   return (
-    <header className="flex items-center justify-between border-b border-foreground/15 bg-background px-6 py-4">
-      <div className="flex items-center gap-3">
-        {/* 모바일 전용 — 사이드바를 오버레이로 연다 */}
-        <button
-          type="button"
-          aria-label="사이드바 열기"
-          onClick={onOpenSidebar}
-          className="border border-foreground/25 px-2 py-1 text-sm text-foreground md:hidden"
-        >
-          메뉴
-        </button>
-        {/* 데스크톱 전용 — 사이드바를 접거나 펼친다 */}
-        <button
-          type="button"
-          aria-label="사이드바 접기/펼치기"
-          onClick={onToggleSidebarCollapse}
-          className="hidden border border-foreground/25 px-2 py-1 text-sm text-foreground md:inline-flex"
-        >
-          메뉴
-        </button>
-        <span className="text-base font-semibold text-foreground">
-          에듀페이 관리자
-        </span>
-      </div>
+    <Header
+      title="에듀페이 관리자"
+      leading={
+        <>
+          {/* 모바일 전용 — 사이드바를 오버레이로 연다 */}
+          <button
+            type="button"
+            aria-label="사이드바 열기"
+            onClick={onOpenSidebar}
+            className="border border-foreground/25 px-2 py-1 text-sm text-foreground md:hidden"
+          >
+            메뉴
+          </button>
+          {/* 데스크톱 전용 — 사이드바를 접거나 펼친다 */}
+          <button
+            type="button"
+            aria-label="사이드바 접기/펼치기"
+            onClick={onToggleSidebarCollapse}
+            className="hidden border border-foreground/25 px-2 py-1 text-sm text-foreground md:inline-flex"
+          >
+            메뉴
+          </button>
+        </>
+      }
+    >
       <form action={logout}>
         <button
           type="submit"
@@ -42,6 +49,6 @@
           로그아웃
         </button>
       </form>
-    </header>
+    </Header>
   );
 }
app/(protected)/(basic)/_components/admin-sidebar.tsx
--- app/(protected)/(basic)/_components/admin-sidebar.tsx
+++ app/(protected)/(basic)/_components/admin-sidebar.tsx
@@ -1,5 +1,6 @@
-import Link from 'next/link';
 import { usePathname } from 'next/navigation';
+import { Sidebar } from '@/components/ui/sidebar';
+import { SidebarLink } from '@/components/ui/sidebar-link';
 
 interface SidebarNavItem {
   label: string;
@@ -16,8 +17,14 @@
 }
 
 /**
- * 사이드바 View — 내비 목록을 그린다. 데스크톱 push/접힘과 모바일 오버레이+백드롭은 전부
- * Tailwind 반응형 클래스로 처리한다(CSS-first) — JS로 브레이크포인트를 판별하지 않는다.
+ * 사이드바 배치 — 비주얼 셸(Sidebar)과 내비 항목(SidebarLink)은 design 공용 컴포넌트를 그대로
+ * 쓰고, 데스크톱 push/접힘과 모바일 오버레이+백드롭 등 반응형 배치(공용 컴포넌트가 제공하지 않음)만
+ * className 패스스루로 처리한다. 전부 Tailwind 반응형 클래스(CSS-first) — JS로 브레이크포인트를
+ * 판별하지 않는다.
+ *
+ * 주의(공용 컴포넌트 부족분): SidebarLink는 아이콘 전용(라벨 숨김) variant를 제공하지 않는다.
+ * 현재 유일한 내비 항목("홈")은 라벨이 1글자라 접힘 폭(w-16)에서도 시각적으로 문제없지만,
+ * 라벨이 긴 항목이 추가되면 SidebarLink에 collapsed/아이콘 전용 variant 추가가 필요하다.
  */
 export function AdminSidebar({
   isMobileOpen,
@@ -33,42 +40,23 @@
           type="button"
           aria-label="사이드바 닫기"
           onClick={onCloseMobile}
-          className="fixed inset-0 z-30 bg-foreground/50 md:hidden"
+          className="fixed inset-0 z-30 bg-scrim md:hidden"
         />
       )}
-      <aside
-        className={`fixed inset-y-0 left-0 z-40 flex flex-col overflow-hidden border-r border-foreground/15 bg-background transition-all duration-200 md:relative md:z-auto md:translate-x-0 ${
-          isMobileOpen ? 'w-64 translate-x-0' : 'w-64 -translate-x-full'
+      <Sidebar
+        className={`fixed inset-y-0 left-0 z-40 overflow-hidden transition-all duration-200 md:relative md:z-auto md:translate-x-0 ${
+          isMobileOpen ? 'translate-x-0' : '-translate-x-full'
         } ${isDesktopCollapsed ? 'md:w-16' : 'md:w-64'}`}
       >
-        <nav className="flex flex-1 flex-col gap-1 p-4">
-          {SIDEBAR_NAV_ITEMS.map((item) => {
-            const isActive = pathname === item.href;
-
-            return (
-              <Link
-                key={item.href}
-                href={item.href}
-                aria-current={isActive ? 'page' : undefined}
-                className={`rounded-md px-3 py-2 text-sm whitespace-nowrap ${
-                  isActive
-                    ? 'bg-foreground/10 text-foreground'
-                    : 'text-foreground/70'
-                }`}
-              >
-                <span className={isDesktopCollapsed ? 'md:hidden' : undefined}>
-                  {item.label}
-                </span>
-                {isDesktopCollapsed && (
-                  <span aria-hidden className="hidden md:inline">
-                    {item.label.slice(0, 1)}
-                  </span>
-                )}
-              </Link>
-            );
-          })}
-        </nav>
-      </aside>
+        {SIDEBAR_NAV_ITEMS.map((item) => (
+          <SidebarLink
+            key={item.href}
+            href={item.href}
+            label={item.label}
+            isActive={pathname === item.href}
+          />
+        ))}
+      </Sidebar>
     </>
   );
 }
app/(public)/layout.tsx
--- app/(public)/layout.tsx
+++ app/(public)/layout.tsx
@@ -1,13 +1,12 @@
-// Auth 레이아웃 — 배경 위 카드를 화면 중앙에 배치하는 공용 래퍼. `(public)` 하위 화면(로그인 등)이
-// 공유한다.
+import { AuthCard } from '@/components/ui/auth-card';
+
+// Auth 레이아웃 — design 공용 AuthCard(프라이머리 배경 + 로고 + 카드)로 `(public)` 하위 화면
+// (로그인 등)을 감싼다. 화면별 제목은 AuthCard의 title 슬롯 대신 각 페이지 콘텐츠가 자체
+// 렌더한다 — 레이아웃은 여러 화면이 공유하므로 화면별 제목을 여기서 결정하기 어렵다.
 export default function Layout({
   children,
 }: Readonly<{
   children: React.ReactNode;
 }>) {
-  return (
-    <main className="flex min-h-screen flex-1 items-center justify-center bg-background p-6">
-      {children}
-    </main>
-  );
+  return <AuthCard>{children}</AuthCard>;
 }
app/(public)/login/_components/login-form.tsx
--- app/(public)/login/_components/login-form.tsx
+++ app/(public)/login/_components/login-form.tsx
@@ -9,10 +9,7 @@
   const [state, formAction, isPending] = useActionState(login, initialState);
 
   return (
-    <form
-      action={formAction}
-      className="flex w-full max-w-sm flex-col gap-4 border border-foreground/15 p-8"
-    >
+    <form action={formAction} className="flex w-full flex-col gap-4">
       <h1 className="text-xl font-semibold text-foreground">
         에듀페이 관리자 로그인
       </h1>
Add a comment
List