fix(space): 배경 asset fallback 경로와 scene alias 해석 보강
맥락: - /space 에서 forest 배경이 remote manifest asset 대신 기본 이미지로 조용히 fallback 될 수 있었다. - scene key alias 와 manifest 실패 상태가 코드상 드러나지 않아 원인 추적이 어려웠다. 변경사항: - media scene asset key 를 alias-aware 하게 정규화하고 asset source(fallback|remote) 메타를 추가했다. - useMediaCatalog 가 remote manifest 실패와 fallback 사용 여부를 노출하도록 보강했다. - SpaceWorkspaceWidget 에서 manifest 실패와 scene fallback 사용을 진단 로그/상태 메시지로 남기도록 정리했다. - docs/work.md, docs/90_current_state.md, docs/session_brief.md 를 이번 작업 기준으로 갱신했다. 검증: - npx tsc --noEmit 세션-상태: /space 배경 asset lookup 과 manifest fallback 진단을 보강했다. 세션-다음: forest/green-forest manifest 변형을 실제 브라우저에서 QA 한다. 세션-리스크: alias 목록 밖의 legacy scene id 는 추가 정규화가 필요할 수 있다.
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
# 90. Current State
|
||||
|
||||
Last Updated: 2026-03-05
|
||||
Last Updated: 2026-03-11
|
||||
|
||||
## DONE
|
||||
|
||||
- `/space` 배경 asset 해석 안정화:
|
||||
- media manifest scene key를 scene alias까지 정규화해 `green-forest`와 `forest`를 동일 asset으로 해석
|
||||
- scene/sound asset에 `source(fallback|remote)` 메타를 추가해 실제 fallback 사용 여부를 구분 가능하게 정리
|
||||
- remote manifest load 실패 시 error 상태를 노출하고, `/space`에서 manifest 실패/scene fallback 사용을 진단 로그로 남기도록 보강
|
||||
- Focus 피드백 채널 단일화:
|
||||
- HUD 내부 status line을 제거하고 상단 중앙 고정 토스트로 통합
|
||||
- Notes 저장/Undo, Goal 전환, 잠금 안내 피드백이 동일 위치에서 노출
|
||||
@@ -153,12 +157,14 @@ Last Updated: 2026-03-05
|
||||
|
||||
## NEXT
|
||||
|
||||
1. Goal Complete Sheet 플로우(완료 → 다음 한 조각) 전환 감도/카피 마감
|
||||
2. Notes(쓰기) / Inbox(읽기·정리) 복귀 흐름과 30초 숨고르기 톤 정리
|
||||
1. `/space`에서 `forest` / `green-forest` manifest 변형을 실제 브라우저 기준으로 QA
|
||||
2. Goal Complete Sheet 플로우(완료 → 다음 한 조각) 전환 감도/카피 마감
|
||||
3. Stage 가독성/모션/레이어 폴리시 최종 통일
|
||||
|
||||
## RISKS
|
||||
|
||||
- remote manifest 실패 시 원인 진단은 가능해졌지만, 사용자용 복구 액션 UI는 아직 없다
|
||||
- alias 목록에 없는 legacy scene id가 추가되면 같은 fallback 문제가 재발할 수 있다
|
||||
- `npm run build`는 네트워크 제한 시 Google Font fetch 실패 가능
|
||||
- localStorage 포맷 변경 시 이전 세션 저장값과의 호환성 이슈 가능
|
||||
- Scene 추천값과 실제 사용자 선호가 어긋나면 자동 적용 체감 품질이 낮아질 수 있음
|
||||
@@ -182,6 +188,14 @@ Last Updated: 2026-03-05
|
||||
|
||||
## CHANGED FILES
|
||||
|
||||
- (이번 세션)
|
||||
- `src/entities/media/model/types.ts`
|
||||
- `src/entities/media/model/resolveMediaAsset.ts`
|
||||
- `src/entities/media/model/useMediaCatalog.ts`
|
||||
- `src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx`
|
||||
- `docs/work.md`
|
||||
- `docs/90_current_state.md`
|
||||
- `docs/session_brief.md`
|
||||
- (최근 workflow 반영)
|
||||
- `src/widgets/space-workspace/ui/FocusTopToast.tsx`
|
||||
- `src/widgets/space-workspace/ui/SpaceWorkspaceWidget.tsx`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Session Brief
|
||||
|
||||
Last Updated: 2026-03-05
|
||||
Last Updated: 2026-03-11
|
||||
|
||||
세션 시작 시 항상 읽는 초소형 스냅샷 문서.
|
||||
|
||||
@@ -14,12 +14,16 @@ Last Updated: 2026-03-05
|
||||
|
||||
## 현재 우선순위
|
||||
|
||||
1. Goal Complete Sheet 플로우(완료 → 다음 한 조각) 마감 품질 점검
|
||||
2. Notes(쓰기) / Inbox(읽기·정리) 복귀 동선과 30초 숨고르기 카피 정리
|
||||
1. `/space` forest 배경이 `forest` / `green-forest` manifest key 모두에서 동일하게 붙는지 브라우저 QA
|
||||
2. Goal Complete Sheet 플로우(완료 → 다음 한 조각) 마감 품질 점검
|
||||
3. Stage 가독성/모션/레이어 폴리시 최종 정리
|
||||
|
||||
## 최근 세션 상태
|
||||
|
||||
- `/space` 배경 asset 해석을 보강했다.
|
||||
- media manifest scene key를 alias-aware 하게 정규화해 `green-forest`와 `forest`를 같은 scene asset으로 읽는다.
|
||||
- scene/sound asset에 `source(fallback|remote)` 메타를 추가해 remote asset 사용 여부를 코드에서 바로 식별할 수 있다.
|
||||
- remote manifest load 실패와 scene fallback 사용 시 `/space`에서 진단 로그를 남기도록 보강했다.
|
||||
- Focus 피드백 채널을 상단 중앙 1곳으로 통합했다.
|
||||
- HUD 내부 status line 제거
|
||||
- Notes/Goal/잠금 피드백이 동일 위치 토스트로 표시
|
||||
@@ -114,6 +118,8 @@ Last Updated: 2026-03-05
|
||||
|
||||
## 리스크
|
||||
|
||||
- remote manifest 실패 시 원인 진단은 가능하지만, 사용자용 복구 CTA는 아직 없다.
|
||||
- alias 목록에 없는 legacy scene id가 다시 들어오면 scene fallback 문제가 재발할 수 있다.
|
||||
- 네트워크 제한 환경에서는 `npm run build` 시 Google Fonts fetch 실패 가능
|
||||
- localStorage 저장 포맷 변경 시 이전 세션 데이터와의 호환성 이슈가 생길 수 있음
|
||||
- Scene 추천값이 사용자 선호와 어긋나면 자동 추천 체감 품질이 낮을 수 있음
|
||||
|
||||
115
docs/work.md
115
docs/work.md
@@ -17,111 +17,22 @@
|
||||
|
||||
## 작업 1
|
||||
|
||||
- 제목: 코어 루프 완성 — Goal Complete Sheet(다음 한 조각 입력) 마감
|
||||
- 제목: Space 배경 asset 해석 안정화 - forest R2 배경 fallback 제거
|
||||
- 목적:
|
||||
- 이 앱의 재방문/체감은 “완료 → 다음 목표”가 자연스럽게 이어질 때 생긴다.
|
||||
- Focus 화면에서 목표 완료가 폼 UI처럼 보이지 않도록 하고, 완료 후 다음 한 조각 입력 플로우를 프리미엄스럽게 만든다.
|
||||
- `/space`에서 `forest` 배경이 R2 asset 대신 기본 이미지로 조용히 fallback 되는 원인을 제거한다.
|
||||
- scene asset miss가 나도 원인을 코드상 추적 가능하게 만들어 재발을 막는다.
|
||||
- 변경 범위:
|
||||
- Focus HUD의 목표는 “1줄 앵커”로 유지(상시 큰 카드 금지)
|
||||
- 완료 트리거(1개만 선택해 고정):
|
||||
- Goal 1줄 앵커 롱프레스(1초) 또는 작은 ghost ‘완료’(체크박스 금지)
|
||||
- 완료 시 Goal Complete Sheet 표시(하단 시트)
|
||||
- 타이틀 + 입력 1개 + 추천 칩 4개 + CTA 2개(바로 다음 조각 시작 / 잠깐 쉬기)
|
||||
- Primary 클릭 시 다음 목표로 교체(더미) + 시트 닫기
|
||||
- Secondary는 Break(더미) 또는 토스트 + 시트 닫기
|
||||
- 전역 블러/딤 금지, 모션 200~300ms 저자극
|
||||
- media manifest의 scene asset key를 scene alias까지 고려해 해석하도록 보강
|
||||
- `/space` 배경이 scene asset miss 또는 manifest load 실패 시 조용히 기본 이미지로만 끝나지 않도록 진단 정보 추가
|
||||
- 기존 sound playback 동작과 UI 흐름은 유지
|
||||
- 제외 범위:
|
||||
- 서버/DB/통계/실제 타이머 로직 구현 금지
|
||||
- 백엔드 manifest 스키마 변경 금지
|
||||
- R2 업로드 파이프라인 수정 금지
|
||||
- focus timer / session / audio 동작 변경 금지
|
||||
- 완료 조건:
|
||||
- 완료 → 다음 목표 입력 → 바로 시작이 2~3스텝 내로 끝난다.
|
||||
- `forest`와 `green-forest` 어느 key로 scene asset이 내려와도 `/space`에서 같은 asset을 찾는다.
|
||||
- remote manifest를 못 읽는 경우 원인을 코드상 드러낼 수 있다.
|
||||
- 검증:
|
||||
- npx tsc --noEmit
|
||||
- `npx tsc --noEmit`
|
||||
- 커밋 힌트:
|
||||
- feat(goal): Goal Complete Sheet로 다음 한 조각 루프 완성
|
||||
|
||||
---
|
||||
|
||||
## 작업 2
|
||||
|
||||
- 제목: 세션 이어가기(Resume) — 새로고침/재진입 시 “지난 한 조각 이어서”
|
||||
- 목적:
|
||||
- 출시 전이라도 “다시 들어왔을 때 바로 이어서 시작”이 되면 사용성이 급격히 좋아지고 재방문을 만든다.
|
||||
- 변경 범위:
|
||||
- 로컬 저장(더미)으로 마지막 상태를 복원:
|
||||
- 마지막 목표, Scene, Timer, Sound, override flags
|
||||
- /space 진입 시 “지난 한 조각 이어서”를 조용한 CTA로 제공(Setup가 아니라 Focus 진입 직전에 1회)
|
||||
- 사용자가 거절하면 새 세션(목표 입력)로
|
||||
- 카피는 저자극/확정 표현 금지
|
||||
- 제외 범위:
|
||||
- 로그인/서버 동기화 금지
|
||||
- 완료 조건:
|
||||
- 새로고침 후에도 마지막 세션이 이어지는 것처럼 보이고, 이어서 시작이 가능하다.
|
||||
- 검증:
|
||||
- npx tsc --noEmit
|
||||
- 커밋 힌트:
|
||||
- feat(resume): 지난 세션 이어서(더미) 플로우 추가
|
||||
|
||||
---
|
||||
|
||||
## 작업 3
|
||||
|
||||
- 제목: Recover 시그니처 — Notes(쓰기 전용) → Inbox(읽기/정리) + 30초 숨고르기 정리
|
||||
- 목적:
|
||||
- ADHD 타겟의 차별점은 “산만해져도 다시 돌아오는 비용”을 줄이는 것이다.
|
||||
- 쓰기와 읽기/정리를 분리해 몰입을 깨지 않게 한다.
|
||||
- 변경 범위:
|
||||
- Notes 팝오버는 쓰기 전용(리스트/정리 버튼 제거)
|
||||
- Inbox는 도크 시트에서 읽기/정리(완료/삭제 + Undo 더미)
|
||||
- 30초 숨고르기(더미) 흐름 정리:
|
||||
- 버튼 카피/위치/동작을 “다시 돌아오기” 느낌으로
|
||||
- 과한 UI 추가 금지
|
||||
- 제외 범위:
|
||||
- 실제 타이머/오디오 로직 구현 금지
|
||||
- 완료 조건:
|
||||
- Focus 중에는 쓰기만, 정리는 Inbox에서만 가능하며 복귀 흐름이 자연스럽다.
|
||||
- 검증:
|
||||
- npx tsc --noEmit
|
||||
- 커밋 힌트:
|
||||
- feat(recover): Notes→Inbox 복귀 흐름 및 30초 숨고르기 정리
|
||||
|
||||
---
|
||||
|
||||
## 작업 4
|
||||
|
||||
- 제목: Stage 폴리시 규칙 고정 + 마감(가독성/모션/레이어)
|
||||
- 목적:
|
||||
- Portal/LifeAt 느낌은 “미세한 마감”에서 결정된다.
|
||||
- 앞선 코어 동선이 확정된 후, 가독성과 모션/레이어를 일관되게 다듬는다.
|
||||
- 변경 범위:
|
||||
- 밝은/어두운 배경 모두에서 HUD/앵커 가독성 안정(전역 blur 금지, 로컬 스크림 최소)
|
||||
- 모션 200~300ms 저자극 통일
|
||||
- 아이콘/버튼 간격/재질 통일(글래스 톤)
|
||||
- 제외 범위:
|
||||
- 기능 추가 금지(스타일/레이어만)
|
||||
- 완료 조건:
|
||||
- 배경이 달라도 핵심 정보가 항상 읽히고, 전체가 프리미엄스럽게 정돈된다.
|
||||
- 검증:
|
||||
- npx tsc --noEmit
|
||||
- 커밋 힌트:
|
||||
- style(stage): 가독성/모션/레이어 폴리시
|
||||
|
||||
---
|
||||
|
||||
## 작업 5
|
||||
|
||||
- 제목: Pro/Paywall 최소 연결(의도 기반) — Packs/Profiles 중심
|
||||
- 목적:
|
||||
- 기본 기능 잠금 없이, 확장/품질/개인화로 유료 이유를 만든다.
|
||||
- Focus를 방해하지 않고 클릭 의도 기반으로만 paywall을 연다.
|
||||
- 변경 범위:
|
||||
- Time 같은 기본 기능 LOCK 제거 유지
|
||||
- Pro는 Scene Packs / Sound Packs / Profile 저장으로 재배치
|
||||
- Paywall Sheet(더미) 구현: 잠긴 항목 클릭 시에만 노출
|
||||
- 제외 범위:
|
||||
- 실제 결제 연동 금지
|
||||
- 완료 조건:
|
||||
- Pro가 “확장/팩/개인화”로 이해되고, Focus 흐름을 방해하지 않는다.
|
||||
- 검증:
|
||||
- npx tsc --noEmit
|
||||
- 커밋 힌트:
|
||||
- feat(paywall): 의도 기반 Pro 진입/Paywall(더미) 연결
|
||||
- fix(space): 배경 asset fallback 경로와 scene alias 해석 보강
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { SceneTheme } from '@/entities/scene';
|
||||
import { normalizeSceneId, type SceneTheme } from '@/entities/scene';
|
||||
import { DEFAULT_MEDIA_MANIFEST } from './mockMediaManifest';
|
||||
import type {
|
||||
MediaManifest,
|
||||
SceneAssetManifestItem,
|
||||
SceneAssetMap,
|
||||
SoundAssetManifestItem,
|
||||
SoundAssetMap,
|
||||
} from './types';
|
||||
|
||||
const DEFAULT_STAGE_GRADIENT = 'linear-gradient(160deg, #1e293b 0%, #0f172a 100%)';
|
||||
|
||||
const normalizeSceneAssetId = (sceneId: string) => normalizeSceneId(sceneId) ?? sceneId;
|
||||
|
||||
const isAbsoluteUrl = (value: string) => /^(?:[a-z]+:)?\/\//i.test(value);
|
||||
|
||||
const resolveAssetUrl = (value: string | null | undefined, baseUrl?: string | null) => {
|
||||
@@ -34,13 +37,28 @@ const resolveAssetUrl = (value: string | null | undefined, baseUrl?: string | nu
|
||||
|
||||
const mergeSceneAssets = (manifest: MediaManifest) => {
|
||||
const bySceneId = new Map(
|
||||
DEFAULT_MEDIA_MANIFEST.scenes.map((asset) => [asset.sceneId, asset]),
|
||||
DEFAULT_MEDIA_MANIFEST.scenes.map((asset) => {
|
||||
const normalizedSceneId = normalizeSceneAssetId(asset.sceneId);
|
||||
|
||||
return [
|
||||
normalizedSceneId,
|
||||
{
|
||||
...asset,
|
||||
sceneId: normalizedSceneId,
|
||||
source: 'fallback',
|
||||
} as SceneAssetManifestItem,
|
||||
] as const;
|
||||
}),
|
||||
);
|
||||
|
||||
for (const asset of manifest.scenes) {
|
||||
bySceneId.set(asset.sceneId, {
|
||||
...bySceneId.get(asset.sceneId),
|
||||
const normalizedSceneId = normalizeSceneAssetId(asset.sceneId);
|
||||
|
||||
bySceneId.set(normalizedSceneId, {
|
||||
...bySceneId.get(normalizedSceneId),
|
||||
...asset,
|
||||
sceneId: normalizedSceneId,
|
||||
source: 'remote',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,13 +73,20 @@ const mergeSceneAssets = (manifest: MediaManifest) => {
|
||||
|
||||
const mergeSoundAssets = (manifest: MediaManifest) => {
|
||||
const byPresetId = new Map(
|
||||
DEFAULT_MEDIA_MANIFEST.sounds.map((asset) => [asset.presetId, asset]),
|
||||
DEFAULT_MEDIA_MANIFEST.sounds.map((asset) => [
|
||||
asset.presetId,
|
||||
{
|
||||
...asset,
|
||||
source: 'fallback',
|
||||
} as SoundAssetManifestItem,
|
||||
]),
|
||||
);
|
||||
|
||||
for (const asset of manifest.sounds) {
|
||||
byPresetId.set(asset.presetId, {
|
||||
...byPresetId.get(asset.presetId),
|
||||
...asset,
|
||||
source: 'remote',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -91,7 +116,7 @@ export const normalizeMediaManifest = (manifest: Partial<MediaManifest> | null |
|
||||
|
||||
export const buildSceneAssetMap = (manifest: MediaManifest): SceneAssetMap => {
|
||||
return manifest.scenes.reduce<SceneAssetMap>((accumulator, asset) => {
|
||||
accumulator[asset.sceneId] = asset;
|
||||
accumulator[normalizeSceneAssetId(asset.sceneId)] = asset;
|
||||
return accumulator;
|
||||
}, {});
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface SceneAssetManifestItem {
|
||||
hdStageUrl?: string | null;
|
||||
placeholderGradient?: string | null;
|
||||
blurDataUrl?: string | null;
|
||||
source?: 'fallback' | 'remote';
|
||||
}
|
||||
|
||||
export interface SoundAssetManifestItem {
|
||||
@@ -16,6 +17,7 @@ export interface SoundAssetManifestItem {
|
||||
mimeType?: 'audio/mp4' | 'audio/mpeg' | 'audio/webm' | null;
|
||||
durationSec?: number | null;
|
||||
defaultVolume?: number | null;
|
||||
source?: 'fallback' | 'remote';
|
||||
}
|
||||
|
||||
export interface MediaManifest {
|
||||
|
||||
@@ -10,12 +10,22 @@ import {
|
||||
} from './resolveMediaAsset';
|
||||
import type { MediaManifest } from './types';
|
||||
|
||||
let manifestCache = normalizeMediaManifest(DEFAULT_MEDIA_MANIFEST);
|
||||
let manifestRequest: Promise<MediaManifest> | null = null;
|
||||
type MediaCatalogLoadResult = {
|
||||
manifest: MediaManifest;
|
||||
error: string | null;
|
||||
usedFallbackManifest: boolean;
|
||||
};
|
||||
|
||||
const readMediaManifest = async (signal?: AbortSignal) => {
|
||||
let manifestCache = normalizeMediaManifest(DEFAULT_MEDIA_MANIFEST);
|
||||
let manifestRequest: Promise<MediaCatalogLoadResult> | null = null;
|
||||
|
||||
const readMediaManifest = async (signal?: AbortSignal): Promise<MediaCatalogLoadResult> => {
|
||||
if (!MEDIA_MANIFEST_URL) {
|
||||
return manifestCache;
|
||||
return {
|
||||
manifest: manifestCache,
|
||||
error: null,
|
||||
usedFallbackManifest: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (!manifestRequest) {
|
||||
@@ -23,9 +33,21 @@ const readMediaManifest = async (signal?: AbortSignal) => {
|
||||
.getManifest(signal)
|
||||
.then((manifest) => {
|
||||
manifestCache = manifest;
|
||||
return manifest;
|
||||
return {
|
||||
manifest,
|
||||
error: null,
|
||||
usedFallbackManifest: false,
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
const nextError = error instanceof Error ? error.message : null;
|
||||
|
||||
return {
|
||||
manifest: manifestCache,
|
||||
error: nextError,
|
||||
usedFallbackManifest: true,
|
||||
};
|
||||
})
|
||||
.catch(() => manifestCache)
|
||||
.finally(() => {
|
||||
manifestRequest = null;
|
||||
});
|
||||
@@ -36,12 +58,18 @@ const readMediaManifest = async (signal?: AbortSignal) => {
|
||||
|
||||
export const useMediaCatalog = () => {
|
||||
const [manifest, setManifest] = useState<MediaManifest>(manifestCache);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [usedFallbackManifest, setUsedFallbackManifest] = useState(false);
|
||||
const [hasResolvedManifest, setHasResolvedManifest] = useState(!MEDIA_MANIFEST_URL);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
void readMediaManifest(controller.signal).then((nextManifest) => {
|
||||
setManifest(nextManifest);
|
||||
void readMediaManifest(controller.signal).then((result) => {
|
||||
setManifest(result.manifest);
|
||||
setError(result.error);
|
||||
setUsedFallbackManifest(result.usedFallbackManifest);
|
||||
setHasResolvedManifest(true);
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -56,5 +84,8 @@ export const useMediaCatalog = () => {
|
||||
manifest,
|
||||
sceneAssetMap,
|
||||
soundAssetMap,
|
||||
error,
|
||||
usedFallbackManifest,
|
||||
hasResolvedManifest,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -208,8 +208,16 @@ export const SpaceWorkspaceWidget = () => {
|
||||
});
|
||||
const queuedFocusStatusMessageRef = useRef<string | null>(null);
|
||||
const lastSoundPlaybackErrorRef = useRef<string | null>(null);
|
||||
const lastMediaManifestErrorRef = useRef<string | null>(null);
|
||||
const lastFallbackSceneDiagnosticRef = useRef<string | null>(null);
|
||||
const didHydrateServerPreferencesRef = useRef(false);
|
||||
const { sceneAssetMap, soundAssetMap } = useMediaCatalog();
|
||||
const {
|
||||
sceneAssetMap,
|
||||
soundAssetMap,
|
||||
error: mediaCatalogError,
|
||||
usedFallbackManifest,
|
||||
hasResolvedManifest,
|
||||
} = useMediaCatalog();
|
||||
|
||||
const {
|
||||
selectedPresetId,
|
||||
@@ -778,6 +786,49 @@ export const SpaceWorkspaceWidget = () => {
|
||||
});
|
||||
}, [pushStatusLine, soundPlaybackError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mediaCatalogError) {
|
||||
lastMediaManifestErrorRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mediaCatalogError === lastMediaManifestErrorRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastMediaManifestErrorRef.current = mediaCatalogError;
|
||||
console.error('[media] Failed to load remote media manifest.', {
|
||||
error: mediaCatalogError,
|
||||
sceneId: selectedScene.id,
|
||||
});
|
||||
pushStatusLine({
|
||||
message: mediaCatalogError,
|
||||
});
|
||||
}, [mediaCatalogError, pushStatusLine, selectedScene.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasResolvedManifest || usedFallbackManifest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isUsingFallbackSceneAsset = !selectedSceneAsset || selectedSceneAsset.source === 'fallback';
|
||||
|
||||
if (!isUsingFallbackSceneAsset) {
|
||||
lastFallbackSceneDiagnosticRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastFallbackSceneDiagnosticRef.current === selectedScene.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastFallbackSceneDiagnosticRef.current = selectedScene.id;
|
||||
console.warn('[space] Selected scene is using fallback asset data.', {
|
||||
sceneId: selectedScene.id,
|
||||
asset: selectedSceneAsset ?? null,
|
||||
});
|
||||
}, [hasResolvedManifest, selectedScene.id, selectedSceneAsset, usedFallbackManifest]);
|
||||
|
||||
return (
|
||||
<div className="relative h-dvh overflow-hidden text-white">
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user