From 84d72c4f60ac26e2ee49d0618c87e43962bcd0cf Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Mon, 24 Aug 2026 01:41:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=EC=9D=84=20=EC=9E=90=EA=B8=B0=20=EC=A3=BC=EC=86=8C?= =?UTF-8?q?=EB=A1=9C=20=EC=98=AE=EA=B8=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 편집 폼이 릴리즈 목록 아래에 열렸다. 릴리즈가 열 개 스무 개로 늘면 편집하려고 목록 전체를 지나 내려가야 하고, 저장 버튼은 그보다 더 아래에 있다. 목록이 길어질수록 편집이 멀어지는 구조다. `/studio/releases/:id` 를 연다 — 문서와 프로젝트가 각자 편집 주소를 갖는 것과 같은 이유다. 목록의 "편집" 은 토글이 아니라 링크가 되고, 새 릴리즈를 만들면 곧바로 그 화면으로 간다(만들자마자 편집할 것이 분명하다). 라우트가 하나 늘어 CI 게이트가 함께 움직였다 — 아티팩트 기준선 133→134, 증거 개수 112→113, 게이트 형태 다이제스트 재계산(f9e7e521… 을 이전 gates.json 에서 먼저 재현해 계산 방법을 확인했다), 서빙 패턴 하나 추가. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu --- .../TECH_LOG_STUDIO_RELEASE_EDIT.md | 18 + config/ci/gates.json | 7 + login.png | Bin 0 -> 27977 bytes scripts/contracts/ci-gates.ts | 10 +- .../contracts/tech-log-route-contract.ts | 1 + .../studio/components/release-editor.tsx | 355 ++++++++++++++++++ .../studio/components/release-manager.tsx | 122 +----- .../studio/pages/release-edit-page.tsx | 5 + .../presentation/tech-log-route-runtime.tsx | 7 + tests/unit/ci-workflow-generation.test.ts | 4 +- .../unit/task3-selective-integration.test.ts | 2 +- tests/unit/tech-log-serving-contract.test.ts | 1 + vite.config.ts | 1 + 13 files changed, 411 insertions(+), 122 deletions(-) create mode 100644 artifacts/tests/a11y-manual/TECH_LOG_STUDIO_RELEASE_EDIT.md create mode 100644 login.png create mode 100644 src/features/tech-log/presentation/studio/components/release-editor.tsx create mode 100644 src/features/tech-log/presentation/studio/pages/release-edit-page.tsx 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 0000000000000000000000000000000000000000..5dc10c66793019c58e642f55ff20caede83f5131 GIT binary patch literal 27977 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 z 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예전에는 이 폼이 릴리즈 목록 아래에 열렸다. 릴리즈가 열 개 스무 개로 늘면 편집하려고 + * 목록 전체를 지나 내려가야 하고, 저장 버튼은 그보다 더 아래에 있다. 문서·프로젝트가 각자 편집 + * 주소를 갖는 것과 같은 이유로 릴리즈도 자기 주소를 갖는다. + * + *

