# urlu 문서 작성 명세 (AI 에이전트용)

이 파일은 urlu에 HTML 문서를 게시하는 에이전트를 위한 명세다. 사람이 읽는 소개문이 아니라
**지켜야 하는 제약과 정확한 허용 목록**이다. 문서를 작성하기 전에 3장과 4장을 반드시 읽어라.

아래 예시의 `https://YOUR-DOMAIN`을 실제 배포 주소로 바꿔 사용한다.
로컬에서는 `http://localhost:3000`이다.

---

## 0. 30초 요약

1. HTML 파일 하나를 `POST /api/posts`로 보내면 공유 가능한 URL이 돌아온다.
2. 서버가 **렌더할 때 화이트리스트로 필터**한다. `<script>`, `<style>`, `style=`, 직접 만든
   `class`는 전부 제거된다. CSS나 JS를 문서에 담으려는 시도는 모두 실패한다.
3. 자바스크립트가 필요 없는 인터랙션 세 가지만 동작한다: **접히는 섹션**(`<details>`),
   **문서 내부 앵커 링크**, **좌우 스와이프 카드 스트립**(`class="swipe"`),
   **한 장씩 넘기는 페이지 덱**(`class="deck"`, 페이저는 사이트가 그린다).
4. 타이포그래피·색·다크모드는 사이트가 정한다. 문서는 **구조만** 담당한다.
5. **QR 코드는 `<img src="/api/qr?…">`로 여러 개 넣을 수 있다.** 저장 없이 API가
   이미지를 만들어 주므로, 한 페이지에 URL·Wi-Fi·연락처 QR을 그리드로 배치하는
   용도에 적합하다. 6.5절 레시피를 따르라.
6. **차트는 데이터만 보내면 사이트가 그린다.** `<div class="chart" data-chart='{…}'>`
   한 줄이 막대·선·면·누적·도넛이 된다. 색과 축과 다크모드는 사이트가 정한다.
   「차트 (Recharts)」절을 보라.

문서를 "예쁘게" 만드는 방법은 CSS를 쓰는 것이 아니라, 4장의 클래스와
6장의 레시피를 조합하는 것이다.

사람이 직접 쓸 화면은 `/write`에 있다. 이 명세는 API로 게시하는 경우를 다룬다.

---

## 1. 게시

### 1.1 HTML 파일을 그대로 업로드 (권장)

```bash
curl -X POST https://YOUR-DOMAIN/api/posts \
  -H 'Content-Type: text/html' \
  --data-binary @document.html
```

`--data-binary`를 써라. `-d`는 줄바꿈을 없앤다.

### 1.2 JSON으로 보내기

```bash
curl -X POST https://YOUR-DOMAIN/api/posts \
  -H 'Content-Type: application/json' \
  -d '{
        "title": "배포 가이드",
        "format": "html",
        "body": "<h2>제목</h2><p>본문</p>",
        "excerpt": "선택. 없으면 본문에서 자동 추출",
        "expiresIn": 604800
      }'
```

`format`은 `"html"` 또는 `"md"`다. **생략하면 `"md"`(GFM 마크다운)로 처리된다.**
스와이프나 칩이 필요 없는 단순한 글이면 마크다운이 더 짧고 안전하다.

### 1.3 헤더 (`text/html` 업로드에만 해당)

| 헤더 | 뜻 |
| --- | --- |
| `X-Post-Title` | 문서에서 추출한 제목을 덮어쓴다. UTF-8 그대로 보내면 된다 |
| `X-Post-Expires-In` | 수명(초). 지나면 페이지가 404가 된다 |
| `X-Post-Token` | 서버에 `POST_PUBLISH_TOKEN`이 설정된 경우 필요. `Authorization: Bearer <토큰>`도 된다 |

### 1.4 응답 (201)

```json
{
  "id": "kGSsxNJ2",
  "url": "https://YOUR-DOMAIN/p/kGSsxNJ2",  // 짧은 경로. 긴 /posts/{id} 도 그대로 동작한다
  "title": "배포 가이드",
  "excerpt": "…",
  "body": "…",
  "format": "html",
  "createdAt": "2026-07-30T02:00:00.000Z",
  "updatedAt": "2026-07-30T02:00:00.000Z",
  "expiresAt": null,
  "editToken": "64자 hex"
}
```

`url`이 공유할 링크다. **`editToken`은 응답에 한 번만 나오고 유일한 삭제 수단이다.**
사용자에게 반드시 함께 전달하라.

### 1.5 수정

링크(`id`)는 그대로 두고 내용만 바꾼다. 두 가지 방법이 있고, **차이는 생략한 필드를
어떻게 취급하는가**다.

**`PATCH` — 보낸 필드만 바꾼다.** 대부분의 경우 이걸 쓴다.

```bash
curl -X PATCH https://YOUR-DOMAIN/api/posts/kGSsxNJ2 \
  -H 'Content-Type: application/json' \
  -d '{"editToken":"…","title":"새 제목"}'
```

**`PUT` — 문서를 통째로 교체한다.** 생략한 필드는 기본값으로 돌아간다.
`format`은 `md`가 되고 **만료는 제거된다.**

```bash
curl -X PUT https://YOUR-DOMAIN/api/posts/kGSsxNJ2 \
  -H 'Content-Type: application/json' \
  -d '{"editToken":"…","title":"제목","body":"<h2>새 본문</h2>","format":"html"}'
```

| 항목 | PATCH | PUT |
| --- | --- | --- |
| `title`, `body` | 선택 (하나 이상은 필수) | **둘 다 필수** |
| `format` 생략 | 유지 | `md`로 초기화 |
| `expiresIn` 생략 | 유지 | 만료 없음으로 초기화 |
| `expiresIn: null` | 만료 제거 | 만료 제거 |
| `excerpt` 생략 + `body` 변경 | 새 본문에서 다시 뽑음 | 새 본문에서 다시 뽑음 |

둘 다 성공하면 갱신된 문서를 `200`으로 돌려주고, **`editToken`은 바뀌지 않는다.**
토큰이 틀리거나 없는 id면 구분 없이 `404`가 온다 — 어떤 id가 존재하는지 알려주지 않기
위해서다. 바꿀 필드를 하나도 보내지 않은 `PATCH`는 `422`다.

`updatedAt`이 갱신되고, 문서 페이지의 `dateModified`와 OG `modifiedTime`도 따라 바뀐다.

사람이 쓸 화면은 `/write?edit={id}`다. 문서 내용은 공개이므로 자동으로 채워지고,
저장할 때만 토큰을 붙여넣는다.

### 1.6 삭제

```bash
curl -X DELETE https://YOUR-DOMAIN/api/posts/kGSsxNJ2 \
  -H 'Content-Type: application/json' \
  -d '{"editToken":"…"}'
```

### 1.7 한도

