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.
This commit is contained in:
DongHyeonka
2026-08-21 13:30:37 +09:00
parent ab8c6c14db
commit c5e8735041
15 changed files with 296 additions and 92 deletions
@@ -29,8 +29,16 @@ function isAbortError(error: unknown): boolean {
return error instanceof DOMException && error.name === "AbortError";
}
/**
* Decision 은 지울 수 없다. 계약에 삭제 operation 이 없고, 그건 누락이 아니라 판단이다 — 결정의
* 수명주기는 수락·기각·대체이고 그 셋은 무슨 일이 있었는지 남기는 반면 삭제는 없앤다.
*/
const DELETABLE_KINDS = new Set(["CASE", "REFERENCE", "QUESTION"]);
export function DocumentList() {
const { gateway } = useStudio();
const { gateway, managementGateway, setRequestAnnouncement } = useStudio();
const [deletingId, setDeletingId] = useState<string | null>(null);
const [deleteError, setDeleteError] = useState("");
const [searchDraft, setSearchDraft] = useState("");
const [q, setQ] = useState("");
const [kind, setKind] = useState<ListDocumentsQuery["kind"]>();
@@ -42,6 +50,33 @@ export function DocumentList() {
const [error, setError] = useState("");
const [retryGeneration, setRetryGeneration] = useState(0);
/**
* 목록 행에는 version 이 없다 (계약의 `DocumentSummary`). 삭제는 expectedVersion 을 요구하므로
* 지우기 직전에 작업본을 한 번 읽어 그 시점의 version 을 쓴다 — 목록을 띄워 둔 채 다른 곳에서
* 수정된 경우 여기서 409 로 걸리는 편이, 목록이 기억하던 낡은 version 으로 지우는 것보다 낫다.
*/
const removeDocument = async (item: DocumentPage["items"][number]) => {
if (deletingId !== null) return;
setDeletingId(item.id);
setDeleteError("");
try {
const detail = await gateway.getDocument(item.id);
await managementGateway.deleteDocument(
item.kind as "CASE" | "REFERENCE" | "QUESTION",
item.id,
detail.document.version,
);
setRequestAnnouncement(`작업본 ${item.title || "제목 없음"} 을(를) 삭제했습니다.`);
setRetryGeneration((value) => value + 1);
} catch {
setDeleteError(
"삭제하지 못했습니다. 게시 중이거나, 이 기록을 참조하는 곳이 있거나, 다른 곳에서 먼저 수정되었을 수 있습니다.",
);
} finally {
setDeletingId(null);
}
};
useEffect(() => {
const controller = new AbortController();
setLoading(true);
@@ -169,6 +204,11 @@ export function DocumentList() {
{page && !loading && !error ? (
<>
<p className="studio-result-count">{page.items.length} </p>
{deleteError ? (
<p className="studio-screen-error" role="alert">
{deleteError}
</p>
) : null}
{page.items.length ? (
<div className="studio-document-list">
{page.items.map((item) => (
@@ -202,6 +242,16 @@ export function DocumentList() {
</dd>
</div>
</dl>
{DELETABLE_KINDS.has(item.kind) ? (
<button
className="studio-secondary-button"
type="button"
disabled={deletingId !== null}
onClick={() => void removeDocument(item)}
>
</button>
) : null}
</article>
))}
</div>