Skip to main content
Glama
README.md
# ๐Ÿ” Lesson Loop

**๊ฐœ๋… ์ด๋ฆ„ ํ•˜๋‚˜๋ฅผ ๋„ฃ์œผ๋ฉด, ์ถœ์ฒ˜ ์กฐ์‚ฌ๋ถ€ํ„ฐ ๊ฒ€์ˆ˜๊นŒ์ง€ ์Šค์Šค๋กœ ๋Œ๋ ค์„œ ๋ฐ”๋กœ ์ˆ˜์—…์— ์“ธ ์ˆ˜ ์žˆ๋Š” ๋ ˆ์Šจ ํŒฉ์„ ํŒŒ์ผ๋กœ ๋งŒ๋“ค์–ด ์ฃผ๋Š” ์—์ด์ „ํ‹ฑ ์›Œํฌํ”Œ๋กœ ์„œ๋น„์Šค.**

ํ•™์Šต ์ฝ˜ํ…์ธ  ์ œ์ž‘์ž๋Š” ๊ฐœ๋… ํ•˜๋‚˜๋งˆ๋‹ค ๊ฐ™์€ 6๋‹จ๊ณ„๋ฅผ ์†์œผ๋กœ ๋ฐ˜๋ณตํ•œ๋‹ค โ€” ์›๋ฌธ ์ฐพ๊ธฐ โ†’ ํ•™์Šต๋ชฉํ‘œ ์ชผ๊ฐœ๊ธฐ โ†’ ๋ฃจ๋ธŒ๋ฆญ ๋งŒ๋“ค๊ธฐ โ†’ ๋ฌธํ•ญ ์“ฐ๊ธฐ โ†’ **๊ทผ๊ฑฐ ๋Œ€์กฐ** โ†’ ํŒŒ์ผ ์ •๋ฆฌ. ๊ฐœ๋…๋‹น 2~3์‹œ๊ฐ„์ด ๋“ ๋‹ค. Lesson Loop๋Š” ์ด ์ ˆ์ฐจ ์ „์ฒด๋ฅผ ๋Œ€์‹  ๋Œ๋ฆฌ๊ณ , ์‚ฌ๋žŒ์€ ๋‘ ์ง€์ ์—์„œ๋งŒ ํ™•์ธํ•œ๋‹ค.

> ์ž๋™ํ™”ํ•˜๋Š” ๊ฒƒ์€ "๋ฌธํ•ญ ์ƒ์„ฑ"์ด ์•„๋‹ˆ๋ผ **"์ถœ์ฒ˜ ์กฐ์‚ฌ โ†’ ์ƒ์„ฑ โ†’ ๊ทผ๊ฑฐ ๋Œ€์กฐ โ†’ ์‚ฌ๋žŒ ์Šน์ธ"์ด๋ผ๋Š” ์ ˆ์ฐจ**๋‹ค.
> ๊ทธ๋ž˜์„œ ๋ชจ๋“  ๋ฌธํ•ญ์€ ์ถœ์ฒ˜ ์›๋ฌธ์˜ **์‹ค์ œ ๋ฌธ์žฅ**์„ ๊ทผ๊ฑฐ๋กœ ๊ฐ–๋Š”๋‹ค. ๊ทผ๊ฑฐ๋ฅผ ๋ชป ๋Œ€๋ฉด ์ฝ”๋“œ๊ฐ€ ์ €์žฅ์„ ๊ฑฐ๋ถ€ํ•œ๋‹ค.

- ๐Ÿ“‹ ๊ธฐํš ๋ฌธ์„œ: **[PRD.md](PRD.md)**
- ๐Ÿ“Š ์‹คํ—˜ ๋ถ„์„: **[evals/results/ANALYSIS.md](evals/results/ANALYSIS.md)** ยท ์›์ž๋ฃŒ: [RESULTS.md](evals/results/RESULTS.md)
- ๐Ÿ–ผ **์‹คํ–‰ ๊ฒฐ๊ณผ ์บก์ฒ˜: [docs/screenshots/](docs/screenshots/)** โ€” ํ™”๋ฉด 7์žฅ
- ๐Ÿ“„ ๊ฒฐ๊ณผ๋ฌผ ์˜ˆ์‹œ: **[docs/sample_lesson_pack.md](docs/sample_lesson_pack.md)** โ€” ์‹ค์ œ๋กœ ๋งŒ๋“ค์–ด ๋‚ธ ๋ ˆ์Šจ ํŒฉ
- ๐Ÿ”ง ๋„๊ตฌ ์Šคํ‚ค๋งˆ ์›๋ณธ: [docs/tools_schema.json](docs/tools_schema.json)

---

## 1๋ถ„ ์š”์•ฝ

```
๊ฐœ๋… "HTTP ์บ์‹ฑ"  โ”€โ”
                  โ–ผ
  โ‘  ๊ณ„ํš      ๊ธฐํš์ž๊ฐ€ ํ•˜์œ„ ์ฃผ์ œ์™€ ์˜์–ด ๊ฒ€์ƒ‰์–ด๋ฅผ ๋ฝ‘๋Š”๋‹ค
  โ‘ก ์กฐ์‚ฌ      web_search โ†’ fetch_page ๋กœ 1์ฐจ ์ถœ์ฒ˜(MDNยทRFCยทWikipedia)๋ฅผ ํ™•๋ณด
  โธ ์‚ฌ๋žŒ ํ™•์ธ  ์ถœ์ฒ˜๋ฅผ ๋นผ๊ฑฐ๋‚˜ ์ง์ ‘ ์ถ”๊ฐ€ํ•œ๋‹ค              โ† ๊ฐ€์žฅ ์‹ผ ๊ต์ • ์ง€์ 
  โ‘ข ์ž‘์„ฑ      ํ•™์Šต๋ชฉํ‘œยท๋ฃจ๋ธŒ๋ฆญยท๋ฌธํ•ญ. ์ธ์šฉ๋ฌธ์€ ์›๋ฌธ์—์„œ ๊ทธ๋Œ€๋กœ ๋ณต์‚ฌ
  โ‘ฃ ๊ฒ€์ˆ˜      ๋‹ค๋ฅธ ์—์ด์ „ํŠธ๊ฐ€ ์›๋ฌธ๊ณผ ๋Œ€์กฐ. ๋ฌธ์ œ ์žˆ์œผ๋ฉด ํ•ด๋‹น ๋ฌธํ•ญ๋งŒ ์žฌ์ž‘์„ฑ
  โธ ์‚ฌ๋žŒ ํ™•์ธ  ๋ฐœํ–‰ ์Šน์ธ                                 โ† ๋˜๋Œ๋ฆฌ๊ธฐ ์–ด๋ ค์šด ์ž‘์—… ์ง์ „
  โ‘ค ๋ฐœํ–‰      lesson_pack.md + lesson_pack.json
```

