Files
tech-log-frontend/src/features/tech-log/presentation/shared/public-render/asset-resolvers.ts
T
DongHyeonkaandClaude Opus 5 9d91001a31 feat: resolve evidence figures from backend asset descriptors
Public Preview and Publication Snapshot now resolve evidence figures from the
ResolvedAsset descriptor the server already attaches to each EVIDENCE_FIGURE
block, instead of a local literal that only knew one hardcoded key and threw
on anything else. Instant Preview gains an `assets` prop (defaults to `[]`,
unwired until the Asset Picker task) and stops throwing when a key has no
catalog match yet.

Builds on Task 1's existing seam (resolveCaseEvidenceAssets /
ResolveEvidenceAsset) via a new asset-resolvers.ts rather than a parallel
path. Kept out of adapters/ (presentation may not import it) by carrying a
small local copy of the one legacy fixture entry, checked before any
descriptor match so the pre-existing key keeps rendering byte-identically
across surfaces during the migration window. Non-READY assets never resolve;
unresolvable keys return a placeholder instead of throwing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 03:54:30 +09:00

117 lines
4.3 KiB
TypeScript

import type { Asset } from "../../../contracts/studio/contract.ts";
import type {
EvidenceAsset,
ResolveEvidenceAsset,
} from "../../../domain/public-render-content.ts";
/**
* `presentation/` may not import `adapters/` (the
* `feature-presentation-does-not-know-outbound-adapters` architecture rule), so this
* cannot read `adapters/static/evidence-assets.ts` directly. It carries its own copy of
* that one legacy entry instead -- the same pattern `instant-preview.tsx`,
* `public-preview-screen.tsx`, `publication-event-preview-screen.tsx`, and
* `case-document-page.tsx` already each carry independently. This is not a second
* source of truth for new assets; it exists solely so the one pre-existing hardcoded
* key keeps resolving to byte-identical output once screens move onto this resolver.
*/
const LEGACY_STATIC_EVIDENCE_ASSETS: Readonly<Record<string, EvidenceAsset>> = {
"fetch-strategy-boundary": Object.freeze({
src: "/media/fetch-strategy-boundary.svg",
width: 1080,
height: 420,
triggerLabel: "Fetch Join과 Batch Fetch 비교 다이어그램 크게 보기",
dialogLabel: "Fetch Join과 Batch Fetch의 페이징 경계 확대",
}),
};
/**
* `ResolveEvidenceAsset` is a total function. Throwing on an unresolved key would
* take down the whole preview for one bad or not-yet-synced reference, so every
* resolver here returns this placeholder instead. Blocking a genuinely bad
* reference is Publish validation's job, not the renderer's.
*/
export const MISSING_EVIDENCE_ASSET: EvidenceAsset = Object.freeze({
src: "",
width: 1,
height: 1,
triggerLabel: "사용할 수 없는 Evidence",
dialogLabel: "사용할 수 없는 Evidence",
});
/** Structural shape of `components["schemas"]["ResolvedAsset"]`, loosened for reuse. */
export type ResolvedAssetLike = Readonly<{
assetId: string;
assetKey: string;
mediaType: string;
publicPath: string;
width: number | null;
height: number | null;
decorative: boolean;
}>;
function fromDescriptor(descriptor: ResolvedAssetLike): EvidenceAsset {
return Object.freeze({
src: descriptor.publicPath,
width: descriptor.width ?? 1,
height: descriptor.height ?? 1,
triggerLabel: `${descriptor.assetKey} 이미지 크게 보기`,
dialogLabel: `${descriptor.assetKey} 확대`,
});
}
/**
* Shared resolution order for both factories below: the legacy static key always
* wins on a match (protects the one pre-existing hardcoded fixture from splitting
* across render surfaces mid-migration), then the caller-supplied lookup (the real
* backend-sourced descriptor), then the safe placeholder.
*/
function resolveWith(
key: string,
lookup: (key: string) => ResolvedAssetLike | undefined,
): EvidenceAsset {
const legacy = LEGACY_STATIC_EVIDENCE_ASSETS[key];
if (legacy) return legacy;
const descriptor = lookup(key);
return descriptor ? fromDescriptor(descriptor) : MISSING_EVIDENCE_ASSET;
}
/** Instant Preview: resolves against the Asset list the editor has loaded. */
export function createAssetCatalogResolver(
assets: readonly Asset[],
): ResolveEvidenceAsset {
const byKey = new Map<string, Asset>();
for (const asset of assets) {
// QUARANTINED/REJECTED must never render on any surface.
if (asset.managementStatus !== "READY") continue;
byKey.set(asset.assetKey, asset);
}
return (key: string) =>
resolveWith(key, (lookupKey) => {
const asset = byKey.get(lookupKey);
if (!asset || asset.publicPath === null) return undefined;
return {
assetId: asset.id,
assetKey: asset.assetKey,
mediaType: asset.mediaType,
publicPath: asset.publicPath,
width: asset.width,
height: asset.height,
decorative: asset.decorative,
};
});
}
/** Public Preview · Snapshot: resolves from the descriptor the server already attached to the block. */
export function createResolvedAssetResolver(
blocks: readonly Readonly<{ type: string; key?: string; asset?: ResolvedAssetLike }>[],
): ResolveEvidenceAsset {
const byKey = new Map<string, ResolvedAssetLike>();
for (const block of blocks) {
if (block.type !== "EVIDENCE_FIGURE" || !block.asset) continue;
byKey.set(block.asset.assetKey, block.asset);
}
return (key: string) => resolveWith(key, (lookupKey) => byKey.get(lookupKey));
}