feat(mobile): 모바일 레이아웃 시스템 구축 - 컨텐츠 영역만 스크롤되도록 개선

- index.css: 모바일 레이아웃 CSS 시스템 추가 (mobile-layout-container, mobile-content, mobile-toolbar)
- Layout.jsx: MobileLayout에서 레이아웃 및 body 스크롤 제어 통합
- 하단 네비게이션을 fixed에서 flex-shrink-0으로 변경
- 모바일 스크롤바 숨김 처리
- Home, Members, Album, Schedule 페이지 여백 정리
This commit is contained in:
caadiq 2026-01-09 09:26:51 +09:00
parent 20546599cc
commit cca25b456c
7 changed files with 93 additions and 19 deletions

View file

@ -80,7 +80,7 @@ function App() {
<Route path="/album" element={<MobileLayout pageTitle="앨범"><MobileAlbum /></MobileLayout>} /> <Route path="/album" element={<MobileLayout pageTitle="앨범"><MobileAlbum /></MobileLayout>} />
<Route path="/album/:name" element={<MobileLayout pageTitle="앨범"><MobileAlbumDetail /></MobileLayout>} /> <Route path="/album/:name" element={<MobileLayout pageTitle="앨범"><MobileAlbumDetail /></MobileLayout>} />
<Route path="/album/:name/gallery" element={<MobileLayout pageTitle="앨범"><MobileAlbumGallery /></MobileLayout>} /> <Route path="/album/:name/gallery" element={<MobileLayout pageTitle="앨범"><MobileAlbumGallery /></MobileLayout>} />
<Route path="/schedule" element={<MobileLayout hideHeader><MobileSchedule /></MobileLayout>} /> <Route path="/schedule" element={<MobileLayout useCustomLayout><MobileSchedule /></MobileLayout>} />
</Routes> </Routes>
</MobileView> </MobileView>
</BrowserRouter> </BrowserRouter>

View file

@ -1,5 +1,6 @@
import { NavLink, useLocation } from 'react-router-dom'; import { NavLink, useLocation } from 'react-router-dom';
import { Home, Users, Disc3, Calendar } from 'lucide-react'; import { Home, Users, Disc3, Calendar } from 'lucide-react';
import { useEffect } from 'react';
// //
function MobileHeader({ title }) { function MobileHeader({ title }) {
@ -30,7 +31,7 @@ function MobileBottomNav() {
]; ];
return ( return (
<nav className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 z-50 safe-area-bottom"> <nav className="flex-shrink-0 bg-white border-t border-gray-200 z-50 safe-area-bottom">
<div className="flex items-center justify-around h-16"> <div className="flex items-center justify-around h-16">
{navItems.map((item) => { {navItems.map((item) => {
const Icon = item.icon; const Icon = item.icon;
@ -58,11 +59,30 @@ function MobileBottomNav() {
// //
// pageTitle: ( fromis_9) // pageTitle: ( fromis_9)
// hideHeader: true ( ) // hideHeader: true ( )
function MobileLayout({ children, pageTitle, hideHeader = false }) { // useCustomLayout: true (mobile-layout-container )
function MobileLayout({ children, pageTitle, hideHeader = false, useCustomLayout = false }) {
// (body )
useEffect(() => {
document.documentElement.classList.add('mobile-layout');
return () => {
document.documentElement.classList.remove('mobile-layout');
};
}, []);
// (Schedule )
if (useCustomLayout) {
return (
<div className="mobile-layout-container bg-gray-50">
{children}
<MobileBottomNav />
</div>
);
}
return ( return (
<div className="min-h-screen flex flex-col bg-gray-50"> <div className="mobile-layout-container bg-gray-50">
{!hideHeader && <MobileHeader title={pageTitle} />} {!hideHeader && <MobileHeader title={pageTitle} />}
<main className={`flex-1 pb-20 ${hideHeader ? 'pt-0' : ''}`}>{children}</main> <main className="mobile-content">{children}</main>
<MobileBottomNav /> <MobileBottomNav />
</div> </div>
); );

View file

@ -19,6 +19,57 @@ body {
} }
} }
/* 모바일 레이아웃 시스템 */
@media (max-width: 1023px) {
/* 모바일에서 html,body 스크롤 방지 */
html.mobile-layout,
html.mobile-layout body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
}
/* 모바일 레이아웃 컨테이너 */
.mobile-layout-container {
display: flex;
flex-direction: column;
height: 100dvh;
overflow: hidden;
}
/* 모바일 툴바 (기본 56px) */
.mobile-toolbar {
flex-shrink: 0;
background-color: #ffffff;
}
/* 일정 페이지 툴바 (헤더 + 날짜 선택기) */
.mobile-toolbar-schedule {
flex-shrink: 0;
background-color: #ffffff;
}
/* 하단 네비게이션 */
.mobile-bottom-nav {
flex-shrink: 0;
}
/* 컨텐츠 영역 - 스크롤 가능, 스크롤바 숨김 */
.mobile-content {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
.mobile-content::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
/* 모바일 safe-area 지원 (노치, 홈 인디케이터) */ /* 모바일 safe-area 지원 (노치, 홈 인디케이터) */
.safe-area-bottom { .safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom, 0); padding-bottom: env(safe-area-inset-bottom, 0);

View file

@ -27,7 +27,7 @@ function MobileAlbum() {
} }
return ( return (
<div className="px-4 py-6"> <div className="px-4 py-4">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{albums.map((album, index) => ( {albums.map((album, index) => (
<motion.div <motion.div

View file

@ -33,7 +33,7 @@ function MobileHome() {
}, []); }, []);
return ( return (
<div className="pb-4"> <div>
{/* 히어로 섹션 */} {/* 히어로 섹션 */}
<section className="relative bg-gradient-to-br from-primary to-primary-dark py-12 px-4 overflow-hidden"> <section className="relative bg-gradient-to-br from-primary to-primary-dark py-12 px-4 overflow-hidden">
<div className="absolute inset-0 bg-black/10" /> <div className="absolute inset-0 bg-black/10" />
@ -122,7 +122,7 @@ function MobileHome() {
</section> </section>
{/* 일정 섹션 */} {/* 일정 섹션 */}
<section className="px-4 py-6"> <section className="px-4 py-4">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold">다가오는 일정</h2> <h2 className="text-lg font-bold">다가오는 일정</h2>
<button <button

View file

@ -41,7 +41,7 @@ function MobileMembers() {
); );
return ( return (
<div className="px-4 py-6"> <div className="px-4 py-4">
{/* 현재 멤버 */} {/* 현재 멤버 */}
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
{members.map((member) => renderMemberCard(member))} {members.map((member) => renderMemberCard(member))}

View file

@ -97,19 +97,17 @@ function MobileSchedule() {
setCalendarViewDate(newDate); setCalendarViewDate(newDate);
}; };
// //
useEffect(() => { useEffect(() => {
const preventScroll = (e) => e.preventDefault(); const preventScroll = (e) => e.preventDefault();
if (showCalendar) { if (showCalendar) {
document.body.style.overflow = 'hidden';
document.addEventListener('touchmove', preventScroll, { passive: false }); document.addEventListener('touchmove', preventScroll, { passive: false });
} else { } else {
document.body.style.overflow = '';
document.removeEventListener('touchmove', preventScroll); document.removeEventListener('touchmove', preventScroll);
} }
return () => { return () => {
document.body.style.overflow = '';
document.removeEventListener('touchmove', preventScroll); document.removeEventListener('touchmove', preventScroll);
}; };
}, [showCalendar]); }, [showCalendar]);
@ -177,8 +175,11 @@ function MobileSchedule() {
// ref // ref
const dateScrollRef = useRef(null); const dateScrollRef = useRef(null);
// // +
useEffect(() => { useEffect(() => {
//
window.scrollTo(0, 0);
if (dateScrollRef.current) { if (dateScrollRef.current) {
const selectedDay = selectedDate.getDate(); const selectedDay = selectedDate.getDate();
const buttons = dateScrollRef.current.querySelectorAll('button'); const buttons = dateScrollRef.current.querySelectorAll('button');
@ -189,9 +190,9 @@ function MobileSchedule() {
}, [selectedDate]); }, [selectedDate]);
return ( return (
<div className="pb-4"> <>
{/* 헤더 */} {/* 툴바 (헤더 + 날짜 선택기) */}
<div className="sticky top-0 z-50 bg-white shadow-sm"> <div className="mobile-toolbar-schedule shadow-sm z-50">
{isSearchMode ? ( {isSearchMode ? (
<div className="flex items-center gap-2 px-4 py-3"> <div className="flex items-center gap-2 px-4 py-3">
<div className="flex-1 flex items-center gap-2 bg-gray-100 rounded-full px-4 py-2"> <div className="flex-1 flex items-center gap-2 bg-gray-100 rounded-full px-4 py-2">
@ -420,8 +421,9 @@ function MobileSchedule() {
)} )}
</AnimatePresence> </AnimatePresence>
{/* 컨텐츠 */} {/* 컨텐츠 영역 */}
<div className="px-4 py-4"> <div className="mobile-content">
<div className="px-4 pt-4 pb-4">
{isSearchMode && searchTerm ? ( {isSearchMode && searchTerm ? (
// //
<div className="space-y-3"> <div className="space-y-3">
@ -476,7 +478,8 @@ function MobileSchedule() {
</div> </div>
)} )}
</div> </div>
</div> </div>
</>
); );
} }