fix(travel): 앨범 커버 지정이 반영되지 않던 문제 수정

- useTravelData: 앨범 목록을 GET /api/travel/albums API로 빌드 (커버 정보 포함)
- 커버 지정 성공 시 refreshAlbums → 앨범 카드 즉시 갱신
- onCoverChange 콜백 체인: Travel → AlbumDetail → HeroLightbox

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 12:32:46 +09:00
parent 093ca6635a
commit 59bb05ba22
4 changed files with 35 additions and 72 deletions

View File

@@ -29,6 +29,7 @@ export default function AlbumDetail({
onClose,
onLoadMore,
onReload,
onCoverChange,
}) {
const isMobile = useIsMobile();
@@ -209,6 +210,7 @@ export default function AlbumDetail({
onClose={handleLightboxClose}
onNavigate={handleLightboxNavigate}
onLoadMore={onLoadMore}
onCoverChange={onCoverChange}
/>
)}
</>

View File

@@ -43,6 +43,7 @@ export default function HeroLightbox({
onClose,
onNavigate,
onLoadMore,
onCoverChange,
}) {
const isMobile = useIsMobile();
const [phase, setPhase] = useState('enter');
@@ -163,12 +164,13 @@ export default function HeroLightbox({
});
if (!res.ok) throw new Error(`${res.status}`);
setCoverStatus('done');
onCoverChange?.();
} catch {
setCoverStatus('error');
}
if (coverTimerRef.current) clearTimeout(coverTimerRef.current);
coverTimerRef.current = setTimeout(() => setCoverStatus(null), 2000);
}, [selectedIndex, photos, albumName, coverStatus]);
}, [selectedIndex, photos, albumName, coverStatus, onCoverChange]);
/* — Current photo — */
const photo = photos[selectedIndex];

View File

@@ -25,6 +25,7 @@ const Travel = () => {
loadMorePhotos,
reloadAlbumPhotos,
getFilteredAlbums,
refreshAlbums,
} = useTravelData();
/* ── Local state ──────────────────────────── */
@@ -220,6 +221,7 @@ const Travel = () => {
onClose={handleCloseAlbum}
onLoadMore={handleLoadMore}
onReload={handleReload}
onCoverChange={refreshAlbums}
/>
)}
</div>

View File

@@ -93,80 +93,36 @@ const useTravelData = () => {
return () => controller.abort();
}, []);
/* ── Build album list when regions arrive ── */
useEffect(() => {
if (!regions?.features?.length) return;
const controller = new AbortController();
(async () => {
/* ── Build album list from /api/travel/albums ── */
const fetchAlbums = useCallback(async (signal) => {
setLoadingAlbums(true);
const builtAlbums = [];
for (const feature of regions.features) {
if (controller.signal.aborted) break;
const regionId = feature?.properties?.id;
const regionName = feature?.properties?.name || regionId || '';
if (!regionId) continue;
// Use cached album metadata if fresh
const cached = albumCacheRef.current.get(regionId);
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
builtAlbums.push(...cached.albums);
continue;
}
try {
const res = await fetch(
`/api/travel/photos?region=${encodeURIComponent(regionId)}&page=1&size=${PAGE_SIZE}`,
{ signal: controller.signal }
);
if (!res.ok) continue; // skip failed regions silently
const json = await res.json();
const { normalized, matchedAlbums } = parsePhotoResponse(json);
const regionAlbums = matchedAlbums.map((ma) => {
// Find first photo that belongs to this album for coverThumb
const cover = normalized.find((p) => p.album === ma.album);
return {
id: `${regionId}::${ma.album}`,
name: ma.album,
region: regionId,
regionName,
photoCount: ma.count ?? 0,
coverThumb: cover?.src || '',
};
});
// If API returned no matched_albums, create a single implicit album
if (regionAlbums.length === 0 && normalized.length > 0) {
regionAlbums.push({
id: `${regionId}::`,
name: regionName,
region: regionId,
regionName,
photoCount: normalized.length,
coverThumb: normalized[0]?.src || '',
});
}
albumCacheRef.current.set(regionId, {
timestamp: Date.now(),
albums: regionAlbums,
});
builtAlbums.push(...regionAlbums);
} catch (err) {
if (err?.name === 'AbortError') break;
// Non-fatal — continue with other regions
}
}
if (!controller.signal.aborted) {
const res = await fetch('/api/travel/albums', { signal });
if (!res.ok) throw new Error(`앨범 로딩 실패 (${res.status})`);
const rows = await res.json();
const builtAlbums = rows.map((r) => ({
id: `${r.region}::${r.album}`,
name: r.album,
region: r.region,
regionName: r.regionName || r.region,
photoCount: r.count ?? 0,
coverThumb: r.cover_thumb || '',
}));
setAlbums(builtAlbums);
} catch (err) {
if (err?.name !== 'AbortError') {
setError(err?.message ?? String(err));
}
} finally {
setLoadingAlbums(false);
}
})();
}, []);
useEffect(() => {
const controller = new AbortController();
fetchAlbums(controller.signal);
return () => controller.abort();
}, [regions]);
}, [fetchAlbums]);
/* ── loadAlbumPhotos — initial load ────── */
const loadAlbumPhotos = useCallback(async (regionId, albumName) => {
@@ -362,6 +318,7 @@ const useTravelData = () => {
loadMorePhotos,
reloadAlbumPhotos,
getFilteredAlbums,
refreshAlbums: () => fetchAlbums(),
};
};