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:
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user