| 항목 | 한도 | 넘기면 |
| --- | --- | --- |
| 요청 본문 | 128 KB | `413 payload_too_large` |
| 문서 길이 | 100,000자 | `413 payload_too_large` |
| 제목 | 200자 | 초과분 잘림(자동 추출 시) / `422`(JSON) |
| 발췌 | 300자 | `422 validation_failed` |
| 만료 | 최대 5년 | `422 validation_failed` |

### 1.8 미리보기 카드 없이 공유하기

받은 `url` 뒤에 `?preview=off`를 붙이면 카카오톡·슬랙 같은 채팅앱이 제목·설명·
이미지 카드를 펼치지 않고 URL만 보여준다. **`?p=0` 도 같은 뜻이다** — 사람이 손으로
옮겨 적는 자리라 짧은 쪽을 함께 받는다.

```
https://YOUR-DOMAIN/p/kGSsxNJ2?preview=off
```

사람이 열면 평소와 같은 문서다. 미리보기 스크레이퍼의 요청만 골라 제목도 설명도 없는
404를 돌려주는 방식이라, 목록에 없는 스크레이퍼는 카드를 만들 수 있다. 그 URL은
`noindex`가 붙는다.

제목만 봐도 내용이 드러나는 문서를 조용히 보낼 때 쓴다. 평소에는 카드에 제목과
발췌가 뜨는 편이 받는 사람에게 낫다.

---

## 2. 제목과 발췌가 정해지는 규칙

**제목** — 우선순위대로 하나만 쓰인다.

1. `X-Post-Title` 헤더 (또는 JSON의 `title`)
2. 문서의 `<title>`
3. 문서의 첫 `<h1>`
4. 아무것도 없으면 → `400 bad_request`

**발췌** — JSON의 `excerpt`가 없으면 본문 텍스트에서 앞부분 160자를 뽑는다.
`<script>`, `<style>`, `<title>`, `<head>` 안의 텍스트는 제외된다.
발췌는 `<meta name="description">`과 OG 카드 설명으로 쓰인다.

**중요:** 페이지는 제목을 문서 위에 `<h1>`으로 이미 렌더한다.
그러므로 **본문에는 `<h1>`을 쓰지 말고 `<h2>`부터 시작하라.** 제목은 `<head><title>`로 준다.
`<h1>`을 본문에 쓰면 같은 제목이 화면에 두 번 나온다.

---

## 3. 허용되는 HTML

### 3.1 태그 (이 목록에 없으면 제거된다)

```
a abbr b blockquote br code dd del details div dl dt em
figcaption figure h1 h2 h3 h4 h5 h6 hr i img input ins kbd li mark
ol p picture pre q rp rt ruby s samp section small source span strike strong
sub summary sup table tbody td tfoot th thead tr tt u ul var
```

### 3.2 속성

- 공통으로 쓸 수 있는 것: `id`, `title`, `lang`, `dir`, `align`, `colSpan`, `rowSpan`,
  `scope`, `width`, `height`, `start`, `value`, `open`
- `<a>`: `href`
- `<img>`: `src`, `alt`, `title`, `width`, `height`, `loading`
- `<abbr>`: `title` (마우스 올리면 툴팁이 뜬다 — 유용하다)
- `<div>`: `data-chart` (차트 스펙 JSON. 「차트 (Recharts)」절)
- `<code>`: `class="language-…"` (하이라이팅은 하지 않지만 유지된다)
- `<input>`: `type="checkbox"`와 `checked`만. `type`은 무엇을 보내도 `checkbox`로 바뀌고
  `disabled`가 강제로 붙는다. 즉 **읽기 전용 체크리스트만 만들 수 있고 폼은 불가능하다.**
  `checked`를 주면 처음부터 체크된 상태로 보인다
- `class`: 4장의 허용 값만. `<div>`·`<span>`·`<p>` 외에 `<section>`, `<figure>`,
  `<figcaption>`, `<h2>`~`<h6>`, `<ul>`·`<ol>`·`<li>`, 표의 태그, `<a>`, `<img>`,
  `<code>`, `<details>`, `<blockquote>`, `<pre>` 등에도 붙는다

**목록에 없는 속성은 조용히 삭제된다.** 특히 `style`, `onclick` 같은 이벤트 속성, `target`,
`rel`, `srcset`은 남지 않는다.

### 3.3 링크와 이미지 주소

- `href`: `http`, `https`, `mailto`, `irc`, `ircs`, `xmpp`, 그리고 `#앵커`
- `src`: `http`, `https`, 그리고 같은 사이트의 경로(`/api/qr?…`). QR은 경로로 넣는 편이
  도메인이 바뀌어도 살아남는다
- **`data:` URI는 막혀 있다.** 이미지를 base64로 심을 수 없고, 이미 외부에
  https로 올라가 있는 이미지만 참조할 수 있다
- `javascript:`는 프로토콜 자체가 허용 목록에 없어 `href`가 통째로 사라진다

### 3.4 제거 방식 두 가지 — 결과가 다르다

**내용까지 통째로 사라지는 것** (`strip`):
`<script>`, `<style>`, `<title>`, `<head>`, `<noscript>`, `<template>`

**태그만 벗겨지고 안의 내용은 남는 것** (`unwrap`):
그 외 허용되지 않은 모든 태그. 예를 들어

```html
<form action="…"><button>보내기</button></form>
```

는 `보내기`라는 **글자만 덩그러니 남는다.** `<iframe>`, `<video>`, `<button>`,
`<canvas>`, `<svg>`도 같다. 그래서 유튜브·지도 임베드는 불가능하고, 시도하면
깨진 흔적만 남는다. **임베드가 필요하면 대신 링크를 걸어라.**

### 3.5 왜 이렇게 막는가 (판단이 필요할 때의 기준)

문서는 사이트의 도메인에서 서비스된다. 스크립트를 허용하면 남이 올린 코드가 이 사이트
권한으로 실행된다. `style`을 허용하면 `position:fixed` 한 줄로 화면 전체를 덮는 가짜 로그인
화면을 만들 수 있다. **문서에 표현력을 더하고 싶을 때 우회로를 찾지 말고, 4장의 클래스로
표현할 수 있는 형태로 문서를 다시 설계하라.**

---

## 4. 사이트가 제공하는 클래스 (31개) + Tailwind 유틸리티

`<div>`, `<span>`, `<p>`, `<section>`, `<figure>`, `<figcaption>`, `<h2>`~`<h6>`, 목록·표의
태그, `<a>`, `<img>`, `<code>`, `<details>` 등 대부분의 태그에 붙일 수 있고,
**아래 값 이외에는 전부 삭제된다.** 직접 만든 클래스명(`class="my-card"`)은 통하지 않는다.

