diff --git a/docs/superpowers/specs/2026-04-24-travel-gallery-redesign.md b/docs/superpowers/specs/2026-04-24-travel-gallery-redesign.md new file mode 100644 index 0000000..ef6b430 --- /dev/null +++ b/docs/superpowers/specs/2026-04-24-travel-gallery-redesign.md @@ -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 가속