docs: 모바일 앨범 갤러리 UI 구현 문서 추가
- Swiper ViewPager 라이트박스 사용법 - LightboxIndicator 컴포넌트 (width prop) - 2열 지그재그 Masonry 그리드 패턴 - 뒤로가기 처리 패턴 - 바텀시트 드래그 닫기 패턴
This commit is contained in:
parent
d6bc8d79ba
commit
5a5e601f63
1 changed files with 124 additions and 0 deletions
|
|
@ -397,3 +397,127 @@ docker compose -f docker-compose.dev.yml up -d
|
||||||
| `frontend/src/pages/mobile/public/Schedule.jsx` | 52KB | 모바일 일정 |
|
| `frontend/src/pages/mobile/public/Schedule.jsx` | 52KB | 모바일 일정 |
|
||||||
| `backend/services/youtube-bot.js` | 17KB | YouTube 수집 |
|
| `backend/services/youtube-bot.js` | 17KB | YouTube 수집 |
|
||||||
| `backend/services/x-bot.js` | 16KB | X 수집 |
|
| `backend/services/x-bot.js` | 16KB | X 수집 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. 모바일 앨범 갤러리 UI
|
||||||
|
|
||||||
|
### 주요 컴포넌트
|
||||||
|
|
||||||
|
| 파일 | 설명 |
|
||||||
|
| ------------------------------------------------------ | ----------------------------- |
|
||||||
|
| `frontend/src/pages/mobile/public/AlbumGallery.jsx` | 모바일 앨범 갤러리 (전체보기) |
|
||||||
|
| `frontend/src/pages/mobile/public/AlbumDetail.jsx` | 모바일 앨범 상세 |
|
||||||
|
| `frontend/src/components/common/LightboxIndicator.jsx` | 공통 슬라이딩 점 인디케이터 |
|
||||||
|
|
||||||
|
### Swiper ViewPager 스타일 라이트박스
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
|
import { Virtual } from "swiper/modules";
|
||||||
|
|
||||||
|
<Swiper
|
||||||
|
modules={[Virtual]}
|
||||||
|
virtual
|
||||||
|
initialSlide={selectedIndex}
|
||||||
|
onSwiper={(swiper) => {
|
||||||
|
swiperRef.current = swiper;
|
||||||
|
}}
|
||||||
|
onSlideChange={(swiper) => setSelectedIndex(swiper.activeIndex)}
|
||||||
|
slidesPerView={1}
|
||||||
|
resistance={true}
|
||||||
|
resistanceRatio={0.5}
|
||||||
|
>
|
||||||
|
{photos.map((photo, index) => (
|
||||||
|
<SwiperSlide key={index} virtualIndex={index}>
|
||||||
|
<img src={photo.medium_url} />
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### LightboxIndicator 사용법
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import LightboxIndicator from '../../../components/common/LightboxIndicator';
|
||||||
|
|
||||||
|
// PC (기본 width 200px)
|
||||||
|
<LightboxIndicator
|
||||||
|
count={photos.length}
|
||||||
|
currentIndex={selectedIndex}
|
||||||
|
goToIndex={(i) => swiperRef.current?.slideTo(i)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
// 모바일 (width 120px로 축소)
|
||||||
|
<LightboxIndicator
|
||||||
|
count={photos.length}
|
||||||
|
currentIndex={selectedIndex}
|
||||||
|
goToIndex={(i) => swiperRef.current?.slideTo(i)}
|
||||||
|
width={120}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2열 지그재그 Masonry 그리드
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// 1,3,5번 → 왼쪽 열 / 2,4,6번 → 오른쪽 열
|
||||||
|
const distributePhotos = () => {
|
||||||
|
const leftColumn = [];
|
||||||
|
const rightColumn = [];
|
||||||
|
photos.forEach((photo, index) => {
|
||||||
|
if (index % 2 === 0) leftColumn.push({ ...photo, originalIndex: index });
|
||||||
|
else rightColumn.push({ ...photo, originalIndex: index });
|
||||||
|
});
|
||||||
|
return { leftColumn, rightColumn };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 뒤로가기 처리 패턴
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
// 모달/라이트박스 열 때 히스토리 추가
|
||||||
|
const openLightbox = useCallback((images, index, options = {}) => {
|
||||||
|
setLightbox({ open: true, images, index, ...options });
|
||||||
|
window.history.pushState({ lightbox: true }, "");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// popstate 이벤트로 닫기
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePopState = () => {
|
||||||
|
if (showModal) setShowModal(false);
|
||||||
|
else if (lightbox.open) setLightbox((prev) => ({ ...prev, open: false }));
|
||||||
|
};
|
||||||
|
window.addEventListener("popstate", handlePopState);
|
||||||
|
return () => window.removeEventListener("popstate", handlePopState);
|
||||||
|
}, [showModal, lightbox.open]);
|
||||||
|
|
||||||
|
// X 버튼도 history.back() 호출
|
||||||
|
<button onClick={() => window.history.back()}>
|
||||||
|
<X size={24} />
|
||||||
|
</button>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 바텀시트 (정보 표시)
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: "100%" }}
|
||||||
|
animate={{ y: 0 }}
|
||||||
|
exit={{ y: "100%" }}
|
||||||
|
drag="y"
|
||||||
|
dragConstraints={{ top: 0, bottom: 0 }}
|
||||||
|
dragElastic={{ top: 0, bottom: 0.5 }}
|
||||||
|
onDragEnd={(_, info) => {
|
||||||
|
if (info.offset.y > 100 || info.velocity.y > 300) {
|
||||||
|
window.history.back();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="bg-zinc-900 rounded-t-3xl"
|
||||||
|
>
|
||||||
|
{/* 드래그 핸들 */}
|
||||||
|
<div className="flex justify-center pt-3 pb-2">
|
||||||
|
<div className="w-10 h-1 bg-zinc-600 rounded-full" />
|
||||||
|
</div>
|
||||||
|
{/* 내용 */}
|
||||||
|
</motion.div>
|
||||||
|
```
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue