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" 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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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))}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue