fix: Decision 미리보기의 결정일 요구를 풀고, 화면 테스트가 실제 동작을 다시 말하게 한다
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
This commit is contained in:
co-authored by
Claude Opus 5
parent
e5770dfbc8
commit
fd73bc88a1
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"packageId": "@tech-log/management-contract",
|
||||
"version": "1.0.0",
|
||||
"digest": "sha256:72650735061fde627f5037571eb986cb758f44a546f065c88408399f8eec4a55",
|
||||
"sourceRevision": "06ae075",
|
||||
"sourceRevision": "83148b2",
|
||||
"operationIds": [
|
||||
"createCaseDraft",
|
||||
"getCaseForEdit",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"packageId": "@tech-log/public-contract",
|
||||
"version": "2.1.0",
|
||||
"digest": "sha256:702d6666a8feba9899c7eb7c2a94a0880bcb23b178c7ed2009c6e69d9a1c848c",
|
||||
"sourceRevision": "06ae075",
|
||||
"sourceRevision": "83148b2",
|
||||
"operationIds": [
|
||||
"getPublicSite",
|
||||
"getPublicHome",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"][];
|
||||
|
||||
@@ -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" } }
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -16,6 +16,16 @@ export function DocumentToc({ headings, variant }: DocumentTocProps) {
|
||||
const detailsRef = useRef<HTMLDetailsElement>(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));
|
||||
|
||||
@@ -435,7 +435,11 @@ function ProjectDecisionDocument({
|
||||
<header>
|
||||
<div>
|
||||
<span>{model.status}</span>
|
||||
<time dateTime={model.decidedOn}>{displayDate(model.decidedOn)}</time>
|
||||
{model.decidedOn ? (
|
||||
<time dateTime={model.decidedOn}>{displayDate(model.decidedOn)}</time>
|
||||
) : (
|
||||
<span>결정일 미정</span>
|
||||
)}
|
||||
</div>
|
||||
<h2>{model.title}</h2>
|
||||
<p><PlainText text={model.statement} /></p>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user