# 54편 검증 기록 — `Magica-Chen/GaussWorldTrader`

작성 2026-09-24. 기사: [`docs/tutorials/a/54-gaussworldtrader.md`](../../../docs/tutorials/a/54-gaussworldtrader.md).
이 문서는 **실제로 돌린 것만** 적는다. 못 한 것은 「미확인」으로 남긴다.

---

## 0. 고정

| | |
|---|---|
| 저장소 | `github.com/Magica-Chen/GaussWorldTrader` |
| **커밋** | **`14f357e61ba630d334ee9e75a3ff01edaa95ed3f`** (2026-09-07 23:58:02 +0100, "refine brand artwork and remove unused images") |
| 라이선스 | MIT |
| ★ / 포크 | 113 / 23 (GitHub API, 관측 2026-09-24) |
| 생성 / 마지막 푸시 | 2025-08-18 / 2026-09-07T23:02:55Z |
| 설치 위치 | `~/dev/weltiq-tools/GaussWorldTrader` (venv 는 그 안 `.venv`) |
| 파이썬 | 3.13.12 (uv). **3.14.7 에서도 `list-strategies` 동작 확인** |
| 코드 크기 | `src/` 27,415줄 · `tests/` 3,877줄 (`wc -l`) |
| 날짜·시각 | 2026-09-23 18:3x~18:5x UTC = 2026-09-24 03:3x~03:5x KST. **미국 장중** |

설치:

```bash
brew install ta-lib                     # 0.6.4 (기설치)
uv venv --python 3.13 .venv
TA_INCLUDE_PATH=/opt/homebrew/include TA_LIBRARY_PATH=/opt/homebrew/lib \
  uv pip install -r requirements.txt    # 4.19s (uv, 캐시 있음)
```

테스트: `pytest -q` → **235 passed · 1 failed · 1 skipped · 5.95s**.
실패 1건 = `tests/test_four_agent_research.py::test_t23_subprocess_worker_has_no_broker_environment_and_completes`
— 앞의 두 `assert`(키 미유출)는 통과하고 `outcome.state == "COMPLETED"` 에서
`AssertionError: ('RuntimeError',)`.

---

## 1. 판단 산출 형태 — **미확인이던 칸의 답** ⭐

기획서(`CONTENT_PLAN_V3` §4.11a · §6-3 54번)가 「**방향/레벨/배분 미확인 — 돌려 봐야 확정**」
이라고 남겨 둔 칸이다. 답: **셋 다 낸다.** 그리고 층이 셋으로 나뉘어 있다.

### 1.1 층 구조 (`src/strategy/base.py`)

| 층 | 클래스 | 무엇이 들었나 |
|---|---|---|
| ① 방향만 | `SignalSnapshot` (base.py:94) | `signal`(BUY/SELL/HOLD) · `indicators` · `signal_strength` · `reason` |
| ② 방향 + **레벨** | `ActionPlan` (base.py:107) | ① + `target_price` · `stop_loss` · `take_profit` — **수량 없음** |
| ③ 방향 + 레벨 + **배분** | `StrategySignal` (base.py:38) | ② + `quantity` · `price` (+ 옵션 칸 7개: `strike_price`·`delta`·`premium`·`expiration_date` 등) |

CLI 가 실제로 인쇄하는 것은 ③ 을 실행기가 감싼 `ExecutionDecision` 이다:

```
ExecutionDecision(symbol='SPY', side='buy', quantity=6.441442638324476,
  order_type='limit', limit_price=767.76, action='BUY',
  reason='Fast mode weighted vote: BUY=0.55, SELL=0.00, HOLD=0.21, risk=medium',
  stop_loss=756.2436, take_profit=790.7928, metadata={...})
```

원문: `raw/run-strategy-multiagent-SPY.txt` · `raw/run-strategy-scalping.txt` ·
`raw/run-strategy-trendfollowing-engine.txt` · `raw/run-strategy-multiagent-fast.txt`.

**`metadata` 에 더 들어 있는 것** (같은 파일):

- `decision`: `confidence`(실측 0.7254871504666395) · `participating_agents` · `dissenting_agents` · `debate_summary`
- `risk_assessment`: `risk_level` · **`max_position_pct` 0.0325** · `stop_loss_pct` · `take_profit_pct` · `atr` · `volatility_pct` · `risk_flags`(`agent_disagreement` 등) · `rationale`
- `reports`: 위원회 각자의 `action`·`confidence`·`thesis`·`metrics`
- `usage`: `calls` · `prompt_tokens` · `completion_tokens` · `estimated_cost_usd` · `latency_ms`

