docs: 모바일 앨범 갤러리 UI 구현 문서 추가

- Swiper ViewPager 라이트박스 사용법
- LightboxIndicator 컴포넌트 (width prop)
- 2열 지그재그 Masonry 그리드 패턴
- 뒤로가기 처리 패턴
- 바텀시트 드래그 닫기 패턴
This commit is contained in:
caadiq 2026-01-11 23:20:08 +09:00
parent d6bc8d79ba
commit 5a5e601f63

View file

@ -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>
```