| 클래스 | 붙이는 곳 | 결과 |
| --- | --- | --- |
| `swipe` | 컨테이너 `div` | 좌우로 스와이프되는 가로 스트립. 카드 단위로 딱 맞춰 멈춘다 |
| `swipe-item` | `swipe` 안의 `div` | 카드 한 장 (테두리·둥근 모서리·그림자) |
| `deck` | 컨테이너 `div` | 한 화면에 한 장씩 넘겨 보는 스트립 (페이지 넘김) |
| `deck-page` | `deck` 안의 `section`·`div` | 화면 폭을 꽉 채우는 패널 한 장 |
| ~~`deck-nav`~~ | — | **더 이상 쓰지 않는다.** 적어도 렌더되지 않는다 — 사이트가 `‹ 1 / 2 ›` 페이저를 대신 그린다 |
| `chips` | 컨테이너 `div` | 칩을 가로로 나열하고 줄바꿈시킴 |
| `chip` | `span` | 지름 32px 원형 칩. 기본은 테두리만 |
| `chip-1` | `chip`과 함께 | 노랑 |
| `chip-2` | `chip`과 함께 | 파랑 |
| `chip-3` | `chip`과 함께 | 빨강 |
| `chip-4` | `chip`과 함께 | 회색 |
| `chip-5` | `chip`과 함께 | 초록 |
| `chip-hit` | `chip`과 함께 | 테두리 대신 색을 꽉 채운다 → "이게 중요한 것" |
| `badge` | `span` | 작은 회색 알약 라벨 |
| `meta` | `p`, `span`, `div` | 작고 흐린 보조 텍스트 |
| `chart` | `div` (`data-chart` 와 함께) | 보낸 데이터를 차트로 그린다. 「차트 (Recharts)」절 |
| `pending` | `div`, `p` | "아직 쓰는 중" 배너 + **페이지가 스스로 갱신** (아래 참고) |
| `num` | `th`, `td` | 숫자 칸 — 우측 정렬 + 고정폭 숫자 |
| `paper` | 컨테이너 `div` | 로또 용지 격자 (1~45, 한 줄에 7칸) |
| `paper-cell` | `paper` 안의 `span` | 용지의 칸 하나 |
| `paper-band` | `paper-cell`과 함께 | 그 칸의 행 **또는** 열에서 당첨번호가 나옴 |
| `paper-cross` | `paper-cell`과 함께 | 행과 열 **둘 다**에서 나옴 (더 진하게) |
| `paper-hit` | `paper-cell`과 함께 | 그 칸이 당첨번호 |
| `paper-bonus` | `paper-cell`과 함께 | 보너스 번호 (테두리 강조) |

`chip-1`~`chip-5`는 **`chip-hit`과 같이 써야 색이 보인다.** `chip` + `chip-3` 만 쓰면
테두리만 있는 칩이 된다. 이 대비가 "해당됨/아님"을 표현하는 기본 도구다.

### 4.1 스와이프 스트립

```html
<div class="swipe">
  <div class="swipe-item">
    <p class="meta">2026-07-25</p>
    <h3>1234회</h3>
    <p><span class="badge">기준 회차</span></p>
    <div class="chips">
      <span class="chip chip-1 chip-hit">01</span>
      <span class="chip chip-2 chip-hit">15</span>
      <span class="chip chip-4">31</span>
    </div>
    <p class="meta">보조 설명</p>
  </div>
  <div class="swipe-item">…</div>
</div>
```

- 카드 폭은 사이트가 정한다(모바일 224px / 데스크톱 240px). 문서가 바꿀 수 없다
- 카드 안에서는 `<h3>`이 카드 제목 크기로 조정된다
- 카드 20~30장이 적당하다. 100장을 넣으면 사용자가 끝까지 밀지 않는다
- 스트립 위나 아래에 "좌우로 밀어보세요" 같은 안내 문장을 반드시 넣어라.
  데스크톱 사용자는 가로 스크롤을 눈치채지 못한다

### 4.2 페이지 넘김 덱 (`deck`)

QR을 여러 장 보여주는데 **스크롤이 길어지는 게 문제일 때** 쓴다. 한 화면에 패널 한 장이
꽉 차고, 스와이프하거나 아래 페이저로 넘긴다.

**문서는 패널만 적는다.** 아래에 붙는 `‹ 1 / 2 ›` 컨트롤은 사이트가 그린다 — 지금 몇 번째
장을 보고 있는지는 스크롤에서만 읽히고, 그건 문서가 할 수 없는 일이다. 패널 수도 사이트가
센다.

```html
<div class="deck">
  <section class="deck-page" id="qr-1">
    <figure class="text-center">
      <img src="/api/qr?data=https%3A%2F%2Fexample.com%2Fmenu&size=320" width="320" height="320" alt="메뉴판 QR" class="mx-auto" />
      <figcaption class="meta">메뉴판</figcaption>
    </figure>
  </section>
  <section class="deck-page" id="qr-2">…</section>
  <section class="deck-page" id="qr-3">…</section>
</div>
```

- **`deck-nav`를 적지 마라.** 예전에는 번호 링크 줄을 문서가 직접 적었는데, 이제 사이트가
  페이저를 그리므로 렌더되지 않는다. 옛 문서가 계속 적고 있어도 조용히 사라질 뿐 깨지지는
  않는다
- **패널 안에 `1 / 3` 같은 위치 표시를 넣지 마라.** 페이저가 이미 말한다
- `deck-page`의 `id`는 문서 안에서 그 패널로 링크를 걸 때만 필요하다. 페이저에는 필요 없다
- 패널 폭은 사이트가 정한다(컨테이너 폭 전체). 문서가 바꿀 수 없다
- **`deck` 안의 `<img>`에는 `loading="lazy"`를 쓰지 마라.** 지금 보고 있지 않은 장은
  화면에서 감춰져 있어서, lazy 이미지는 그 장으로 넘어가는 순간에야 불러오기 시작한다 —
  QR 자리가 잠깐 비어 보인다. 그리드(6.5절)에서는 `loading="lazy"`가 정상 동작한다
- `1 / 3` 처럼 몇 번째인지 적어라. 번호 링크만으로는 지금 위치를 알 수 없다
- **인쇄하면 패널 하나가 한 페이지씩 찍히고 번호 줄은 사라진다.** 인쇄용 QR 묶음에 맞다
- 장수가 3장 이하라면 그리드(`grid`)가 낫다. 넘길 것이 없으면 넘기는 UI는 방해가 된다

### 4.3 로또 용지 격자

실제 용지처럼 1~45를 한 줄에 7칸씩 놓는다. 행은 `⌊(번호−1)/7⌋+1`, 열은 `((번호−1) % 7)+1`이다.
칸마다 상태를 클래스로 표시하면 사이트가 색을 입힌다 — 좌표나 색을 직접 계산하지 않는다.

```html
<div class="paper">
  <span class="paper-cell paper-hit">1</span>
  <span class="paper-cell paper-band">2</span>
  <span class="paper-cell paper-cross">3</span>
  <span class="paper-cell">4</span>
  <!-- … 45번까지 45개 -->
</div>
```

- 칸은 **정확히 45개**를 순서대로 넣는다. 빠지면 격자가 밀린다
- 한 칸에 여러 상태가 겹치면 `paper-hit`이 가장 강하다. 당첨번호 칸에는 `paper-band`를 같이 붙이지 않아도 된다
- 격자만 두면 무슨 회차인지 알 수 없다. 바로 위나 아래에 회차·날짜를 `<p class="meta">`로 적어라