### 1.2 세션 경로는 더 촘촘하다 (`src/runtime/models.py`)

상주 서비스 쪽은 같은 판단이 pydantic 불변 레코드 사슬로 남는다.

| 클래스 | 새로 붙는 칸 |
|---|---|
| `Alternative` (:383) | `entry_price` · `stop_price` · `expected_net_value` · `estimated_fees` · `evidence_ids` · `short_instrument` |
| `SizingResult` (:409) | `quantity` · `capital` · `planned_loss` · **`binding_limit`**(무엇이 수량을 묶었는지) · `feasible` |
| `AccountSuitabilityReport` (:420) | `ranking_scores` · `operating_cost` · `uncertainty` · `refresh_conditions` |
| `SessionPlan` (:479) | **`entry_rules`·`invalidation_rules`**(구조화된 `Rule`) · `max_buy_price` · **`valid_from`·`entry_expires_at`·`close_at`** |
| `ValidationRecord` (:514) | `outcome` ∈ {ELIGIBLE, DEFERRED, REJECTED, INVALIDATED, ENTRY_EXPIRED, REVIEW_REQUIRED} |
| `OrderIntent` (:534) | `legs`(최대 2 — 수직 스프레드까지) · `limit_price` · `limit_effect` · `expires_at` |
| `RiskDecision` (:566) | `approved` · `approved_quantity` · `capital_reserved` · `loss_reserved` |

`one-shot` 리포트가 사람 눈에 보여 주는 형태는 표다 — 종목마다
**Trigger · Max entry · Stop · Target** 네 칸(`raw/gauss-bot-once.txt`).
**배분은 거기서 숫자가 아니라 산식**으로 나온다(계좌가 아직 안 묶여서):

> `Shares = floor(min(0.0025 × equity / (entry − stop), 0.10 × equity / entry, available cash / entry))`

### 1.3 한 줄 결론

> **방향 · 진입 레벨 · 손절 · 목표 · 수량 · 유효 시각까지 전부 낸다.**
> 다만 CLI 경로는 `(방향, 지정가, 손절, 목표, 수량)` 다섯이고,
> 세션 경로는 여기에 `(진입 규칙, 무효화 규칙, 유효 구간, 승인 수량, 예약 자본)` 이 더 붙는다.

---

## 2. B축 판정

| | |
|---|---|
| **참가 가능한가** | **가능하다.** 단 조건 하나 (아래) |
| **어느 회차** | 기획서 지정대로 **B2 · B7**(미국 주식). 예비 B12 도 무리 없다 |
| **판단 산출 형태** | §1 — 방향 + 레벨(진입·손절·목표) + 배분(수량·비중 상한). **B축 표의 「레벨형」 칸에 그대로 들어간다** |
| **참가 형태** | **Claude 구동형.** `run-strategy` 는 한 방짜리다. 점검 시점마다 다시 불러야 한다. 상주 서비스(`gauss_bot.py`)는 자율형이지만 그쪽을 쓰면 우리 슬롯이 아니라 알파카에 붙는다 |
| **우리 쪽 붙는 법** | 검증 완료 — §3. `ExecutionDecision` → `/v1/strategy-rules` → `/v1/containers`. 스크립트 `to_weltiq.py` |

### 2.1 조건 — LLM 모드는 1차 키가 있어야 한다

`src/llm/providers.py` 의 모든 제공자가 `base_url` 을 코드에 박는다.

```python
# providers.py:394 (OpenAI) · :492 (DeepSeek) · :569 (Anthropic) · :638 (Moonshot)
self.base_url = "https://api.openai.com/v1"
```

`.env.example:17` 은 `OPENAI_BASE_URL` 을 설정 항목으로 안내하는데 **읽는 코드가 없다**
(저장소 전체 grep 결과 그 한 줄뿐). **OpenRouter 경유 불가.**

→ **B축에서 이 도구를 LLM 모드로 출전시키려면 OpenAI·Anthropic·DeepSeek·Moonshot 중
하나의 1차 키가 필요하다.** 없으면 `MULTI_AGENT_MODE=fast`(산술 위원회)로 출전해야 하고,
그때는 "LLM 에이전트 대결"의 참가자가 아니라 **비LLM 기준선**에 가깝다.

