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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0c071aaabc
commit
9d91001a31
@@ -1,10 +1,11 @@
|
||||
import type { components } from "../../../contracts/studio/generated.ts";
|
||||
import type { WorkingCopyInput } from "../../../contracts/studio/contract.ts";
|
||||
import type { Asset, WorkingCopyInput } from "../../../contracts/studio/contract.ts";
|
||||
import { ContentFormatError } from "../../../domain/content-format/parse-case-content.ts";
|
||||
import {
|
||||
projectWorkingCopy,
|
||||
resolveCaseEvidenceAssets,
|
||||
} from "../../../domain/content-format/project-public-render-model.ts";
|
||||
import { createAssetCatalogResolver } from "../../shared/public-render/asset-resolvers.ts";
|
||||
import { PublicRecordRenderer } from "../../shared/public-render/public-record-renderer.tsx";
|
||||
import { useStudio } from "../use-studio.ts";
|
||||
|
||||
@@ -16,39 +17,64 @@ function supportsPreviewEvidenceKey(key: string): boolean {
|
||||
return key === "fetch-strategy-boundary";
|
||||
}
|
||||
|
||||
function resolvePreviewEvidenceAsset(key: string) {
|
||||
if (!supportsPreviewEvidenceKey(key)) {
|
||||
throw new Error(`Unknown local evidence asset: ${key}`);
|
||||
}
|
||||
return {
|
||||
src: "/media/fetch-strategy-boundary.svg",
|
||||
width: 1080,
|
||||
height: 420,
|
||||
triggerLabel: "Fetch Join과 Batch Fetch 비교 다이어그램 크게 보기",
|
||||
dialogLabel: "Fetch Join과 Batch Fetch의 페이징 경계 확대",
|
||||
/**
|
||||
* `resolveCaseEvidenceAssets` only needs *a* `ResolvedAsset` to turn the authoring
|
||||
* model into a genuine `PublicRenderModel` -- nothing downstream reads `block.asset`
|
||||
* for the pixels actually shown; that comes from `resolveEvidenceAsset` below
|
||||
* (`createAssetCatalogResolver`). So this must never throw: a key the Asset Picker
|
||||
* (Task 10) inserts, with no catalog match yet, still gets a placeholder descriptor
|
||||
* instead of blanking the whole preview behind an error panel.
|
||||
*/
|
||||
function resolveAssetDescriptor(assets: readonly Asset[]) {
|
||||
return (key: string): ResolvedAsset => {
|
||||
const asset = assets.find(
|
||||
(candidate) => candidate.assetKey === key && candidate.managementStatus === "READY",
|
||||
);
|
||||
if (asset?.publicPath) {
|
||||
return {
|
||||
assetId: asset.id,
|
||||
assetKey: asset.assetKey,
|
||||
mediaType: asset.mediaType,
|
||||
publicPath: asset.publicPath,
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
decorative: asset.decorative,
|
||||
};
|
||||
}
|
||||
if (supportsPreviewEvidenceKey(key)) {
|
||||
// Fixed literal, not derived: this file only ever resolves this one legacy
|
||||
// key today, so the id only needs to be stable, not computed.
|
||||
return {
|
||||
assetId: "00000000-0000-4000-8000-000000000001",
|
||||
assetKey: key,
|
||||
mediaType: "image/svg+xml",
|
||||
publicPath: "/media/fetch-strategy-boundary.svg",
|
||||
width: 1080,
|
||||
height: 420,
|
||||
decorative: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
assetId: "00000000-0000-4000-8000-000000000000",
|
||||
assetKey: key,
|
||||
mediaType: "application/octet-stream",
|
||||
publicPath: "",
|
||||
width: null,
|
||||
height: null,
|
||||
decorative: true,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// This preview has no adapter-level asset catalog access, so it resolves the
|
||||
// canonical descriptor from the same local fixture data as `resolvePreviewEvidenceAsset`
|
||||
// above. The `assetId` is a fixed literal (not derived) because this file only ever
|
||||
// resolves this one key; it only needs to be stable, not computed.
|
||||
function resolvePreviewEvidenceAssetDescriptor(key: string): ResolvedAsset {
|
||||
if (!supportsPreviewEvidenceKey(key)) {
|
||||
throw new Error(`Unknown local evidence asset: ${key}`);
|
||||
}
|
||||
return {
|
||||
assetId: "00000000-0000-4000-8000-000000000001",
|
||||
assetKey: key,
|
||||
mediaType: "image/svg+xml",
|
||||
publicPath: "/media/fetch-strategy-boundary.svg",
|
||||
width: 1080,
|
||||
height: 420,
|
||||
decorative: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function InstantPreview({ draft, catalog }: { draft: WorkingCopyInput; catalog: CatalogEntry[] }) {
|
||||
export function InstantPreview({
|
||||
draft,
|
||||
catalog,
|
||||
assets = [],
|
||||
}: {
|
||||
draft: WorkingCopyInput;
|
||||
catalog: CatalogEntry[];
|
||||
assets?: readonly Asset[];
|
||||
}) {
|
||||
const studio = useStudio();
|
||||
let model: PublicRenderModel | null = null;
|
||||
let issues: string[] | null = null;
|
||||
@@ -60,7 +86,7 @@ export function InstantPreview({ draft, catalog }: { draft: WorkingCopyInput; ca
|
||||
{ mode: "PREVIEW", publishedAt: null },
|
||||
supportsPreviewEvidenceKey,
|
||||
),
|
||||
resolvePreviewEvidenceAssetDescriptor,
|
||||
resolveAssetDescriptor(assets),
|
||||
);
|
||||
} catch (error) {
|
||||
issues = error instanceof ContentFormatError
|
||||
@@ -73,7 +99,7 @@ export function InstantPreview({ draft, catalog }: { draft: WorkingCopyInput; ca
|
||||
<PublicRecordRenderer
|
||||
model={model!}
|
||||
embedded
|
||||
resolveEvidenceAsset={resolvePreviewEvidenceAsset}
|
||||
resolveEvidenceAsset={createAssetCatalogResolver(assets)}
|
||||
resolvePublishedLabel={studio.resolvePublishedLabel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
} from "../../../contracts/studio/contract.ts";
|
||||
import { deriveValidationState } from "../../../domain/studio/document-state.ts";
|
||||
import { createLocalId } from "../../../domain/studio/local-id.ts";
|
||||
import { createResolvedAssetResolver } from "../../shared/public-render/asset-resolvers.ts";
|
||||
import { PublicRecordRenderer } from "../../shared/public-render/public-record-renderer.tsx";
|
||||
import { useStudio } from "../use-studio.ts";
|
||||
import { GuardedStudioLink } from "./guarded-studio-link.tsx";
|
||||
@@ -45,19 +46,6 @@ function formatDateTime(value: string): string {
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function resolvePreviewEvidenceAsset(key: string) {
|
||||
if (key !== "fetch-strategy-boundary") {
|
||||
throw new Error(`Unknown local evidence asset: ${key}`);
|
||||
}
|
||||
return {
|
||||
src: "/media/fetch-strategy-boundary.svg",
|
||||
width: 1080,
|
||||
height: 420,
|
||||
triggerLabel: "Fetch Join과 Batch Fetch 비교 다이어그램 크게 보기",
|
||||
dialogLabel: "Fetch Join과 Batch Fetch의 페이징 경계 확대",
|
||||
};
|
||||
}
|
||||
|
||||
export function PublicPreviewScreen({ documentId }: { documentId: string }) {
|
||||
const studio = useStudio();
|
||||
const [retryKey, setRetryKey] = useState(0);
|
||||
@@ -287,7 +275,11 @@ export function PublicPreviewScreen({ documentId }: { documentId: string }) {
|
||||
<PublicRecordRenderer
|
||||
model={previewDetail.preview.renderModel}
|
||||
embedded
|
||||
resolveEvidenceAsset={resolvePreviewEvidenceAsset}
|
||||
resolveEvidenceAsset={createResolvedAssetResolver(
|
||||
previewDetail.preview.renderModel.kind === "CASE"
|
||||
? previewDetail.preview.renderModel.bodyBlocks
|
||||
: [],
|
||||
)}
|
||||
resolvePublishedLabel={studio.resolvePublishedLabel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+4
-15
@@ -5,7 +5,7 @@ import {
|
||||
type StudioGatewayError,
|
||||
} from "../../../application/ports/studio-gateway-error.ts";
|
||||
import type { PublicationSnapshot } from "../../../contracts/studio/contract.ts";
|
||||
import type { EvidenceAsset } from "../../../domain/public-render-content.ts";
|
||||
import { createResolvedAssetResolver } from "../../shared/public-render/asset-resolvers.ts";
|
||||
import { PublicRecordRenderer } from "../../shared/public-render/public-record-renderer.tsx";
|
||||
import { GuardedStudioLink } from "./guarded-studio-link.tsx";
|
||||
import {
|
||||
@@ -20,19 +20,6 @@ const eventLabels = {
|
||||
UNPUBLISHED: "게시 취소",
|
||||
} as const;
|
||||
|
||||
function resolveEvidenceAsset(key: string): EvidenceAsset {
|
||||
if (key !== "fetch-strategy-boundary") {
|
||||
throw new Error(`Unknown local evidence asset: ${key}`);
|
||||
}
|
||||
return {
|
||||
src: "/media/fetch-strategy-boundary.svg",
|
||||
width: 1080,
|
||||
height: 420,
|
||||
triggerLabel: "Fetch Join과 Batch Fetch 비교 다이어그램 크게 보기",
|
||||
dialogLabel: "Fetch Join과 Batch Fetch의 페이징 경계 확대",
|
||||
};
|
||||
}
|
||||
|
||||
function dateTime(value: string) {
|
||||
return new Intl.DateTimeFormat("ko-KR", {
|
||||
dateStyle: "long",
|
||||
@@ -166,7 +153,9 @@ export function PublicationEventPreviewScreen({
|
||||
<PublicRecordRenderer
|
||||
model={renderModel}
|
||||
embedded
|
||||
resolveEvidenceAsset={resolveEvidenceAsset}
|
||||
resolveEvidenceAsset={createResolvedAssetResolver(
|
||||
renderModel.kind === "CASE" ? renderModel.bodyBlocks : [],
|
||||
)}
|
||||
resolvePublishedLabel={studio.resolvePublishedLabel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user