Commit Graph
4 Commits
Author SHA1 Message Date
DongHyeonka c5e8735041 feat: read the profile's topics from Studio, and add working-copy deletion
Two things an author could not control from Studio.

The profile's "주요 관심 주제" was four strings in the JSX. Creating or removing
a topic in Studio changed nothing, and correcting the list meant a rebuild and
a redeploy. It now renders the published topic list. The old literal opened
with "Backend Architecture", which no record in the catalogue actually carries
— the profile was advertising a topic that did not exist, and nothing could
have caught that while the list lived in the markup.

The working-copy list gained a delete control. It routes by kind because the
contract and the storage both do: Case and Reference share one table split by
type, Question is its own. Decision has no delete — its lifecycle is accept,
reject, supersede, which records what happened rather than erasing it — so the
control does not appear for it.

The list summary carries no version, so deletion reads the working copy first
and uses the version it finds. A stale version from a list left open should
fail as a conflict, not delete whatever is there now.
2026-08-21 13:30:37 +09:00
DongHyeonka 760071156d fix: give each API surface its own error-code enum
The public site answered every screen with the terminal error surface. Three
defects stacked, and each one hid the next.

The first refused the request outright: `attachCredentials` asks the Studio
helper, which returns null for a profile it does not own, and the fallback
below read the session and rejected anything not authenticated. Public reads
declare the ANONYMOUS profile, so a signed-out visitor — the public site's
entire audience — never got a request out of the browser. An anonymous profile
carries no credentials by definition and must never consult the session.

With requests flowing, the second surfaced: `envelopeError()` pinned
`ApiError.code` to the Studio enum and all three surfaces shared it. Public and
Management each declare their own enum in their own contract, so every error
they returned failed validation and arrived as a CONTRACT_VIOLATION — an
unclassifiable transport fault — rather than the domain error it was. A strict
enum checked against the wrong surface's contract still looks strict, which is
why no gate caught it. Each surface now passes its own contract's codes.

The third was the not-found path: it read `status` and `code` off the problem
body, but the envelope has no `status` and names the code for its surface
(PUBLIC_RESOURCE_NOT_FOUND, not NOT_FOUND). The HTTP status from the transport
is the authoritative signal and the only one that holds across both shapes.

The regression test composes the real runtime adapters against the deployed
backend's actual 404 body. Neither the gateway tests (which stub the executor)
nor the screen tests (which stub the gateway) cover this seam, and the whole
outage lived in it.

Two page-level fixes came out of the same investigation: the profile page asked
for two project slugs that only ever existed in the static fixture, and the
index pages held their fixed header copy behind a request that had nothing to
do with it. Headers now paint immediately; only the sections that are actually
waiting show a fallback, and an empty list says so instead of rendering blank.
2026-08-21 01:14:20 +09:00
DongHyeonka 4566f2d7a8 refactor: make the public read port async so a network adapter can implement it
`PublicContentQueries` returned arrays, not promises. That signature is only
implementable by something already in memory, so the port could hold exactly
one adapter — the bundled fixture — and no amount of configuration could put
the public site on the backend. Turning it async is the change that makes a
second adapter possible; the adapter itself follows.

The markup is untouched. Every page reads a value and hands it to a
presentational component, so the shape those components receive is mapped at
the adapter boundary and nothing below the page changes.

Screens load through one query, not one per read. Several pages read in a loop
— the home timeline walks every project for its activity, the explore filter
walks search results to resolve titles — and a hook per read would mean a
variable number of hooks per render, which React forbids. `usePublicContent`
takes the whole screen's reads as one loader, where a loop is a loop and
`Promise.all` is available; the loops that used to be N sequential lookups now
issue together.

Two places deliberately do not show the loading surface. The explore filter
sits inside a page that already renders one, so a second skeleton would move
the layout under it — it keeps its structure and fills its options in when they
arrive. The search dialog is a type-ahead: re-querying per keystroke would
replace the results with a skeleton on every key, so it loads the catalog once
and applies the same predicate locally.

`usePublicContent` requires an object because `undefined` is how the query
layer says "no result yet". A loader returning the record itself would make a
missing slug indistinguishable from a request in flight, and the page would sit
on a skeleton instead of rendering its not-found route.

Studio's `resolvePublishedLabel` stays synchronous. It is called from inside
the public renderer, so making it async would push awaits through the render
tree; the shell loads the catalog once and the callback remains a lookup.

The component tests now assemble the query providers the running app assembles.
Without them the render throws "No QueryClient set" — not a harness quirk, but
the same failure the app would produce if it were mounted without its query
layer.
2026-08-20 16:53:51 +09:00
DongHyeonka 2b6fa42620 feat: complete TechLog public screens 2026-08-15 23:19:04 +09:00