diff --git a/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_RELEASE_EDIT.md b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_RELEASE_EDIT.md new file mode 100644 index 0000000..8053a79 --- /dev/null +++ b/artifacts/tests/a11y-manual/TECH_LOG_STUDIO_RELEASE_EDIT.md @@ -0,0 +1,18 @@ +# TECH_LOG_STUDIO_RELEASE_EDIT accessibility review + +Status: pending-manual-review +Route ID: TECH_LOG_STUDIO_RELEASE_EDIT +Release ID: +Reviewer: +Reviewed at: +Signature: +Attestation: pending +M1 Keyboard: pending +M2 Visible focus: pending +M3 Route focus: pending +M4 Modal focus: pending +M5 Error association: pending +M6 Color signal: pending +M7 Reduced motion: pending +Screen reader: pending +Notes: Human review pending; automated evidence does not replace signed keyboard, focus, and screen-reader review. diff --git a/config/ci/gates.json b/config/ci/gates.json index 259b9ac..7629081 100644 --- a/config/ci/gates.json +++ b/config/ci/gates.json @@ -1214,6 +1214,12 @@ "schemaId": "markdown", "production": "source-controlled" }, + { + "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-RELEASE-EDIT-md", + "path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_RELEASE_EDIT.md", + "schemaId": "markdown", + "production": "source-controlled" + }, { "id": "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-NOT-FOUND-md", "path": "artifacts/tests/a11y-manual/TECH_LOG_STUDIO_NOT_FOUND.md", @@ -1970,6 +1976,7 @@ "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-TAXONOMY-md", "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-PROJECT-EDIT-md", "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-RELEASES-md", + "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-RELEASE-EDIT-md", "artifact-artifacts-tests-a11y-manual-TECH-LOG-STUDIO-NOT-FOUND-md", "artifact-artifacts-tests-a11y-manual-NOT-FOUND-md", "artifact-artifacts-tests-a11y-manual-report-json" diff --git a/login.png b/login.png new file mode 100644 index 0000000..5dc10c6 Binary files /dev/null and b/login.png differ diff --git a/scripts/contracts/ci-gates.ts b/scripts/contracts/ci-gates.ts index 174600a..1751c61 100644 --- a/scripts/contracts/ci-gates.ts +++ b/scripts/contracts/ci-gates.ts @@ -471,7 +471,9 @@ const CANONICAL_GATE_SHAPE_SHA256 = // 프로젝트 편집 화면: FE-GATE-009 가 TECH_LOG_STUDIO_PROJECT_EDIT 증거 아티팩트를 // 얻어 다시 계산했다. 같은 방법이다 — 187dbd96… 을 이전 gates.json 에서 먼저 재현해, // 이 값을 만든 계산이 상수가 고정될 때 쓰인 그 계산임을 확인했다. - "f9e7e521c1ea587c992f475e38f0c5f2ca39721970724529fa98678cc078a6c7"; + // 릴리즈 편집 화면: 같은 방법으로 다시 계산했다. f9e7e521… 을 이전 gates.json 에서 먼저 + // 재현했다. + "fb138e7c51fdf969f755cd8ff32cf627f1750b966d212c33ee996c8c578db0e3"; function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string { const normalized = gates.map( @@ -529,8 +531,10 @@ function canonicalAuthorityBaselineFailures(contract: CiGateContract): string[] // The project edit route did the same: it is what finally lets a project carry // a purpose, a current objective, and a next step, so the public screens that // read those fields stop rendering blanks. - if (contract.artifacts.length !== 133) { - failures.push(`artifact authority baseline must contain exactly 133 artifacts; received ${contract.artifacts.length}`); + // The release edit route followed: the editor used to open below the release + // list, so editing meant scrolling past every release to reach it. + if (contract.artifacts.length !== 134) { + failures.push(`artifact authority baseline must contain exactly 134 artifacts; received ${contract.artifacts.length}`); } if (contract.stages.length !== 5) { failures.push(`stage authority baseline must contain exactly 5 stages; received ${contract.stages.length}`); diff --git a/src/features/tech-log/contracts/tech-log-route-contract.ts b/src/features/tech-log/contracts/tech-log-route-contract.ts index 1a7a5dc..24e04f5 100644 --- a/src/features/tech-log/contracts/tech-log-route-contract.ts +++ b/src/features/tech-log/contracts/tech-log-route-contract.ts @@ -51,6 +51,7 @@ const TECH_LOG_ROUTE_SPECS = [ defineSpec({ routeId: "TECH_LOG_STUDIO_TAXONOMY", path: "/studio/taxonomy", layoutGroup: "STUDIO", paramsSchema: null, searchSchema: null, title: "주제와 프로젝트", navigationLabel: "주제·프로젝트", navigationOrder: 40 }), defineSpec({ routeId: "TECH_LOG_STUDIO_PROJECT_EDIT", path: "/studio/projects/:id", layoutGroup: "STUDIO", paramsSchema: "TechLogDocumentIdParams", searchSchema: null, title: "프로젝트 편집", navigationLabel: null, navigationOrder: null }), defineSpec({ routeId: "TECH_LOG_STUDIO_RELEASES", path: "/studio/releases", layoutGroup: "STUDIO", paramsSchema: null, searchSchema: null, title: "릴리즈", navigationLabel: "릴리즈", navigationOrder: 50 }), + defineSpec({ routeId: "TECH_LOG_STUDIO_RELEASE_EDIT", path: "/studio/releases/:id", layoutGroup: "STUDIO", paramsSchema: "TechLogDocumentIdParams", searchSchema: null, title: "릴리즈 편집", navigationLabel: null, navigationOrder: null }), defineSpec({ routeId: "TECH_LOG_STUDIO_NOT_FOUND", path: "/studio/*", layoutGroup: "STUDIO", paramsSchema: "TechLogStudioSplat", searchSchema: null, title: "Studio 화면을 찾을 수 없습니다", navigationLabel: null, navigationOrder: null }), defineSpec({ routeId: "NOT_FOUND", path: "*", layoutGroup: "PUBLIC", paramsSchema: "NotFoundSplat", searchSchema: null, title: "페이지를 찾을 수 없습니다.", navigationLabel: null, navigationOrder: null }), ] as const; diff --git a/src/features/tech-log/presentation/studio/components/release-editor.tsx b/src/features/tech-log/presentation/studio/components/release-editor.tsx new file mode 100644 index 0000000..0c39e6c --- /dev/null +++ b/src/features/tech-log/presentation/studio/components/release-editor.tsx @@ -0,0 +1,355 @@ +import { useCallback, useEffect, useState } from "react"; + +import type { + ReleaseEditResponse, + ReleaseUpdateRequest, +} from "../../../contracts/management/contract.ts"; +import { managementFailureMessage } from "../../../application/ports/management-gateway-error.ts"; +import { useRouteInput } from "../../../../../presentation/routes/route-input.tsx"; +import { GuardedStudioLink } from "./guarded-studio-link.tsx"; +import { useStudio } from "../use-studio.ts"; + +/** + * 릴리즈 한 편의 편집 화면. + * + *
예전에는 이 폼이 릴리즈 목록 아래에 열렸다. 릴리즈가 열 개 스무 개로 늘면 편집하려고 + * 목록 전체를 지나 내려가야 하고, 저장 버튼은 그보다 더 아래에 있다. 문서·프로젝트가 각자 편집 + * 주소를 갖는 것과 같은 이유로 릴리즈도 자기 주소를 갖는다. + * + *
본문이 마크다운 여섯 칸으로 나뉜 것은 계약의 모양이자 릴리즈 노트의 성격이다. 한 칸짜리 자유
+ * 서술이었다면 "검증을 안 썼다"를 아무도 알아채지 못한다.
+ */
+
+/** 백엔드 {@code UpdateReleaseUseCase.CHANGE_TYPES} 와 같은 집합이다. */
+const CHANGE_TYPES = [
+ { value: "FEATURE", label: "기능" },
+ { value: "FIX", label: "수정" },
+ { value: "REFACTOR", label: "구조" },
+ { value: "DOCS", label: "문서" },
+ { value: "INFRA", label: "인프라" },
+ { value: "BREAKING", label: "호환 깨짐" },
+] as const;
+
+const SECTIONS = [
+ { key: "reasonMarkdown", label: "왜 바꿨나" },
+ { key: "changesMarkdown", label: "무엇을 바꿨나" },
+ { key: "userImpactMarkdown", label: "사용자에게 달라지는 것" },
+ { key: "implementationImpactMarkdown", label: "구현에 남는 것" },
+ { key: "verificationMarkdown", label: "어떻게 검증했나" },
+ { key: "knownLimitationsMarkdown", label: "아직 못 한 것" },
+] as const;
+
+type Draft = Readonly<{
+ expectedVersion: number;
+ versionLabel: string;
+ title: string;
+ summary: string;
+ releasedOn: string;
+ changeTypes: readonly string[];
+ reasonMarkdown: string;
+ changesMarkdown: string;
+ userImpactMarkdown: string;
+ implementationImpactMarkdown: string;
+ verificationMarkdown: string;
+ knownLimitationsMarkdown: string;
+}>;
+
+function toDraft(release: ReleaseEditResponse): Draft {
+ return {
+ expectedVersion: release.version,
+ versionLabel: release.versionLabel,
+ title: release.title,
+ summary: release.summary ?? "",
+ releasedOn: release.releasedOn ?? "",
+ changeTypes: release.changeTypes ?? [],
+ reasonMarkdown: release.reasonMarkdown ?? "",
+ changesMarkdown: release.changesMarkdown ?? "",
+ userImpactMarkdown: release.userImpactMarkdown ?? "",
+ implementationImpactMarkdown: release.implementationImpactMarkdown ?? "",
+ verificationMarkdown: release.verificationMarkdown ?? "",
+ knownLimitationsMarkdown: release.knownLimitationsMarkdown ?? "",
+ };
+}
+
+export function ReleaseEditor() {
+ const { params } = useRouteInput<"TECH_LOG_STUDIO_RELEASE_EDIT">();
+ const releaseId = params.id;
+ const { managementGateway, setRequestAnnouncement } = useStudio();
+
+ const [draft, setDraft] = useState
+ RELEASE · {STATUS_LABELS[status] ?? status}
+ 공개 변경 기록 화면과 푸터 링크가 이 값을 그대로 읽습니다.
+ {error}
+
+ 릴리즈를 불러오는 중입니다.
+ RELEASE · {draft.versionLabel} 변경 기록 화면과 푸터 링크가 이 값을 그대로 읽습니다.{draft?.title || "릴리즈"}
+ {draft.title || "릴리즈"}
+