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/:name" element={<MobileLayout pageTitle="앨범"><MobileAlbumDetail /></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>
</MobileView>
</BrowserRouter>

View file

@ -1,5 +1,6 @@
import { NavLink, useLocation } from 'react-router-dom';
import { Home, Users, Disc3, Calendar } from 'lucide-react';
import { useEffect } from 'react';
//
function MobileHeader({ title }) {
@ -30,7 +31,7 @@ function MobileBottomNav() {
];
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">
{navItems.map((item) => {
const Icon = item.icon;
@ -58,11 +59,30 @@ function MobileBottomNav() {
//
// pageTitle: ( fromis_9)
// 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="min-h-screen flex flex-col bg-gray-50">
<div className="mobile-layout-container bg-gray-50">
{children}
<MobileBottomNav />
</div>
);
}
return (
<div className="mobile-layout-container bg-gray-50">
{!hideHeader && <MobileHeader title={pageTitle} />}
<main className={`flex-1 pb-20 ${hideHeader ? 'pt-0' : ''}`}>{children}</main>
<main className="mobile-content">{children}</main>
<MobileBottomNav />
</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-bottom {
padding-bottom: env(safe-area-inset-bottom, 0);

View file

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

View file

@ -33,7 +33,7 @@ function MobileHome() {
}, []);
return (
<div className="pb-4">
<div>
{/* 히어로 섹션 */}
<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" />
@ -122,7 +122,7 @@ function MobileHome() {
</section>
{/* 일정 섹션 */}
<section className="px-4 py-6">
<section className="px-4 py-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold">다가오는 일정</h2>
<button

View file

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

View file

@ -97,19 +97,17 @@ function MobileSchedule() {
setCalendarViewDate(newDate);
};
//
useEffect(() => {
const preventScroll = (e) => e.preventDefault();
if (showCalendar) {
document.body.style.overflow = 'hidden';
document.addEventListener('touchmove', preventScroll, { passive: false });
} else {
document.body.style.overflow = '';
document.removeEventListener('touchmove', preventScroll);
}
return () => {
document.body.style.overflow = '';
document.removeEventListener('touchmove', preventScroll);
};
}, [showCalendar]);
@ -177,8 +175,11 @@ function MobileSchedule() {
// ref
const dateScrollRef = useRef(null);
//
// +
useEffect(() => {
//
window.scrollTo(0, 0);
if (dateScrollRef.current) {
const selectedDay = selectedDate.getDate();
const buttons = dateScrollRef.current.querySelectorAll('button');
@ -189,9 +190,9 @@ function MobileSchedule() {
}, [selectedDate]);
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 ? (
<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">
@ -420,8 +421,9 @@ function MobileSchedule() {
)}
</AnimatePresence>
{/* 컨텐츠 */}
<div className="px-4 py-4">
{/* 컨텐츠 영역 */}
<div className="mobile-content">
<div className="px-4 pt-4 pb-4">
{isSearchMode && searchTerm ? (
//
<div className="space-y-3">
@ -477,6 +479,7 @@ function MobileSchedule() {
)}
</div>
</div>
</>
);
}