### 4.4 칩만 따로 쓰기

태그·등급·번호 표시에 쓴다. 표 안에서도 된다.

```html
<div class="chips">
  <span class="chip chip-2 chip-hit">7</span>
  <span class="chip chip-3">21</span>
</div>
```

### 4.5 Tailwind 유틸리티 (238개)

레이아웃·여백·타이포·색을 **Tailwind 클래스 이름 그대로** 쓸 수 있다. 단, 아래 목록에 있는
값만이다. 목록 밖의 클래스는 조용히 삭제되므로, **기억에 있는 Tailwind 클래스를 그냥 쓰지 말고
이 목록에서 골라라.**

`<div>`, `<span>`, `<p>`, `<section>`, `h2`~`h6`, `ul`/`ol`/`li`, `table`/`tr`/`th`/`td`/`caption`,
`a`, `img`, `figure`, `blockquote`, `pre`, `code`, `details`, `summary`, `strong`, `em`, `mark`,
`abbr` 에 붙일 수 있다.

**표시와 흐름**

```
block inline-block inline flex inline-flex grid hidden flex-row flex-col flex-wrap flex-1 shrink-0 grow sm:flex-row sm:flex-col
```

**그리드 열**

```
grid-cols-1 grid-cols-2 grid-cols-3 grid-cols-4 sm:grid-cols-2 sm:grid-cols-3 sm:grid-cols-4 col-span-2 col-span-3 col-span-full
```

**정렬**

```
items-start items-center items-end items-baseline justify-start justify-center justify-between justify-end self-start self-center self-end mx-auto
```

**간격(gap)**

```
gap-0 gap-1 gap-2 gap-3 gap-4 gap-5 gap-6 gap-8
```

**안쪽 여백**

```
p-0 p-1 p-2 p-3 p-4 p-5 p-6 p-8 px-0 px-1 px-2 px-3 px-4 px-6 py-0 py-1 py-2 py-3 py-4 py-6 pt-0 pt-2 pt-4 pt-6 pb-0 pb-2 pb-4 pb-6 pl-2 pl-4 pr-2 pr-4
```

**바깥 여백**

```
m-0 mx-0 my-0 my-2 my-4 my-6 my-8 mt-0 mt-1 mt-2 mt-3 mt-4 mt-6 mt-8 mt-12 mb-0 mb-1 mb-2 mb-3 mb-4 mb-6 mb-8
```

**크기**

```
w-full w-auto w-fit h-full h-auto max-w-xs max-w-sm max-w-md max-w-lg max-w-xl max-w-2xl max-w-full min-w-0
```

**타이포그래피**

```
text-xs text-sm text-base text-lg text-xl text-2xl text-3xl sm:text-lg sm:text-xl sm:text-2xl sm:text-3xl font-normal font-medium font-semibold font-bold italic not-italic underline no-underline line-through uppercase lowercase capitalize tracking-tight tracking-normal tracking-wide leading-none leading-snug leading-normal leading-relaxed text-left text-center text-right text-balance text-pretty tabular-nums truncate font-mono font-sans whitespace-nowrap break-words list-none list-disc list-decimal
```

**글자색**

```
text-white text-neutral-400 text-neutral-500 text-neutral-600 text-neutral-700 text-neutral-800 text-neutral-900 text-red-600 text-red-700 text-amber-600 text-amber-700 text-emerald-600 text-emerald-700 text-sky-600 text-sky-700 text-violet-600 text-violet-700 dark:text-white dark:text-neutral-100 dark:text-neutral-200 dark:text-neutral-300 dark:text-neutral-400 dark:text-red-300 dark:text-amber-200 dark:text-emerald-300 dark:text-sky-300 dark:text-violet-300
```

**배경색**

```
bg-transparent bg-white bg-neutral-50 bg-neutral-100 bg-neutral-200 bg-neutral-800 bg-neutral-900 bg-red-50 bg-red-100 bg-amber-50 bg-amber-100 bg-emerald-50 bg-emerald-100 bg-sky-50 bg-sky-100 bg-violet-50 bg-violet-100 dark:bg-neutral-800 dark:bg-neutral-900 dark:bg-red-950 dark:bg-amber-950 dark:bg-emerald-950 dark:bg-sky-950 dark:bg-violet-950
```

**테두리와 반경**

```
border border-0 border-2 border-t border-b border-l-4 border-neutral-200 border-neutral-300 border-red-200 border-amber-200 border-emerald-200 border-sky-200 border-violet-200 dark:border-neutral-700 dark:border-neutral-800 rounded-none rounded-sm rounded-md rounded-lg rounded-xl rounded-2xl rounded-full
```

**기타**

```
shadow-none shadow-sm shadow-md overflow-hidden overflow-x-auto aspect-square aspect-video object-contain object-cover
```

#### 반드시 지켜야 할 것

- **위치 지정 유틸리티는 전부 금지다.** `fixed`, `absolute`, `sticky`, `inset-*`,
  `top/right/bottom/left-*`, `z-*`, `translate-*`, `scale-*`, `rotate-*` 는 목록에 없다.
  문서가 사이트 도메인에서 서비스되므로, 요소를 흐름 밖으로 빼내 화면을 덮는 것은
  가짜 로그인 화면을 만드는 것과 같은 일이다.
- **대괄호 임의값 금지.** `w-[9999px]` 같은 표기는 통하지 않는다. 필요한 값이 목록에 없으면
  가장 가까운 값을 쓰거나, 그 디자인을 포기하라.
- **`!` 중요도 표기 금지.**
- **색을 쓰면 다크모드 짝을 같이 써라.** `bg-amber-50` 만 쓰면 다크모드에서 눈이 아프다.
  `bg-amber-50 dark:bg-amber-950`, `text-amber-700 dark:text-amber-200` 처럼 붙인다.
  색을 안 쓰면 자동으로 양쪽 테마에서 맞는다 — **색은 필요할 때만 쓰는 게 안전하다.**
- 반응형은 `sm:` 만 있다. `grid-cols-1 sm:grid-cols-3` 이 좁은 화면을 먼저 생각하는 올바른 순서다.

#### 유틸리티와 4장 클래스 중 무엇을 쓸까

## 시간이 걸리는 문서 (`class="pending"`)

집계·분석에 시간이 걸려서 **링크를 먼저 주고 내용을 나중에 채우고 싶을 때** 쓴다.

1. 상태 블록만 담아 발행한다. 응답의 `editToken`을 반드시 보관하라 — 한 번만 내려온다.

```html
<div class="pending">번호를 읽고 표를 만들고 있어요. 다 되면 이 화면이 저절로 채워져요.</div>
```

2. 준비가 끝나면 같은 문서를 갱신한다.

```bash
curl -X PATCH https://YOUR-DOMAIN/api/posts/POST_ID \
  -H 'content-type: application/json' \
  -d '{"editToken":"EDIT_TOKEN","body":"<h2>결론</h2>…","format":"html"}'
```