우회로 하나: `providers.py` 한 줄을 고치면 된다(`self.base_url = os.getenv("OPENAI_BASE_URL", ...)`).
**이 글에서는 고치지 않았다** — 도구를 고쳐 놓고 "돌아간다"고 쓰면 독자가 재현 못 한다.

### 2.2 막히는 자리 — `shadow` 가 지키는 범위

`shadow` 기본값은 **상주 서비스의 것**이다. CLI 는 그 설정을 보지 않는다.

| 경로 | 무엇이 주문을 막나 |
|---|---|
| `gauss_bot.py` (상주) | `RuntimeConfig.execution_mode = SHADOW` (`models.py:112`) + `service.py:1827` 의 모드 게이트 |
| `main_cli.py run-strategy` | **`--execute` 플래그 하나뿐.** `ExecutionEngine.execute_decision` 이 보는 것은 `self.execute` 다 (`src/trade/engine/execution.py:205`). `execution_mode` 참조 0건 |

B축 운영에서 중요하다 — **"shadow 라서 안전하다"를 CLI 경로에 적용하면 안 된다.**
이 글에서는 `--execute` 를 **한 번도 켜지 않았다.** 알파카로 나간 주문은 0건이다.

---

## 3. weltiq 연결 실측

키는 `reports/a/31-alpaca-trading-agent/api.py` 와 같은 자리에서 읽었다(파일 경로만; 값은 이 문서에 없다).

### 3.1 1판 — 도구 값 그대로

| | |
|---|---|
| 입력 | `ExecutionDecision(SPY, buy, 6.441442638324476, limit 767.76, stop 756.2436, tp 790.7928)` |
| `POST /v1/strategy-rules` | **422 없음.** `doc_hash ce7bb23e724f0548ca8c4ae8db864d32350a5a8cefc0c79588ba3b45564b764f` (`raw/70-rules-doc.json`, `raw/71-rules-submit.json`) |
| `POST /v1/ai/thesis` | ok, 첫 시도 성공 (`raw/72-thesis.json`) |
| `POST /v1/containers` | 생성. `ct_c34458acea951f94` · `dep_pa_0e4852a017c468ed` · us-equity · SPY · 1m · balance 100,000 · `stop_max_loss 400` · `after_hours 2` (`raw/73-container.json`) |
| 생성 시각 | 2026-09-24T03:39:24.465288+09:00 |
| **체결** | 2026-09-24T03:40:12.805719+09:00 — **48.3초 뒤** |

체결 원문 (`raw/76-export.json`):

```json
{"symbol":"SPY","side":"buy","qty":"6","px":"767.846773","fee":"0E-8",
 "liquidity":"taker","expected_px":"767.75","quote_source":"alpaca-iex",
 "quote_scope":"iex","order_type":"market","action":"OPEN"}
```

**두 가지가 여기서 드러난다.**

| # | |
|---|---|
| C1 | **소수 주수가 잘린다.** 도구 6.441442638324476 → 원장 **6**. 0.4414주(≈$339) 소실. 도구 쪽은 알파카 계좌의 `fractional_trading` 을 읽어 소수를 그대로 쓴다(`main_cli.py:234`) |
| C2 | **`limit` 이 지정가로 안 간다.** 우리 쪽에서는 `close ≤ 767.76` 이 확정 봉 **진입 조건**이 되고 주문은 `market` 으로 나간다. 체결 767.846773 − 기대 767.75 = **+0.096773 (1.26bp)**, 도구 지정가보다 **+0.086773** 위 |

보호 주문은 걸렸다 — 슬롯 화면 ORDERS 칸에 `sell (close) / stop_market / 6 / acked`
(`shots/05-weltiq-slot.png`).

### 3.2 2판 — 실측으로 규칙을 고쳐 재주문 (S6)

