Files
tech-log-frontend/src/features/tech-log/adapters/http/http-management-gateway.ts
T
DongHyeonkaandClaude Opus 5 16e5b9f4ec feat: 프로젝트를 편집하고 활동을 남길 수 있게 한다
홈의 집중 카드가 제목만 보여 주고 "현재 목표"·"다음 작업" 칸이 비어 있었다.
프로젝트 페이지의 "활동" 도 늘 비어 있었다.

프로젝트를 만들 수는 있었지만 고칠 화면이 없었다. 그래서 이름과 slug 말고는 아무
값도 가질 수 없었고, 그 값을 읽는 공개 화면들은 빈칸을 그렸다 — 백엔드의
`updateProject` 는 처음부터 구현돼 있었고 채울 화면만 없었다.

`/studio/projects/:id` 를 연다. 프로젝트 필드 전부와 활동 목록을 한 화면에 둔다 —
프로젝트 밖의 활동은 존재하지 않고, 무엇이 공개 타임라인에 실리는지를 프로젝트 필드와
같은 자리에서 보는 편이 낫다.

이미 공개된 프로젝트는 저장한 뒤 투영도 함께 갱신한다. 투영은 게시할 때 세워지므로,
저장만 하면 공개 화면에는 예전 값이 남는다.

단계와 활동 유형의 선택지는 DB CHECK 제약과 같은 목록이다. 화면이 더 많은 값을
보여 주면 저장이 제약에서 터지고, 작성자는 왜 안 되는지 알 수 없다.

라우트가 하나 늘어 CI 게이트가 함께 움직였다 — FE-GATE-009 는 설치된 라우트마다 수동
접근성 증거를 하나씩 요구하고 그 집합이 정확히 일치하지 않으면 거절한다. 아티팩트
기준선 132→133, 증거 개수 111→112, 게이트 형태 다이제스트 재계산(이전 상수 187dbd96…
을 이전 gates.json 에서 먼저 재현해 계산 방법을 확인했다), 서빙 패턴 하나 추가.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
2026-08-23 19:45:00 +09:00

140 lines
6.1 KiB
TypeScript

