fix: 매니페스트 로드 실패 시 로컬 자산으로 안전하게 대체되지 않는 버그 수정

- normalizeMediaManifest에서 빈 데이터 수신 시 DEFAULT_MEDIA_MANIFEST를 활용하도록 수정
- 매니페스트 로드 실패 시 상세 정보(URL, 상태 코드)를 에러 메시지에 포함
- useMediaCatalog에서 fetch 에러 발생 시 명시적으로 로컬 기반 매니페스트를 적용하도록 보강
This commit is contained in:
2026-03-11 15:16:00 +09:00
parent 35f1dfb92d
commit 972be117cb
3 changed files with 35 additions and 18 deletions

View File

@@ -36,6 +36,7 @@ export const mediaManifestApi = {
return DEFAULT_MEDIA_MANIFEST; return DEFAULT_MEDIA_MANIFEST;
} }
try {
const response = await fetch(MEDIA_MANIFEST_URL, { const response = await fetch(MEDIA_MANIFEST_URL, {
method: 'GET', method: 'GET',
cache: MEDIA_MANIFEST_FETCH_CACHE, cache: MEDIA_MANIFEST_FETCH_CACHE,
@@ -43,10 +44,18 @@ export const mediaManifestApi = {
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(copy.media.manifestLoadFailed); throw new Error(`${copy.media.manifestLoadFailed} (${response.status} at ${MEDIA_MANIFEST_URL})`);
} }
const payload = (await response.json()) as Partial<MediaManifest>; const payload = (await response.json()) as Partial<MediaManifest>;
return normalizeMediaManifest(payload); return normalizeMediaManifest(payload);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw error;
}
throw new Error(
`${copy.media.manifestLoadFailed}: ${error instanceof Error ? error.message : String(error)} (URL: ${MEDIA_MANIFEST_URL})`
);
}
}, },
}; };

View File

@@ -99,18 +99,18 @@ const mergeSoundAssets = (manifest: MediaManifest) => {
}; };
export const normalizeMediaManifest = (manifest: Partial<MediaManifest> | null | undefined): MediaManifest => { export const normalizeMediaManifest = (manifest: Partial<MediaManifest> | null | undefined): MediaManifest => {
const mergedManifest: MediaManifest = { const baseManifest: MediaManifest = {
version: manifest?.version ?? DEFAULT_MEDIA_MANIFEST.version, version: manifest?.version ?? DEFAULT_MEDIA_MANIFEST.version,
updatedAt: manifest?.updatedAt ?? DEFAULT_MEDIA_MANIFEST.updatedAt, updatedAt: manifest?.updatedAt ?? DEFAULT_MEDIA_MANIFEST.updatedAt,
cdnBaseUrl: manifest?.cdnBaseUrl ?? DEFAULT_MEDIA_MANIFEST.cdnBaseUrl, cdnBaseUrl: manifest?.cdnBaseUrl ?? DEFAULT_MEDIA_MANIFEST.cdnBaseUrl,
scenes: manifest?.scenes ?? [], scenes: manifest?.scenes ?? DEFAULT_MEDIA_MANIFEST.scenes,
sounds: manifest?.sounds ?? [], sounds: manifest?.sounds ?? DEFAULT_MEDIA_MANIFEST.sounds,
}; };
return { return {
...mergedManifest, ...baseManifest,
scenes: mergeSceneAssets(mergedManifest), scenes: mergeSceneAssets(baseManifest),
sounds: mergeSoundAssets(mergedManifest), sounds: mergeSoundAssets(baseManifest),
}; };
}; };

View File

@@ -40,10 +40,18 @@ const readMediaManifest = async (signal?: AbortSignal): Promise<MediaCatalogLoad
}; };
}) })
.catch((error) => { .catch((error) => {
const nextError = error instanceof Error ? error.message : null; // Only return abort errors up the chain if we're not using fallback
if (error instanceof Error && error.name === 'AbortError') {
throw error;
}
const nextError = error instanceof Error ? error.message : String(error);
// Explicitly use normalize with null to get a pure fallback manifest
const fallbackManifest = normalizeMediaManifest(null);
return { return {
manifest: manifestCache, manifest: fallbackManifest,
error: nextError, error: nextError,
usedFallbackManifest: true, usedFallbackManifest: true,
}; };