feat(travel): 모바일 반응형 — 풀다운 리프레시 + 풀스크린 라이트박스

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 14:47:01 +09:00
parent e7427ff1d5
commit 033b89f87d
2 changed files with 56 additions and 2 deletions

View File

@@ -200,6 +200,24 @@
.tv-map { .tv-map {
height: 300px; height: 300px;
} }
/* 지도 높이 축소 */
.tv-map {
height: 35vh !important;
}
/* 라이트박스 풀스크린 */
.lightbox {
border-radius: 0;
}
.lightbox__inner {
max-width: 100vw;
max-height: 100vh;
width: 100vw;
height: 100vh;
border-radius: 0;
}
} }
/* Leaflet map tooltip override */ /* Leaflet map tooltip override */
@@ -1020,6 +1038,10 @@
.photo-card--wide { .photo-card--wide {
grid-column: span 2; grid-column: span 2;
} }
.photo-mosaic {
grid-template-columns: 1fr;
}
} }
/* ═══════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════

View File

@@ -2,6 +2,8 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import { GeoJSON, MapContainer, TileLayer, useMap } from 'react-leaflet'; import { GeoJSON, MapContainer, TileLayer, useMap } from 'react-leaflet';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import './Travel.css'; import './Travel.css';
import { useIsMobile } from '../../hooks/useIsMobile';
import PullToRefresh from '../../components/PullToRefresh';
/* ───────────────────────────────────────────── /* ─────────────────────────────────────────────
Constants Constants
@@ -547,6 +549,8 @@ const Travel = () => {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [hasNext, setHasNext] = useState(true); const [hasNext, setHasNext] = useState(true);
const isMobile = useIsMobile();
const touchStartXRef = useRef(null); const touchStartXRef = useRef(null);
const pendingAdvanceRef = useRef(null); const pendingAdvanceRef = useRef(null);
const toastTimerRef = useRef(null); const toastTimerRef = useRef(null);
@@ -685,6 +689,34 @@ const Travel = () => {
} }
}, [hasNext, loading, loadingMore, page, photoSummary, selectedRegion]); }, [hasNext, loading, loadingMore, page, photoSummary, selectedRegion]);
/* ── Reload photos (pull-to-refresh) ──── */
const reloadPhotos = useCallback(async () => {
if (!selectedRegion) return;
cacheRef.current.delete(selectedRegion.id);
const res = await fetch(
`/api/travel/photos?region=${encodeURIComponent(selectedRegion.id)}&page=1&size=${PAGE_SIZE}`
);
if (!res.ok) throw new Error(`지역 사진 로딩 실패 (${res.status})`);
const json = await res.json();
const items = Array.isArray(json) ? json : json.items ?? [];
const meta = Array.isArray(json) ? {} : json ?? {};
const normalized = normalizePhotos(items);
const nextHasNext = typeof meta.has_next === 'boolean'
? meta.has_next
: typeof meta.hasNext === 'boolean' ? meta.hasNext : normalized.length >= PAGE_SIZE;
const summary = hasSummaryInfo(meta)
? { total: meta.total, albums: meta.matched_albums ?? [] }
: null;
setPhotos(normalized);
setPage(2);
setHasNext(nextHasNext);
cacheRef.current.set(selectedRegion.id, {
timestamp: Date.now(),
items: normalized, page: 2, hasNext: nextHasNext, summary,
});
if (summary) setPhotoSummary(summary);
}, [selectedRegion]);
/* ── Slide helpers ─────────────────────── */ /* ── Slide helpers ─────────────────────── */
const bumpSlide = (dir) => { const bumpSlide = (dir) => {
setSlideDirection(dir); setSlideDirection(dir);
@@ -921,7 +953,7 @@ const Travel = () => {
{/* ── Photo Mosaic ─────────────────── */} {/* ── Photo Mosaic ─────────────────── */}
{!loading && !error && selectedRegion && photos.length > 0 && ( {!loading && !error && selectedRegion && photos.length > 0 && (
<> <PullToRefresh onRefresh={reloadPhotos}>
<div className="tv-album-header"> <div className="tv-album-header">
<div className="tv-album-header__left"> <div className="tv-album-header__left">
<span <span
@@ -951,7 +983,7 @@ const Travel = () => {
hasNext={hasNext} hasNext={hasNext}
isLoadingMore={loadingMore} isLoadingMore={loadingMore}
/> />
</> </PullToRefresh>
)} )}
</section> </section>