From 1502960151791eb2946a5791bef3fc29b81f8e25 Mon Sep 17 00:00:00 2001 From: chpark Date: Fri, 8 May 2026 09:23:27 +0900 Subject: [PATCH] =?UTF-8?q?feat(=EB=AA=A8=EB=B0=94=EC=9D=BC=20=EB=B0=98?= =?UTF-8?q?=EC=9D=91=ED=98=95):=20=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20?= =?UTF-8?q?=ED=96=84=EB=B2=84=EA=B1=B0=20=EC=98=A4=EB=B2=84=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=20+=20=EC=9E=90=EB=8F=99=20=EB=8B=AB=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 증상: 모바일로 로그인 시 사이드바가 콘텐츠를 덮어 사용 불가능. 원인: 사이드바가 모든 폭에서 항상 정상 폭으로 자리잡음. [레이아웃] - 사이드바를 모바일에서 fixed + translate-x-full 로 화면 밖에 두고, mobileOpen=true 시 translate-x-0 슬라이드 인 (200ms transition) - 모바일 오버레이 배경 클릭 시 닫기 - lg 이상에서는 기존대로 좌측 고정 [헤더] - 모바일에서만 햄버거(≡) 버튼 노출 → setMobileOpen(true) - 사용자명 모바일 width 줄이고 부서명 숨김 (110px → sm 이상 200px) [사이드바] - 헤더 우측에 모바일 전용 X 버튼 추가 (lg:hidden) - 데스크탑 햄버거 토글은 hidden lg:flex 로 분리 - handleSubMenuClick 에서 setMobileOpen(false) 호출 → 메뉴 선택 시 자동 닫힘 [스토어] - mobileOpen 상태 + setMobileOpen 액션 추가 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/(main)/layout.tsx | 44 ++++++++++++++++++------------- src/components/layout/header.tsx | 24 ++++++++++++----- src/components/layout/sidebar.tsx | 16 ++++++++--- src/store/menu-store.ts | 5 ++++ 4 files changed, 61 insertions(+), 28 deletions(-) diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 04c5474..3f9bda4 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -3,43 +3,51 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { useAuthStore } from "@/store/auth-store"; +import { useMenuStore } from "@/store/menu-store"; import { Sidebar } from "@/components/layout/sidebar"; import { Header } from "@/components/layout/header"; import { Loading } from "@/components/ui/loading"; -// mainFS.jsp 대응 - 프레임셋 → Sidebar + Header + Content 레이아웃 export default function MainLayout({ children }: { children: React.ReactNode }) { const router = useRouter(); const { user, isLoading, fetchUser } = useAuthStore(); + const { mobileOpen, setMobileOpen } = useMenuStore(); + + useEffect(() => { fetchUser(); }, [fetchUser]); useEffect(() => { - fetchUser(); - }, [fetchUser]); - - useEffect(() => { - if (!isLoading && !user) { - router.push("/login"); - } + if (!isLoading && !user) router.push("/login"); }, [isLoading, user, router]); - if (isLoading) { - return ; - } - + if (isLoading) return ; if (!user) return null; return (
- {/* 사이드바 (menu.jsp 대응) */} - + {/* 사이드바 — 데스크탑은 정상, 모바일은 오버레이로 등장 */} +
+ +
+ + {/* 모바일 오버레이 배경 — 사이드바 펼쳤을 때 어둡게 */} + {mobileOpen && ( + + + {/* 좌측 여백 */}
{/* 매뉴얼 보기 (새 탭) */} @@ -43,12 +52,13 @@ export function Header() { {/* 우측: 사용자명(프로필 링크) + 로그아웃 */} - - - {user?.userName} {user?.deptName ? `(${user.deptName})` : ""} + + + {user?.userName} + {user?.deptName ? ` (${user.deptName})` : ""} diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index dbf5577..2a0556b 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -11,7 +11,7 @@ import { ShoppingCart, FileText, Warehouse, Boxes, Factory, Wrench, Headset, Clock, Calculator, Coins, Truck, Settings, ClipboardCheck, Compass, GitBranch, Puzzle, Stamp, Folder, - ChevronDown, Menu as MenuIcon, + ChevronDown, Menu as MenuIcon, X, } from "lucide-react"; const ICON_COMPONENTS: Record = { @@ -34,7 +34,7 @@ export function Sidebar() { const router = useRouter(); const { sideMenus, activeSubMenu, isCollapsed, - setActiveSubMenu, toggleCollapsed, + setActiveSubMenu, toggleCollapsed, setMobileOpen, } = useMenuStore(); const [openCategories, setOpenCategories] = useState>(new Set()); // 축소 모드 호버 팝업 @@ -61,6 +61,7 @@ export function Sidebar() { const handleSubMenuClick = (menuObjId: string, menuUrl: string) => { setActiveSubMenu(menuObjId); setHoveredCategory(null); // 팝업 닫기 + setMobileOpen(false); // 모바일에서 메뉴 선택 시 자동 닫기 const path = mapMenuUrl(menuUrl); if (path) router.push(path); }; @@ -120,9 +121,18 @@ export function Sidebar() { ) : ( MOMO )} + {/* 모바일: X로 닫기, 데스크탑: 햄버거로 축소/확장 */} + diff --git a/src/store/menu-store.ts b/src/store/menu-store.ts index 36a5be7..b6e4ba9 100644 --- a/src/store/menu-store.ts +++ b/src/store/menu-store.ts @@ -7,11 +7,14 @@ interface MenuState { activeTopMenu: string; activeSubMenu: string; isCollapsed: boolean; + /** 모바일에서 사이드바 오버레이로 펼침 여부 */ + mobileOpen: boolean; setTopMenus: (menus: { OBJID: string; MENU_NAME_KOR: string }[]) => void; setSideMenus: (menus: MenuItem[]) => void; setActiveTopMenu: (id: string) => void; setActiveSubMenu: (id: string) => void; toggleCollapsed: () => void; + setMobileOpen: (v: boolean) => void; fetchTopMenus: () => Promise; fetchSideMenus: (menuObjId: string) => Promise; } @@ -22,12 +25,14 @@ export const useMenuStore = create((set) => ({ activeTopMenu: "", activeSubMenu: "", isCollapsed: false, + mobileOpen: false, setTopMenus: (topMenus) => set({ topMenus }), setSideMenus: (sideMenus) => set({ sideMenus }), setActiveTopMenu: (activeTopMenu) => set({ activeTopMenu }), setActiveSubMenu: (activeSubMenu) => set({ activeSubMenu }), toggleCollapsed: () => set((s) => ({ isCollapsed: !s.isCollapsed })), + setMobileOpen: (v) => set({ mobileOpen: v }), fetchTopMenus: async () => { const res = await fetch("/api/menu/top");