문서에 `pending` 블록이 있는 동안 페이지는 **4초마다 스스로 다시 읽는다**(최대 30번, 2분).
그래서 "새로고침해 주세요"라고 적을 필요가 없다. 갱신한 본문에서 `pending` 블록을 빼면 멈춘다.

`pending` 상태의 문서는 **검색에 색인되지 않는다** — 반쯤 쓰인 문서가 잡히면 안 되기 때문이다.
내용을 채우면 색인 대상으로 돌아온다.

**빈 문서를 발행하지 마라.** 링크를 먼저 받은 사람이 열었을 때 아무것도 없으면 고장으로 읽힌다.
무엇을 하고 있고 얼마나 기다리면 되는지 `pending` 블록에 적어라.

---

`swipe`, `chip`, `badge`, `meta`, `num`, `paper` 는 **사이트가 디자인을 책임지는 부품**이고,
Tailwind 유틸리티는 **직접 조립하는 재료**다. 부품이 있으면 부품을 써라 — 다크모드와 여백이
이미 맞춰져 있고, 다른 문서와 같은 모습이 된다. 유틸리티는 부품으로 안 되는 배치
(2열 그리드, 통계 타일, 여백 조정)에 쓴다.

```html
<!-- 좋다: 부품 + 배치용 유틸리티 -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 my-6">
  <div class="rounded-xl border border-neutral-200 dark:border-neutral-800 p-4">
    <p class="text-xs uppercase tracking-wide text-neutral-500">클릭</p>
    <p class="text-2xl font-bold tabular-nums">1,234</p>
  </div>
</div>

<!-- 나쁘다: 부품으로 되는 것을 손으로 재조립 -->
<span class="inline-block rounded-full bg-neutral-100 px-2 text-xs">라벨</span>
<!-- → <span class="badge">라벨</span> -->
```
---

## 차트 (Recharts)

데이터를 보내면 사이트가 차트를 그린다. 문서는 **자리표시자 하나**만 적는다.

```html
<div class="chart" data-chart='{
  "type": "bar",
  "title": "월별 매출",
  "x": "month",
  "xLabel": "월",
  "unit": "만원",
  "series": [{ "key": "revenue", "label": "매출" }],
  "data": [
    { "month": "6월", "revenue": 1200 },
    { "month": "7월", "revenue": 1850 },
    { "month": "8월", "revenue": 2400 }
  ]
}'></div>
```

`data-chart` 값은 **JSON 문자열**이다. HTML 속성이므로 작은따옴표로 감싸고 안에서는
큰따옴표를 쓰면 편하다. 줄바꿈은 허용된다.

`<svg>` 도 `<script>` 도 새니타이저를 통과하지 못하므로 문서가 직접 그릴 방법은 없다.
그래서 이 분업이다 — **문서는 숫자만 말하고, 색·축·격자·툴팁·다크모드는 사이트가 맡는다.**
색을 고르려 하지 마라. 지정할 수단이 없고, 팔레트는 색약 검증을 통과한 순서로 고정돼 있다.

### 어떤 타입을 쓸까

독자가 그 차트로 **무엇을 해야 하는지**가 타입을 정한다.

| 독자가 할 일 | `type` | 비고 |
| --- | --- | --- |
| 크기 비교 | `bar` | 기본. 항목 이름이 길거나 6개를 넘으면 `"horizontal": true` |
| 시간에 따른 추세 | `line` | 시리즈 2개 이상일 때 |
| 시간에 따른 추세, 시리즈 하나 | `area` | 면은 10% 워시로 깔린다 |
| 부분-전체 | `stacked-bar` | **원형보다 이걸 먼저 고려하라** |
| 부분-전체를 한눈에, 조각 6개 이하 | `pie` | 도넛으로 그려진다 |

**차트가 아닌 답이 맞을 때가 있다.**

- 값이 **하나**면 차트가 아니다. 막대 하나짜리 차트 대신 문장 안의 숫자나
  `<p class="num">`을 써라.
- 의미를 가진 분류가 **7개를 넘으면** 표가 낫다. 색이 더 있어야 하는 게 아니라 색으로
  풀 문제가 아니다.
- 조각 2개짜리 원형은 쓰지 마라. 두 숫자를 적는 게 낫다.
- 값이 가까운 것들을 비교해야 하면 원형이 아니라 `bar` 다. 각도는 길이보다 읽기 어렵다.

### 필드

| 필드 | 필수 | 설명 |
| --- | --- | --- |
| `type` | ✅ | `bar` · `stacked-bar` · `line` · `area` · `pie` |
| `x` | ✅ | 가로축(원형은 조각 이름)이 될 **키 이름**. `data` 각 행에 있어야 한다 |
| `series` | ✅ | 그릴 값들. `[{ "key": "…", "label": "…" }]`, 최대 **8개** |
| `data` | ✅ | 행 배열. 최대 **300행** |
| `xLabel` | | 그 축의 사람이 읽을 이름. 표 첫 열 머리글이 된다. 없으면 키가 그대로 보인다 |
| `title` | | 차트 위 설명. 최대 120자. 바로 위에 `<h2>` 가 있으면 생략해도 된다 |
| `unit` | | 값 단위. `"원"`, `"%"`, `"명"` 처럼 짧게. 최대 12자 |
| `horizontal` | | `true` 면 막대를 눕힌다 (`bar`·`stacked-bar` 전용) |
| `height` | | 차트 높이 px. 200–640, 기본 320. 가로축 라벨까지 포함한 값이다 |

`series[].label` 과 `xLabel` 을 생략하면 키 이름이 그대로 화면에 나온다. `revenue` 대신
`매출`, `month` 대신 `월` 이 보이길 원하면 둘 다 적어라 — 특히 표 뷰의 머리글이 키 이름이면
읽는 사람에게는 뜻이 없다.

### 예시

**여러 시리즈 — 범례가 자동으로 붙는다**

```html
<div class="chart" data-chart='{
  "type": "stacked-bar",
  "title": "채널별 유입",
  "x": "month",
  "series": [
    { "key": "organic", "label": "검색" },
    { "key": "social",  "label": "소셜" },
    { "key": "direct",  "label": "직접" }
  ],
  "data": [
    { "month": "6월", "organic": 520, "social": 310, "direct": 180 },
    { "month": "7월", "organic": 610, "social": 280, "direct": 240 },
    { "month": "8월", "organic": 700, "social": 410, "direct": 260 }
  ]
}'></div>
```

**항목 이름이 길 때 — 눕힌다**

```html
<div class="chart" data-chart='{
  "type": "bar",
  "x": "feature",
  "horizontal": true,
  "series": [{ "key": "uses", "label": "사용" }],
  "data": [
    { "feature": "QR 코드 생성", "uses": 5400 },
    { "feature": "URL 단축",    "uses": 3100 },
    { "feature": "문서 발행",    "uses": 328 }
  ]
}'></div>
```

**도넛 — 시리즈 1개, 조각 6개까지**

