마크다운
메시지 · 댓글 · 문서 · AI 답을 한 부품이 같은 계약으로 그립니다. 편집기와 같은 원문이 같은 그림이 됩니다.
요소 덮어쓰기(components)는 열지 않습니다. 앱마다 덮어 쓰면 취소선 · 링크 · 표 · 할 일 규칙이 다시 갈라집니다. 원시 HTML 은 실행하지 않고 원문을 읽기 전용 칸으로 보입니다. 흐르는 답은 굳은 블록과 꼬리로 나눠 그리고, 블록마다 따로 파싱하므로 다른 블록에 있는 참조식 링크 정의([1]: url)는 풀리지 않습니다.
- import
- import { Markdown } from "@uyoolabs/ui/markdown";
- 설치
- pnpm add react-markdown remark-gfm remark-breaks (./markdown 의 선택 peer)
- 고르는 법
- 채널 메시지 · 댓글 → variant="message" · "comment"(14 · 줄바꿈 하나 = br · 제목 한 단계 내림). 문서 읽기 화면 → "document"(콘텐츠 언어의 명조 · 읽기 폭 · h1 허용). 사이드 패널 · 스레드 → size="sm". AI 답 → streaming.
- 쓰지 않는 경우
- 편집할 글에는 편집기를 씁니다(이 부품은 읽기 전용). 사용자 입력이 아닌 화면 문구에 마크다운을 쓰지 않습니다. 렌더마다 새 mentions · 플러그인 배열을 넘기지 않습니다(useMemo).
Markdown
멘션 · 할 일 · 인용 · 표 · 원시 HTML. 인용은 세로 강조선 대신 여는 따옴표와 파인 면입니다.
재시도 정책 정리
님, 실패한 결제는 세 번까지 다시 시도합니다. 간격은 maxAttempts 로 바꿉니다. 자세한 값은 PG 문서 (새 창)를 봅니다.
처리 기간은 3~5일, 환불은 7~9일입니다. 다음 주 화요일 배포 → 목요일로 미뤘습니다.
- 웹훅 서명 검증 —
- 재시도 큐 만들기
- 알림 문구 정하기
같은 카드로 하루 세 번 넘게 실패하면 사람에게 넘긴다.
| 시도 | 간격 | 알림 |
|---|---|---|
| 1 | 1분 | — |
| 2 | 10분 | 채널 |
HTML 원문 — 그리지 않습니다
<iframe src="https://x.example"></iframe>onOpen: —
| 키 | 동작 |
|---|---|
| Tab·Enter | 링크로 이동 · 멘션은 onOpen(HoverCard — Esc 로 닫고 멘션으로) |
| Tab·←·→ | 넘치는 표 · 코드에 들어와 민다 |
| 할 일 체크 | 포커스를 받지 않는다(읽기 전용) |
흐르는 중
끝에 2px 커서(aria-hidden)와 aria-busy. 줄인 모션에서는 커서가 깜빡이지 않습니다. 완료 알림은 AI 패널이 한 번만 냅니다.
재시도는 지수 백오프로 늘립니다.
- 첫 시도는 1분 뒤
- 둘째는 10분 뒤
ts
await retry(charge, { maxAttempts: 3, base: 60_000 });세 번 모두 실패하면 담당자에게 알림을 보냅니다.
마크다운 계약
원문을 어떻게 그리는가. 편집기 스파이크(docs/spikes/0001)와 같은 계약입니다.
| 원문 | 그림 | 규칙 |
|---|---|---|
| ~~취소~~ · 3~5일 | 취소선 · 글자 그대로 | remark-gfm singleTilde: false. 물결 하나는 글자다 |
| [@김서연](mention:u_12) | 멘션 칩 | 편집기와 같은 계약. 나를 부른 멘션은 data-self(primary 면) |
| [#일반](channel:c_1) | 채널 칩 | 멘션과 같은 방식(결정 #33). channels.resolve |
| # 제목 | h2 | headingOffset 1. 제품 화면에 h1 금지, document 만 0 |
| 줄바꿈 하나 | message: br · document: 공백 | remark-breaks 는 message · comment 만 |
| [문서](https://…) · /settings · //x.com | 새 창 + 아이콘 · 앱 안 · 새 창 | ‘/’ 하나는 LinkProvider 로 앱 안 이동, ‘//’ 는 바깥. javascript: 는 href 를 지운다 |
| ```(언어 없음) | CodeBlock(라벨 없음) | 인라인 코드로 그리지 않는다 |
| - [x] 일 | 읽기 전용 체크 | 글머리 점 없음 · 이름 ‘완료: …’ · 줄을 긋지 않는다 |
| <iframe> 등 HTML | 원문 칸 | 실행 · 렌더하지 않는다. 주석은 버리고 <br> 은 줄바꿈 |
props
소스의 타입과 JSDoc 에서 생성한 표입니다. 컴포넌트를 고치면 pnpm gen 으로 다시 만듭니다.
Markdown
| prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| source필수 | string | — | 마크다운 원문 |
| variant | "message" | "comment" | "document" | "message" | message · comment: 14/1.6 · 줄바꿈 하나 = br · 제목 한 단계 내림. document: 콘텐츠 언어의 명조 · 읽기 폭 · 제목 그대로 |
| size | "sm" | "md" | "md" | sm 13 — 사이드 패널 · 스레드 |
| headingOffset | 0 | 1 | 2 | — | 제목을 내리는 단계. variant 기본값(message · comment 1, document 0)을 덮는다 |
| breaks | boolean | — | 줄바꿈 하나를 br 로. variant 기본값(message · comment 켬, document 끔)을 덮는다 |
| streaming | boolean | — | 흐르는 중(AI 답). 굳은 블록은 한 번만 그리고 꼬리만 다시 그린다. 끝에 커서 · aria-busy |
| mentions | MarkdownMentions | — | 멘션 칩 |
| channels | MarkdownChannels | — | 채널 참조 칩 |
| linkBase | string | — | ‘/’ 로 시작하는 앱 안 링크의 앞(워크스페이스 경로 등) |
| remarkPlugins | Options["remarkPlugins"] | — | 앱 remark 플러그인. 우리 플러그인(멘션 · 원시 HTML · 제목) 뒤에 돈다 |
| rehypePlugins | Options["rehypePlugins"] | — | 앱 rehype 플러그인(키 → 링크 등). code · pre 안은 건드리지 않게 플러그인이 스스로 거른다 |
| components | never | — | 요소 덮어쓰기는 열지 않는다. 앱마다 덮어 쓰면 링크 · 표 · 할 일 규칙이 다시 갈라진다 |
| lang | "ko" | "en" | "ja" | — | 콘텐츠 언어. lang 속성 · 명조 · 기본 문구를 고른다 |
| labels | Partial<MarkdownLabels> | — | 기본 문구 중 바꿀 것만. 렌더마다 새 객체면 굳은 블록도 다시 그린다(useMemo 로 넘긴다) |
| className | string | — |