feat: 앱 셸 모바일 레이아웃 — BottomNav 통합 + 사이드바 조건부 렌더링

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 14:38:49 +09:00
parent d53108f1c9
commit 0922261c74
4 changed files with 47 additions and 95 deletions

View File

@@ -62,6 +62,7 @@
@media (max-width: 768px) { @media (max-width: 768px) {
.site-main { .site-main {
padding: 16px; padding: 16px;
padding-bottom: calc(var(--bottom-nav-h, 64px) + var(--safe-area-bottom, 0px) + 16px);
} }
} }

View File

@@ -1,11 +1,15 @@
import React from 'react'; import React from 'react';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
import Navbar from './components/Navbar'; import Navbar from './components/Navbar';
import BottomNav from './components/BottomNav';
import PageHeader from './components/PageHeader'; import PageHeader from './components/PageHeader';
import Loading from './components/Loading'; import Loading from './components/Loading';
import { useIsMobile } from './hooks/useIsMobile';
import './App.css'; import './App.css';
function App() { function App() {
const isMobile = useIsMobile();
return ( return (
<div className="app-shell"> <div className="app-shell">
<Navbar /> <Navbar />
@@ -17,6 +21,7 @@ function App() {
</React.Suspense> </React.Suspense>
</main> </main>
</div> </div>
{isMobile && <BottomNav />}
</div> </div>
); );
} }

View File

@@ -334,26 +334,6 @@
@media (max-width: 768px) { @media (max-width: 768px) {
.sidebar { .sidebar {
transform: translateX(-100%);
}
.sidebar.is-open {
transform: translateX(0);
}
.sidebar-toggle {
display: flex;
}
}
/* ── 데스크톱: 토글 버튼 숨김 ────────────────────────────────────────── */
@media (min-width: 769px) {
.sidebar-toggle {
display: none;
}
.sidebar__overlay {
display: none; display: none;
} }
} }

View File

@@ -1,92 +1,58 @@
import React, { useEffect, useState } from 'react'; import React from 'react';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { navLinks } from '../routes.jsx'; import { navLinks } from '../routes.jsx';
import { useIsMobile } from '../hooks/useIsMobile';
import mainLogo from '../assets/main_logo.png'; import mainLogo from '../assets/main_logo.png';
import './Navbar.css'; import './Navbar.css';
const Navbar = () => { const Navbar = () => {
const [menuOpen, setMenuOpen] = useState(false); const isMobile = useIsMobile();
const closeMenu = () => setMenuOpen(false);
useEffect(() => { // 모바일에서는 BottomNav가 대체하므로 사이드바 미렌더링
document.body.style.overflow = menuOpen ? 'hidden' : ''; if (isMobile) return null;
return () => {
document.body.style.overflow = '';
};
}, [menuOpen]);
return ( return (
<> <aside className="sidebar">
{/* 모바일 오버레이 */} <div className="sidebar__brand">
<div <img src={mainLogo} alt="Logo" className="sidebar__logo" />
className={`sidebar__overlay${menuOpen ? ' is-visible' : ''}`} <div className="sidebar__brand-text">
onClick={closeMenu} <p className="sidebar__brand-name">Jaeoh</p>
aria-hidden="true" <p className="sidebar__brand-sub">MANAGEMENT ROOM</p>
/>
{/* 모바일 토글 버튼 */}
<button
type="button"
className="sidebar-toggle"
onClick={() => setMenuOpen((prev) => !prev)}
aria-label="메뉴 열기/닫기"
aria-expanded={menuOpen}
>
<span className={`sidebar-toggle__icon${menuOpen ? ' is-open' : ''}`}>
<span />
<span />
<span />
</span>
</button>
{/* 사이드바 본체 */}
<aside className={`sidebar${menuOpen ? ' is-open' : ''}`}>
{/* 브랜드 섹션 */}
<div className="sidebar__brand">
<img src={mainLogo} alt="Logo" className="sidebar__logo" />
<div className="sidebar__brand-text">
<p className="sidebar__brand-name">Jaeoh</p>
<p className="sidebar__brand-sub">MANAGEMENT ROOM</p>
</div>
</div> </div>
</div>
{/* 구분선 */} <div className="sidebar__divider" />
<nav className="sidebar__nav">
<p className="sidebar__section-label">NAVIGATION</p>
{navLinks.map((link) => (
<NavLink
key={link.id}
to={link.path}
className={({ isActive }) =>
`sidebar__item${isActive ? ' is-active' : ''}`
}
style={{ '--item-accent': link.accent }}
end={link.path === '/'}
>
<span className="sidebar__item-icon">{link.icon}</span>
<span className="sidebar__item-label">{link.label}</span>
<span className="sidebar__item-dot" />
</NavLink>
))}
</nav>
<div className="sidebar__footer">
<div className="sidebar__divider" /> <div className="sidebar__divider" />
<div className="sidebar__footer-content">
{/* 네비게이션 */} <div className="sidebar__status">
<nav className="sidebar__nav"> <span className="sidebar__status-dot" />
<p className="sidebar__section-label">NAVIGATION</p> <span className="sidebar__status-text">System Online</span>
{navLinks.map((link) => (
<NavLink
key={link.id}
to={link.path}
onClick={closeMenu}
className={({ isActive }) =>
`sidebar__item${isActive ? ' is-active' : ''}`
}
style={{ '--item-accent': link.accent }}
end={link.path === '/'}
>
<span className="sidebar__item-icon">{link.icon}</span>
<span className="sidebar__item-label">{link.label}</span>
<span className="sidebar__item-dot" />
</NavLink>
))}
</nav>
{/* 사이드바 푸터 */}
<div className="sidebar__footer">
<div className="sidebar__divider" />
<div className="sidebar__footer-content">
<div className="sidebar__status">
<span className="sidebar__status-dot" />
<span className="sidebar__status-text">System Online</span>
</div>
<p className="sidebar__version">v2.0.0</p>
</div> </div>
<p className="sidebar__version">v2.0.0</p>
</div> </div>
</aside> </div>
</> </aside>
); );
}; };