| | 1판 | 2판 |
|---|---|---|
| 진입 조건 | `close ≤ 767.76` | **`close ≤ 767.66`** (C2 의 0.0968 만큼 내림) |
| 손절·목표·수량 | 도구 값 | **그대로** |
| 중지 조건 | 400 / 2h | **12 / 1h** (S7 을 노리고 좁게) |
| `doc_hash` | `ce7bb23e…` | `e8aa3458190406b17a7cab52b49ed0f188b6695f5e91df657fcb6bbf085f3645` |
| 컨테이너 | `ct_c34458acea951f94` | `ct_f02eecfdfab19986` · `dep_pa_f9fb26ec9256ccb7` |
| 결과 | 48.3초 만에 체결 | **체결 0건** |

원자료: `raw/80-rules-doc-v2.json` · `raw/81-rules-submit-v2.json` · `raw/82-container-v2.json` ·
`raw/83-poll-both.jsonl`(양쪽 동시 관측 99회, 03:47:13~04:10:16).

**2판이 왜 안 샀나.** 관측 구간 SPY 최저 **767.69** — 2판 문턱 767.66 에 **3센트 모자랐다.**
미끄러짐을 메우려고 문턱을 내렸더니 진입 자체가 사라졌다. 1판만 돌렸으면 안 보였을 것.

### 3.2b 1판 결산 (청산까지)

| | |
|---|---|
| 보유 | **17분 48.5초** (03:40:12.805719 → 03:58:01.341910 KST) |
| 진입 | buy 6 @ **767.846773** · 기대 767.75 · fee **0** · taker · market |
| 청산 | sell 6 @ **768.1081765** · 기대 **768.21** · fee **0.09493817** · taker · market · `action: CLOSE` |
| 청산 사유 | 사람이 `POST /stop` — 규칙의 stop/limit 은 안 닿았다 |
| 미끄러짐 | 진입 **+0.096773** · 청산 **−0.101824** → 왕복 **0.198597** |
| 실현 손익 | **+1.5684210** (수수료 전) |
| 수수료 뺀 자산 | **100,001.47348283** · `return_pct_mark 0.0015` |
| 왕복 미끄러짐 / 손익 | **12.66%** |

원자료 `raw/86-export-final-v1.json`. **수수료가 매수 0 · 매도 0.09493817** 로 비대칭이다.

### 3.3 S7 은 못 봤다

2판에 최대 손실 12 USD 를 걸었지만 **진입 자체를 안 해서 발동 기회가 없었다.**
우리 쪽 `stop_max_loss` 는 `runner.cumulative_pnl`(실현 손익 누적)을 본다
(`weltiq/runner/slots.py:128`). 포지션이 없으면 아무리 좁게 걸어도 안 걸린다.

**「미확인」이 아니라 「시도했고 조건이 안 왔다」로 기록한다.** 도구의 숫자를 바꿔
억지로 손실을 내는 방법이 있었지만, 그 순간 도구의 출력이 아니게 되므로 하지 않았다.

### 3.4 정리 — **1판은 삭제가 막혔다** (weltiq 결함 B4)

| 슬롯 | 정지 | 삭제 |
|---|---|---|
| `ct_f02eecfdfab19986` (2판) | ✅ | ✅ `deleted: true` |
| `ct_c34458acea951f94` (1판) | ✅ `stopped` · `attached: false` · 원장 flat | ❌ **409 `positions_open` 반복** |

**원인.** `containers.delete()` 가 보는 `held_qty(m)` 는 `initial_state.live.position` —
**엔진이 쓰는 실시간 스냅샷**이다(`weltiq/containers.py:826`, `open_positions_note` 주석이
그렇게 적어 둔다). 그 스냅샷이 `at: 2026-09-23T18:57:59.160695+00:00` 에서 얼었는데
청산 체결은 **18:58:01.341910** 이다. **1.2초 차이로 엔진이 분리되면서 마지막 스냅샷이
청산 전 상태로 남았다.** 원장(`fills` 2건 · `open_positions: []` · `unrealized_pnl: 0`)은
정확하다 — 틀린 것은 삭제 검사가 보는 쪽이다.

안내받은 우회로(`POST …/stop {"liquidate": true}`)도 409 `slot_not_attached` 로 막힌다.
그 오류가 다시 안내하는 것은 `POST …/start` 로 재부착 후 청산인데, **재부착이 같은 스냅샷의
`remaining_qty 6` 을 물려받으면 없는 6주를 파는 주문이 나간다.** 그러면 원장에 가짜 숏이
생긴다 — 발행할 기록을 오염시키는 쪽이라 **하지 않았다.**

