An uploaded figure never appeared, and no request for it was ever made. The resolver substituted 1x1 when an asset carried no dimensions, and a 1x1 box with `loading="lazy"` never enters the viewport — so the browser had no reason to fetch it. The image was not failing to load; it was never asked for. Unknown dimensions now say so. The figure omits the attributes and loads eagerly, letting the browser size the image from the file, and reserves layout space only when the size is actually known. Guessing a number to fill an attribute is what turned a missing measurement into a missing picture.
140 lines
5.6 KiB
TypeScript
140 lines
5.6 KiB
TypeScript
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<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,
|
|
// 치수를 모르면 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<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));
|
|
}
|