```html
<div class="chart" data-chart='{
  "type": "pie",
  "title": "저장소 사용 비중",
  "x": "kind",
  "unit": "MB",
  "series": [{ "key": "size" }],
  "data": [
    { "kind": "이미지", "size": 420 },
    { "kind": "PDF",   "size": 210 },
    { "kind": "영상",   "size": 180 },
    { "kind": "기타",   "size": 60 }
  ]
}'></div>
```

### 사이트가 알아서 하는 것 — 문서가 신경 쓸 필요 없다

- **색.** 시리즈 순서대로 고정 슬롯이 배정된다. 값 크기나 순위로 색이 바뀌지 않는다.
  라이트·다크 각각 검증된 팔레트를 쓴다.
- **범례.** 시리즈가 2개 이상일 때만 붙는다. 하나면 제목이 이미 그 이름을 말하므로 안 붙는다.
- **표 뷰.** 모든 차트 아래에 "값을 표로 보기"가 접힌 채 함께 나간다. 색을 구별하기
  어려운 독자, 호버가 없는 입력장치, 스크린리더 모두 같은 값에 닿을 수 있어야 하기 때문에
  **끌 수 없다.** 문서가 같은 표를 또 적을 필요는 없다.
- **툴팁.** 막대·조각에 커서를 올리면 값이 뜬다. 선·면은 세로 기준선이 x 를 잡는다.
- **반응형.** 화면 폭에 맞춰 다시 그려진다. 폭을 지정할 수단은 없다.
- **숫자 서식.** 천 단위 구분, 로케일별 표기를 사이트가 한다. `1200` 을 보내면 `1,200`
  으로 보인다. 미리 `"1,200"` 같은 문자열로 만들어 보내지 마라 — 축과 표에서 숫자로
  다뤄지지 않는다.

### 하지 말 것

- **`data` 값에 서식 있는 문자열을 넣지 마라.** `"1,200"`, `"12%"`, `"₩1200"` 은 모두
  숫자가 아니다. `1200` 을 보내고 단위는 `unit` 에 적어라.
- **축 두 개를 만들려 하지 마라.** 규모가 다른 두 측정치는 차트 두 개로 나눠라.
  방문자(0–30,000)와 전환율(0–5)을 한 차트에 겹치면 없는 상관관계를 만든다.
- **시리즈 9개를 넣지 마라.** 8개가 천장이고, 9번째 색은 색약에서 기존 색과 구별되지
  않는다. 꼬리를 `기타` 로 접거나 차트를 나눠라.
- **한 문서에 차트를 잔뜩 넣지 마라.** 차트가 3개를 넘으면 대개 표 하나가 낫다.
- **`class="chart"` 를 빼먹지 마라.** 그 클래스가 색 팔레트를 정의하는 자리다.

### 스펙이 틀렸을 때

차트가 조용히 사라지지 않는다. 그 자리에 무엇이 틀렸는지 적힌 안내 상자가 남는다.

| 상황 | 화면에 나오는 것 |
| --- | --- |
| JSON 파싱 실패 | `data-chart 가 올바른 JSON 이 아닙니다` |
| 필드 형식·한도 위반 | `차트 스펙의 형식이 맞지 않습니다` |
| `x` 나 `series[].key` 가 `data` 의 어느 행에도 없음 | `x 나 series 의 key 가 …없습니다` |
| 원형인데 시리즈 2개 이상 또는 조각 7개 이상 | `원형 차트는 시리즈 1개, 조각 6개까지입니다` |

발행 뒤 문서를 열어 확인하라. 안내 상자가 보이면 `PUT /api/posts/{id}` 로 고쳐 보내면 된다.

---

## 5. 목차와 앵커

그냥 평범하게 쓰면 된다.

```html
<h2 id="intro">들어가며</h2>
<a href="#intro">들어가며로</a>
```

내부적으로 `id`에는 `user-content-` 접두사가 붙지만(DOM clobbering 방지),
`href="#…"`도 같은 규칙으로 함께 보정되므로 **문서에서는 신경 쓸 필요가 없다.**
단, 문서 밖에서 `https://…/posts/abc#intro` 같은 링크를 만들 때는 실제 id가
`user-content-intro`임을 기억하라.

---

## 6. 레시피

### 6.1 기본 골격

```html
<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <title>여기가 문서 제목이 된다</title>
</head>
<body>
  <p class="meta">2026-07-30 · 부제나 날짜</p>

  <p>도입 한 문단. 이 문서가 무엇인지, 무엇을 보면 되는지.</p>

  <h2 id="section-1">첫 번째 절</h2>
  <p>…</p>

  <hr>

  <p class="meta">출처나 주의사항</p>
</body>
</html>
```

### 6.2 접히는 섹션

긴 부록, 트러블슈팅, 전체 표를 접어둘 때 쓴다. 중첩도 된다.

```html
<details>
  <summary>전체 표 보기</summary>
  <table>…</table>
</details>

<details open>
  <summary>처음부터 펼쳐진 섹션</summary>
  <p>…</p>
</details>
```

### 6.3 표

수치가 셋 이상이면 문장에 나열하지 말고 표로 만든다. 규칙은 세 가지다.

1. `<thead>`를 넣는다. 헤더 행에 배경색이 들어간다.
2. **글자는 왼쪽, 숫자는 오른쪽.** 숫자 열의 `<th>`와 `<td>`에 `class="num"`을 붙이면
   우측 정렬 + 고정폭 숫자가 되어 열을 따라 값을 비교할 수 있다.
3. 데이터의 범위·기준은 `<caption>`에 적는다. 표 아래 작은 글씨로 붙어서, 표와
   그 전제가 떨어지지 않는다.

특정 칸을 강조하려면 `<mark>`를 쓴다 — 표 셀 안의 `<mark>`은 칸 전체를 칠한다.

```html
<table>
  <caption>최근 30회(1205~1234회) 기준 · 표본 180개</caption>
  <thead><tr><th>끝수</th><th class="num">출현</th><th class="num">비중</th></tr></thead>
  <tbody>
    <tr><td>5</td><td class="num"><mark>23</mark></td><td class="num">12.8%</td></tr>
    <tr><td>6</td><td class="num">13</td><td class="num">7.2%</td></tr>
  </tbody>
</table>
```

이 세 규칙은 Vercel의 리포트 작성 지침을 따른 것이다 —
근거를 찾아보는 데이터는 semantic `<table>`로, 글자는 왼쪽·숫자는 오른쪽으로 맞춘다.

넓은 표는 사이트가 가로 스크롤 컨테이너에 담는다 — 문서가 따로 할 일은 없다. 열이
10개를 넘으면 그래도 한눈에 안 들어온다. 그럴 때는 표 대신 4.1의 카드 스트립을 고려하라.

### 6.4 코드 블록

```html
<pre><code>npm run build</code></pre>
```

`<pre>` 안의 `<`와 `&`는 `&lt;`, `&amp;`로 이스케이프해야 한다. 안 하면 브라우저가
태그로 파싱하고, 새니타이저가 그것을 지운다.