ํ‰๊ฐ€ ์„ธํŠธ 10๊ฐœ ์‹ค์ธก (gpt-4o-mini): **์™„๋ฃŒ์œจ 10/10 ยท ๊ทผ๊ฑฐ์œจ 100% (190๋ฌธํ•ญ ์ „๋ถ€) ยท ์ค‘์•™๊ฐ’ 71~83์ดˆ ยท ํ‰๊ท  $0.021 ยท ํ‰๊ท  26๋‹จ๊ณ„.**
์ž์„ธํ•œ ์ˆ˜์น˜์™€ ๊ฐœ์„  ์ด๋ ฅ์€ [ANALYSIS.md](evals/results/ANALYSIS.md).

---

## ๋น ๋ฅด๊ฒŒ ์‹คํ–‰ํ•˜๊ธฐ

```bash
git clone https://github.com/C-Lake864/lesson-loop.git && cd lesson-loop
python -m venv .venv && .venv/Scripts/activate     # macOS/Linux: source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env                                # .env๋ฅผ ์—ด์–ด OPENAI_API_KEY๋ฅผ ์ฑ„์šด๋‹ค
uvicorn app.main:app --reload --port 8000
```

๋ธŒ๋ผ์šฐ์ €์—์„œ <http://localhost:8000> ์„ ์—ฐ๋‹ค.

**์›น ์—†์ด ํ•œ ํŒ ๋Œ๋ ค ๋ณด๊ธฐ** (ํŠธ๋ ˆ์ด์Šค๊ฐ€ ํ„ฐ๋ฏธ๋„์— ๊ทธ๋Œ€๋กœ ํ๋ฅธ๋‹ค):

```bash
python -m app.cli "HTTP ์บ์‹ฑ" --questions 5 --auto
```

### ํ™˜๊ฒฝ๋ณ€์ˆ˜

| ๋ณ€์ˆ˜ | ํ•„์ˆ˜ | ๊ธฐ๋ณธ๊ฐ’ | ์„ค๋ช… |
| --- | :-: | --- | --- |
| `OPENAI_API_KEY` | โœ… | โ€” | ์„œ๋ฒ„์—์„œ๋งŒ ์“ฐ์ธ๋‹ค. ํ”„๋ก ํŠธ์—”๋“œ๋กœ ์ ˆ๋Œ€ ๋‚ด๋ ค๊ฐ€์ง€ ์•Š๋Š”๋‹ค |
| `LESSONLOOP_MODEL` | | `gpt-4o-mini` | ๊ธฐ๋ณธ ๋ชจ๋ธ |
| `LESSONLOOP_REVIEW_MODEL` | | (๋™์ผ) | ๊ฒ€์ˆ˜์ž๋งŒ ๋‹ค๋ฅธ ๋ชจ๋ธ๋กœ ์˜ฌ๋ฆฌ๊ณ  ์‹ถ์„ ๋•Œ |
| `LESSONLOOP_MAX_STEPS` | | `50` | run๋‹น LLM ํ˜ธ์ถœ ์ƒํ•œ |
| `LESSONLOOP_MAX_COST_USD` | | `0.50` | run๋‹น ๋น„์šฉ ์ƒํ•œ (๋„˜์œผ๋ฉด ์ž๋™ ์ค‘๋‹จ) |
| `LESSONLOOP_MAX_SECONDS` | | `420` | run๋‹น ์‹œ๊ฐ„ ์ƒํ•œ |
| `LESSONLOOP_MAX_REVISIONS` | | `1` | ๊ฒ€์ˆ˜ โ†’ ์žฌ์ž‘์„ฑ ์ตœ๋Œ€ ํšŸ์ˆ˜ |
| `TAVILY_API_KEY` | | โ€” | ์žˆ์œผ๋ฉด ๊ฒ€์ƒ‰ 1์ˆœ์œ„๋กœ ์‚ฌ์šฉ. ์—†์–ด๋„ ๋™์ž‘ํ•œ๋‹ค |
| `LESSONLOOP_WEBHOOK_TOKEN` | | โ€” | ์„ค์ •ํ•˜๋ฉด `POST /api/webhook/run` ํ™œ์„ฑํ™” |
| `LESSONLOOP_SCHEDULER` | | `0` | `1`์ด๋ฉด ํ๋ฅผ cron์œผ๋กœ ์ž๋™ ์‹คํ–‰ |
| `LESSONLOOP_RATE_LIMIT_PER_HOUR` | | `10` | IP๋‹น ์‹œ๊ฐ„๋‹น ์‹คํ–‰ ์ˆ˜ |

์ „์ฒด ๋ชฉ๋ก์€ [.env.example](.env.example) ์ฐธ๊ณ . **`.env`๋Š” ์ ˆ๋Œ€ ์ปค๋ฐ‹ํ•˜์ง€ ์•Š๋Š”๋‹ค** (`.gitignore`์— ์žˆ์Œ).

---

## ํ™”๋ฉด

| ํ™”๋ฉด | ๊ฒฝ๋กœ | ๋‹ด๋Š” ๊ฒƒ |
| --- | --- | --- |
| ์ƒˆ ์‹คํ–‰ | `/` | ๊ฐœ๋… ์ž…๋ ฅ, ์˜ˆ์‚ฐ ์Šฌ๋ผ์ด๋”, ์„ค์ • ์„ ํƒ, ํ, ์—ฐ๊ฒฐ๋œ ๋„๊ตฌ 8์ข…์˜ ์Šคํ‚ค๋งˆ |
| ์‹คํ–‰ ์ƒ์„ธ | `/run/{id}` | **์‹ค์‹œ๊ฐ„ ํŠธ๋ ˆ์ด์Šค**(SSE) ยท ์ถœ์ฒ˜ ยท ๊ฒฐ๊ณผ๋ฌผ ๋ผ์ด๋ธŒ ๋ทฐ ยท ์Šน์ธ ํŒจ๋„ ยท ๋‹ค์šด๋กœ๋“œ |
| ํ‰๊ฐ€ยท๋น„์šฉ | `/evals` | ์„ค์ •๋ณ„ ๋น„๊ตํ‘œ ยท ๋‹จ๊ณ„๋ณ„ ๋ณ‘๋ชฉ ยท ์‹คํŒจ ์‚ฌ๋ก€ ํ‘œ |
| API ๋ฌธ์„œ | `/api/docs` | FastAPI ์ž๋™ ์ƒ์„ฑ |

ํŠธ๋ ˆ์ด์Šค์˜ ๊ฐ ์Šคํ…์—๋Š” **์–ด๋–ค ๋„๊ตฌ๋ฅผ ์™œ ๋ถˆ๋ €๊ณ  ๋ฌด์—‡์„ ๋ฐ›์•˜๋Š”์ง€**๊ฐ€ ์ธ์žยท๊ฒฐ๊ณผ JSON์งธ๋กœ ๋‚จ๊ณ , ์†Œ์š” ์‹œ๊ฐ„ยทํ† ํฐยท๋น„์šฉ์ด ํ•จ๊ป˜ ์ฐํžŒ๋‹ค.

### ์‹ค์ œ ํ™”๋ฉด

| | |
| --- | --- |
| [![์‹คํ–‰ ํผ](docs/screenshots/01-home.png)](docs/screenshots/01-home.png)<br>**โ‘  ์ƒˆ ์‹คํ–‰** โ€” ์˜ˆ์‚ฐ ์Šฌ๋ผ์ด๋”, ์„ค์ • ์„ ํƒ, ์—ฐ๊ฒฐ๋œ ๋„๊ตฌ 8์ข… | [![์Šน์ธ ๋Œ€๊ธฐ](docs/screenshots/03a-hitl-approval.png)](docs/screenshots/03a-hitl-approval.png)<br>**โ‘ก ์‚ฌ๋žŒ ์Šน์ธ ๋Œ€๊ธฐ** โ€” ์ถœ์ฒ˜ ์ œ์™ธยท์ถ”๊ฐ€ยท์š”์ฒญ ๋‚จ๊ธฐ๊ธฐ |
| [![๋ฐœํ–‰ ์™„๋ฃŒ](docs/screenshots/03-run-published.png)](docs/screenshots/03-run-published.png)<br>**โ‘ข ๋ฐœํ–‰ ์™„๋ฃŒ** โ€” ํŒŒ์ผ ํฌ๊ธฐ ํ™•์ธ ํ›„ ๋œจ๋Š” ๋ฐฐ๋„ˆ, ๊ฒฐ๊ณผ๋ฌผ, ๊ฒ€์ˆ˜ ์ง€์ , ๋กค๋ฐฑ | [![ํ‰๊ฐ€ ๋Œ€์‹œ๋ณด๋“œ](docs/screenshots/06-evals-dashboard.png)](docs/screenshots/06-evals-dashboard.png)<br>**โ‘ฃ ํ‰๊ฐ€ยท๋น„์šฉ** โ€” ๋‹จ๊ณ„๋ณ„ ๋ณ‘๋ชฉ, ์„ค์ •๋ณ„ ๋น„๊ต, ์‹คํŒจ ์‚ฌ๋ก€ |

์ „์ฒด ํŠธ๋ ˆ์ด์Šค(115์Šคํ…)์™€ ์ธ์žยท๊ฒฐ๊ณผ JSON๊นŒ์ง€ ํŽผ์นœ ํ™”๋ฉด, ๋„๊ตฌ ์Šคํ‚ค๋งˆ ์ „๋ฌธ์€
**[docs/screenshots/](docs/screenshots/)** ์— ์žˆ๋‹ค. ์ „๋ถ€ `python docs/capture.py`๋กœ ์žฌํ˜„๋œ๋‹ค.

---

## ์—ฐ๊ฒฐ๋œ ๋„๊ตฌ 8์ข…

| ๋„๊ตฌ | ์ข…๋ฅ˜ | ๋ถ€์ž‘์šฉ | ๊ถŒํ•œ | ์Šน์ธ |
| --- | --- | --- | --- | :-: |
| `web_search` | ์™ธ๋ถ€ API | ์—†์Œ | ๊ฒ€์ƒ‰์–ด๋งŒ ์ „์†ก | |
| `fetch_page` | ์™ธ๋ถ€ HTTP | ์—†์Œ | **๋„๋ฉ”์ธ allowlist๋งŒ**, ์‚ฌ์„ค IP ์ฐจ๋‹จ, 15์ดˆยท200KB ์ƒํ•œ, ์ฃผ์ œ ๊ด€๋ จ์„ฑ ๊ฒ€์‚ฌ | |
| `read_sources` | DB ์ฝ๊ธฐ | ์—†์Œ | ํ˜„์žฌ run์˜ ์ถœ์ฒ˜๋งŒ | |
| `save_draft` | DB ์“ฐ๊ธฐ | ์žˆ์Œ | ํ˜„์žฌ run์—๋งŒ insert | |
| `save_questions` | DB ์“ฐ๊ธฐ | ์žˆ์Œ | ํ˜„์žฌ run์—๋งŒ insert | |
| `read_draft` | DB ์ฝ๊ธฐ | ์—†์Œ | ํ˜„์žฌ run๋งŒ | |
| `save_review` | DB ์“ฐ๊ธฐ | ์žˆ์Œ | ํ˜„์žฌ run์—๋งŒ insert | |
| `export_pack` | **ํŒŒ์ผ ์ƒ์„ฑ** | **๋˜๋Œ๋ฆฌ๊ธฐ ์–ด๋ ค์›€** | `artifacts/{run_id}/` ๋ฐ–์œผ๋กœ ๋ชป ๋‚˜๊ฐ | **โœ… ํ•„์ˆ˜** |

๊ฐ ๋„๊ตฌ์˜ ์ž…๋ ฅยท์ถœ๋ ฅ ์Šคํ‚ค๋งˆ๋Š” ์‹คํ–‰ ํ™”๋ฉด ์˜ค๋ฅธ์ชฝ "์—ฐ๊ฒฐ๋œ ๋„๊ตฌ"์—์„œ ํŽผ์ณ ๋ณผ ์ˆ˜ ์žˆ๊ณ , `GET /api/meta`๋กœ๋„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.

### ์„ค๊ณ„์—์„œ ์ง€ํ‚จ ๊ฒƒ

**๋ถ€์ž‘์šฉ์€ ์ „๋ถ€ ๋„๊ตฌ๋ฅผ ํ†ต๊ณผํ•œ๋‹ค.** ์—์ด์ „ํŠธ๋Š” DB์— ์ง์ ‘ ์“ฐ์ง€ ์•Š๊ณ  ํŒŒ์ผ๋„ ์ง์ ‘ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค. ๊ทธ๋ž˜์„œ ๋ชจ๋“  ๋ถ€์ž‘์šฉ์ด 100% ๋กœ๊ทธ์— ๋‚จ๋Š”๋‹ค.

**์Šน์ธ์€ ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์•„๋‹ˆ๋ผ ์ฝ”๋“œ๊ฐ€ ํ™•์ธํ•œ๋‹ค.** `export_pack`์€ DB์— ์Šน์ธ ํ† ํฐ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ์—†์œผ๋ฉด ๋„๊ตฌ ๋ ˆ์ด์–ด์—์„œ ๊ฑฐ๋ถ€ํ•œ๋‹ค. ํ”„๋กฌํ”„ํŠธ๋ฅผ ์•„๋ฌด๋ฆฌ ์„ค๋“ํ•ด๋„ ์šฐํšŒํ•  ์ˆ˜ ์—†๋‹ค.

**์‹คํŒจ๋Š” ์˜ˆ์™ธ๋กœ ํ„ฐ๋œจ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.** ๋ชจ๋“  ๋„๊ตฌ ์˜ค๋ฅ˜๋Š” `{"ok": false, "error": {"code", "message", "hint"}}`๋กœ ๋ชจ๋ธ์—๊ฒŒ ๋Œ์•„๊ฐ„๋‹ค. ์—์ด์ „ํŠธ๊ฐ€ ์Šค์Šค๋กœ ํšŒ๋ณตํ•  ๊ธฐํšŒ๋ฅผ ์ฃผ๊ธฐ ์œ„ํ•ด์„œ๋‹ค. ํšŒ๋ณต ์‹œ๋„๊นŒ์ง€ ์ „๋ถ€ ํŠธ๋ ˆ์ด์Šค์— ๋‚จ๋Š”๋‹ค.

| ์‹คํŒจ ์œ ํ˜• | ์ฒ˜๋ฆฌ |
| --- | --- |
| ํƒ€์ž„์•„์›ƒยท5xx | ์žฌ์‹œ๋„ 2ํšŒ (0.5s โ†’ 2s) |
| ๊ฒ€์ƒ‰ ๋ฐฑ์—”๋“œ ์‹คํŒจ | **4๋‹จ ํด๋ฐฑ**: Tavily โ†’ ๋ฌธ์„œ ์‚ฌ์ดํŠธ ์ž์ฒด ๊ฒ€์ƒ‰(MDNยทWikipedia) โ†’ DuckDuckGo โ†’ ๋‚ด์žฅ ์‹œ๋“œ ์ฝ”ํผ์Šค |
| ํŽ˜์ด์ง€๋ฅผ ๋ชป ๊ฐ€์ ธ์˜ด | ์‹œ๋“œ ์ฝ”ํผ์Šค์— ๊ฐ™์€ URL์ด ์žˆ์œผ๋ฉด ๊ทธ๊ฒƒ์œผ๋กœ ๋Œ€์ฒด |
| allowlist ๋ฐ– / ์ฃผ์ œ ๋ฌด๊ด€ | ์žฌ์‹œ๋„ ์—†์ด ๊ตฌ์กฐํ™” ์˜ค๋ฅ˜ ๋ฐ˜ํ™˜ โ†’ ์—์ด์ „ํŠธ๊ฐ€ ๋‹ค๋ฅธ URL ์„ ํƒ |
| ์Šคํ‚ค๋งˆยท๊ทผ๊ฑฐ ๊ฒ€์ฆ ์‹คํŒจ | ๋ฌด์—‡์ด ์™œ ํ‹€๋ ธ๋Š”์ง€ + **์›๋ฌธ์—์„œ ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ๊ตฌ๊ฐ„**์„ ํ•จ๊ป˜ ๋ฐ˜ํ™˜ |
| ๊ฐ™์€ ๋„๊ตฌยท๊ฐ™์€ ์ธ์ž 3ํšŒ | ๋ฃจํ”„ ๊ฐ์ง€ โ†’ ์ฐจ๋‹จ |

**๋ฌดํ•œ ๋ฃจํ”„๋Š” ์„ธ ๊ฒน์œผ๋กœ ๋ง‰๋Š”๋‹ค.** โ‘  ์˜ˆ์‚ฐ(๋‹จ๊ณ„ 50 / ๋น„์šฉ $0.50 / ์‹œ๊ฐ„ 420์ดˆ / ๋„๊ตฌ ํ˜ธ์ถœ 80ํšŒ) โ‘ก ๊ฐ™์€ ํ˜ธ์ถœ ๋ฐ˜๋ณต ๊ฐ์ง€ โ‘ข ์ •์ฒด ๊ฐ์ง€ โ€” ๋„๊ตฌ๋ฅผ ๊ณ„์† ๋ถ€๋ฅด๋ฉด์„œ ๋งˆ๋ฌด๋ฆฌ ๋„๊ตฌ๋ฅผ ์•ˆ ๋ถ€๋ฅด๋ฉด "์ง€๊ธˆ ๊ฐ€์ง„ ์ •๋ณด๋กœ ๋งˆ๋ฌด๋ฆฌํ•˜๋ผ"๋Š” ์ง€์‹œ๋ฅผ ์ฃผ์ž…ํ•œ๋‹ค.

---

## ์ค‘๊ฐ„์— ๋Š๊ฒจ๋„ ์ด์–ด์ง„๋‹ค

์—์ด์ „ํŠธ์˜ ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ๊ฐ€ ๋งค ํšŒ์ฐจ DB์— ํ†ต์งธ๋กœ ์ €์žฅ๋œ๋‹ค. ๊ทธ๋ž˜์„œ:

- **์Šน์ธ ๋Œ€๊ธฐ ์ค‘ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ๋‹ซ์•„๋„** ๋‚˜์ค‘์— ๋งํฌ๋กœ ๋Œ์•„์™€ ์ด์–ด์„œ ์Šน์ธํ•˜๋ฉด ๊ทธ ์ง€์ ๋ถ€ํ„ฐ ์ง„ํ–‰๋œ๋‹ค
- **์„œ๋ฒ„๋ฅผ ์žฌ์‹œ์ž‘ํ•ด๋„** `running` ์ƒํƒœ๋กœ ์–ผ์–ด๋ถ™์€ run์„ ์‹œ์ž‘ ์‹œ ์ž๋™์œผ๋กœ ์ด์–ด ๋Œ๋ฆฐ๋‹ค (`recover_interrupted`)
- **์‹คํŒจํ•œ run๋„** ํ™”๋ฉด์˜ "์ด์–ด์„œ ์žฌ๊ฐœ" ๋ฒ„ํŠผ์œผ๋กœ ์ €์žฅ๋œ ๋‹จ๊ณ„๋ถ€ํ„ฐ ๋‹ค์‹œ ๋ฏผ๋‹ค

```bash
# ์ง์ ‘ ํ™•์ธํ•ด ๋ณด๊ธฐ: ์‹คํ–‰ ์ค‘์— ์„œ๋ฒ„๋ฅผ ์ฃฝ์˜€๋‹ค ์‚ด๋ฆฐ๋‹ค
uvicorn app.main:app --port 8000     # Ctrl+C ๋กœ ์ค‘๋‹จ ํ›„ ๋‹ค์‹œ ์‹คํ–‰
# โ†’ ๋กœ๊ทธ์— "[lessonloop] ์ค‘๋‹จ๋œ run N๊ฐœ๋ฅผ ์žฌ๊ฐœํ–ˆ๋‹ค."
```

---

## ์‚ฌ๋žŒ ๊ฐœ์ž… ์ง€์ 

| ์ง€์  | ์œ„์น˜ | ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ |
| --- | --- | --- |
| **HITL #1** | ์กฐ์‚ฌ ์™„๋ฃŒ โ†’ ์ž‘์„ฑ ์‹œ์ž‘ ์ „ | ์ถœ์ฒ˜ ๊ฐœ๋ณ„ ์ œ์™ธ ยท URL ์ง์ ‘ ์ถ”๊ฐ€ ยท ์ž‘์„ฑ์ž์—๊ฒŒ ์š”์ฒญ ๋‚จ๊ธฐ๊ธฐ ยท ๊ฑฐ๋ถ€ ํ›„ ์žฌ์กฐ์‚ฌ |
| **HITL #2** | ๋ฐœํ–‰ ์ง์ „ | ๊ฒฐ๊ณผ๋ฌผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ™•์ธ ยท ์Šน์ธ ยท ๋ฐœํ–‰ ์ทจ์†Œ |
| ์ƒ์‹œ | ์‹คํ–‰ ์ค‘ ์–ธ์ œ๋“  | ์ค‘๋‹จ(์‚ฐ์ถœ๋ฌผ ๋ณด์กด) ยท ์‹คํŒจ ํ›„ ์žฌ๊ฐœ ยท ์ดˆ์•ˆ ์ด์ „ ๋ฒ„์ „์œผ๋กœ ๋กค๋ฐฑ |

`auto_approve`๋ฅผ ์ผœ๋ฉด ๋‘ ์ง€์ ์„ ์ž๋™ ํ†ต๊ณผํ•œ๋‹ค. ํ‰๊ฐ€์šฉ์ด๋ฉฐ, ์ผœ๋ฉด ํ™”๋ฉด์— ๊ฒฝ๊ณ ๊ฐ€ ๋œฌ๋‹ค.

**"์™„๋ฃŒํ–ˆ๋‹ค"๋Š” ๋ง๊ณผ ๊ฒฐ๊ณผ๋ฌผ์€ ๋‹ค๋ฅด๋‹ค.** ๋ฐœํ–‰ ์™„๋ฃŒ ๋ฐฐ๋„ˆ๋Š” ํŒŒ์ผ์ด ๋””์Šคํฌ์— ์‹ค์ œ๋กœ ์“ฐ์˜€๊ณ  ํฌ๊ธฐ๊ฐ€ 0์ด ์•„๋‹˜์„ ๋˜์ฝ์–ด ํ™•์ธํ•œ ๋’ค์—๋งŒ ๋œจ๊ณ , ํŒŒ์ผ๋ช…ยท๋ฐ”์ดํŠธ ์ˆ˜ยท๋ฌธํ•ญ ์ˆ˜ยท๊ทผ๊ฑฐ์œจ์„ ํ•จ๊ป˜ ์ฐ๋Š”๋‹ค.

---

## ์‹คํ—˜ & ํ‰๊ฐ€

```bash
# ์„ค์ • 4์ข… ร— ์‹œ๋‚˜๋ฆฌ์˜ค 4์ข…์„ ๋Œ๋ฆฌ๊ณ  ํ‘œ๋กœ ๋‚จ๊ธด๋‹ค
python -m evals.run --configs baseline,no_review,tools_terse,single_agent \
                    --only http-caching,css-box-model,hash-table,tx-isolation \
                    --workers 3
```

- ํ‰๊ฐ€ ์„ธํŠธ: [evals/dataset.jsonl](evals/dataset.jsonl) โ€” ์‹ค์ œ ์ปค๋ฆฌํ˜๋Ÿผ์— ๋„ฃ์„ ๋ฒ•ํ•œ ๊ฐœ๋… 10๊ฐœ. ๊ฐ ํ•ญ๋ชฉ์— `must_cover`(๋ฐ˜๋“œ์‹œ ๋‹ค๋ค„์•ผ ํ•  ํ•˜์œ„ ๊ฐœ๋…)์™€ `must_cite_domain`(๊ธฐ๋Œ€ํ•˜๋Š” 1์ฐจ ์ถœ์ฒ˜)์ด ๋ถ™์–ด ์žˆ๋‹ค.
- ๋น„๊ต ์„ค์ •: [app/configs.py](app/configs.py) โ€” ํŒŒ์ดํ”„๋ผ์ธ ๋ชจ์–‘ ยท ๋„๊ตฌ ์„ค๋ช… ์ƒ์„ธ๋„ ยท ๊ฒ€์ˆ˜ ์œ ๋ฌด ยท ๊ณ„ํš ์œ ๋ฌด ยท ๋ชจ๋ธ
- **ํ•ด์„ยท๊ฐœ์„  ์ด๋ ฅยท์‹คํŒจ ์‚ฌ๋ก€ ๋ถ„์„: [evals/results/ANALYSIS.md](evals/results/ANALYSIS.md)**
- ๋ฐฐ์น˜ ์›์ž๋ฃŒ: [evals/results/RESULTS.md](evals/results/RESULTS.md)
- ๋ฐฐ์น˜ ๊ฐ„ ๋น„๊ต(ํ˜„์žฌ ์ฑ„์  ๊ธฐ์ค€์œผ๋กœ ์žฌ๊ณ„์‚ฐ): `python -m evals.rescore`
- ์ˆ˜์šฉ ๊ธฐ์ค€ ์ž๋™ ์ ๊ฒ€: `python -m evals.acceptance --base <์ฃผ์†Œ> --live`

์ง€ํ‘œ๋Š” ์ „๋ถ€ ์ฝ”๋“œ๊ฐ€ ๊ณ„์‚ฐํ•œ๋‹ค. ์—์ด์ „ํŠธ๊ฐ€ "์ž˜ ๋๋ƒˆ๋‹ค"๊ณ  ๋งํ•œ ๊ฒƒ์€ ์„ธ์ง€ ์•Š๋Š”๋‹ค. ํŠนํžˆ **๊ทผ๊ฑฐ์œจ**์€ ๋ชจ๋ธ์˜ ์ฃผ์žฅ์ด ์•„๋‹ˆ๋ผ ์ธ์šฉ๋ฌธ์„ ์›๋ฌธ๊ณผ ๋ฌธ์ž ๋‹จ์œ„๋กœ ๋Œ€์กฐํ•ด ๋‹ค์‹œ ๊ณ„์‚ฐํ•œ ๊ฐ’์ด๋‹ค.

---

## ๋ฐฐํฌ

> **ํ˜„์žฌ ์ƒํƒœ**: ๊ณต๊ฐœ URL๋กœ ๋ฐฐํฌํ•˜์ง€๋Š” ์•Š์•˜๋‹ค. ๋Œ€์‹  ๋กœ์ปฌ์—์„œ ์‹ค์ œ๋กœ ๋Œ๋ฆฐ ํ™”๋ฉด์„
> [docs/screenshots/](docs/screenshots/)์— ์บก์ฒ˜๋กœ ๋‚จ๊ฒผ๊ณ , ์‚ฐ์ถœ๋ฌผ ์˜ˆ์‹œ์™€ ๋„๊ตฌ ์Šคํ‚ค๋งˆ ์›๋ณธ๋„
> ์ €์žฅ์†Œ์— ํ•จ๊ป˜ ๋‘์—ˆ๋‹ค. ์•„๋ž˜ ์ ˆ์ฐจ๋Œ€๋กœ ํ•˜๋ฉด ๊ทธ๋Œ€๋กœ ๋ฐฐํฌ๋œ๋‹ค.

ํ•œ ํ”„๋กœ์„ธ์Šค๊ฐ€ API์™€ ์ •์  ํ”„๋ก ํŠธ์—”๋“œ๋ฅผ ํ•จ๊ป˜ ์„œ๋น™ํ•˜๋ฏ€๋กœ ๋ฐฐํฌ ๋‹จ์œ„๊ฐ€ ํ•˜๋‚˜๋‹ค.

### ๋ฐฉ๋ฒ• A โ€” Hugging Face Spaces (์นด๋“œ ๋“ฑ๋ก ๋ถˆํ•„์š”, ๊ฐ€์žฅ ์‰ฌ์›€)

1. <https://huggingface.co/new-space> โ†’ SDK **Docker**, ํ•˜๋“œ์›จ์–ด **CPU basic (free)** ๋กœ Space ์ƒ์„ฑ
2. Space์˜ **Settings โ†’ Variables and secrets โ†’ New secret** ์—์„œ `OPENAI_API_KEY` ์ถ”๊ฐ€
   (Secret์œผ๋กœ ๋„ฃ์–ด์•ผ ๋กœ๊ทธยท์†Œ์Šค์— ๋…ธ์ถœ๋˜์ง€ ์•Š๋Š”๋‹ค)
3. ์ด ์ €์žฅ์†Œ๋ฅผ Space์— ์˜ฌ๋ฆฐ๋‹ค

   ```bash
   git remote add space https://huggingface.co/spaces/<๊ณ„์ •>/lesson-loop
   git push space main
   ```

4. Space์˜ `README.md`๋Š” YAML ํ”„๋ก ํŠธ๋งคํ„ฐ๊ฐ€ ํ•„์š”ํ•˜๋‹ค. [deploy/huggingface-space-README.md](deploy/huggingface-space-README.md)
   ๋ฅผ Space ์ €์žฅ์†Œ ๋ฃจํŠธ์˜ `README.md`๋กœ ๋ณต์‚ฌํ•ด ์“ฐ๋ฉด ๋œ๋‹ค (`app_port: 7860`์ด ๋“ค์–ด ์žˆ๋‹ค)
5. ๋นŒ๋“œ๊ฐ€ ๋๋‚˜๋ฉด `https://<๊ณ„์ •>-lesson-loop.hf.space/api/meta` ๊ฐ€ `{"llm_ready": true}` ๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•œ๋‹ค

### ๋ฐฉ๋ฒ• B โ€” Render

1. Render โ†’ **New โ†’ Web Service** โ†’ ์ด GitHub ์ €์žฅ์†Œ ์—ฐ๊ฒฐ (`render.yaml`์ด ์ž๋™ ์ธ์‹๋œ๋‹ค)
2. **Environment** ํƒญ์—์„œ `OPENAI_API_KEY`๋ฅผ ์ง์ ‘ ์ž…๋ ฅํ•œ๋‹ค (์ €์žฅ์†Œ์—๋Š” ๋“ค์–ด ์žˆ์ง€ ์•Š๋‹ค)
3. ๋ฐฐํฌ ํ›„ `/api/meta`๊ฐ€ `{"llm_ready": true}`๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด ์ •์ƒ

### ๋ฐฉ๋ฒ• C โ€” ์ง์ ‘ Docker