본문이 마크다운 여섯 칸으로 나뉜 것은 계약의 모양이자 릴리즈 노트의 성격이다. 한 칸짜리 자유 + * 서술이었다면 "검증을 안 썼다"를 아무도 알아채지 못한다. + */ + +/** 백엔드 {@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(null); + const [status, setStatus] = useState(""); + const [pending, setPending] = useState(false); + const [error, setError] = useState(""); + const [generation, setGeneration] = useState(0); + + const reload = useCallback(() => setGeneration((value) => value + 1), []); + + useEffect(() => { + let cancelled = false; + void managementGateway.getRelease(releaseId).then( + (release) => { + if (cancelled) return; + setDraft(toDraft(release)); + setStatus(release.workflowStatus); + setError(""); + }, + () => { + if (!cancelled) setError("릴리즈를 불러오지 못했습니다."); + }, + ); + return () => { + cancelled = true; + }; + }, [managementGateway, releaseId, generation]); + + const update = (patch: Partial) => + setDraft((current) => (current === null ? current : { ...current, ...patch })); + + const requestOf = (current: Draft): ReleaseUpdateRequest => + ({ + expectedVersion: current.expectedVersion, + versionLabel: current.versionLabel.trim(), + title: current.title.trim(), + summary: current.summary, + changeTypes: [...current.changeTypes], + changesMarkdown: current.changesMarkdown, + verificationMarkdown: current.verificationMarkdown, + ...(current.releasedOn ? { releasedOn: current.releasedOn } : {}), + reasonMarkdown: current.reasonMarkdown, + userImpactMarkdown: current.userImpactMarkdown, + implementationImpactMarkdown: current.implementationImpactMarkdown, + knownLimitationsMarkdown: current.knownLimitationsMarkdown, + }) as ReleaseUpdateRequest; + + const save = async () => { + if (pending || draft === null) return; + setPending(true); + setError(""); + try { + const saved = await managementGateway.updateRelease(releaseId, requestOf(draft)); + setDraft(toDraft(saved)); + setRequestAnnouncement(`릴리즈 ${saved.versionLabel} 을(를) 저장했습니다.`); + reload(); + } catch (failure) { + setError(managementFailureMessage(failure, "저장하지 못했습니다.")); + } finally { + setPending(false); + } + }; + + const publish = async () => { + if (pending || draft === null) return; + setPending(true); + setError(""); + try { + /* + 먼저 저장한다. 발행은 서버에 저장된 릴리즈를 검사하는데(`PublishReleaseUseCase`), 예전에는 + 저장하지 않고 발행만 불렀다 — 화면의 칸을 다 채우고 공개를 눌러도 서버 쪽은 여전히 빈 + 초안이라 "모두 채워져야 합니다" 가 떴다. 채웠는데 안 된다는 말이 나온 이유가 이것이다. + */ + const saved = await managementGateway.updateRelease(releaseId, requestOf(draft)); + setDraft(toDraft(saved)); + const published = await managementGateway.publishRelease(releaseId, saved.version); + setRequestAnnouncement(`릴리즈를 공개했습니다: ${published.canonicalPath}`); + reload(); + } catch (failure) { + // 무엇이 모자란지는 서버가 안다. 예전에는 그 답을 버리고 필수 항목을 전부 나열했는데, + // 그러면 이미 채운 칸까지 비었다고 말하게 된다. + setError( + managementFailureMessage( + failure, + "공개하지 못했습니다. 버전, 제목, 한 줄 요약, 변경 유형, 변경 내용, 검증, 공개일이 모두 채워져야 합니다.", + ), + ); + } finally { + setPending(false); + } + }; + + const archive = async () => { + if (pending || draft === null) return; + setPending(true); + setError(""); + try { + await managementGateway.archiveRelease(releaseId, draft.expectedVersion); + setRequestAnnouncement("릴리즈를 공개에서 내렸습니다."); + reload(); + } catch (failure) { + setError(managementFailureMessage(failure, "공개에서 내리지 못했습니다.")); + } finally { + setPending(false); + } + }; + + const toggleChangeType = (value: string) => { + if (draft === null) return; + update({ + changeTypes: draft.changeTypes.includes(value) + ? draft.changeTypes.filter((entry) => entry !== value) + : [...draft.changeTypes, value], + }); + }; + + return ( +

+
+
+

+ RELEASE · {STATUS_LABELS[status] ?? status} +

+

{draft?.title || "릴리즈"}

+

공개 변경 기록 화면과 푸터 링크가 이 값을 그대로 읽습니다.

+
+ + 릴리즈 목록으로 + +
+ + {error ? ( +

+ {error} +

+ ) : null} + {draft === null && !error ? ( +

+ 릴리즈를 불러오는 중입니다. +

+ ) : null} + + {draft === null ? null : ( + <> +
+
+

RELEASE · {draft.versionLabel}

+

{draft.title || "릴리즈"}

+

변경 기록 화면과 푸터 링크가 이 값을 그대로 읽습니다.

+
+
+ + + +