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> = { "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(); 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(); 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)); }