```bash
docker build -t lesson-loop .
docker run -p 7860:7860 -e OPENAI_API_KEY=sk-... lesson-loop
```

`PORT` ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ๋”ฐ๋ฅด๋ฏ€๋กœ FlyยทCloud Run ๋“ฑ์—๋„ ๊ทธ๋Œ€๋กœ ์˜ฌ๋ผ๊ฐ„๋‹ค.

> **์•Œ๋ ค์ง„ ํ•œ๊ณ„**: SQLite์™€ ์‚ฐ์ถœ๋ฌผ์ด ์ปจํ…Œ์ด๋„ˆ ํŒŒ์ผ์‹œ์Šคํ…œ์— ์žˆ์–ด ์žฌ๋ฐฐํฌ ์‹œ ์ดˆ๊ธฐํ™”๋œ๋‹ค.
> `LESSONLOOP_DB_PATH`ยท`LESSONLOOP_ARTIFACT_DIR`์„ ์˜๊ตฌ ๋ณผ๋ฅจ์œผ๋กœ ์ง€์ •ํ•˜๋ฉด ์œ ์ง€๋œ๋‹ค.
> HF Spaces์—์„œ๋Š” Persistent Storage(์œ ๋ฃŒ)๋ฅผ ๋ถ™์ด๊ฑฐ๋‚˜, ๋ฐ๋ชจ ๋ชฉ์ ์ด๋ฉด ๊ทธ๋Œ€๋กœ ๋‘์–ด๋„ ๋ฌด๋ฐฉํ•˜๋‹ค.

> **๋ฐฐํฌ ์‹œ ๊ถŒ์žฅ ์„ค์ •**: ๋‚จ์šฉ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด `LESSONLOOP_MAX_COST_USD=0.30`,
> `LESSONLOOP_RATE_LIMIT_PER_HOUR=6` ์ •๋„๋กœ ๋‚ฎ์ถฐ ๋‘๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

### ๋ฐฐํฌ ํ›„ ์ˆ˜์šฉ ๊ธฐ์ค€

[PRD.md ยง8.2](PRD.md)์˜ A1~A7์„ ์Šคํฌ๋ฆฝํŠธ๋กœ ๋‹ค์‹œ ํ™•์ธํ•œ๋‹ค.

```bash
python -m evals.acceptance --base https://<๋ฐฐํฌ์ฃผ์†Œ> --live --write
```

ํ™•์ธ ๊ฒฐ๊ณผ๋Š” [evals/results/RESULTS.md](evals/results/RESULTS.md) ํ•˜๋‹จ์— ๊ธฐ๋ก๋œ๋‹ค.

---

## ํ™•์žฅ

### MCP ์„œ๋ฒ„ โ€” ์‚ฐ์ถœ๋ฌผ์„ ๋‹ค๋ฅธ ์—์ด์ „ํŠธ์˜ ๋„๊ตฌ๋กœ

์ด ์„œ๋น„์Šค๋Š” ๋„๊ตฌ๋ฅผ ์“ฐ๋Š” ์ชฝ์ด์ž, ์ž๊ธฐ ์‚ฐ์ถœ๋ฌผ์„ ๋„๊ตฌ๋กœ ๋‚ด๋†“๋Š” ์ชฝ์ด๊ธฐ๋„ ํ•˜๋‹ค.

```bash
python mcp_server.py
```

`.mcp.json`์— ๋“ฑ๋กํ•˜๋ฉด Claude Code ๊ฐ™์€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋งŒ๋“ค์–ด์ง„ ๋ ˆ์Šจ ํŒฉ์„ ์ง์ ‘ ์“ธ ์ˆ˜ ์žˆ๋‹ค.

```json
{ "mcpServers": { "lesson-loop": {
    "command": "python", "args": ["mcp_server.py"], "cwd": "<์ด ์ €์žฅ์†Œ ๊ฒฝ๋กœ>" } } }
```

| MCP ๋„๊ตฌ | ํ•˜๋Š” ์ผ |
| --- | --- |
| `list_packs` | ๋งŒ๋“ค์–ด์ง„ ๋ ˆ์Šจ ํŒฉ ๋ชฉ๋ก |
| `get_pack` | ํŒฉ ํ•˜๋‚˜ ์ „์ฒด (json ๋˜๋Š” md) |
| `search_questions` | ๋ชจ๋“  ํŒฉ์„ ๊ฐ€๋กœ์งˆ๋Ÿฌ ๋ฌธํ•ญ ๊ฒ€์ƒ‰ โ€” ๋ฌธ์ œ์€ํ–‰์ฒ˜๋Ÿผ |
| `get_rubric` | ํ•™์Šต๋ชฉํ‘œยท๋ฃจ๋ธŒ๋ฆญ๋งŒ |
| `enqueue_concept` | ์ƒˆ ๊ฐœ๋…์„ ์ œ์ž‘ ํ์— ๋“ฑ๋ก |

**๋ฐœํ–‰ยท์‚ญ์ œ๋Š” ๋…ธ์ถœํ•˜์ง€ ์•Š๋Š”๋‹ค.** ๋˜๋Œ๋ฆฌ๊ธฐ ์–ด๋ ค์šด ์ผ์€ ์‚ฌ๋žŒ ์Šน์ธ ์ง€์ ์ด ์žˆ๋Š” ์›น UI์—๋งŒ ๋‚จ๊ฒจ ๋’€๋‹ค.

### ์ž๋™ ํŠธ๋ฆฌ๊ฑฐ

```bash
# ์›นํ›… (LESSONLOOP_WEBHOOK_TOKEN ์„ค์ • ์‹œ)
curl -X POST https://<๋ฐฐํฌ์ฃผ์†Œ>/api/webhook/run \
  -H "X-Webhook-Token: $TOKEN" -H "content-type: application/json" \
  -d '{"concept":"CORS","n_questions":5,"auto_approve":true}'

# ์Šค์ผ€์ค„๋Ÿฌ โ€” ํ์— ์Œ“์ธ ๊ฐœ๋…์„ cron์— ๋งž์ถฐ ์ž๋™ ์‹คํ–‰
LESSONLOOP_SCHEDULER=1 LESSONLOOP_SCHEDULER_CRON="0 3 * * *" uvicorn app.main:app
```

### ๊ทธ ๋ฐ–์—

