홈의 집중 카드가 제목만 보여 주고 "현재 목표"·"다음 작업" 칸이 비어 있었다. 프로젝트 페이지의 "활동" 도 늘 비어 있었다. 프로젝트를 만들 수는 있었지만 고칠 화면이 없었다. 그래서 이름과 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
140 lines
6.1 KiB
TypeScript
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 });
|
|
},
|
|
});
|
|
}
|