diff --git a/src/features/tech-log/presentation/shared/public-render/inline-renderer.tsx b/src/features/tech-log/presentation/shared/public-render/inline-renderer.tsx index d6e8fd1..5216346 100644 --- a/src/features/tech-log/presentation/shared/public-render/inline-renderer.tsx +++ b/src/features/tech-log/presentation/shared/public-render/inline-renderer.tsx @@ -33,6 +33,15 @@ function renderText(text: string, key: number) { ); } +/** + * 작성자가 직접 쓴 평문. 요약·문제·결론·환경처럼 Markdown 을 거치지 않고 그대로 그려지는 칸들이 + * 여기 온다 — 그 칸들은 텍스트 노드에 줄바꿈이 그대로 들어가고, HTML 이 그것을 공백으로 접어 + * 작성자가 나눠 쓴 줄이 이어져 보였다. 본문(Markdown)만 고쳤을 때 이 칸들이 남아 있던 이유다. + */ +export function PlainText({ text }: { text: string }) { + return renderText(text, 0); +} + function renderInline(inline: Inline, key: number) { switch (inline.type) { case "TEXT": 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 8d5d3e6..ab68645 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 @@ -2,6 +2,7 @@ import { Link } from "react-router-dom"; import type { components } from "../../../contracts/studio/generated.ts"; import { inlinePlainText } from "../../../domain/content-format/inline-plain-text.ts"; +import { PlainText } from "./inline-renderer.tsx"; import type { ResolveEvidenceAsset, ResolvePublishedLabel, @@ -92,7 +93,7 @@ function ModelDocumentHeader({ ) : null}

{model.title}

-

{model.summary}

+

</p> <dl> <div> <dt>유형</dt> @@ -149,21 +150,21 @@ function GenericCase({ <section className="document-snapshot" aria-label="문제와 결론"> <div> <p className="snapshot-label">문제</p> - <p>{model.problem}</p> + <p><PlainText text={model.problem} /></p> </div> <div> <p className="snapshot-label snapshot-label--answer">결론</p> - <p>{model.conclusion}</p> + <p><PlainText text={model.conclusion} /></p> </div> </section> <dl className="document-facts"> <div> <dt>검증 환경</dt> - <dd>{model.environment}</dd> + <dd><PlainText text={model.environment} /></dd> </div> <div> <dt>검증 데이터</dt> - <dd>{model.reproduction}</dd> + <dd><PlainText text={model.reproduction} /></dd> </div> </dl> <article className="public-document-body"> @@ -213,27 +214,27 @@ function FetchJoinCase({ </nav> <h1>{model.title}</h1> - <p className="case-summary">{model.summary}</p> + <p className="case-summary"><PlainText text={model.summary} /></p> <section className="case-snapshot" aria-label="문제와 결론"> <div> <p className="snapshot-label">문제</p> - <p>{model.problem}</p> + <p><PlainText text={model.problem} /></p> </div> <div> <p className="snapshot-label snapshot-label--answer">결론</p> - <p>{model.conclusion}</p> + <p><PlainText text={model.conclusion} /></p> </div> </section> <dl className="case-meta"> <div> <dt>검증 환경</dt> - <dd>{model.environment}</dd> + <dd><PlainText text={model.environment} /></dd> </div> <div> <dt>데이터셋</dt> - <dd>{model.reproduction.replace(/^Dataset:\s*/, "")}</dd> + <dd><PlainText text={model.reproduction.replace(/^Dataset:\s*/, "")} /></dd> </div> <div> <dt>기록</dt> @@ -287,7 +288,7 @@ function ReferenceDocument({ <section className="reference-purpose" aria-labelledby="purpose-title"> <p className="section-kicker">Purpose</p> <h2 id="purpose-title">이 기준을 쓰는 이유</h2> - <p>{model.purpose}</p> + <p><PlainText text={model.purpose} /></p> </section> <article className="public-document-body reference-body"> <section aria-labelledby="rules-title"> @@ -434,7 +435,7 @@ function QuestionDocument({ > <p className="section-kicker">Next</p> <h2 id="next-validation-title">다음 검증</h2> - <p>{model.nextValidation}</p> + <p><PlainText text={model.nextValidation} /></p> </section> </article> <PublicDocumentRelations relations={publicRelations(model.relations)} /> @@ -476,11 +477,11 @@ function ProjectDecisionDocument({ <time dateTime={model.decidedOn}>{displayDate(model.decidedOn)}</time> </div> <h2>{model.title}</h2> - <p>{model.statement}</p> + <p><PlainText text={model.statement} /></p> </header> <section> <h3>판단 이유</h3> - <p>{model.rationale}</p> + <p><PlainText text={model.rationale} /></p> </section> <section> <h3>영향</h3> diff --git a/tests/component/inline-line-breaks.test.tsx b/tests/component/inline-line-breaks.test.tsx index c7e2797..7dc635a 100644 --- a/tests/component/inline-line-breaks.test.tsx +++ b/tests/component/inline-line-breaks.test.tsx @@ -3,7 +3,10 @@ import { render } from "@testing-library/react"; import { describe, expect, it } from "vitest"; -import { InlineRenderer } from "../../src/features/tech-log/presentation/shared/public-render/inline-renderer.tsx"; +import { + InlineRenderer, + PlainText, +} from "../../src/features/tech-log/presentation/shared/public-render/inline-renderer.tsx"; /** * 작성자가 엔터로 나눠 쓴 글이 한 줄로 이어져 보였다. Markdown 이 한 번의 줄바꿈을 문단 내 공백으로 @@ -34,3 +37,19 @@ describe("문단 안의 줄바꿈", () => { expect(container.querySelectorAll("br")).toHaveLength(2); }); }); + +/** + * 요약·문제·결론·환경은 Markdown 을 거치지 않고 그대로 그려진다. 본문만 고쳤을 때 이 칸들이 + * 여전히 이어져 보인 이유이고, 작성자가 "아직 안 고쳐졌다" 고 본 것도 이쪽이다. + */ +describe("Markdown 을 거치지 않는 평문 칸", () => { + it("작성자가 나눠 쓴 줄을 지킨다", () => { + const { container } = render(<PlainText text={"문제 첫 줄\n문제 둘째 줄"} />); + expect(container.querySelectorAll("br")).toHaveLength(1); + }); + + it("한 줄이면 <br> 를 넣지 않는다", () => { + const { container } = render(<PlainText text="한 줄" />); + expect(container.querySelectorAll("br")).toHaveLength(0); + }); +});