Files
web-page-backend/docs/superpowers/specs/2026-04-24-travel-gallery-redesign.md
2026-04-24 01:03:23 +09:00

12 KiB

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 가속