레이아웃 변경 대시보드

This commit is contained in:
2026-02-10 17:29:57 +09:00
parent ca01f33d71
commit e5a518b211
3 changed files with 97 additions and 13 deletions

View File

@@ -9,43 +9,54 @@ import { MenuItem } from "../types";
const MENU_ITEMS: MenuItem[] = [
{
title: "대시보드",
href: "/",
href: "/dashboard",
icon: Home,
variant: "default",
matchExact: true,
showInBottomNav: true,
},
{
title: "자동매매",
href: "/trade",
icon: BarChart2,
variant: "ghost",
badge: "LIVE",
showInBottomNav: true,
},
{
title: "자산현황",
href: "/assets",
icon: Wallet,
variant: "ghost",
showInBottomNav: true,
},
{
title: "프로필",
href: "/profile",
icon: User,
variant: "ghost",
showInBottomNav: false,
},
{
title: "설정",
href: "/settings",
icon: Settings,
variant: "ghost",
showInBottomNav: true,
},
];
/**
* @description 메인 좌측 사이드바(데스크탑): 기본 축소 상태에서 hover/focus 시 확장됩니다.
* @see features/layout/components/sidebar.tsx MENU_ITEMS 한 곳에서 메뉴/배지/모바일 탭 구성을 함께 관리합니다.
*/
export function Sidebar() {
const pathname = usePathname();
return (
<aside className="hidden h-[calc(100vh-4rem)] shrink-0 overflow-y-auto border-r border-zinc-200 bg-white py-6 pl-2 pr-6 dark:border-zinc-800 dark:bg-black md:sticky md:top-16 md:block md:w-64 lg:w-72">
<div className="flex flex-col space-y-1">
<aside className="group/sidebar hidden h-[calc(100vh-4rem)] shrink-0 overflow-y-auto border-r border-zinc-200 bg-white px-2 py-5 transition-[width] duration-200 dark:border-zinc-800 dark:bg-black md:sticky md:top-16 md:block md:w-[74px] md:hover:w-64 md:focus-within:w-64">
{/* ========== SIDEBAR ITEMS ========== */}
<div className="flex flex-col space-y-1.5">
{MENU_ITEMS.map((item) => {
const isActive = item.matchExact
? pathname === item.href
@@ -55,22 +66,46 @@ export function Sidebar() {
<Link
key={item.href}
href={item.href}
title={item.title}
className={cn(
"group flex items-center rounded-md px-3 py-2 text-sm font-medium hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-50 transition-colors",
"group/item relative flex items-center rounded-xl px-3 py-2.5 text-sm transition-colors",
"hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-50",
isActive
? "bg-zinc-100 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50"
? "bg-zinc-100 text-zinc-900 shadow-sm dark:bg-zinc-800 dark:text-zinc-50"
: "text-zinc-500 dark:text-zinc-400",
)}
>
<item.icon
{/* ========== ACTIVE BAR ========== */}
<span
className={cn(
"mr-3 h-5 w-5 shrink-0 transition-colors",
isActive
? "text-zinc-900 dark:text-zinc-50"
: "text-zinc-400 group-hover:text-zinc-900 dark:text-zinc-500 dark:group-hover:text-zinc-50",
"absolute left-0 top-1/2 h-5 -translate-y-1/2 rounded-r-full transition-all",
isActive ? "w-1.5 bg-brand-500" : "w-0",
)}
/>
{item.title}
{/* ========== ICON + DOT BADGE ========== */}
<item.icon
className={cn(
"h-5 w-5 shrink-0 transition-colors",
isActive
? "text-zinc-900 dark:text-zinc-50"
: "text-zinc-400 group-hover/item:text-zinc-900 dark:text-zinc-500 dark:group-hover/item:text-zinc-50",
)}
/>
{item.badge && (
<span className="absolute left-7 top-2 h-2 w-2 rounded-full bg-brand-500 md:group-hover/sidebar:hidden md:group-focus-within/sidebar:hidden" />
)}
{/* ========== LABEL (EXPAND ON HOVER/FOCUS) ========== */}
<span className="ml-3 flex min-w-0 items-center gap-1.5 overflow-hidden whitespace-nowrap transition-all duration-200 md:max-w-0 md:opacity-0 md:group-hover/sidebar:max-w-[180px] md:group-hover/sidebar:opacity-100 md:group-focus-within/sidebar:max-w-[180px] md:group-focus-within/sidebar:opacity-100">
<span className="truncate font-medium">{item.title}</span>
{item.badge && (
<span className="shrink-0 rounded-full bg-brand-100 px-1.5 py-0.5 text-[10px] font-semibold text-brand-700">
{item.badge}
</span>
)}
</span>
</Link>
);
})}
@@ -78,3 +113,49 @@ export function Sidebar() {
</aside>
);
}
/**
* @description 모바일 하단 빠른 탭 네비게이션.
* @see features/layout/components/sidebar.tsx Sidebar와 같은 MENU_ITEMS를 공유해 중복 정의를 줄입니다.
*/
export function MobileBottomNav() {
const pathname = usePathname();
const bottomItems = MENU_ITEMS.filter((item) => item.showInBottomNav !== false);
return (
<nav
aria-label="모바일 빠른 메뉴"
className="fixed inset-x-0 bottom-0 z-40 border-t border-zinc-200 bg-white/95 backdrop-blur-sm supports-backdrop-filter:bg-white/80 dark:border-zinc-800 dark:bg-black/95 dark:supports-backdrop-filter:bg-black/80 md:hidden"
>
{/* ========== BOTTOM NAV ITEMS ========== */}
<div className={cn("grid", bottomItems.length === 4 ? "grid-cols-4" : "grid-cols-5")}>
{bottomItems.map((item) => {
const isActive = item.matchExact
? pathname === item.href
: pathname.startsWith(item.href);
return (
<Link
key={`bottom-${item.href}`}
href={item.href}
className={cn(
"flex min-h-16 flex-col items-center justify-center gap-1.5 text-[11px] font-medium transition-colors",
isActive
? "text-brand-700"
: "text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-50",
)}
>
<span className="relative">
<item.icon className={cn("h-4 w-4", isActive && "text-brand-600")} />
{item.badge && (
<span className="absolute -right-1 -top-1 h-2 w-2 rounded-full bg-brand-500" />
)}
</span>
<span className="leading-none">{item.title}</span>
</Link>
);
})}
</div>
</nav>
);
}

View File

@@ -6,4 +6,6 @@ export interface MenuItem {
icon: LucideIcon;
variant: "default" | "ghost";
matchExact?: boolean;
badge?: string;
showInBottomNav?: boolean;
}