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:
parent
20546599cc
commit
cca25b456c
7 changed files with 93 additions and 19 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue