# 수학도장(MathDojang) 문제 등록 API — LLM 안내서

이 문서는 AI 어시스턴트가 사용자를 대신해 수학 문제를 등록할 수 있도록 작성되었습니다.

> 참고: MCP를 지원하는 클라이언트(ChatGPT 커넥터, Claude 등)는 `https://mathdojang.com/api/mcp`를
> MCP 커넥터로 연결하면 아래 REST 절차 없이 도구 호출만으로 같은 작업을 할 수 있습니다.
> 이 경우 아래 "본문·해설 HTML 형식" 장의 규칙만 그대로 지키면 됩니다. 객관식 보기 형식은
> 도구마다 다르니 "객관식 보기 수식" 항목을 함께 확인하세요.

## 1. 인증

모든 요청에 발급받은 API 키를 Bearer 토큰으로 넣으세요 (30일 유효):

```
Authorization: Bearer mdk_...
```

키가 동작하는지 먼저 확인하려면: `GET https://mathdojang.com/api/auth/me` — 200이면 유효합니다.
이 키로는 다음 작업만 가능합니다:

- `GET https://mathdojang.com/api/auth/me`: 내 정보 확인
- `POST https://mathdojang.com/api/uploads/sessions`: 이미지 업로드 세션 생성 (아래 "이미지 넣기" 장)
- `GET https://mathdojang.com/api/uploads/sessions/{id}`: 업로드 세션의 이미지 목록 조회
- `POST https://mathdojang.com/api/uploads/images`: 검증 가능한 이미지 자산과 presigned PUT URL 생성 (파일 바이트를 직접 PUT할 수 있는 환경 전용)
- `POST https://mathdojang.com/api/uploads/images/{assetId}/finalize`: PUT한 이미지의 실제 바이트 검증 완료
- `POST https://mathdojang.com/api/problems`: 문제 등록

> MCP 클라이언트에서 `/api/mcp`에 같은 API 키를 연결하면 위 REST 허용 경로 목록과 별개로
> 커뮤니티 도구도 사용할 수 있습니다. 커뮤니티 게시글 목록·상세 조회·작성,
> 본인 일반 글 수정·삭제가 가능하며, 모더레이터 계정은 공지 작성·수정·삭제도 가능합니다.
> API 키로 `/api/posts` REST 쓰기 경로를 직접 호출하는 것은 허용되지 않습니다.

## 2. 문제 등록 — POST https://mathdojang.com/api/problems

Content-Type: application/json. 필드 규칙:

| 필드 | 타입 | 규칙 |
|---|---|---|
| `title` | string | 필수, 1~100자 |
| `body` | string | 필수, 문제 본문 리치 HTML (아래 "본문·해설 HTML 형식" 장), 최대 40000자. 맨 앞에 문항 번호("28." "28)" 등)를 적으면 저장이 거부됩니다 |
| `answerType` | `"number"` \| `"choice"` | 필수. 단답형(숫자) / 5지선다 |
| `answer` | string | 필수. number형: 정답 값. choice형: `"3"` 또는 복수 정답 `"1,3"` (1~5, 중복 불가) |
| `choices` | string[5] | choice형일 때 필수, 정확히 5개. 각 보기는 일반 텍스트와 `$...$` 인라인 수식 조각을 함께 쓰는 문자열 |
| `difficulty` | int | 필수, 1~30 (클수록 어려움). **정하기 전에 난이도 선택 안내 공지를 읽으세요 (아래 "참고 자료" 장)** |
| `subject` | string | 필수, 아래 과목 코드 |
| `unit` | string | 필수, **선택한 과목에 속한** 단원 코드 |
| `category` | enum | 선택, 미지정 시 `"csat"`. `"csat"`(수능형) / `"school"`(내신형) / `"olympiad"`(경시형) / `"etc"`(기타문제). `"past"`(기출문제)는 운영진 전용이며 기출 시험·학년도·시행년도 태그가 최소 1개 필요하고, 축마다 1개를 넘길 수 없습니다 |
| `solution` | string | `solution`과 `solutions` 중 하나 필수. 해설 리치 HTML (본문과 동일 형식). "해설"이라는 버전 하나로 등록됩니다 |
| `solutions` | object[] | `solution` 대신 해설을 이름 붙은 버전 여러 개로 보낼 때. `[{ "name": "해설", "content": "<p>…</p>" }, …]` 1~10개, 첫 항목이 원본이고 보낸 순서대로 보입니다. `name`은 1~30자이고 겹치면 안 되며, `content`는 본문과 동일 형식입니다. `solution`과 함께 보내면 `solution`이 첫 항목 본문과 같을 때만 받고(`solutions`로 저장), 다르면 400입니다 |
| `baseSolutionIds` | string[] | 수정할 때만 선택. 조회한 `solutions`의 `id` 목록을 함께 보내면, 그 사이 다른 사람이 더한 버전을 지우지 않고 뒤에 남깁니다 |
| `tagIds` | int[] | 선택, 최대 50개. 카탈로그: `GET https://mathdojang.com/api/problems/tags/all` (인증 불필요) |
| `attestOriginal` | true | 필수. 자작 문제임을 확약 — **반드시 사용자에게 확인받은 뒤** true로 보내세요 |
| `attestCopyright` | true | 필수. 저작권 침해가 없음을 확약 — 위와 동일 |

`imageKeys`·`solutionImageKeys`는 legacy 갤러리 필드이므로 LLM 흐름에서는 보내지 마세요. 에디터 이미지는 아래 "이미지 넣기" 장의 인라인 이미지 업로드 절차를 사용해 `body`·`solution` HTML 안에 넣습니다.

### 객관식 보기 수식

- REST API의 `choices`는 일반 텍스트와 `$...$` 수식 조각을 함께 사용합니다.
- 루트는 `"$\sqrt{3}$"`, 분수는 `"$\frac{1}{2}$"`, 지수는 `"$3^{1/2}$"`로 보냅니다.
- 문장과 수식을 섞을 수 있습니다: `"속력은 $\sqrt{3}\,\mathrm{m/s}$이다."`
- MCP 도구의 `create_problem`은 각 보기를 `"\sqrt{3}"`처럼 하나의 순수 인라인 LaTeX 식으로 받으며, 서버가 `$...$`로 정규화합니다.
- MCP 도구의 `update_problem`은 정규화하지 않고 받은 문자열을 그대로 저장합니다. `get_my_problem`에서 받은 값을
  그대로 보내고, 고치는 보기도 위 REST와 같은 형식(일반 텍스트와 `$...$` 수식 조각)으로 씁니다.

### 해설 버전 (MCP 수정 도구)

MCP 도구로 문제를 고칠 때도 해설은 `solution` 또는 `solutions`로 보냅니다. `get_my_problem`은 둘 다
돌려주며, 그대로 돌려보내도 `solution`이 원본 본문과 같으면 받습니다. `solutions`의 원본 본문을 고치면서
`solution`을 남기면 어긋나 400이니 그때는 `solution`을 빼세요. `solution` 한 칸으로 수정하면 **원본 버전
본문만 바뀌고** 다른 버전은 그대로 남습니다. 버전 이름을 바꾸거나 버전을 더하거나 지우려면 조회한 `solutions`
전체를 각 `id`를 그대로 둔 채 고쳐 보내세요(목록에서 뺀 버전은 지워집니다). 조회한 `solutions`의 `id`
목록을 `baseSolutionIds`로 함께 보내면 조회한 뒤 생긴 버전(해설 요청 승인, 모더 추가)은 지우지 않습니다.

### 과목·단원 코드

현재 과목·단원 코드와 이름은 `GET https://mathdojang.com/api/subjects`에서 조회합니다. `subject`는 과목 code, `unit`은 그 과목의 units에 속한 code를 사용하세요. active=false인 항목은 새 문제에 사용할 수 없습니다.

## 3. 본문·해설 HTML 형식 (중요)

`body`와 `solution`은 수학도장 에디터와 호환되는 다음 HTML이어야 합니다.

### 에디터 서식

- 문단·제목: `<p>`, `<h2>`, `<h3>`
- 강조·링크: `<strong>`, `<em>`, `<a href="https://...">`
- 목록·인용: `<ul>`, `<ol>`, `<li>`, `<blockquote>`
- 줄바꿈: `<br>`
- 보기·조건 상자: 시험지처럼 네모 상자로 묶이는 영역은 `<div data-box="보기"><p>ㄱ. ...</p><p>ㄴ. ...</p></div>`로 감쌉니다. `data-box` 값이 라벨로 상자 상단 테두리에 표시됩니다(꺾쇠 `< >`는 자동 부가 — 라벨에 넣지 마세요). 라벨 없는 민짜 상자(조건·증명 과정)는 `data-box=""`. 상자 중첩은 불가합니다.
- 수식 안의 (가)(나)(다) 빈칸 상자는 `data-box`가 아니라 LaTeX `\boxed{\,\text{(가)}\,}`로 표기합니다.
- 그래프: 수학도장 그래프 계산기(/graph)에 저장한 그래프는 공유 창의 iframe 코드 `<iframe src="https://mathdojang.com/embed/graph/공유ID" width="600" height="400" title="제목"></iframe>`를 `<p>` 밖에 그대로 넣습니다. 코드는 사용자가 공유 창에서 복사해 준 것을 쓰고 공유ID를 지어내지 마세요. 수학도장 그래프가 아닌 iframe은 400 `CONTENT_UNSUPPORTED_EMBED`로 저장이 거절됩니다.
- `class`는 보내지 마세요. 이미지 폭을 제외한 임의 `style`도 제거됩니다.
- **수식은 반드시** 아래 형식만 사용합니다. `$...$`, `\(...\)`, MathML 등은 렌더링되지 않습니다:
  - 인라인 수식: `<span data-math="inline" data-latex="a^2+b^2"></span>` (태그 내용은 비움)
  - 블록(별도 줄) 수식: `<div data-math="block" data-latex="\int_0^1 x\,dx"></div>` (`<p>` 밖에 배치)