### 6.5 QR 코드 여러 개 (한 페이지)

행사 안내, 매장 Wi-Fi, 메뉴 링크처럼 **스캔용 QR을 여러 개 한 화면에** 보여줄 때 쓴다.
각 QR은 `/api/qr`가 PNG로 그려 주는 `<img>` 하나다. QR을 파일로 업로드할 필요가 없다.

**규칙**

1. **한 QR = `<figure>` 하나.** `<img>` 아래 `<figcaption>`에 라벨(메뉴명, SSID 등)을 적는다.
2. **여러 개는 그리드로.** `grid grid-cols-1 sm:grid-cols-2`(또는 `sm:grid-cols-3`) +
   `gap-4`~`gap-6`. QR이 4개 이상이면 2열, 6개 이상이면 3열을 고려한다.
3. **`size`는 256~512.** `width`/`height`를 `size`와 같게 맞춘다. 인쇄용이면 `512`, 화면용이면 `320`.
4. **URL이 길면 먼저 단축한다.** `/api/shorten`으로 짧은 URL을 받은 뒤 그 값을 `data=`에 넣는다.
   긴 URL QR은 인쇄·저조도에서 스캔 실패가 잦다.
5. **쿼리 문자열은 URL 인코딩한다.** `data`, `ssid`, `password` 등에 `&`, `#`, 공백, 한글이
   있으면 `encodeURIComponent`로 인코딩한 값을 쿼리에 넣는다.
6. **`alt`는 필수.** 스캔 대상을 짧게 적는다 (예: `alt="점심 메뉴 QR"`).

**콘텐츠 타입별 API 경로**

| 종류 | 예시 (`YOUR-DOMAIN`을 실제 주소로) |
| --- | --- |
| URL | `/api/qr?data=https%3A%2F%2Fexample.com&size=320` |
| 텍스트 | `/api/qr?type=text&data=안녕하세요&size=320` |
| Wi-Fi | `/api/qr?type=wifi&ssid=MyCafe&password=secret&encryption=WPA&size=320` |
| 전화 | `/api/qr?type=tel&phone=%2B821012345678&size=320` |
| 이메일 | `/api/qr?type=email&to=hello%40example.com&size=320` |
| vCard | `/api/qr?type=vcard&firstName=홍&lastName=길동&phone=01012345678&size=320` |
| 위치 | `/api/qr?type=geo&latitude=37.5665&longitude=126.978&size=320` |

공통 옵션: `margin`(0~20, 기본 2), `ecc`(L/M/Q/H, 기본 M), `dark`/`light`(색상 hex).

**완성 예시 — 카페 안내 (URL 2개 + Wi-Fi 1개)**

```html
<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <title>카페 MyCafe QR 안내</title>
</head>
<body>
  <p class="meta">스캔용 QR · 2026-08-05</p>
  <p>아래 QR을 스캔하면 메뉴·예약·Wi-Fi에 바로 연결됩니다.</p>

  <h2 id="qr">QR 코드</h2>
  <div class="grid grid-cols-1 sm:grid-cols-3 gap-6 my-6">
    <figure class="text-center">
      <img
        src="https://YOUR-DOMAIN/api/qr?data=https%3A%2F%2Fexample.com%2Fmenu&size=320&margin=2"
        width="320" height="320" alt="메뉴판 QR" loading="lazy"
      />
      <figcaption class="meta">메뉴판</figcaption>
    </figure>
    <figure class="text-center">
      <img
        src="https://YOUR-DOMAIN/api/qr?data=https%3A%2F%2Fexample.com%2Freserve&size=320&margin=2"
        width="320" height="320" alt="예약 QR" loading="lazy"
      />
      <figcaption class="meta">예약</figcaption>
    </figure>
    <figure class="text-center">
      <img
        src="https://YOUR-DOMAIN/api/qr?type=wifi&ssid=MyCafe&password=hunter2&encryption=WPA&size=320&margin=2"
        width="320" height="320" alt="Wi-Fi MyCafe QR" loading="lazy"
      />
      <figcaption class="meta">Wi-Fi · MyCafe</figcaption>
    </figure>
  </div>

  <p class="meta">Wi-Fi 비밀번호는 QR 스캔 시 자동 입력됩니다.</p>
</body>
</html>
```

**에이전트 워크플로**

1. 사용자에게 QR 목록(종류, 라벨, URL/SSID 등)을 확인한다.
2. URL은 필요하면 `POST /api/shorten`으로 짧게 만든다.
3. 위 레시피로 HTML을 작성하고 `POST /api/posts`로 게시한다.
4. 응답의 `url`과 `editToken`을 사용자에게 함께 전달한다.

QR만 필요하고 본문 설명이 거의 없다면 HTML이 `format` 기본값(`md`)보다 낫다.
마크다운에는 `<img>`로 `/api/qr` URL을 여러 개 넣는 것도 가능하지만, 그리드·캡션 배치는
HTML이 더 정확하다.

**QR이 많아 스크롤이 길어지면 그리드 대신 `deck`을 쓴다** (4.2절). 한 화면에 한 장씩
넘겨 보고, 번호로 바로 갈 수 있고, 인쇄하면 한 장이 한 페이지가 된다. 판단 기준은 단순하다 —
한눈에 비교해야 하면 그리드, 한 장씩 스캔하게 할 거면 덱.

---

## 7. 하지 말 것

| 하지 말 것 | 결과 | 대신 |
| --- | --- | --- |
| `<style>` 또는 `style="…"` | 통째로 제거 | 4장 클래스 |
| `class="내가-만든-이름"` | 클래스만 제거 | 4장의 값 + Tailwind 부분집합만 |
| `<script>` | 내용까지 제거 | 인터랙션은 `<details>`·앵커·`swipe` |
| `<iframe>`, `<video>`, 임베드 | 태그가 벗겨져 잔해만 남음 | 링크를 걸어라 |
| `<img src="data:image/png;base64,…">` | `src` 제거 | https 주소의 이미지 |
| 본문에 `<h1>` | 제목이 두 번 표시됨 | `<head><title>`, 본문은 `<h2>`부터 |
| 색·폰트·여백 지정 | 불가능 | 구조만 쓰고 스타일은 사이트에 맡겨라 |
| 데이터를 추정해서 채우기 | 사실이 아닌 문서 | 근거가 없으면 비워두고 사용자에게 물어라 |
| 칩 안에 설명 넣기 (`<span class="chip">15(끝수5, 7회)</span>`) | 칩은 지름 32px 원이라 글자가 넘쳐 레이아웃이 깨진다 | 칩에는 번호 한두 자리만. 나머지는 표의 열로 |
| 일반 메타데이터를 배지·알약으로 나열 | 눈만 어지럽고 비교가 안 된다 | 표의 열이나 `<caption>`, `p.meta` |

---

## 8. 게시 전 체크리스트