**최종 상태**: 1판 슬롯은 `stopped` · 분리 · 원장 flat. 아무것도 하지 않는다. 삭제만 못 했다.
돌고 있던 다른 슬롯은 조회도 하지 않았다. 엔진·웹 프로세스는 건드리지 않았다.
증거 `raw/90-cleanup.json`.

---

## 4. 시나리오 판정 (BLOG_GUIDE §4.2)

이 도구의 행 = **「분석·의견을 낸다 (협업군)」** → 필수 **S6 + D**, 선택 S5·S7·S9.

| | 결과 |
|---|---|
| **D (= S4)** | ✅ 돌림. `gauss_bot.py --once` **56.17초**. 11,577 상장 → 11,555 최근 봉 → 2,802 유동성 통과 → 199 분석 → **조건부 워치리스트 5** (`raw/gauss-bot-once.txt`) |
| **S5** | ✅ 돌림 (우리 쪽). §3.1 |
| **S6** | ✅ 돌림. §3.1 → §3.2 |
| S1 | 안 돌림. 한 종목 보기는 이 도구의 자리가 아니라 D 로 대신 |
| S2 | **이 도구로는 안 된다.** 알파카 전용 — 한국 종목 경로 0 |
| S3 | 안 돌림. 외부 기록 입구 없음 |
| S7 | 시도했고 **조건이 안 왔다.** §3.3 |
| S8 | 안 돌림. 이 글의 판단 경로가 상주 서비스를 안 거쳐 끊길 것이 없다 |
| S9 · S10 | 안 돌림. B축 몫 |

---

## 5. 결함 목록

### 저쪽 (GaussWorldTrader)

| # | 결함 | 영향 |
|---|---|---|
| A1 | `.env.example:17` 이 `OPENAI_BASE_URL` 을 안내하는데 **읽는 코드가 없다.** 제공자 넷 모두 `base_url` 하드코딩 | 중계 키(OpenRouter 등) 사용자는 LLM 모드 불가. **문서가 없는 기능을 안내한다** |
| A2 | **`shadow` 기본값이 CLI 를 안 덮는다.** `execution.py:205` 가 보는 것은 `--execute` 뿐 | "기본이 shadow 라 안전"이 CLI 경로에서는 사실이 아니다 |
| A3 | 기본 `--days 60` 이 SMA50 워밍업을 못 채워 애널리스트 둘이 `warmup_incomplete` 로 기권 | 그래도 판단은 나온다(남은 하나의 표로). 리포트를 안 읽으면 위원회 셋이 다 일한 줄 안다 |
| A4 | 테스트 1건 실패 (`test_t23_subprocess_worker_…`, `RuntimeError`) | 리서치 워커 하위 프로세스 경로. LLM 모드 사용자가 먼저 만날 자리 |
| A5 | 키 없이 이벤트 달력이 통째로 죽는다 (`earnings: UNAVAILABLE (ValueError)`) | **리포트에 그대로 적어 준다.** 결함이라기보다 정직한 처리 — 기록만 해 둔다 |

### 이쪽 (weltiq) — 이번에 걸린 것

| # | |
|---|---|
| B1 | **us-equity 소수 주수 미지원.** 도구가 소수 주수를 내는 것은 이 연재에서 처음이 아니다. 지금은 조용히 잘린다 — `qty_truncated` 같은 로그가 없다 |
| B2 | 규칙 문서에 **비중 상한(`max_position_pct`)·확신도(`confidence`)** 를 담을 칸이 없다 (31편 B3 와 같은 지적) |
| B3 | `POST /v1/containers` 응답의 `quotes[].status: "unverifiable"` — *"us-equity 상장 목록이 비어 있어 SPY 가 거래 가능한 종목인지 확인하지 못했습니다"* 가 뜨는데 **실제로는 48초 뒤 정상 체결됐다.** 경고가 틀렸다 |
| **B4** | **청산한 슬롯이 삭제되지 않는다.** 삭제 검사가 원장이 아니라 `live` 스냅샷을 보는데, 엔진이 청산 체결 1.2초 전에 분리되면서 그 스냅샷이 얼었다. 안내받은 우회로 둘(`stop{liquidate:true}` → `start` → 재청산)이 모두 막히거나 위험하다. §3.4. **재현 조건: 포지션 보유 중 `POST /stop` → 같은 봉에서 청산** |
| B5 | 체결 수수료가 매수 0 · 매도 0.09493817 로 비대칭인데 어느 문서에도 그 규칙이 없다. 미국 주식 규제 수수료(SEC/TAF)를 흉내 낸 것으로 보이나 **확인 못 함 — 미확인** |

