From e3b5a874b3beb34953416affbcf87e8f522427df Mon Sep 17 00:00:00 2001 From: theorose49 Date: Sun, 28 Jun 2026 10:55:54 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A0=84=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=B0=98=EC=9D=91=ED=98=95(=EB=AA=A8=EB=B0=94=EC=9D=BC)=20+=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B4=20=EC=A7=81=EA=B8=89=20+=20=EC=B4=88?= =?UTF-8?q?=EA=B3=BC=EA=B7=BC=EB=AC=B4=20=ED=83=AD=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?+=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20URL=20+=20=EB=A1=9C?= =?UTF-8?q?=EA=B3=A0=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모바일 셸: 하단 탭바 + 더보기 드로어, 사이드바/탑바 반응형, safe-area, 폼/모달/테이블 반응형 - 근무: 유저 초과근무 탭 제거(관리자만 집계), 승인 관리 초과근무 섹션 제거 - 직급 인턴 추가, 직책 필드 제거, 부서는 관리자 설정→드롭다운(기존) - 로그아웃: /me의 LOGOUT_URL 사용(SSO 완전 로그아웃), 회사 로고 가운데 정렬 - 디바이스 등록(FCM)·계정 메뉴·계정 설정 (이전 커밋 포함) Co-Authored-By: Claude Opus 4.8 (1M context) --- index.html | 7 ++- src/components/AccountMenu.tsx | 6 +- src/components/AppShell.tsx | 22 +++++-- src/components/MobileDrawer.tsx | 92 ++++++++++++++++++++++++++++++ src/components/MobileTabBar.tsx | 43 ++++++++++++++ src/components/Sidebar.tsx | 15 ++--- src/components/Topbar.tsx | 17 ++++-- src/components/ui.tsx | 7 ++- src/context/Auth.tsx | 5 +- src/index.css | 17 ++++++ src/lib/api.ts | 11 ++-- src/pages/AccountSettings.tsx | 2 +- src/pages/Attendance.tsx | 64 +++------------------ src/pages/Dashboard.tsx | 2 +- src/pages/Profile.tsx | 8 +-- src/pages/ProjectDetail.tsx | 4 +- src/pages/Projects.tsx | 8 +-- src/pages/admin/Accounting.tsx | 2 +- src/pages/admin/Approvals.tsx | 27 +-------- src/pages/admin/IncentiveAdmin.tsx | 2 +- src/pages/admin/Members.tsx | 9 ++- src/pages/admin/Settings.tsx | 12 ++-- src/types.ts | 3 +- 23 files changed, 248 insertions(+), 137 deletions(-) create mode 100644 src/components/MobileDrawer.tsx create mode 100644 src/components/MobileTabBar.tsx 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 }) {