- `data-latex` 값은 KaTeX 호환 LaTeX이며 HTML 속성이므로 `"`는 `&quot;`, `<`는 `&lt;`로 이스케이프합니다.
- 해설에만: 개념 활용 밑줄은 `<span data-action="활용id">구절</span>`, 블록 수식은 `data-latex` 뒤에 `data-action="활용id"`로 답니다(id는 공백으로 나눠 최대 4개). 없는 활용이나 합치지 않고 뺀 활용이면 400 `SOLUTION_ACTION_INVALID`이고, 다른 활용으로 합친 활용은 받습니다. 문제 본문(`body`)과 해설 밖의 칸에서는 밑줄이 벗겨지고 글자만 남습니다.

## 4. 이미지 넣기 — 업로드 세션 (권장)

대화에 첨부된 이미지는 당신이 직접 R2로 업로드할 수 없습니다. 대신 **업로드 세션**을 만들어
사용자가 브라우저에서 올리게 하고, 완료 후 조회한 `publicUrl`을 본문에 넣으세요.

1. 업로드 세션을 생성합니다 (12시간 유효, 세션당 최대 10장):

```bash
curl -X POST "https://mathdojang.com/api/uploads/sessions" -H "Authorization: Bearer mdk_..."
```

```json
{ "success": true, "data": { "id": "...", "pageUrl": "https://mathdojang.com/llm-upload/...", "expiresAt": "..." } }
```

2. `pageUrl`을 사용자에게 전달하며 안내하세요: "이 링크를 열어 문제에 넣을 이미지를 올려 주세요
   (수학도장 로그인 필요). 다 올리셨으면 알려 주세요." — 링크는 반드시 응답의 `pageUrl` 그대로 사용합니다.
3. 사용자가 완료를 알리면 세션을 조회해 이미지 목록을 받습니다:

```bash
curl "https://mathdojang.com/api/uploads/sessions/{id}" -H "Authorization: Bearer mdk_..."
```

```json
{
  "success": true,
  "data": {
    "id": "...",
    "expiresAt": "...",
    "expired": false,
    "images": [{ "id": "...", "publicUrl": "https://public-image-url..." }]
  }
}
```

4. 각 `publicUrl`을 본문 또는 해설 HTML에 넣습니다:

```html
<figure data-figure="" data-align="center" style="width:60%"><img src="PUBLIC_URL" alt="이미지 설명" loading="lazy"><figcaption>선택 설명</figcaption></figure>
```

- `data-align`은 `left`, `center`, `right` 중 하나입니다.
- `style`은 `width:20%`부터 `width:100%`까지 정수로 사용합니다(서버는 0보다 크고 100 이하인 퍼센트만 남기고 그 밖의 폭 지정은 지웁니다). 고정값을 쓰지 말고
  그림 성격에 맞춰 정하세요 — 수직선·좁은 도형 25~40%, 일반 도형·그래프 50~75%,
  표·와이드 그래프 85~100%. 사용자가 에디터에서 드래그로 조정할 수 있는 값과
  같으므로, 애매하면 넉넉한 쪽보다 실제 그림 비율에 가까운 쪽을 고르세요.
- 조회로 받은 `publicUrl` 외의 이미지 URL을 만들거나 추측하지 마세요. `images`가 비어 있으면
  사용자에게 업로드 상태를 다시 확인하세요.
- 어떤 이미지가 어떤 위치에 들어갈지는 사용자와 대화로 확인하세요 (조회 순서 = 업로드 순서).
- 인라인 에디터 이미지는 legacy `imageKeys`·`solutionImageKeys`에 넣지 마세요.
- 세션이 만료되면(`expired: true`) 새 이미지는 못 올리지만 이미 올린 이미지 조회는 계속 됩니다.
  더 올려야 하면 새 세션을 만드세요.
### 파일 바이트를 직접 PUT할 수 있는 환경

WebP 변환을 우선 시도하되 변환 실패만으로 업로드를 중단하지 마세요. PNG, JPEG, WebP, GIF를
모두 허용하며, 변환 후 실제 파일의 MIME과 정확한 바이트 수를 신고해야 합니다.

1. `POST https://mathdojang.com/api/uploads/images`에 다음처럼 요청합니다.

```json
{
  "purpose": "content",
  "variants": [{ "name": "content", "contentType": "image/png", "size": 1024 }]
}
```