import type {
CreateDraftResponse,
HomeFocusRequest,
HomeFocusResponse,
ProjectActivityRequest,
ProjectActivityResponse,
UpdateProjectActivityRequest,
ProjectEditResponse,
ProjectIndexPage,
ProjectUpdateRequest,
PublishResponse,
ReleaseEditResponse,
ReleaseIndexPage,
ReleaseUpdateRequest,
TopicEdit,
} from "../../contracts/management/contract.ts";
import type { ManagementGateway } from "../../application/ports/management-gateway.ts";
import { ManagementGatewayError } from "../../application/ports/management-gateway-error.ts";
import type { StudioOperationExecutor } from "./http-studio-gateway.ts";
export type { ManagementGateway };
const ROUTE_ID = "TECH_LOG_STUDIO";
/**
* 주제·프로젝트 관리 게이트웨이.
*
* <p>Studio 게이트웨이와 같은 실패 규약을 쓴다 — 실패는 던지고, 화면은 `usePublicContent` 가 아니라
* Studio 쪽 상태 처리를 그대로 쓴다. 여기서 Result 로 감싸면 이 표면만 다른 규약이 된다.
*/
export {
ManagementGatewayError,
managementFailureMessage,
} from "../../application/ports/management-gateway-error.ts";
export function createHttpManagementGateway(
deps: Readonly<{ operations: StudioOperationExecutor }>,
): ManagementGateway {
async function run<T>(operationId: string, input: unknown): Promise<T> {
const outcome = await deps.operations.execute(operationId, input, { routeId: ROUTE_ID });
if (outcome.kind === "SUCCESS") return outcome.value as T;
if (outcome.kind === "PROBLEM") {
// The management surface answers with the ADR-006 envelope, which nests
// the code under `error` — reading `problem.code` found nothing and every
// failure surfaced as the literal "PROBLEM", matching no i18n key.
const body = outcome.problem as
| Readonly<{
code?: unknown;
detail?: unknown;
error?: Readonly<{ code?: unknown; message?: unknown }>;
}>
| null;
const code =
typeof body?.code === "string"
? body.code
: typeof body?.error?.code === "string"
? body.error.code
: "PROBLEM";
const detail =
typeof body?.error?.message === "string"
? body.error.message
: typeof body?.detail === "string"
? body.detail
: "";
throw new ManagementGatewayError(operationId, code, detail);
}
throw new ManagementGatewayError(operationId, outcome.kind, "");
}
return Object.freeze({
listTopics: () => run<TopicEdit[]>("listStudioTopics", {}),
createTopic: (input: TopicEdit) => run<TopicEdit>("createTopic", input),
updateTopic: (id: string, body: TopicEdit) => run<TopicEdit>("updateTopic", { id, body }),
deleteTopic: async (id: string, expectedVersion: number) => {
await run<void>("deleteTopic", { id, expectedVersion });
},
listProjects: (page?: number, size?: number) =>
run<ProjectIndexPage>("listStudioProjects", { ...(page !== undefined ? { page } : {}), ...(size !== undefined ? { size } : {}) }),
getProject: (id: string) => run<ProjectEditResponse>("getProjectForEdit", { id }),
createProject: (title: string) => run<CreateDraftResponse>("createProject", { title }),
updateProject: (id: string, body: ProjectUpdateRequest) =>
run<ProjectEditResponse>("updateProject", { id, body }),
listReleases: (page?: number, size?: number) =>
run<ReleaseIndexPage>("listStudioReleases", { ...(page !== undefined ? { page } : {}), ...(size !== undefined ? { size } : {}) }),
getRelease: (id: string) => run<ReleaseEditResponse>("getReleaseForEdit", { id }),
createRelease: (title: string) => run<CreateDraftResponse>("createRelease", { title }),
updateRelease: (id: string, body: ReleaseUpdateRequest) =>
run<ReleaseEditResponse>("updateRelease", { id, body }),
deleteRelease: async (id: string, expectedVersion: number) => {
await run<void>("deleteRelease", { id, expectedVersion });
},
listProjectActivities: (id: string) =>
run<ProjectActivityResponse[]>("listStudioProjectActivities", { id }),
createProjectActivity: (id: string, body: ProjectActivityRequest) =>
run<ProjectActivityResponse>("createProjectActivity", { id, body }),
updateProjectActivity: (
id: string,
activityId: string,
body: UpdateProjectActivityRequest,
) => run<ProjectActivityResponse>("updateProjectActivity", { id, activityId, body }),
deleteProjectActivity: async (id: string, activityId: string, expectedVersion: number) => {
await run<void>("deleteProjectActivity", { id, activityId, expectedVersion });
},
publishProject: (
id: string,
expectedVersion: number,
visibility: "PUBLIC" | "UNLISTED" = "PUBLIC",
) => run<PublishResponse>("publishProject", { id, expectedVersion, visibility }),
unpublishProject: (id: string, expectedVersion: number) =>
run<ProjectEditResponse>("unpublishProject", { id, expectedVersion }),
getHomeFocus: () => run<HomeFocusResponse>("getHomeFocus", {}),
updateHomeFocus: (body: HomeFocusRequest) =>
run<HomeFocusResponse>("updateHomeFocus", body),
publishRelease: (id: string, expectedVersion: number) =>
run<PublishResponse>("publishRelease", { id, expectedVersion }),
archiveRelease: (id: string, expectedVersion: number) =>
run<ReleaseEditResponse>("archiveRelease", { id, expectedVersion }),
deleteDocument: async (
kind: "CASE" | "REFERENCE" | "QUESTION",
id: string,
expectedVersion: number,
) => {
const operationId =
kind === "CASE"
? "deleteCaseDraft"
: kind === "REFERENCE"
? "deleteReferenceDraft"
: "deleteQuestion";
await run<void>(operationId, { id, expectedVersion });
},
deleteDecision: async (projectId: string, decisionId: string, expectedVersion: number) => {
await run<void>("deleteProjectDecision", { id: projectId, decisionId, expectedVersion });
},
deleteProject: async (id: string, expectedVersion: number) => {
await run<void>("deleteProject", { id, expectedVersion });
},
});
}