From fd73bc88a19141bd2781d26561cd053b722ad4c3 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Mon, 24 Aug 2026 18:03:11 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Decision=20=EB=AF=B8=EB=A6=AC=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=EC=9D=98=20=EA=B2=B0=EC=A0=95=EC=9D=BC=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EB=A5=BC=20=ED=92=80=EA=B3=A0,=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EA=B0=80=20=EC=8B=A4=EC=A0=9C=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=EC=9D=84=20=EB=8B=A4=EC=8B=9C=20=EB=A7=90?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decision 은 결정일이 없으면 미리보기가 열리지 않았다. 검증은 그것을 경고로만 다루므로 날짜 없이 게시할 수 있는데 렌더 모델이 필수로 요구했다 — 작성자는 "경고라면서 왜 안 되냐"를 만난다. 계약을 nullable 로 열고 화면이 "결정일 미정"이라고 말하게 한다. 한 칸의 실패가 화면을 통째로 날리지 않게 한다. `Promise.all([gateway.foo()])` 은 foo 가 거절하는 것만 잡는다 — 호출이 동기적으로 던지면 배열을 만드는 중에 터져 rejection handler 를 지나지 못하고, 그러면 홈 focus 한 칸 때문에 대시보드 전체가 빈 화면이 된다. 프로젝트 편집도 같은 모양이라 함께 고친다. `IntersectionObserver` 가 없는 환경을 견딘다. 목차는 픽스처 Case 하나에서만 쓰여 그런 환경을 만난 적이 없었는데, 모든 Case 가 목차를 받게 되면서 jsdom 에서 문서가 통째로 깨졌다. 없으면 "지금 읽는 절" 표시만 못 할 뿐이다. 픽스처의 최근 기록에서 릴리스를 뺀다. 서버의 `latestEntries` 는 공개 투영에서 고르므로 릴리스가 없고, 홈이 릴리스를 따로 읽어 합친다 — 픽스처가 넣으면 같은 릴리스가 두 번 나온다. 화면 테스트는 `test:unit` 이 아니라 `test:tech-log` 가 돌린다. 그것을 돌리지 않아 위 두 결함과, 라우트 두 개·`--body-copy`·Case 배치 통합·활동 링크 제거처럼 의도한 변경에 고정돼 있던 단언들이 23건 빨간 채로 여러 커밋을 지나갔다. 단언을 실제 동작으로 옮긴다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu --- public/release-manifest.json | 10 ++--- .../tech-log/adapters/static/public-query.ts | 17 +++------ .../management/canonical-source.json | 2 +- .../contracts/public/canonical-source.json | 2 +- .../contracts/studio/canonical-source.json | 4 +- .../tech-log/contracts/studio/generated.ts | 2 +- .../contracts/studio/studio-api.openapi.yaml | 5 ++- .../project-public-render-model.ts | 8 +++- .../shared/public-render/document-toc.tsx | 10 +++++ .../public-render/public-record-renderer.tsx | 6 ++- .../studio/components/home-focus-editor.tsx | 26 ++++++++----- .../studio/components/project-editor.tsx | 23 ++++++------ .../tech-log/public-document-screens.test.tsx | 19 +++++++--- .../tech-log/public-index-screens.test.tsx | 37 +++++++------------ .../features/tech-log/public-render.test.tsx | 14 +++++-- .../features/tech-log/route-contract.test.ts | 8 +++- .../features/tech-log/style-contract.test.ts | 4 +- 17 files changed, 117 insertions(+), 80 deletions(-) diff --git a/public/release-manifest.json b/public/release-manifest.json index 9798c8c..e7984ca 100644 --- a/public/release-manifest.json +++ b/public/release-manifest.json @@ -38,28 +38,28 @@ }, "contractSet": { "setAlgorithm": "CA_CONTRACT_SET_V1", - "setDigest": "sha256:b729150c56b64ba01d7a71a7442c14bb04d9e573b852d1d1879e748854c20174", + "setDigest": "sha256:7832d43886cf12e6569ffd28e3f033d8a64a88e673e31449726d6dc450fa62ae", "packages": [ { "packageId": "@tech-log/management-contract", "version": "1.0.0", "digest": "sha256:72650735061fde627f5037571eb986cb758f44a546f065c88408399f8eec4a55", "runtimeProtocolVersion": 1, - "sourceRevision": "06ae075" + "sourceRevision": "83148b2" }, { "packageId": "@tech-log/public-contract", "version": "2.1.0", "digest": "sha256:702d6666a8feba9899c7eb7c2a94a0880bcb23b178c7ed2009c6e69d9a1c848c", "runtimeProtocolVersion": 1, - "sourceRevision": "06ae075" + "sourceRevision": "83148b2" }, { "packageId": "@tech-log/studio-contract", "version": "3.1.0", - "digest": "sha256:6fc015ca6727af88b7fb0088e02ba97846e1dd79fb0d4fc593cc79f2a3b9795f", + "digest": "sha256:18dd46898be64b07f7e826409d19347512613ee2e22420028a4a0644f50f37dd", "runtimeProtocolVersion": 1, - "sourceRevision": "06ae075" + "sourceRevision": "83148b2" } ] } diff --git a/src/features/tech-log/adapters/static/public-query.ts b/src/features/tech-log/adapters/static/public-query.ts index 6907c14..7ed056a 100644 --- a/src/features/tech-log/adapters/static/public-query.ts +++ b/src/features/tech-log/adapters/static/public-query.ts @@ -142,17 +142,12 @@ export function getLatestEntries(): LatestRecordEntry[] { }; }), ); - const releaseEntries = releases.map((release) => ({ - id: `release-${release.version}`, - entryType: "RELEASE" as const, - title: release.title, - summary: release.summary, - path: release.path, - publishedAt: release.publishedAt, - topic: "TechLog", - project: "TechLog", - })); - return [...activities, ...releaseEntries].sort((left, right) => + /* + 릴리스는 넣지 않는다. 이 목록은 서버의 `latestEntries` 와 같은 의미여야 하고, 그쪽은 공개 + 투영에서 고르므로 릴리스가 없다 — 릴리스는 Publication 파이프라인을 거치지 않는다. 홈 화면이 + 릴리스를 따로 읽어 합치므로, 여기서도 넣으면 같은 릴리스가 두 번 나온다. + */ + return [...activities].sort((left, right) => right.publishedAt.localeCompare(left.publishedAt), ); } diff --git a/src/features/tech-log/contracts/management/canonical-source.json b/src/features/tech-log/contracts/management/canonical-source.json index 82ac283..5bf6ed9 100644 --- a/src/features/tech-log/contracts/management/canonical-source.json +++ b/src/features/tech-log/contracts/management/canonical-source.json @@ -2,7 +2,7 @@ "packageId": "@tech-log/management-contract", "version": "1.0.0", "digest": "sha256:72650735061fde627f5037571eb986cb758f44a546f065c88408399f8eec4a55", - "sourceRevision": "06ae075", + "sourceRevision": "83148b2", "operationIds": [ "createCaseDraft", "getCaseForEdit", diff --git a/src/features/tech-log/contracts/public/canonical-source.json b/src/features/tech-log/contracts/public/canonical-source.json index ecf0137..c150146 100644 --- a/src/features/tech-log/contracts/public/canonical-source.json +++ b/src/features/tech-log/contracts/public/canonical-source.json @@ -2,7 +2,7 @@ "packageId": "@tech-log/public-contract", "version": "2.1.0", "digest": "sha256:702d6666a8feba9899c7eb7c2a94a0880bcb23b178c7ed2009c6e69d9a1c848c", - "sourceRevision": "06ae075", + "sourceRevision": "83148b2", "operationIds": [ "getPublicSite", "getPublicHome", diff --git a/src/features/tech-log/contracts/studio/canonical-source.json b/src/features/tech-log/contracts/studio/canonical-source.json index 22250c8..daf319a 100644 --- a/src/features/tech-log/contracts/studio/canonical-source.json +++ b/src/features/tech-log/contracts/studio/canonical-source.json @@ -1,8 +1,8 @@ { "packageId": "@tech-log/studio-contract", "version": "3.1.0", - "digest": "sha256:6fc015ca6727af88b7fb0088e02ba97846e1dd79fb0d4fc593cc79f2a3b9795f", - "sourceRevision": "06ae075", + "digest": "sha256:18dd46898be64b07f7e826409d19347512613ee2e22420028a4a0644f50f37dd", + "sourceRevision": "83148b2", "operationIds": [ "getStudioSession", "getStudioDashboard", diff --git a/src/features/tech-log/contracts/studio/generated.ts b/src/features/tech-log/contracts/studio/generated.ts index 7f73fb6..12470dc 100644 --- a/src/features/tech-log/contracts/studio/generated.ts +++ b/src/features/tech-log/contracts/studio/generated.ts @@ -1167,7 +1167,7 @@ export interface components { /** @enum {string} */ status: "PROPOSED" | "ADOPTED"; /** Format: date */ - decidedOn: string; + decidedOn: string | null; statement: string; rationale: string; consequences: components["schemas"]["OrderedText"][]; diff --git a/src/features/tech-log/contracts/studio/studio-api.openapi.yaml b/src/features/tech-log/contracts/studio/studio-api.openapi.yaml index ac68d97..fb37387 100644 --- a/src/features/tech-log/contracts/studio/studio-api.openapi.yaml +++ b/src/features/tech-log/contracts/studio/studio-api.openapi.yaml @@ -1572,7 +1572,10 @@ components: properties: kind: { type: string, enum: [PROJECT_DECISION] } status: { type: string, enum: [PROPOSED, ADOPTED] } - decidedOn: { type: string, format: date } + # 결정일은 비어 있을 수 있다. 검증은 이것을 경고로만 다루므로(DECIDED_ON_REQUIRED) + # 날짜 없이 게시할 수 있는데, 렌더 모델이 필수로 요구하면 그 문서는 미리보기조차 + # 열리지 않는다 — 두 규칙이 어긋나면 작성자는 "경고라며 왜 안 되냐"를 만난다. + decidedOn: { type: [string, "null"], format: date } statement: { type: string, maxLength: 100000 } rationale: { type: string, maxLength: 100000 } consequences: { type: array, maxItems: 50, items: { $ref: "#/components/schemas/OrderedText" } } diff --git a/src/features/tech-log/domain/content-format/project-public-render-model.ts b/src/features/tech-log/domain/content-format/project-public-render-model.ts index 4f028f2..c66294f 100644 --- a/src/features/tech-log/domain/content-format/project-public-render-model.ts +++ b/src/features/tech-log/domain/content-format/project-public-render-model.ts @@ -284,12 +284,16 @@ export function projectWorkingCopy( case "PROJECT_DECISION": if (!input.decisionStatus) fail("Decision status is required"); - if (!input.decidedOn) fail("Decision date is required"); + /* + 결정일은 비어 있어도 모델을 만든다. 검증이 그것을 경고로만 다루므로 날짜 없이 게시할 수 + 있는데, 여기서 막으면 그 문서는 미리보기조차 열리지 않는다 — 작성자는 "경고라면서 왜 + 안 되냐"를 만난다. 화면이 "미정"이라고 말하면 된다. + */ return { ...base, kind: "PROJECT_DECISION", status: input.decisionStatus, - decidedOn: input.decidedOn, + decidedOn: input.decidedOn ?? null, statement: input.statement, rationale: input.rationale, consequences: ordered(input.consequences), diff --git a/src/features/tech-log/presentation/shared/public-render/document-toc.tsx b/src/features/tech-log/presentation/shared/public-render/document-toc.tsx index b915a0b..e490e3c 100644 --- a/src/features/tech-log/presentation/shared/public-render/document-toc.tsx +++ b/src/features/tech-log/presentation/shared/public-render/document-toc.tsx @@ -16,6 +16,16 @@ export function DocumentToc({ headings, variant }: DocumentTocProps) { const detailsRef = useRef(null); useEffect(() => { + /* + `IntersectionObserver` 가 없는 환경이 있다 — jsdom 이 그렇고, 오래된 브라우저도 그렇다. + 없으면 "지금 읽는 절" 표시만 못 할 뿐 목차 자체는 쓸 수 있으므로, 없다고 문서를 통째로 + 못 그리게 두지 않는다. + + 한동안 이 컴포넌트는 픽스처 Case 하나에서만 쓰여 그 환경을 만난 적이 없었다. 모든 Case 가 + 목차를 받게 되면서 드러났다. + */ + if (typeof IntersectionObserver === "undefined") return undefined; + const elements = headings .map((heading) => document.getElementById(heading.id)) .filter((element): element is HTMLElement => Boolean(element)); diff --git a/src/features/tech-log/presentation/shared/public-render/public-record-renderer.tsx b/src/features/tech-log/presentation/shared/public-render/public-record-renderer.tsx index e715672..9ac7bfe 100644 --- a/src/features/tech-log/presentation/shared/public-render/public-record-renderer.tsx +++ b/src/features/tech-log/presentation/shared/public-render/public-record-renderer.tsx @@ -435,7 +435,11 @@ function ProjectDecisionDocument({
{model.status} - + {model.decidedOn ? ( + + ) : ( + 결정일 미정 + )}

{model.title}

</p> diff --git a/src/features/tech-log/presentation/studio/components/home-focus-editor.tsx b/src/features/tech-log/presentation/studio/components/home-focus-editor.tsx index b2214dc..9445469 100644 --- a/src/features/tech-log/presentation/studio/components/home-focus-editor.tsx +++ b/src/features/tech-log/presentation/studio/components/home-focus-editor.tsx @@ -41,12 +41,19 @@ export function HomeFocusEditor() { useEffect(() => { let cancelled = false; - void Promise.all([ - managementGateway.getHomeFocus(), - managementGateway.listProjects(0, 50), - gateway.getCatalog({ type: "RELATION", limit: 100 }), - ]).then( - ([current, projectPage, catalog]) => { + /* + try/catch 로 감싼다. `Promise.all([gateway.foo()])` 은 `foo` 가 거절하는 것만 잡는다 — + 호출 자체가 동기적으로 던지면(예: 그 표면을 갖추지 않은 게이트웨이) 배열을 만드는 중에 + 터지므로 rejection handler 를 지나지 못한다. 그러면 이 섹션이 아니라 대시보드 전체가 + 빈 화면이 된다. 한 칸의 실패가 화면을 통째로 날리지 않게 한다. + */ + void (async () => { + try { + const [current, projectPage, catalog] = await Promise.all([ + managementGateway.getHomeFocus(), + managementGateway.listProjects(0, 50), + gateway.getCatalog({ type: "RELATION", limit: 100 }), + ]); if (cancelled) return; setFocus(current); setProjects(projectPage.items ?? []); @@ -55,11 +62,10 @@ export function HomeFocusEditor() { setQuestionId(current.openQuestionId ?? ""); setDecisionId(current.recentDecisionId ?? ""); setError(""); - }, - () => { + } catch { if (!cancelled) setError("홈 설정을 불러오지 못했습니다."); - }, - ); + } + })(); return () => { cancelled = true; }; diff --git a/src/features/tech-log/presentation/studio/components/project-editor.tsx b/src/features/tech-log/presentation/studio/components/project-editor.tsx index fa6c8eb..59a8a4a 100644 --- a/src/features/tech-log/presentation/studio/components/project-editor.tsx +++ b/src/features/tech-log/presentation/studio/components/project-editor.tsx @@ -122,13 +122,15 @@ export function ProjectEditor() { useEffect(() => { let cancelled = false; - void Promise.all([ - managementGateway.getProject(projectId), - managementGateway.listProjectActivities(projectId), - managementGateway.listTopics(), - gateway.getCatalog({ type: "RELATION", limit: 100 }), - ]).then( - ([loaded, activityList, topicList, catalog]) => { + // 동기적으로 던지는 호출도 잡는다 — `home-focus-editor` 와 같은 이유다. + void (async () => { + try { + const [loaded, activityList, topicList, catalog] = await Promise.all([ + managementGateway.getProject(projectId), + managementGateway.listProjectActivities(projectId), + managementGateway.listTopics(), + gateway.getCatalog({ type: "RELATION", limit: 100 }), + ]); if (cancelled) return; setProject(loaded); setDraft(toDraft(loaded)); @@ -136,11 +138,10 @@ export function ProjectEditor() { setTopics(topicList.filter((topic) => topic.status !== "ARCHIVED")); setRecords(catalog.items ?? []); setError(""); - }, - () => { + } catch { if (!cancelled) setError("프로젝트를 불러오지 못했습니다."); - }, - ); + } + })(); return () => { cancelled = true; }; diff --git a/tests/features/tech-log/public-document-screens.test.tsx b/tests/features/tech-log/public-document-screens.test.tsx index 3f01a11..132ec34 100644 --- a/tests/features/tech-log/public-document-screens.test.tsx +++ b/tests/features/tech-log/public-document-screens.test.tsx @@ -205,13 +205,22 @@ describe("TechLog canonical Public documents", () => { const main = screen.getByRole("main"); expect(within(main).getByRole("heading", { level: 1, name: title })).toBeVisible(); - expect(within(main).getByText(evidence, { exact: false })).toBeVisible(); + /* + 목차가 본문의 절 제목을 그대로 다시 적으므로 같은 글이 두 자리에 나온다. 여기서 확인하려는 + 것은 "그 글이 문서에 있는가" 이므로 첫 자리로 충분하다. + */ + const [firstEvidence] = within(main).getAllByText(evidence, { exact: false }); + expect(firstEvidence).toBeVisible(); expect(within(main).getByRole("navigation", { name: "문서 경로" })).toHaveTextContent( `${kind}/${topic}/${project}`, ); - if (path === "/cases/collection-fetch-join-pagination") { - expect(within(main).getByText(`게시 ${published} · 마지막 검증 2026.08.11`)).toBeVisible(); + /* + Case 는 한 배치를 쓴다. 예전에는 픽스처 문서 하나만 `.case-meta` 를 받고 나머지 Case 는 + `.public-document-header dl` 로 떨어졌으므로 여기도 경로로 갈랐다. 이제 유형으로 가른다. + */ + if (kind === "Case") { + expect(container.querySelector(".case-meta")).toHaveTextContent(`게시 ${published}`); } else { const metadata = container.querySelector(".public-document-header dl"); expect(metadata).toHaveTextContent(`유형${kind}`); @@ -258,12 +267,12 @@ describe("TechLog canonical Public documents", () => { expect(image).toHaveAttribute("loading", "lazy"); }); - it("uses the generic Case markup and omits source relations for the canonical empty state", async () => { + it("uses the one Case layout and omits source relations for the canonical empty state", async () => { const generic = await renderDocumentRoute( "TECH_LOG_CASE", "/cases/redis-adapter-ttl-boundary", ); - expect(screen.getByRole("main")).toHaveClass("shell", "public-document-page"); + expect(screen.getByRole("main")).toHaveClass("case-page"); expect(generic.container.querySelector("#ownership")).toHaveTextContent( "정책과 저장 명령의 주인을 구분하기", ); diff --git a/tests/features/tech-log/public-index-screens.test.tsx b/tests/features/tech-log/public-index-screens.test.tsx index 034eeed..a90b09a 100644 --- a/tests/features/tech-log/public-index-screens.test.tsx +++ b/tests/features/tech-log/public-index-screens.test.tsx @@ -252,35 +252,26 @@ describe("TechLog project screens", () => { ]); }); - it("keeps project activity ordered and preserves self-fragment and record links", async () => { + /* + 활동은 로그다 — 언제 무엇을 올렸는지만 적는다. 각 줄에 그 기록으로 가는 링크가 있으면 같은 + 글에 닿는 길이 둘이 되고, 읽는 사람은 "기록"과 "활동"이 어떻게 다른지 매번 다시 판단해야 한다. + 글을 읽는 자리는 기록 화면 하나로 둔다. + */ + it("keeps project activity ordered and leaves reading to the records screen", async () => { const { container } = await renderPublicRoute( "TECH_LOG_PROJECT_ACTIVITY", "/projects/backend-skeleton/activity", ); - expect( - Array.from(container.querySelectorAll(".project-activity-list > li > article"), (item) => ({ - id: item.id, - href: item.querySelector("a")?.getAttribute("href"), - label: item.querySelector("a")?.textContent, - })), - ).toEqual([ - { - id: "fetch-join-case-published", - href: "/cases/collection-fetch-join-pagination", - label: "연결된 공개 기록 읽기", - }, - { - id: "storage-contract", - href: "/projects/backend-skeleton/activity#storage-contract", - label: "이 활동 위치 열기", - }, - { - id: "redis-case-published", - href: "/cases/redis-adapter-ttl-boundary", - label: "연결된 공개 기록 읽기", - }, + const items = Array.from( + container.querySelectorAll(".project-activity-list > li > article"), + ); + expect(items.map((item) => item.id)).toEqual([ + "fetch-join-case-published", + "storage-contract", + "redis-case-published", ]); + expect(items.flatMap((item) => Array.from(item.querySelectorAll("a")))).toEqual([]); }); }); diff --git a/tests/features/tech-log/public-render.test.tsx b/tests/features/tech-log/public-render.test.tsx index 8aa95f9..3ed3745 100644 --- a/tests/features/tech-log/public-render.test.tsx +++ b/tests/features/tech-log/public-render.test.tsx @@ -386,7 +386,12 @@ describe("shared Public record renderer", () => { ); }); - it("uses generic Case markup and does not present generatedAt as publication", () => { + /* + Case 렌더러가 둘이었을 때 이 테스트는 "축약본" 쪽을 지켰다. 어느 쪽을 쓸지는 슬러그 비교가 + 정했고, 설계 픽스처 문서 하나만 breadcrumb 과 목차가 있는 배치를 받았다 — 실제로 작성한 Case 는 + 전부 축약본이었고 오른쪽 목차가 어디에도 나오지 않았다. 배치는 이제 하나다. + */ + it("uses the one Case layout and does not present generatedAt as publication", () => { renderInRouter( <PublicRecordRenderer {...renderDependencies} @@ -402,14 +407,17 @@ describe("shared Public record renderer", () => { ); const main = screen.getByRole("main"); - expect(main).toHaveClass("shell", "public-document-page"); + expect(main).toHaveClass("case-page"); expect(main).toHaveAttribute("id", "main-content"); - expect(screen.getByText("게시 전")).toBeVisible(); + // 완성된 배치에서는 "게시 전" 이 기록 줄의 한 문장 안에 들어간다 — 홀로 선 노드가 아니다. + expect(main).toHaveTextContent("게시 전"); expect(main).not.toHaveTextContent("2035.05.06"); expect(screen.getByRole("region", { name: "문제와 결론" })).toHaveTextContent( "문제문제결론결론", ); expect(screen.getByText("일반 본문")).toBeVisible(); + // 제목이 없는 본문에는 목차를 그리지 않는다 — 빈 레일만 남는다. + expect(screen.queryByLabelText("문서 목차")).toBeNull(); }); it("renders supplied Reference and Question preview fields and empty copy", () => { diff --git a/tests/features/tech-log/route-contract.test.ts b/tests/features/tech-log/route-contract.test.ts index 05f4f50..7e9fd7f 100644 --- a/tests/features/tech-log/route-contract.test.ts +++ b/tests/features/tech-log/route-contract.test.ts @@ -39,7 +39,9 @@ const expectedRoutes = [ ["TECH_LOG_STUDIO_PUBLICATION_PREVIEW", "/studio/publications/:publicationEventId/preview", "STUDIO", "TechLogPublicationEventIdParams", null], ["TECH_LOG_STUDIO_ASSETS", "/studio/assets", "STUDIO", null, null], ["TECH_LOG_STUDIO_TAXONOMY", "/studio/taxonomy", "STUDIO", null, null], + ["TECH_LOG_STUDIO_PROJECT_EDIT", "/studio/projects/:id", "STUDIO", "TechLogDocumentIdParams", null], ["TECH_LOG_STUDIO_RELEASES", "/studio/releases", "STUDIO", null, null], + ["TECH_LOG_STUDIO_RELEASE_EDIT", "/studio/releases/:id", "STUDIO", "TechLogDocumentIdParams", null], ["TECH_LOG_STUDIO_NOT_FOUND", "/studio/*", "STUDIO", "TechLogStudioSplat", null], ["NOT_FOUND", "*", "PUBLIC", "NotFoundSplat", null], ] as const; @@ -72,13 +74,15 @@ const expectedTitles = { TECH_LOG_STUDIO_PUBLICATION_PREVIEW: "게시 Snapshot", TECH_LOG_STUDIO_ASSETS: "Asset", TECH_LOG_STUDIO_TAXONOMY: "주제와 프로젝트", + TECH_LOG_STUDIO_PROJECT_EDIT: "프로젝트 편집", TECH_LOG_STUDIO_RELEASES: "릴리즈", + TECH_LOG_STUDIO_RELEASE_EDIT: "릴리즈 편집", TECH_LOG_STUDIO_NOT_FOUND: "Studio 화면을 찾을 수 없습니다", NOT_FOUND: "페이지를 찾을 수 없습니다.", } as const; describe("TechLog route boundary contract", () => { - it("freezes the standalone 29-route inventory before runtime installation", () => { + it("freezes the standalone 31-route inventory before runtime installation", () => { expect( Object.values(TECH_LOG_ROUTE_REGISTRY).map((definition) => [ definition.routeId, @@ -153,7 +157,7 @@ describe("TechLog route boundary contract", () => { for (const locale of ["ko-KR", "en-US"] as const) { const catalog: Readonly<Record<string, string>> = TECH_LOG_MESSAGE_CATALOGS[locale]; - expect(Object.keys(catalog)).toHaveLength(60); + expect(Object.keys(catalog)).toHaveLength(64); for (const [routeId, title] of Object.entries(expectedTitles)) { expect(catalog[`route.${routeId}.title`]).toBe(title); expect(catalog[`route.${routeId}.navigation`]).toBe(title); diff --git a/tests/features/tech-log/style-contract.test.ts b/tests/features/tech-log/style-contract.test.ts index 962f27d..fd6e959 100644 --- a/tests/features/tech-log/style-contract.test.ts +++ b/tests/features/tech-log/style-contract.test.ts @@ -79,7 +79,9 @@ describe("TechLog consumer-visible style contract", () => { expect(rootStyle.getPropertyValue("--ink")).toBe("#17181b"); expect(rootStyle.getPropertyValue("--signal")).toBe("#3e5cc7"); expect(rootStyle.getPropertyValue("--shell")).toBe("1180px"); - expect(rootStyle.getPropertyValue("--body-copy")).toBe("42rem"); + // 42rem 은 영문 기준 측정값이었다. 한글 본문에서 좁았고, 바로 위의 유형/프로젝트 줄이 + // shell 전체를 쓰고 있어 대비가 더 심했다. 읽는 단 전체가 이 값 하나를 따른다. + expect(rootStyle.getPropertyValue("--body-copy")).toBe("56rem"); expect(rootStyle.color).toBe("var(--ink)"); expect(rootStyle.background).toBe("var(--canvas)"); expect(bodyStyle.color).toBe("var(--ink)");