diff --git a/.gitignore b/.gitignore index 93d7131..16a0896 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ __pycache__/ # 디자인 참고 자료 (git에 올리지 않음) design_handoff/ +img.png \ No newline at end of file diff --git a/README.md b/README.md index c6f857f..8425513 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ - **뮤지션 인증 플로우** — 일반 유저의 뮤지션 인증 신청 → 어드민 승인/거부 - **어드민 관리** — 뮤지션·공연장·공연 등록/수정(출연 라인업 지정 포함), 뮤지션 인증 대기 목록 관리 - **공연장 좌표 크롤링** — Kakao Local API로 기존 공연장 주소를 위도/경도로 변환해 저장 +- **모바일 반응형 UI** — 768px 이하에서 햄버거 메뉴 + 슬라이드 드로어로 전환, 그리드 1열 재배치 --- diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index bc1c906..93e977c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,6 +4,7 @@ import './styles/global.css'; import Topbar from './components/Topbar'; import Sidebar from './components/Sidebar'; import AdminSidebar from './components/AdminSidebar'; +import MobileNav from './components/MobileNav'; // 모바일 전용 내비게이션 드로어 import Toast from './components/Toast'; import KakaoLoginModal from './pages/auth/KakaoLoginModal'; @@ -57,6 +58,8 @@ export default function App() { // me: 로그인한 유저 정보 (null = 비로그인 or 아직 로드 중) // 로그인 후: { id, nickname, role, musicianId?, stageName?, position?, bio?, snsUrl? } const [me, setMe] = useState(null); + // menuOpen: 모바일 드로어(햄버거 메뉴) 열림 여부 — PC 화면에서는 사용되지 않음 + const [menuOpen, setMenuOpen] = useState(false); const route = routeState.route; const routeParams = routeState.params; @@ -125,6 +128,7 @@ export default function App() { : '#' + r; history.pushState({ r, params }, '', encoded); setRouteState({ route: r, params }); + setMenuOpen(false); // 화면 이동 시 모바일 드로어는 항상 닫는다 window.scrollTo(0, 0); }; @@ -235,21 +239,30 @@ export default function App() { auth={auth} onLoginClick={openLogin} onLogout={handleLogout} + onMenuClick={() => setMenuOpen((o) => !o)} // 햄버거 버튼: 드로어 열기/닫기 토글 /> {useLayout ? ( <> + {/* 대시보드/관리자 화면: 사이드바가 모바일에서는 드로어(mobileOpen)로 동작 */} {layoutAdmin ? ( - + ) : ( - + )}
{renderContent()}
) : ( - renderContent() + <> + {/* 일반 화면: 모바일 전용 메뉴 드로어 (PC에서는 CSS로 숨겨짐) */} + { setMenuOpen(false); openLogin(reason); }} /> + {renderContent()} + )} + {/* 드로어가 열려 있을 때 본문을 어둡게 덮는 배경 — 탭하면 드로어 닫힘 */} + {menuOpen &&
setMenuOpen(false)} />} + {toast && } {loginModal && setLoginModal(false)} onLogin={handleLogin} reason={loginReason} />}
diff --git a/frontend/src/components/AdminSidebar.jsx b/frontend/src/components/AdminSidebar.jsx index 1687acb..c0fdf7d 100644 --- a/frontend/src/components/AdminSidebar.jsx +++ b/frontend/src/components/AdminSidebar.jsx @@ -4,7 +4,8 @@ import Avatar from './Avatar'; // 이니셜 아바타 import Icon from './Icon'; // 아이콘 import { api } from '../api/client'; // 백엔드 API 호출 함수 모음 -export default function AdminSidebar({ route, navigate, onLogout }) { +// mobileOpen: 모바일 드로어 열림 여부 — true면 .open 클래스가 붙어 슬라이드 인 (PC에서는 영향 없음) +export default function AdminSidebar({ route, navigate, onLogout, mobileOpen }) { // 각 항목의 건수 — 실제 API 데이터 기반 const [pendingCount, setPendingCount] = useState(null); // 인증 대기 const [musicianCount, setMusicianCount] = useState(null); // 뮤지션 수 @@ -28,7 +29,7 @@ export default function AdminSidebar({ route, navigate, onLogout }) { ]; return ( -
+
관리자 콘솔
{items.map(([r, ico, label, count]) => ( navigate(r)}> diff --git a/frontend/src/components/Icon.jsx b/frontend/src/components/Icon.jsx index 6bd8a3a..1329f54 100644 --- a/frontend/src/components/Icon.jsx +++ b/frontend/src/components/Icon.jsx @@ -45,6 +45,7 @@ export default function Icon({ name, size = 18, color = 'currentColor', stroke = case 'logout': return ; case 'chat': return ; case 'lock': return ; + case 'menu': return ; // 햄버거(☰) — 모바일 메뉴 버튼 default: return ; } } \ No newline at end of file diff --git a/frontend/src/components/MobileNav.jsx b/frontend/src/components/MobileNav.jsx new file mode 100644 index 0000000..c2a269f --- /dev/null +++ b/frontend/src/components/MobileNav.jsx @@ -0,0 +1,83 @@ +// 모바일 전용 내비게이션 드로어 — PC에서 topbar에 가로로 나열되던 메뉴를 대신한다. +// 대시보드/관리자 화면에서는 Sidebar/AdminSidebar가 드로어 역할을 하므로 이 컴포넌트는 쓰이지 않는다. +// CSS의 .sidebar.mobile-nav 규칙에 의해 PC(769px 이상)에서는 아예 표시되지 않는다. +import Icon from './Icon'; // 메뉴 아이콘 + +// open: 드로어 열림 여부 (true면 .open 클래스가 붙어 슬라이드 인) +// route/navigate: 현재 라우트와 화면 이동 함수 +// auth: 로그인 상태 — 역할(guest/general/musician/admin)에 따라 하단 메뉴가 달라짐 +// onLoginClick: 카카오 로그인 모달 열기 +export default function MobileNav({ open, route, navigate, auth = { role: 'guest' }, onLoginClick }) { + // 모든 방문자에게 보이는 공용 메뉴 (topbar의 가로 메뉴와 동일) + const links = [ + ['directory', 'users', '뮤지션'], + ['venues', 'building', '공연장'], + ['concerts', 'ticket', '공연'], + ['playdb', 'graph', '인맥지도'], + ]; + + return ( + // .sidebar 스타일(드로어 슬라이드)을 재사용하고, .mobile-nav로 PC에서는 숨긴다 +
+
둘러보기
+ {links.map(([r, ico, label]) => ( +
navigate(r)}> + + {label} + + ))} + + {/* 비로그인: 로그인/뮤지션 등록 안내 (topbar에서 hide-sm으로 숨긴 항목의 대체) */} + {auth.role === 'guest' && ( + <> +
계정
+ onLoginClick && onLoginClick()}> + + 카카오 로그인 + + onLoginClick && onLoginClick('become')}> + + 뮤지션이세요? + + + )} + + {/* 일반 회원: 뮤지션 등록 신청 */} + {auth.role === 'general' && ( + <> +
내 계정
+ navigate('become-musician')}> + + 뮤지션 등록 + + + )} + + {/* 뮤지션: 마이페이지/공개 프로필 바로가기 */} + {auth.role === 'musician' && ( + <> +
내 계정
+ navigate('dashboard')}> + + 마이페이지 + + navigate('profile-public')}> + + 공개 프로필 + + + )} + + {/* 관리자: 관리자 콘솔 바로가기 */} + {auth.role === 'admin' && ( + <> +
관리
+ navigate('admin-home')}> + + 관리자 대시보드 + + + )} +
+ ); +} diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 6230b9d..a32cd8e 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -1,7 +1,8 @@ import Avatar from './Avatar'; import Icon from './Icon'; -export default function Sidebar({ route, navigate, me, onLogout }) { +// mobileOpen: 모바일 드로어 열림 여부 — true면 .open 클래스가 붙어 슬라이드 인 (PC에서는 영향 없음) +export default function Sidebar({ route, navigate, me, onLogout, mobileOpen }) { const items = [ ['dashboard', 'home', '대시보드'], ['profile-edit', 'user', '내 프로필'], @@ -10,7 +11,7 @@ export default function Sidebar({ route, navigate, me, onLogout }) { ['graph-mine', 'graph', '관계도'], ]; return ( -
+
관리
{items.map(([r, ico, label]) => ( navigate(r)}> diff --git a/frontend/src/components/Topbar.jsx b/frontend/src/components/Topbar.jsx index b8e3654..198e2c2 100644 --- a/frontend/src/components/Topbar.jsx +++ b/frontend/src/components/Topbar.jsx @@ -2,11 +2,16 @@ import Logo from './Logo'; import Avatar from './Avatar'; import Icon from './Icon'; -export default function Topbar({ route, navigate, mode = 'visitor', me, auth = { role: 'guest' }, onLoginClick, onLogout }) { +// onMenuClick: 모바일 햄버거(☰) 버튼 클릭 시 드로어를 열고 닫는 콜백 (App.jsx에서 전달) +export default function Topbar({ route, navigate, mode = 'visitor', me, auth = { role: 'guest' }, onLoginClick, onLogout, onMenuClick }) { const publicNav = true; // 모든 역할에서 공연장/공연/인맥지도 nav 표시 return (
+ {/* 햄버거 버튼 — CSS(.menu-btn)에 의해 768px 이하에서만 보임 */} + navigate('directory')} /> {publicNav ? (