---

## 6. 비용·호출

| | |
|---|---|
| LLM 호출 | **0회.** `MULTI_AGENT_MODE=fast` 로만 돌렸다. 도구가 스스로 `usage: {'calls': 0, 'prompt_tokens': 0, 'completion_tokens': 0, 'total_tokens': 0, 'estimated_cost_usd': 0.0, 'latency_ms': 0.0}` 을 찍는다 (`raw/run-strategy-multiagent-SPY.txt`) |
| OpenRouter | **한 번도 안 불렀다.** §2.1 의 벽 때문에 애초에 붙지 않는다 |
| 알파카 | 무료 키. 읽기만. **`--execute` 를 켠 적이 없어 알파카로 나간 주문은 0건** |
| weltiq | 페이퍼 슬롯 2개. 1판 가동 18분 37초(체결 2건, 순손익 +1.47) · 2판 가동 23분(체결 0건) |

---

## 7. 캡처

| 파일 | 무엇 | 주인공 |
|---|---|---|
| `shots/01-dashboard-shadow.png` | 키 없이 뜬 오프라인 미리보기. SHADOW MODE 띠 | **도구** |
| `shots/02-list-strategies.png` | `list-strategies` — 전략 12개 | **도구** |
| `shots/03-research-watchlist.png` | `gauss_bot.py --once` — 11,577 스크리닝 → 조건부 워치리스트 | **도구** |
| `shots/04-execution-decision.png` | `ExecutionDecision` + `metadata` 세 절 | **도구** |
| `shots/05-weltiq-slot.png` | weltiq 슬롯 상세 — 주문·체결·컨테이너 로그 | 우리 |

**4 : 1.** BLOG_GUIDE §3.3 의 비율 조건을 만족한다.

재생성: `/Users/unolee/dev/weltiq/.venv/bin/python shots.py`
(`web` 인자로 대시보드만, 인자 없이 터미널 캡처 — 파일 안 주석 참고).

---

## 8. 파일

```
reports/a/54-gaussworldtrader/
├── RUN-LOG.md            이 문서
├── api.py                weltiq API helper (키는 파일에서 읽고 인쇄 안 함)
├── to_weltiq.py          ExecutionDecision → /v1/strategy-rules 규칙 문서
├── shots.py              캡처 생성
├── shots/                01~05
└── raw/
    ├── list-strategies.txt
    ├── gauss-bot-once.txt                     11,577 스크리닝 전문
    ├── run-strategy-scalping.txt
    ├── run-strategy-multiagent-fast.txt
    ├── run-strategy-multiagent-SPY.txt        ⭐ 판단 산출 형태 원문
    ├── run-strategy-trendfollowing-engine.txt
    ├── 70-rules-doc.json · 71-rules-submit.json · 72-thesis.json
    ├── 73-container.json · 74-slot-poll.jsonl · 75-container-state.json
    ├── 76-export.json                         ⭐ 체결·expected_px
    ├── 80-rules-doc-v2.json · 81-rules-submit-v2.json · 82-container-v2.json
    ├── 83-poll-both.jsonl                     양쪽 슬롯 동시 관측 99회
    ├── 86-export-final-v1.json                ⭐ 청산까지 — 왕복 미끄러짐·수수료
    ├── 87-export-final-v2.json                체결 0건 확인
    └── 90-cleanup.json                        정지·삭제 증거 (1판 삭제 실패 원문 포함)
```

## 9. 안 한 것

- `--execute` 를 켜지 않았다. 알파카로 주문을 낸 적이 없다
- `providers.py` 를 고치지 않았다 (§2.1)
- LLM 모드를 못 돌렸다 — **미확인**: LLM 위원회의 `debate_positions`·`DecisionMakerAgent` 출력 형태
- 옵션 전략(`wheel`·`vertical_spread`)을 안 돌렸다 — 기본 설정이 `options_enabled = false`
- 백테스트(`main_cli.py backtest`)를 안 돌렸다
- PDF 를 안 만들었다 (BLOG_GUIDE §9.3 — **한 판 뒤처져 있다**)