2. 응답의 `assetId`를 보관하고 `variants[0].uploadUrl`로 정확히 1,024바이트를 PUT합니다.
   `Content-Type`은 신고한 `image/png`와 정확히 같아야 하고
   `Cache-Control: public, max-age=31536000, immutable` 헤더도 정확히 보내야 합니다.
   응답이 200 또는 204인지 확인합니다.
3. `POST https://mathdojang.com/api/uploads/images/{assetId}/finalize`를 호출합니다. 서버가 실제 바이트,
   MIME, 크기, 디코딩 가능 여부와 치수를 검증합니다.
4. finalize 성공 응답의 `variants[].publicUrl`만 본문·해설에 넣습니다. finalize 전에 URL을
   삽입하지 마세요.

## 5. 응답 형식

- 성공: HTTP 201, `{ "success": true, "data": { "id": "...", ... } }` — 등록된 문제는 임시제출(draft) 상태가 됩니다. 사용자가 사이트에서 `https://mathdojang.com/problems/{id}` 미리보기로 확인·수정한 뒤 "검토 요청"을 눌러야 검수 대기열에 들어갑니다.
- 실패: `{ "success": false, "error": { "code": "...", "message": "...", "fieldErrors": [{ "path": "필드", "message": "사유" }] } }` (`fieldErrors`는 입력 형식 오류일 때만 있습니다)
  - 401 `API_KEY_INVALID`: 키가 만료·폐기됨 — 사용자에게 새 키 발급을 요청하세요.
  - 400 `BAD_REQUEST`: 입력 형식 오류입니다. `fieldErrors`의 `path`가 가리키는 필드를 `message`대로 고쳐 재시도하세요.
  - 그 밖의 400(예: `CONTENT_EXTERNAL_IMAGE`, `CONTENT_UNSUPPORTED_EMBED`, `INVALID_TAGS`): `error.code`와 `message`가 사유를 알려 줍니다. 해당 내용을 고쳐 재시도하세요.

## 6. 참고 자료 — 운영 공지 (인증 불필요)

출제 기준·난이도 배분 등은 운영 공지로 안내됩니다. 시작하기 전에:

1. 공지 목록 조회: `GET https://mathdojang.com/api/posts?category=notice` — 응답의 `data.items[]`에서 `id`와 `title` 확인
2. **제목을 보고 출제에 필요한 공지라면** 상세를 읽으세요: `GET https://mathdojang.com/api/posts/{id}` — `data.body`가 본문(HTML)
3. 특히 **"문제 난이도 선택 안내"** 공지는 `difficulty` 값을 정하기 전에 반드시 읽고 그 기준을 따르세요.

## 7. 요청 예시

```bash
curl -X POST "https://mathdojang.com/api/problems" \
  -H "Authorization: Bearer mdk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "title": "지수방정식의 해",
    "body": "<p>방정식 <span data-math=\"inline\" data-latex=\"2^{x+1}=16\"></span>을 만족시키는 실수 <span data-math=\"inline\" data-latex=\"x\"></span>의 값을 구하시오.</p>",
    "answerType": "number",
    "answer": "3",
    "difficulty": 3,
    "subject": "math1",
    "unit": "exp_log",
    "solution": "<p><span data-math=\"inline\" data-latex=\"2^{x+1}=2^4\"></span>이므로 <span data-math=\"inline\" data-latex=\"x+1=4\"></span>, 따라서 <span data-math=\"inline\" data-latex=\"x=3\"></span>이다.</p>",
    "attestOriginal": true,
    "attestCopyright": true
  }'
```

## 8. 진행 방법 권장사항

1. 공지 목록에서 필요한 공지(특히 난이도 선택 안내)를 읽으세요.
2. 사용자와 대화하며 에디터 호환 HTML로 문제 본문과 해설을 완성하세요.
3. 문제에 이미지가 필요하면 업로드 세션을 만들어 pageUrl을 안내하고, 사용자의 완료 신호 후
   세션 조회로 받은 publicUrl을 본문·해설에 넣으세요 ("이미지 넣기" 장).
4. 등록 전 최종 JSON을 사용자에게 보여 주세요.
5. 자작·저작권 확약 두 가지를 각각 확인받은 뒤에만 true로 보내세요.
6. `POST https://mathdojang.com/api/problems`로 등록하고 성공·실패 결과를 사용자에게 알리세요.
7. 등록 성공 시 문제는 임시제출(draft) 상태입니다 — 사용자에게 `https://mathdojang.com/problems/{id}`에서 미리보기로 확인하고, 필요하면 수정한 뒤 "검토 요청" 버튼으로 검수를 신청하라고 안내하세요.
8. HTTP 요청을 직접 보낼 수 없는 환경이면, 사용자가 터미널에 붙여넣을 수 있는 완성된 curl 명령을 만들어 주세요.
