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,
|
onClose,
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
onReload,
|
onReload,
|
||||||
|
onCoverChange,
|
||||||
}) {
|
}) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
@@ -209,6 +210,7 @@ export default function AlbumDetail({
|
|||||||
onClose={handleLightboxClose}
|
onClose={handleLightboxClose}
|
||||||
onNavigate={handleLightboxNavigate}
|
onNavigate={handleLightboxNavigate}
|
||||||
onLoadMore={onLoadMore}
|
onLoadMore={onLoadMore}
|
||||||
|
onCoverChange={onCoverChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export default function HeroLightbox({
|
|||||||
onClose,
|
onClose,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
|
onCoverChange,
|
||||||
}) {
|
}) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [phase, setPhase] = useState('enter');
|
const [phase, setPhase] = useState('enter');
|
||||||
@@ -163,12 +164,13 @@ export default function HeroLightbox({
|
|||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`${res.status}`);
|
if (!res.ok) throw new Error(`${res.status}`);
|
||||||
setCoverStatus('done');
|
setCoverStatus('done');
|
||||||
|
onCoverChange?.();
|
||||||
} catch {
|
} catch {
|
||||||
setCoverStatus('error');
|
setCoverStatus('error');
|
||||||
}
|
}
|
||||||
if (coverTimerRef.current) clearTimeout(coverTimerRef.current);
|
if (coverTimerRef.current) clearTimeout(coverTimerRef.current);
|
||||||
coverTimerRef.current = setTimeout(() => setCoverStatus(null), 2000);
|
coverTimerRef.current = setTimeout(() => setCoverStatus(null), 2000);
|
||||||
}, [selectedIndex, photos, albumName, coverStatus]);
|
}, [selectedIndex, photos, albumName, coverStatus, onCoverChange]);
|
||||||
|
|
||||||
/* — Current photo — */
|
/* — Current photo — */
|
||||||
const photo = photos[selectedIndex];
|
const photo = photos[selectedIndex];
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const Travel = () => {
|
|||||||
loadMorePhotos,
|
loadMorePhotos,
|
||||||
reloadAlbumPhotos,
|
reloadAlbumPhotos,
|
||||||
getFilteredAlbums,
|
getFilteredAlbums,
|
||||||
|
refreshAlbums,
|
||||||
} = useTravelData();
|
} = useTravelData();
|
||||||
|
|
||||||
/* ── Local state ──────────────────────────── */
|
/* ── Local state ──────────────────────────── */
|
||||||
@@ -220,6 +221,7 @@ const Travel = () => {
|
|||||||
onClose={handleCloseAlbum}
|
onClose={handleCloseAlbum}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
onReload={handleReload}
|
onReload={handleReload}
|
||||||
|
onCoverChange={refreshAlbums}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,80 +93,36 @@ const useTravelData = () => {
|
|||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/* ── Build album list when regions arrive ── */
|
/* ── Build album list from /api/travel/albums ── */
|
||||||
|
const fetchAlbums = useCallback(async (signal) => {
|
||||||
|
setLoadingAlbums(true);
|
||||||
|
try {
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
if (!regions?.features?.length) return;
|
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
(async () => {
|
fetchAlbums(controller.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) {
|
|
||||||
setAlbums(builtAlbums);
|
|
||||||
setLoadingAlbums(false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [regions]);
|
}, [fetchAlbums]);
|
||||||
|
|
||||||
/* ── loadAlbumPhotos — initial load ────── */
|
/* ── loadAlbumPhotos — initial load ────── */
|
||||||
const loadAlbumPhotos = useCallback(async (regionId, albumName) => {
|
const loadAlbumPhotos = useCallback(async (regionId, albumName) => {
|
||||||
@@ -362,6 +318,7 @@ const useTravelData = () => {
|
|||||||
loadMorePhotos,
|
loadMorePhotos,
|
||||||
reloadAlbumPhotos,
|
reloadAlbumPhotos,
|
||||||
getFilteredAlbums,
|
getFilteredAlbums,
|
||||||
|
refreshAlbums: () => fetchAlbums(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user