diff --git a/index.html b/index.html index 64abe1b..2989503 100644 --- a/index.html +++ b/index.html @@ -3,8 +3,13 @@ - + + spin · Special Partners + + - {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} :
{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 }) {