feat: 앱 셸 모바일 레이아웃 — BottomNav 통합 + 사이드바 조건부 렌더링
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,18 @@
|
|||||||
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__overlay${menuOpen ? ' is-visible' : ''}`}
|
|
||||||
onClick={closeMenu}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 모바일 토글 버튼 */}
|
|
||||||
<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">
|
<div className="sidebar__brand">
|
||||||
<img src={mainLogo} alt="Logo" className="sidebar__logo" />
|
<img src={mainLogo} alt="Logo" className="sidebar__logo" />
|
||||||
<div className="sidebar__brand-text">
|
<div className="sidebar__brand-text">
|
||||||
@@ -50,17 +21,14 @@ const Navbar = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 구분선 */}
|
|
||||||
<div className="sidebar__divider" />
|
<div className="sidebar__divider" />
|
||||||
|
|
||||||
{/* 네비게이션 */}
|
|
||||||
<nav className="sidebar__nav">
|
<nav className="sidebar__nav">
|
||||||
<p className="sidebar__section-label">NAVIGATION</p>
|
<p className="sidebar__section-label">NAVIGATION</p>
|
||||||
{navLinks.map((link) => (
|
{navLinks.map((link) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={link.id}
|
key={link.id}
|
||||||
to={link.path}
|
to={link.path}
|
||||||
onClick={closeMenu}
|
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
`sidebar__item${isActive ? ' is-active' : ''}`
|
`sidebar__item${isActive ? ' is-active' : ''}`
|
||||||
}
|
}
|
||||||
@@ -74,7 +42,6 @@ const Navbar = () => {
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* 사이드바 푸터 */}
|
|
||||||
<div className="sidebar__footer">
|
<div className="sidebar__footer">
|
||||||
<div className="sidebar__divider" />
|
<div className="sidebar__divider" />
|
||||||
<div className="sidebar__footer-content">
|
<div className="sidebar__footer-content">
|
||||||
@@ -86,7 +53,6 @@ const Navbar = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user