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>
117 lines
4.3 KiB
TypeScript
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));
|
|
}
|