183 lines
5.1 KiB
TypeScript
183 lines
5.1 KiB
TypeScript
import { useEffect, useRef, useState, type FormEvent } from "react";
|
|
|
|
import type {
|
|
CreateDocumentInput,
|
|
RecordKind,
|
|
} from "../../../contracts/studio/contract.ts";
|
|
import { createLocalId } from "../../../domain/studio/local-id.ts";
|
|
import { useStudio } from "../use-studio.ts";
|
|
|
|
const types = [
|
|
{
|
|
kind: "CASE",
|
|
title: "Case",
|
|
description: "문제를 재현하고 검증한 결론을 기록합니다.",
|
|
fields: "문제 · 결론 · 환경 · 재현 · 본문",
|
|
},
|
|
{
|
|
kind: "REFERENCE",
|
|
title: "Reference",
|
|
description: "반복해서 적용할 기술 기준을 정리합니다.",
|
|
fields: "목적 · 규칙 · 적용 조건 · 예외 · 예시",
|
|
},
|
|
{
|
|
kind: "QUESTION",
|
|
title: "Question",
|
|
description: "아직 닫히지 않은 판단과 다음 검증을 관리합니다.",
|
|
fields: "상태 · 사실 · 가정 · 미지수 · 선택지",
|
|
},
|
|
{
|
|
kind: "PROJECT_DECISION",
|
|
title: "Decision",
|
|
description: "프로젝트가 선택한 방향과 그 근거·영향을 기록합니다.",
|
|
fields: "상태 · 결정일 · 결정문 · 판단 이유 · 영향 · 근거",
|
|
},
|
|
] as const;
|
|
|
|
function emptyDocument(kind: RecordKind): CreateDocumentInput {
|
|
const common = {
|
|
title: "",
|
|
slug: "" as const,
|
|
summary: "",
|
|
topicId: null,
|
|
projectId: null,
|
|
relations: [],
|
|
};
|
|
if (kind === "CASE") {
|
|
return {
|
|
...common,
|
|
kind,
|
|
problem: "",
|
|
conclusion: "",
|
|
environment: "",
|
|
reproduction: "",
|
|
lastVerifiedOn: null,
|
|
bodyMarkdown: "",
|
|
};
|
|
}
|
|
if (kind === "REFERENCE") {
|
|
return {
|
|
...common,
|
|
kind,
|
|
purpose: "",
|
|
rules: [],
|
|
applyWhen: [],
|
|
exceptions: [],
|
|
examples: [],
|
|
verifiedOn: null,
|
|
};
|
|
}
|
|
if (kind === "QUESTION") {
|
|
return {
|
|
...common,
|
|
kind,
|
|
questionStatus: null,
|
|
facts: [],
|
|
assumptions: [],
|
|
unknowns: [],
|
|
constraints: [],
|
|
options: [],
|
|
nextValidation: "",
|
|
resolution: null,
|
|
};
|
|
}
|
|
return {
|
|
...common,
|
|
kind,
|
|
decisionStatus: null,
|
|
decidedOn: null,
|
|
statement: "",
|
|
rationale: "",
|
|
consequences: [],
|
|
};
|
|
}
|
|
|
|
function isAbortError(error: unknown): boolean {
|
|
return error instanceof DOMException && error.name === "AbortError";
|
|
}
|
|
|
|
export function NewDocumentForm() {
|
|
const { gateway, navigateInternal, setRequestAnnouncement } = useStudio();
|
|
const [kind, setKind] = useState<RecordKind>("CASE");
|
|
const [pending, setPending] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const locked = useRef(false);
|
|
const activeRequest = useRef<AbortController | null>(null);
|
|
|
|
useEffect(() => () => activeRequest.current?.abort(), []);
|
|
|
|
const submit = async (event: FormEvent) => {
|
|
event.preventDefault();
|
|
if (locked.current) return;
|
|
locked.current = true;
|
|
setPending(true);
|
|
setError("");
|
|
const controller = new AbortController();
|
|
activeRequest.current = controller;
|
|
try {
|
|
const document = await gateway.createDocument(emptyDocument(kind), {
|
|
idempotencyKey: createLocalId("studio-create"),
|
|
signal: controller.signal,
|
|
});
|
|
setRequestAnnouncement(
|
|
`${types.find((type) => type.kind === kind)?.title} 작업본을 만들었습니다.`,
|
|
);
|
|
navigateInternal(`/studio/documents/${document.id}/edit`);
|
|
} catch (reason) {
|
|
if (!isAbortError(reason)) {
|
|
setError("작업본을 만들지 못했습니다. 다시 시도해 주세요.");
|
|
}
|
|
} finally {
|
|
if (activeRequest.current === controller) activeRequest.current = null;
|
|
locked.current = false;
|
|
setPending(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="studio-page studio-new-page">
|
|
<header className="studio-page-heading">
|
|
<p className="studio-eyebrow">NEW WORKING COPY</p>
|
|
<h1>새 문서</h1>
|
|
<p>
|
|
목적에 맞는 기록 종류를 선택하면 빈 작업본을 만들고 바로 편집을 시작합니다.
|
|
</p>
|
|
</header>
|
|
<form
|
|
onSubmit={(event) => {
|
|
void submit(event);
|
|
}}
|
|
>
|
|
<fieldset className="studio-type-list">
|
|
<legend>문서 종류</legend>
|
|
{types.map((type) => (
|
|
<label key={type.kind}>
|
|
<input
|
|
type="radio"
|
|
name="kind"
|
|
value={type.kind}
|
|
checked={kind === type.kind}
|
|
onChange={() => setKind(type.kind)}
|
|
/>
|
|
<strong>{type.title}</strong>
|
|
<span>{type.description}</span>
|
|
<small>{type.fields}</small>
|
|
</label>
|
|
))}
|
|
</fieldset>
|
|
<div className="studio-create-footer">
|
|
<button className="studio-primary-button" type="submit" disabled={pending}>
|
|
{pending ? "만드는 중…" : "작업본 만들기"}
|
|
</button>
|
|
<p>이 화면의 작업본은 현재 Studio 세션에서만 유지됩니다.</p>
|
|
</div>
|
|
{error ? (
|
|
<p className="studio-screen-error" role="alert">
|
|
{error}
|
|
</p>
|
|
) : null}
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|