docs: Travel 갤러리 리디자인 설계 스펙
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
313
docs/superpowers/specs/2026-04-24-travel-gallery-redesign.md
Normal file
313
docs/superpowers/specs/2026-04-24-travel-gallery-redesign.md
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
# Travel Gallery Redesign — Design Spec
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Travel 여행 기록 갤러리를 앨범 기반 진입 + Masonry 그리드 + HERO 확대 라이트박스로 리디자인한다. 모놀리식 1,024줄 컴포넌트를 7-8개 집중된 파일로 분리하고, 시네마틱 여행 감성을 강화한다.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- **포함**: 프론트엔드 리디자인 (컴포넌트 분리 + 새 UX/UI)
|
||||||
|
- **포함**: 동영상 탭 UI 셸 (플레이스홀더)
|
||||||
|
- **제외**: 백엔드 동영상 API (별도 후속 스펙)
|
||||||
|
- **제외**: 핀치 줌 (복잡도 대비 효과 낮음)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
점진적 리팩토링 — 기존 API 호출/캐싱/페이지네이션 로직을 `useTravelData` 훅으로 추출하여 재활용하고, UI 레이어를 새로 구성한다. 라우팅 변경 없이 React 상태 기반으로 앨범 진입/이탈을 관리한다.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- React 18 (기존)
|
||||||
|
- Leaflet + react-leaflet (기존, 미니맵으로 축소)
|
||||||
|
- react-swipeable (기존, 라이트박스 스와이프)
|
||||||
|
- SwipeableView 컴포넌트 (기존, 사진/영상 탭)
|
||||||
|
- CSS columns (Masonry 레이아웃)
|
||||||
|
- IntersectionObserver (무한스크롤 + 스크롤 리빌)
|
||||||
|
- Web Animations API / CSS transitions (shared element transition)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Component Structure & File Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/pages/travel/
|
||||||
|
├── Travel.jsx # 메인 컨테이너 (미니맵 + 앨범 카드 리스트)
|
||||||
|
├── Travel.css # 전체 레이아웃 + CSS 변수
|
||||||
|
├── AlbumCard.jsx # 여행지 앨범 카드
|
||||||
|
├── AlbumCard.css
|
||||||
|
├── AlbumDetail.jsx # 앨범 상세 (탭 + Masonry)
|
||||||
|
├── AlbumDetail.css
|
||||||
|
├── MasonryGrid.jsx # Masonry 레이아웃 + 무한스크롤
|
||||||
|
├── MasonryGrid.css
|
||||||
|
├── HeroLightbox.jsx # HERO 확대 전환 라이트박스
|
||||||
|
├── HeroLightbox.css
|
||||||
|
├── MiniMap.jsx # Leaflet 미니맵
|
||||||
|
├── MiniMap.css
|
||||||
|
├── VideoTab.jsx # 영상 탭 UI 셸
|
||||||
|
├── VideoTab.css
|
||||||
|
└── useTravelData.js # API 호출 + 캐싱 + 페이지네이션 훅
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
|
||||||
|
| 파일 | 책임 |
|
||||||
|
|------|------|
|
||||||
|
| `Travel.jsx` | 페이지 레이아웃, 지역 필터 상태, 앨범 선택 상태 관리 |
|
||||||
|
| `useTravelData.js` | API fetch, 10분 TTL 캐시, 앨범별 그룹핑, 페이지네이션 |
|
||||||
|
| `MiniMap.jsx` | Leaflet 지도 렌더링, GeoJSON 폴리곤, 지역 클릭 이벤트 발행 |
|
||||||
|
| `AlbumCard.jsx` | 대표 사진 + 앨범명 + 사진 수 뱃지, 호버 효과 |
|
||||||
|
| `AlbumDetail.jsx` | 앨범 오버레이, 진입/이탈 애니메이션, 사진/영상 탭 전환 |
|
||||||
|
| `MasonryGrid.jsx` | CSS columns Masonry, IntersectionObserver 무한스크롤 + 스크롤 리빌 |
|
||||||
|
| `HeroLightbox.jsx` | shared element transition, 좌우 스와이프, 썸네일 스트립 |
|
||||||
|
| `VideoTab.jsx` | "영상 기능 준비 중" 플레이스홀더 |
|
||||||
|
|
||||||
|
### Page Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Travel.jsx (메인)
|
||||||
|
├── MiniMap (상단, 접기/펼치기 가능)
|
||||||
|
│ └── 지역 클릭 → selectedRegion 상태 변경 → 앨범 필터
|
||||||
|
├── AlbumCard[] (여행지 카드 리스트)
|
||||||
|
│ └── 클릭 → AlbumDetail (오버레이)
|
||||||
|
│ ├── [사진 탭] MasonryGrid
|
||||||
|
│ │ └── 사진 클릭 → HeroLightbox
|
||||||
|
│ └── [영상 탭] VideoTab
|
||||||
|
└── useTravelData (데이터 레이어)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Main View — MiniMap + Album Card List
|
||||||
|
|
||||||
|
### MiniMap
|
||||||
|
|
||||||
|
- 높이: 데스크톱 200px, 모바일 150px
|
||||||
|
- GeoJSON 지역 폴리곤 유지 (기존 MapLayer 로직 추출)
|
||||||
|
- 클릭 시 해당 지역 앨범만 필터링
|
||||||
|
- 선택된 지역: 지역별 악센트 컬러로 하이라이트
|
||||||
|
- "전체 보기" 버튼으로 필터 해제
|
||||||
|
- 접기/펼치기 토글 (기본: 펼침)
|
||||||
|
- 접힌 상태: 높이 0 + overflow hidden, 토글 버튼만 표시
|
||||||
|
|
||||||
|
### Album Card List
|
||||||
|
|
||||||
|
- **카드 구성**: 대표 사진 배경 (object-fit: cover) + 앨범 이름 + 사진 수 뱃지
|
||||||
|
- **대표 사진**: 앨범 첫 번째 사진의 썸네일 URL
|
||||||
|
- **카드 레이아웃**: `display: grid`
|
||||||
|
- 데스크톱 (>1024px): 3열
|
||||||
|
- 태블릿 (769px-1024px): 2열
|
||||||
|
- 모바일 (<=768px): 1열
|
||||||
|
- **카드 높이**: 데스크톱 240px, 모바일 200px
|
||||||
|
- **호버**: scale(1.03) + 지역 악센트 글로우
|
||||||
|
- **지역 필터 전환**: fade 애니메이션 (opacity 300ms)
|
||||||
|
|
||||||
|
### Album Data Grouping
|
||||||
|
|
||||||
|
백엔드 API 변경 없이 프론트에서 처리:
|
||||||
|
|
||||||
|
1. 각 region에 대해 `GET /api/travel/photos?region={id}&page=1&size=1` 호출
|
||||||
|
2. 응답의 `total` 필드로 사진 수 확보, `items[0]`으로 대표 사진 확보
|
||||||
|
3. region_map.json의 albums 목록에서 앨범명 추출
|
||||||
|
4. 기존 10분 TTL 캐시 로직 재활용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Album Detail — Masonry Grid + Tabs + Transitions
|
||||||
|
|
||||||
|
### Entry Animation (Shared Element Transition)
|
||||||
|
|
||||||
|
1. 앨범 카드 클릭 시 `getBoundingClientRect()`로 카드 시작 위치 캡처
|
||||||
|
2. 카드 clone을 `position: fixed`로 생성
|
||||||
|
3. clone을 `inset: 0` (풀스크린)으로 animate (400ms, cubic-bezier(0.4, 0, 0.2, 1))
|
||||||
|
4. 애니메이션 완료 → clone 제거, AlbumDetail 오버레이 표시
|
||||||
|
|
||||||
|
### Exit Animation
|
||||||
|
|
||||||
|
1. 뒤로가기/닫기 클릭
|
||||||
|
2. AlbumDetail을 숨기고, 원래 카드 위치로 역재생 (400ms)
|
||||||
|
3. 애니메이션 완료 → 앨범 카드 리스트로 복귀
|
||||||
|
|
||||||
|
### Photo/Video Tabs
|
||||||
|
|
||||||
|
- 앨범 상세 상단에 "사진 | 영상" 탭 바
|
||||||
|
- 기존 `SwipeableView` 컴포넌트 재활용 (모바일 스와이프 전환)
|
||||||
|
- 영상 탭: VideoTab 컴포넌트 (플레이스홀더)
|
||||||
|
|
||||||
|
### Masonry Grid (Photo Tab)
|
||||||
|
|
||||||
|
- **레이아웃**: CSS `column-count` 기반
|
||||||
|
- 데스크톱 (>1024px): 4열
|
||||||
|
- 태블릿 (769px-1024px): 3열
|
||||||
|
- 모바일 (<=768px): 2열
|
||||||
|
- **사진 비율**: 원본 유지 (`width: 100%`, `height: auto`)
|
||||||
|
- **갭**: `column-gap: 8px`, 각 사진 `margin-bottom: 8px`
|
||||||
|
- **break-inside**: `avoid` (사진이 컬럼 경계에 걸리지 않도록)
|
||||||
|
- **무한 스크롤**: IntersectionObserver 센티널, rootMargin 300px, page size 20
|
||||||
|
- **스크롤 리빌**: 뷰포트 진입 시 아래에서 20px 올라오며 fade-in, 사진마다 50ms 지연
|
||||||
|
- **lazy loading**: `loading="lazy"` 속성, 첫 8장은 `loading="eager"`
|
||||||
|
|
||||||
|
### Video Tab (Shell)
|
||||||
|
|
||||||
|
- 중앙 정렬된 비디오 아이콘 + "영상 기능 준비 중" 텍스트
|
||||||
|
- 앰버 톤 텍스트, 세리프 폰트
|
||||||
|
- 백엔드 동영상 API 완성 시 이 컴포넌트 내부만 교체
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. HERO Lightbox
|
||||||
|
|
||||||
|
### Shared Element Transition (Photo → Fullscreen)
|
||||||
|
|
||||||
|
1. Masonry에서 사진 클릭 → `getBoundingClientRect()`로 시작 위치 캡처
|
||||||
|
2. 사진 clone을 `position: fixed`로 생성
|
||||||
|
3. clone을 화면 중앙 + 최대 크기로 animate (350ms, cubic-bezier(0.4, 0, 0.2, 1))
|
||||||
|
4. 애니메이션 완료 → clone 제거, 라이트박스 UI 표시
|
||||||
|
5. 배경은 `#000` opacity 0→1 동시 전환
|
||||||
|
|
||||||
|
### Fullscreen Viewer
|
||||||
|
|
||||||
|
- **배경**: 순수 블랙 `#000`, z-index 3000
|
||||||
|
- **사진**: `max-width: 100%`, `max-height: calc(100vh - 140px)`, `object-fit: contain`
|
||||||
|
- **좌우 탐색**:
|
||||||
|
- 데스크톱: 좌우 화살표 버튼 (hover 시 표시)
|
||||||
|
- 모바일: react-swipeable로 좌우 스와이프
|
||||||
|
- 키보드: ArrowLeft/ArrowRight
|
||||||
|
- **하단 썸네일 스트립**:
|
||||||
|
- 높이 68px, 썸네일 52x52px
|
||||||
|
- 활성 썸네일: 앰버 테두리 (2px solid)
|
||||||
|
- 활성 썸네일 자동 센터링 (smooth scroll)
|
||||||
|
- 필름 퍼포레이션 장식 제거 (간소화)
|
||||||
|
- **메타 정보**: 사진 위 또는 아래에 앨범명 + 파일명 (앰버 텍스트, 14px)
|
||||||
|
- **닫기**:
|
||||||
|
- X 버튼 (우상단)
|
||||||
|
- 아래로 스와이프 (모바일, threshold 100px)
|
||||||
|
- ESC 키
|
||||||
|
- 닫기 시 역재생 transition → 원래 그리드 위치로 복귀
|
||||||
|
|
||||||
|
### Slide Animation (이전/다음)
|
||||||
|
|
||||||
|
- 좌우 전환 시 현재 사진이 나가고 새 사진이 들어오는 slide 애니메이션
|
||||||
|
- 280ms, cubic-bezier(0.25, 0.46, 0.45, 0.94)
|
||||||
|
- 방향에 따라 왼쪽/오른쪽에서 진입
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Visual Design — Cinematic Travel Aesthetic
|
||||||
|
|
||||||
|
### Color System
|
||||||
|
|
||||||
|
- **베이스 배경**: `#0f0c09` (깊은 다크)
|
||||||
|
- **베이스 텍스트**: `#f5e6c8` (따뜻한 앰버)
|
||||||
|
- **뮤트 텍스트**: `rgba(245,230,200,0.5)`
|
||||||
|
- **라인/테두리**: `rgba(245,230,200,0.08)`
|
||||||
|
- **지역별 악센트**:
|
||||||
|
- 일본: `#c73e1d` (주홍)
|
||||||
|
- 유럽: `#2563eb` (코발트)
|
||||||
|
- 동남아: `#059669` (에메랄드)
|
||||||
|
- 국내: `#d97706` (호박)
|
||||||
|
- 기타: 기본 앰버 `#d4a574`
|
||||||
|
- 악센트 적용: 앨범 카드 호버 글로우, 미니맵 지역 하이라이트, 탭 활성 상태
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
|
||||||
|
- **제목/앨범명**: `Cormorant Garamond`, serif (기존 유지)
|
||||||
|
- **메타 정보/뱃지**: `Space Mono`, monospace (기존 유지)
|
||||||
|
- **앨범 카드 제목**: 데스크톱 24px, 모바일 18px
|
||||||
|
- **사진 수 뱃지**: 11px 모노, `rgba(15,12,9,0.7)` 배경 위 앰버 텍스트
|
||||||
|
|
||||||
|
### Album Card Visual
|
||||||
|
|
||||||
|
- 대표 사진 위 하단 30% 그라디언트: `linear-gradient(transparent, rgba(15,12,9,0.85))`
|
||||||
|
- 그라디언트 위에 앨범명 + 사진 수
|
||||||
|
- `border-radius: 12px`
|
||||||
|
- `border: 1px solid rgba(245,230,200,0.08)`
|
||||||
|
- 호버: `box-shadow: 0 0 20px rgba({accent}, 0.15)` + `transform: scale(1.03)`
|
||||||
|
|
||||||
|
### Masonry Photo Style
|
||||||
|
|
||||||
|
- `border-radius: 4px`
|
||||||
|
- 호버: `filter: brightness(1.08)` + `cursor: zoom-in`
|
||||||
|
- 스크롤 리빌: translateY(20px) + opacity(0) → translateY(0) + opacity(1), 사진마다 50ms 지연
|
||||||
|
|
||||||
|
### Lightbox Visual
|
||||||
|
|
||||||
|
- 배경: `#000`
|
||||||
|
- 메타 텍스트: 앰버 `#f5e6c8`, 세리프 폰트, 14px
|
||||||
|
- 썸네일 스트립: 활성 아이템에 앰버 2px 테두리
|
||||||
|
- 카운터: "3 / 156" 형태, 우상단, 모노스페이스
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Responsive Design
|
||||||
|
|
||||||
|
### Breakpoints
|
||||||
|
|
||||||
|
| 구간 | 앨범 카드 | Masonry 열 | 미니맵 높이 |
|
||||||
|
|------|----------|-----------|-----------|
|
||||||
|
| >1024px | 3열 | 4열 | 200px |
|
||||||
|
| 769-1024px | 2열 | 3열 | 200px |
|
||||||
|
| <=768px | 1열 | 2열 | 150px |
|
||||||
|
|
||||||
|
### Mobile Specifics
|
||||||
|
|
||||||
|
- 앨범 상세: `position: fixed; inset: 0` (풀스크린 오버레이)
|
||||||
|
- 라이트박스: 100dvh, 화살표 버튼 숨김 (스와이프로 대체)
|
||||||
|
- 미니맵: 기본 접힘 (모바일에서 공간 절약)
|
||||||
|
- 하단 네비게이션 고려: `padding-bottom: calc(var(--bottom-nav-h) + var(--safe-area-bottom))`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Reduced Motion
|
||||||
|
|
||||||
|
`prefers-reduced-motion: reduce` 적용 시:
|
||||||
|
|
||||||
|
- shared element transition (앨범 진입/이탈, 라이트박스 열기/닫기) → 즉시 fade (opacity 0→1, 150ms)
|
||||||
|
- 스크롤 리빌 애니메이션 → 즉시 표시 (opacity 1, transform none)
|
||||||
|
- 카드 호버 scale → 없음 (색상 변화만 유지)
|
||||||
|
- 슬라이드 전환 → 즉시 교체 (fade)
|
||||||
|
- 미니맵 접기/펼치기 → 즉시 전환
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
useTravelData hook
|
||||||
|
├── fetchRegions() → GET /api/travel/regions
|
||||||
|
├── fetchAlbums(region?) → GET /api/travel/photos?region={id}&page=1&size=1 (per region)
|
||||||
|
├── fetchPhotos(region, page) → GET /api/travel/photos?region={id}&page={n}&size=20
|
||||||
|
└── cache (Map, 10min TTL) → 기존 캐시 로직 재활용
|
||||||
|
|
||||||
|
State:
|
||||||
|
- regions: GeoJSON[]
|
||||||
|
- albums: { id, name, region, coverThumb, totalPhotos }[]
|
||||||
|
- selectedRegion: string | null
|
||||||
|
- selectedAlbum: string | null
|
||||||
|
- photos: Photo[]
|
||||||
|
- page, hasNext, loading, loadingMore
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Contract (기존 유지, 변경 없음)
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/travel/regions
|
||||||
|
→ GeoJSON FeatureCollection
|
||||||
|
|
||||||
|
GET /api/travel/photos?region=japan&page=1&size=20
|
||||||
|
→ { region, page, size, total, has_next, items: [{ album, file, url, thumb, mtime }] }
|
||||||
|
|
||||||
|
POST /api/travel/reload
|
||||||
|
→ { status: "ok" }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Performance Considerations
|
||||||
|
|
||||||
|
- **앨범 카드 대표 사진**: page=1&size=1로 최소 데이터만 요청
|
||||||
|
- **Masonry 이미지**: 썸네일(480x480) 사용, 라이트박스에서만 원본 로드
|
||||||
|
- **무한 스크롤**: 20개씩 점진적 로드, rootMargin 300px 선제 로드
|
||||||
|
- **lazy loading**: 브라우저 네이티브 `loading="lazy"`
|
||||||
|
- **캐시**: 10분 TTL, 리전 단위
|
||||||
|
- **스크롤 리빌**: IntersectionObserver 단일 인스턴스로 배치 감시
|
||||||
|
- **shared element transition**: `will-change: transform` 적용, 합성 레이어로 GPU 가속
|
||||||
Reference in New Issue
Block a user