import type { Asset } from "../../../contracts/studio/contract.ts"; import { findResolvableAsset } from "../../../domain/content-format/asset-evidence-catalog.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, // 치수를 모르면 0 으로 둔다 — 1 이 아니라. 1×1 은 "아주 작은 그림" 이라는 거짓말이고, // `loading="lazy"` 와 만나면 브라우저는 화면에 걸리지 않는 1×1 상자를 영영 가져오지 않는다. // 실제로 업로드가 치수를 기록하지 않아 모든 그림이 그렇게 사라졌다. 0 은 figure 가 // 자기 값을 모른다는 뜻이고, 렌더러가 그때 속성을 빼고 즉시 로드로 바꾼다. width: descriptor.width ?? 0, height: descriptor.height ?? 0, triggerLabel: `${descriptor.assetKey} 이미지 크게 보기`, dialogLabel: `${descriptor.assetKey} 확대`, }); } /** * Shared resolution order for both factories below. The legacy registry only * needs to protect the *labels* it hand-authored for the one pre-existing key * (`ResolvedAsset` carries no label fields at all, so nothing else can supply * them) -- the image itself always comes from whichever descriptor actually * resolved the key, catalog or server block, when one exists. This way a future * backend `Asset` whose `assetKey` collides with the legacy slug degrades to, * at worst, a wrong caption on the right image -- never a wrong image shown * under a right caption. Only when no descriptor resolves the key at all does * the legacy registry supply the image too (today's actual Instant Preview * fallback path, with no catalog to consult). */ function resolveWith( key: string, lookup: (key: string) => ResolvedAssetLike | undefined, ): EvidenceAsset { const legacy = LEGACY_STATIC_EVIDENCE_ASSETS[key]; const descriptor = lookup(key); if (descriptor) { const resolved = fromDescriptor(descriptor); return legacy ? Object.freeze({ ...resolved, triggerLabel: legacy.triggerLabel, dialogLabel: legacy.dialogLabel, }) : resolved; } return legacy ?? MISSING_EVIDENCE_ASSET; } /** * Instant Preview: resolves against the Asset list the editor has loaded. * `findResolvableAsset` is the single decision -- the same one the preview's * key gate and its block-descriptor resolver make -- so the image shown here * is always the Asset the gate accepted. Building a local `Map` here instead * was last-wins where the descriptor resolver was first-wins, which meant two * READY Assets sharing one `assetKey` could put one Asset's caption over * another Asset's image. */ export function createAssetCatalogResolver( assets: readonly Asset[], ): ResolveEvidenceAsset { return (key: string) => resolveWith(key, (lookupKey) => { const asset = findResolvableAsset(assets, lookupKey); if (!asset) 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)); }