- [ ] `<head><title>`에 제목이 있고, 본문은 `<h2>`부터 시작하는가
- [ ] `style`, `<script>`, 허용 목록 밖의 `class`가 없는가
- [ ] `<pre>` 안의 `<`, `&`를 이스케이프했는가
- [ ] `swipe`를 썼다면 "좌우로 밀어보세요" 안내가 있는가
- [ ] QR `<img>`의 `src`가 `/api/qr`이고, 쿼리 값은 URL 인코딩했는가
- [ ] QR마다 `alt`와 `<figcaption>` 라벨이 있는가
- [ ] `deck`을 썼다면 `deck-nav`나 `1 / 3` 같은 위치 표시를 적지 않았는가 (사이트가 그린다)
- [ ] 표의 열 수가 좁은 화면에서 감당 가능한가
- [ ] 문서 길이가 100,000자 이내인가
- [ ] 게시 후 `url`과 `editToken`을 사용자에게 함께 전달했는가

---

## 9. 렌더 환경

- 문서는 본문 폭 약 768px 중앙 정렬로 렌더된다
- 다크모드는 사용자 시스템 설정에 따라 자동 전환된다. 문서가 관여할 수 없고, 관여할 필요도 없다
- 링크 미리보기(카카오톡·슬랙 등)용 OG 카드가 제목으로 자동 생성된다
- 만료가 설정된 문서는 검색엔진에서 `noindex` 처리된다. 만료가 없으면 색인 대상이다
- 문서 원문은 제출한 그대로 저장되고, 필터링은 매 렌더 시점에 일어난다.
  즉 이 명세가 나중에 더 엄격해지면 이미 게시된 문서에도 소급 적용된다

---

## 10. 디자인 원칙 — 문서를 보기 좋게 만드는 법

이 사이트의 타이포그래피는 Vercel의 **Geist**(Geist Sans / Geist Mono)를 쓰고, 색과 여백도
Geist의 역할 체계를 따른다. 문서가 그 위에 얹히므로, **문서가 할 일은 스타일이 아니라
정보 구조를 정확히 표현하는 것**이다. 아래 원칙은 4장의 도구를 어떻게 쓸지에 대한 것이다.

### 10.1 위계는 세 단계까지

`<h2>` → `<h3>` → 본문. `<h4>` 이하가 필요하다고 느껴지면 문서를 둘로 쪼개거나
`<details>`로 접어라. 깊은 위계는 화면에서 구별되지 않는다.

날짜·출처·부제 같은 곁가지 정보는 제목 크기를 낮추는 대신 `class="meta"`를 쓴다.
크기로 표현하려 하지 말고 역할로 표현하라.

```html
<p class="meta">2026-07-30 · 사내 배포 문서</p>
<h2 id="deploy">배포 절차</h2>
```

### 10.2 강조는 화면당 하나

한 화면에서 눈이 가장 먼저 갈 곳은 하나여야 한다. `<mark>`, `chip-hit`, `<strong>`,
`badge`를 한꺼번에 쓰면 강조가 서로를 지운다.

- 문장 안의 핵심 단어 → `<strong>`
- 표에서 주목할 칸 → `<mark>` (칸 전체가 칠해진다)
- 카드에서 상태 표시 → `badge` 하나
- 여러 값 중 해당되는 것 → `chip-hit`

### 10.3 색은 의미에만

사이트 자체가 거의 무채색이고, 색이 등장하면 그건 상태를 뜻한다. 문서도 같은 규칙을 따른다.
**칩 색을 예쁘라고 돌려쓰지 마라.** `chip-1`~`chip-5`는 구간·분류·범주처럼
**독자가 색으로 구별해야 할 축이 실제로 있을 때만** 쓴다.

- 좋은 예: 로또 번호대(1~10, 11~20 …), 난이도 5단계, 카테고리 5종
- 나쁜 예: 항목 여섯 개에 그냥 여섯 색을 돌려 입히기

색으로 구별할 축이 없다면 전부 `chip` 하나로 두고, 해당되는 것만 `chip-hit`으로 채운다.
**"채워짐 vs 테두리"** 대비가 이 시스템에서 가장 강한 신호다.

### 10.4 표와 카드 스트립 중 무엇을 쓸까

| 상황 | 선택 |
| --- | --- |
| 열이 2~5개, 값끼리 **비교**해야 함 | 표 |
| 열이 6개 이상이거나 항목마다 설명이 붙음 | `swipe` 카드 스트립 |
| 항목 수가 10개 이하이고 위아래로 읽는 게 자연스러움 | 목록(`<ul>`) |
| 항목마다 여러 값 + 상태 + 부가 정보 | 카드 |

같은 데이터를 **둘 다** 보여주는 것도 좋은 패턴이다. 카드 스트립을 먼저 놓고,
전체 표는 `<details>`로 접어 아래에 둔다. 훑어보기와 정밀 확인을 모두 만족시킨다.

### 10.5 카드 안의 순서

카드는 좁다(224~240px). 위에서부터 **작은 것 → 큰 것 → 상태 → 값 → 부가정보** 순으로
쌓으면 스와이프하면서 눈이 같은 자리를 따라간다.

```html
<div class="swipe-item">
  <p class="meta">2026-07-25</p>          <!-- 언제 -->
  <h3>1234회</h3>                          <!-- 무엇 -->
  <p><span class="badge">기준 회차</span></p><!-- 상태 -->
  <div class="chips">…</div>                <!-- 값 -->
  <p class="meta">1등 18명</p>              <!-- 부가 -->
</div>
```

카드 여섯 장 안에서 이 순서를 한 번이라도 바꾸면 스트립이 어수선해 보인다. **모든 카드가
같은 구조를 가져야 한다.** 값이 없는 카드는 항목을 빼는 대신 "없음"으로 채워라.

### 10.6 여백은 건드리지 않는다

문단 사이, 제목 위아래, 카드 안쪽 간격은 이미 정해져 있다. `<br>`을 여백 용도로 쓰거나
빈 `<p></p>`를 넣어 간격을 벌리지 마라. 간격이 답답하게 느껴진다면 그건 여백 문제가 아니라
한 화면에 너무 많은 것을 넣은 것이다.

구획이 필요하면 `<hr>`을 쓴다. 문서 하나에 두세 개면 충분하다.

### 10.7 문서를 열었을 때 3초

첫 화면에 이 셋이 보여야 한다.

1. 제목 (자동 렌더된다)
2. **이 문서가 무엇인지 한 문단** — 도입부를 생략하고 바로 표부터 시작하지 마라
3. 가장 중요한 값 하나 (요약 숫자, 기준 번호, 결론)

목차는 절이 다섯 개를 넘을 때만 만든다. 짧은 문서의 목차는 스크롤만 늘린다.

### 10.8 마지막 문단

출처, 데이터 기준 시점, 주의사항을 `class="meta"` 문단으로 문서 끝에 둔다.
숫자를 다루는 문서라면 **언제 기준의 데이터인지 반드시 적어라.** 문서는 링크로 공유되고,
읽는 사람은 그것이 언제 만들어졌는지 모른다.
