diff --git a/case-after.png b/case-after.png new file mode 100644 index 0000000..852254b Binary files /dev/null and b/case-after.png differ diff --git a/src/features/tech-log/domain/studio/local-id.ts b/src/features/tech-log/domain/studio/local-id.ts index 1caf6ed..2ac6d6a 100644 --- a/src/features/tech-log/domain/studio/local-id.ts +++ b/src/features/tech-log/domain/studio/local-id.ts @@ -11,3 +11,28 @@ export function createLocalId( const entropy = Math.floor(random() * 1_000_000_000).toString().padStart(9, "0"); return `${prefix}-${now()}-${entropy}`; } + +/** + * 편집기가 새로 만든 목록 항목(관계·근거·규칙·선택지·순서 있는 문장)의 id. + * + * `createLocalId` 와 나눠 두는 이유는 이 값이 화면 밖으로 나가기 때문이다. 그쪽은 접두사를 붙여 + * `relation-` 같은 문자열을 만들고, 그건 React key 나 Idempotency-Key 로는 좋지만 계약에는 + * 넣을 수 없다 — 계약이 요구하는 형식은 uuid 이고, 접두사가 붙은 값은 서버가 파싱조차 하지 못해 + * 400 (`InvalidFormatException`) 이 된다. 저장 버튼이 "계약을 어겼다"는 말만 남기고 아무것도 저장하지 + * 않던 이유가 이것이었다. + * + * 서버는 자기가 소유하지 않은 id 를 신뢰하지 않고 새로 부여한다({@code StudioRelationStore.replace}). + * 그래서 여기서 만드는 값은 "이 줄은 새것"이라는 표시일 뿐, 저장 뒤의 진짜 id 는 서버가 정한다. + */ +export function createNewItemId( + source: RandomUuidSource | null | undefined = globalThis.crypto, + random: () => number = Math.random, +): string { + const uuid = source?.randomUUID?.(); + if (uuid) return uuid; + // randomUUID 가 없는 환경(비보안 컨텍스트)을 위한 대비. 형식만 uuid v4 를 지키면 된다 — 값 자체는 + // 서버가 어차피 새로 부여한다. + const hex = (length: number) => + Array.from({ length }, () => Math.floor(random() * 16).toString(16)).join(""); + return `${hex(8)}-${hex(4)}-4${hex(3)}-${"89ab"[Math.floor(random() * 4)]}${hex(3)}-${hex(12)}`; +} diff --git a/src/features/tech-log/presentation/studio/components/ordered-text-list.tsx b/src/features/tech-log/presentation/studio/components/ordered-text-list.tsx index 929e544..e8381bc 100644 --- a/src/features/tech-log/presentation/studio/components/ordered-text-list.tsx +++ b/src/features/tech-log/presentation/studio/components/ordered-text-list.tsx @@ -1,5 +1,5 @@ import type { components } from "../../../contracts/studio/generated.ts"; -import { createLocalId } from "../../../domain/studio/local-id.ts"; +import { createNewItemId } from "../../../domain/studio/local-id.ts"; type OrderedText = components["schemas"]["OrderedText"]; @@ -25,7 +25,7 @@ export function OrderedTextList({ label, fieldId, items, onChange }: { label: st
))} - + ); } diff --git a/src/features/tech-log/presentation/studio/components/question-fields.tsx b/src/features/tech-log/presentation/studio/components/question-fields.tsx index db1a280..fa6ee3b 100644 --- a/src/features/tech-log/presentation/studio/components/question-fields.tsx +++ b/src/features/tech-log/presentation/studio/components/question-fields.tsx @@ -1,5 +1,5 @@ import type { components } from "../../../contracts/studio/generated.ts"; -import { createLocalId } from "../../../domain/studio/local-id.ts"; +import { createNewItemId } from "../../../domain/studio/local-id.ts"; import { FieldNotice, type FieldIssue } from "./field-issues.tsx"; import { OrderedTextList } from "./ordered-text-list.tsx"; @@ -56,7 +56,7 @@ export function QuestionFields({ draft, evidence, issues, onChange }: { draft: Q