+
{name}{rank ? ` · ${rank}` : ""}
{email}
-
+
{open && (
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index bc6ac16..20eaa4f 100644
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -2,12 +2,15 @@ import { useState } from "react";
import { Outlet } from "react-router-dom";
import { Sidebar } from "./Sidebar";
import { Topbar } from "./Topbar";
+import { MobileTabBar } from "./MobileTabBar";
+import { MobileDrawer } from "./MobileDrawer";
export function AppShell() {
- // Sidebar collapse state, persisted so it survives reloads.
+ // Desktop sidebar collapse (md+), persisted.
const [collapsed, setCollapsed] = useState(
() => localStorage.getItem("spin.sidebarCollapsed") === "1"
);
+ const [drawerOpen, setDrawerOpen] = useState(false);
const toggle = () => {
setCollapsed((c) => {
const next = !c;
@@ -18,13 +21,24 @@ export function AppShell() {
return (
-
+ {/* desktop sidebar (hidden on mobile) */}
+
+
-
-
+ setDrawerOpen(true)}
+ />
+ {/* extra bottom padding on mobile so content clears the tab bar */}
+
+
+ {/* mobile-only nav */}
+
setDrawerOpen(true)} />
+ setDrawerOpen(false)} />
);
}
diff --git a/src/components/MobileDrawer.tsx b/src/components/MobileDrawer.tsx
new file mode 100644
index 0000000..ef33478
--- /dev/null
+++ b/src/components/MobileDrawer.tsx
@@ -0,0 +1,92 @@
+import { useEffect } from "react";
+import { NavLink, Link } from "react-router-dom";
+import { useQuery } from "@tanstack/react-query";
+import { X, UserCircle, Settings, LogOut, LayoutDashboard } from "lucide-react";
+import { getNav, getApprovals, logout, avatarUrl } from "@/lib/api";
+import { useAuth } from "@/context/Auth";
+import { SpinLogo } from "./SpinLogo";
+import { WorkStatusMenu } from "./WorkStatusMenu";
+import { ICONS } from "./Sidebar";
+import { classNames } from "@/lib/format";
+import type { NavItem } from "@/types";
+
+// 모바일 '더보기' 드로어: 전체 nav(관리자 포함) + 근무상태 + 계정/로그아웃.
+export function MobileDrawer({ open, onClose }: { open: boolean; onClose: () => void }) {
+ const { me, isAdmin } = useAuth();
+ const navQ = useQuery({ queryKey: ["nav"], queryFn: getNav, staleTime: 5 * 60_000 });
+ const apprQ = useQuery({ queryKey: ["approvals-count"], queryFn: getApprovals, enabled: isAdmin, staleTime: 60_000 });
+ const approvalCount = (apprQ.data?.leave.length ?? 0) + (apprQ.data?.overtime.length ?? 0);
+
+ useEffect(() => {
+ if (!open) return;
+ const onEsc = (e: KeyboardEvent) => e.key === "Escape" && onClose();
+ window.addEventListener("keydown", onEsc);
+ return () => window.removeEventListener("keydown", onEsc);
+ }, [open, onClose]);
+
+ if (!open) return null;
+ const items = navQ.data ?? [];
+ const sections = Array.from(new Set(items.map((i) => i.section)));
+ const name = me?.member?.displayName || me?.user.name || "사용자";
+ const avatar = avatarUrl(me?.member?.id, me?.member?.avatarKey);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {avatar ?

:
{name.slice(0, 1)}
}
+
+
{name}
+
{me?.user.email}
+
+
+
내 프로필
+
계정 설정
+
+
+
+
+ );
+}
diff --git a/src/components/MobileTabBar.tsx b/src/components/MobileTabBar.tsx
new file mode 100644
index 0000000..20ccbc8
--- /dev/null
+++ b/src/components/MobileTabBar.tsx
@@ -0,0 +1,43 @@
+import { NavLink } from "react-router-dom";
+import { LayoutDashboard, Clock, FolderKanban, Coins, Menu, type LucideIcon } from "lucide-react";
+import { classNames } from "@/lib/format";
+
+// 모바일 하단 탭바 (Toss식). 주요 4개 + '더보기'(드로어 열기). md 이상에선 숨김.
+const TABS: { to: string; label: string; icon: LucideIcon; end?: boolean }[] = [
+ { to: "/", label: "홈", icon: LayoutDashboard, end: true },
+ { to: "/attendance", label: "근무", icon: Clock },
+ { to: "/projects", label: "프로젝트", icon: FolderKanban },
+ { to: "/incentive", label: "인센티브", icon: Coins },
+];
+
+export function MobileTabBar({ onMore }: { onMore: () => void }) {
+ return (
+
+ );
+}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index 7c2cf60..fb98cf3 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -12,12 +12,12 @@ import { WorkStatusMenu } from "./WorkStatusMenu";
import { classNames } from "@/lib/format";
import type { NavItem } from "@/types";
-const ICONS: Record
= {
+export const ICONS: Record = {
LayoutDashboard, Clock, FolderKanban, Coins, CheckSquare, Calculator, Wallet, Users, Settings, FolderCog,
Inbox, UserCircle, ClipboardList,
};
-export function Sidebar({ collapsed = false }: { collapsed?: boolean }) {
+export function Sidebar({ collapsed = false, className }: { collapsed?: boolean; className?: string }) {
const { isAdmin } = useAuth();
const navQ = useQuery({ queryKey: ["nav"], queryFn: getNav, staleTime: 5 * 60_000 });
const apprQ = useQuery({ queryKey: ["approvals-count"], queryFn: getApprovals, enabled: isAdmin, staleTime: 60_000 });
@@ -30,7 +30,8 @@ export function Sidebar({ collapsed = false }: { collapsed?: boolean }) {