- **๋ฉ€ํ‹ฐ ์—์ด์ „ํŠธ** โ€” ๊ธฐํš์ž / ์กฐ์‚ฌ์ž / ์ž‘์„ฑ์ž / ๊ฒ€์ˆ˜์ž 4์—ญํ• . `single_agent` ์„ค์ •์œผ๋กœ ๋‹จ์ผ ์—์ด์ „ํŠธ์™€ ๋น„๊ตํ•  ์ˆ˜ ์žˆ๋‹ค
- **๋กค๋ฐฑ** โ€” ์ดˆ์•ˆ์€ append-only๋กœ ๋ฒ„์ „์ด ์Œ“์ธ๋‹ค. ์‹คํ–‰ ํ™”๋ฉด์—์„œ ์ด์ „ ๋ฒ„์ „์œผ๋กœ ๋˜์‚ด๋ฆด ์ˆ˜ ์žˆ๋‹ค
- **๋น„์šฉ ๋Œ€์‹œ๋ณด๋“œ** โ€” `/evals`์—์„œ ๋‹จ๊ณ„๋ณ„ ๋ˆ„์  ์‹œ๊ฐ„ยท๋น„์šฉ์œผ๋กœ ๋ณ‘๋ชฉ์„ ๋ณธ๋‹ค

---

## ๊ตฌ์กฐ

```
app/
  main.py         FastAPI โ€” API ยท SSE ยท ์ •์  ์„œ๋น™
  orchestrator.py ๋‹จ๊ณ„ ์ƒํƒœ ๋จธ์‹  ยท ์‚ฌ๋žŒ ๊ฐœ์ž… ์ง€์  ยท ์‹คํŒจ ๋ถ„๋ฅ˜
  agent.py        ์—์ด์ „ํŠธ ๋ฃจํ”„ (๊ณ„ํš โ†’ ๋„๊ตฌ โ†’ ๊ด€์ฐฐ โ†’ ๋‹ค์Œ ํ–‰๋™)
  tools/
    base.py       ๊ถŒํ•œ ยท ์Šน์ธ ยท ์žฌ์‹œ๋„ ยท ๋ฃจํ”„ ๊ฐ์ง€๊ฐ€ ๋ชจ์ด๋Š” ๋„๊ตฌ ์ง„์ž…์ 
    search.py     web_search (4๋‹จ ํด๋ฐฑ)
    fetch.py      fetch_page (allowlist ยท SSRF ๋ฐฉ์–ด ยท ์ฃผ์ œ ๊ด€๋ จ์„ฑ)
    draft.py      save_draft ยท save_questions (๊ฒ€์ฆ ๊ฒŒ์ดํŠธ)
    store.py      read_sources (์ธ์šฉ๋ฌธ ์ถ”์ถœ ํฌํ•จ) ยท save_review
    grounding.py  ์ธ์šฉ๋ฌธ โ†” ์›๋ฌธ ๋Œ€์กฐ โ€” ํ™˜๊ฐ ๋ฐฉ์ง€์˜ ํ•ต์‹ฌ
    export.py     export_pack (์Šน์ธ ํ•„์ˆ˜ ยท ๋˜์ฝ์–ด ํ™•์ธ)
  db.py           SQLite โ€” run ยท trace ยท ๋ฉ”์‹œ์ง€ ํžˆ์Šคํ† ๋ฆฌ ยท ์Šน์ธ ยท ์•„ํ‹ฐํŒฉํŠธ
  llm.py          OpenAI ํ˜ธ์ถœ + ํ† ํฐยท๋น„์šฉ ๊ณ„๋Ÿ‰
  prompts.py      ์—ญํ• ๋ณ„ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ
  configs.py      ์‹คํ—˜ ์„ค์ •
static/           ํ”„๋ก ํŠธ์—”๋“œ (๋นŒ๋“œ ๋„๊ตฌ ์—†์Œ)
evals/            ํ‰๊ฐ€ ์„ธํŠธ ยท ๋Ÿฌ๋„ˆ ยท ๊ฒฐ๊ณผ
mcp_server.py     MCP ์„œ๋ฒ„
data/seed_sources.json   ๊ฒ€์ƒ‰์ด ์ „๋ถ€ ๋ง‰ํ˜”์„ ๋•Œ์˜ ์ตœํ›„ ํด๋ฐฑ ์ฝ”ํผ์Šค
```

## ์™ธ๋ถ€ ์„œ๋น„์Šค ์‚ฌ์šฉ์— ๋Œ€ํ•ด

- ์ถœ์ฒ˜๋Š” ๊ณต๊ฐœ ๊ธฐ์ˆ  ๋ฌธ์„œ ์‚ฌ์ดํŠธ๋กœ ํ•œ์ •ํ•œ๋‹ค ([app/config.py](app/config.py)์˜ `FETCH_ALLOWLIST`).
- ๋ณธ๋ฌธ์€ **์ธ์šฉ ๋ชฉ์ ์˜ ๋ฐœ์ทŒ**๋งŒ ์ €์žฅํ•˜๊ณ , ๊ฒฐ๊ณผ๋ฌผ์—๋Š” ์›๋ฌธ URL์„ ํ•ญ์ƒ ํ‘œ๊ธฐํ•œ๋‹ค.
- MDNยทWikipedia์˜ ๊ณต๊ฐœ ๊ฒ€์ƒ‰ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์“ด๋‹ค. Wikipedia์˜ User-Agent ์ •์ฑ…์— ๋งž์ถฐ ์—ฐ๋ฝ์ฒ˜๊ฐ€ ๋‹ด๊ธด UA๋ฅผ ๋ณด๋‚ธ๋‹ค.
- ๊ฐ€์ ธ์˜จ ๋ณธ๋ฌธ์€ ํ•ญ์ƒ `<untrusted_document>`๋กœ ๊ฐ์‹ธ ๋ชจ๋ธ์—๊ฒŒ "๋ฐ์ดํ„ฐ์ด์ง€ ์ง€์‹œ๊ฐ€ ์•„๋‹˜"์„ ๋ช…์‹œํ•œ๋‹ค. ๋ฌธ์„œ์— ํ”„๋กฌํ”„ํŠธ ์ธ์ ์…˜์ด ์‹ฌ๊ฒจ ์žˆ์–ด๋„ ๋„๊ตฌ ๊ถŒํ•œ์€ ์ฝ”๋“œ๊ฐ€ ๋ง‰๋Š”๋‹ค.