From 033b89f87d6a07c6144283bf825e9990f7e720b4 Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 23 Apr 2026 14:47:01 +0900 Subject: [PATCH] =?UTF-8?q?feat(travel):=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=EB=B0=98=EC=9D=91=ED=98=95=20=E2=80=94=20=ED=92=80=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20+=20?= =?UTF-8?q?=ED=92=80=EC=8A=A4=ED=81=AC=EB=A6=B0=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=EB=B0=95=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/pages/travel/Travel.css | 22 ++++++++++++++++++++++ src/pages/travel/Travel.jsx | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/pages/travel/Travel.css b/src/pages/travel/Travel.css index 3c7dacb..4782a25 100644 --- a/src/pages/travel/Travel.css +++ b/src/pages/travel/Travel.css @@ -200,6 +200,24 @@ .tv-map { 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 */ @@ -1020,6 +1038,10 @@ .photo-card--wide { grid-column: span 2; } + + .photo-mosaic { + grid-template-columns: 1fr; + } } /* ═══════════════════════════════════════════════════ diff --git a/src/pages/travel/Travel.jsx b/src/pages/travel/Travel.jsx index 0703369..14a5b29 100644 --- a/src/pages/travel/Travel.jsx +++ b/src/pages/travel/Travel.jsx @@ -2,6 +2,8 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { GeoJSON, MapContainer, TileLayer, useMap } from 'react-leaflet'; import 'leaflet/dist/leaflet.css'; import './Travel.css'; +import { useIsMobile } from '../../hooks/useIsMobile'; +import PullToRefresh from '../../components/PullToRefresh'; /* ───────────────────────────────────────────── Constants @@ -547,6 +549,8 @@ const Travel = () => { const [page, setPage] = useState(1); const [hasNext, setHasNext] = useState(true); + const isMobile = useIsMobile(); + const touchStartXRef = useRef(null); const pendingAdvanceRef = useRef(null); const toastTimerRef = useRef(null); @@ -685,6 +689,34 @@ const Travel = () => { } }, [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 ─────────────────────── */ const bumpSlide = (dir) => { setSlideDirection(dir); @@ -921,7 +953,7 @@ const Travel = () => { {/* ── Photo Mosaic ─────────────────── */} {!loading && !error && selectedRegion && photos.length > 0 && ( - <> +
{ hasNext={hasNext} isLoadingMore={loadingMore} /> - + )}