o2o-site-AEO/docs/API_USAGE.md
Mina Choi 6784e59ca5 최초 커밋 — 기존 코드 전체 + 문서 체계 신설
git 저장소가 없어 히스토리·협업 기반이 아예 없던 상태를 연다.
함께 문서를 재편했다. 그동안 문서가 있어도 "이 제품이 뭘 푸는가"와
"어떻게 도는가"를 담은 문서가 없어서, 목표 문장이 backend/frontend
README 두 곳에 복붙돼 있었다 — 상위 문서가 없어 아래로 샌 것이다.

신설
  README.md               레포 진입점 + 문서 지도 + 문서 규칙 4가지
  AGENTS.md               에이전트·신규 합류자용 함정 목록과 규약
                          (CLAUDE.md 는 여기로 걸린 심볼릭 링크)
  docs/PRODUCT.md         제품 정의 — 문제·사용자·원칙·**non-goals**·성공 기준
  docs/ARCHITECTURE.md    payload 경계·발행 파이프라인·서빙 결정·앱 분리 설계

이동
  backend/docs/DECISIONS.md → docs/DECISIONS.md
    백엔드만의 결정이 아니다. 게다가 코드 주석 ~25곳이 이미
    `docs/DECISIONS.md` 로 적고 있어 레포 루트 기준으로는 그게 맞다.

갱신
  docs/DEPLOY.md          서빙 결정 반영 — nginx 정적 서빙이 지금 경로(3절),
                          Azure 는 나중에 켤 때(4절)로 분리
  docs/ARCHITECTURE.md    사이트 = 한 장(2026-08-31) 구조 반영
  docs/COLLECTION_SEO_AEO_FLOW.md
                          robots.txt·sitemap.xml 은 오리진 루트에만 굽는다는 점 명시
  frontend/site/scripts/prerender.ts
                          헤더 주석의 렌더 보고서 경로가 실제(422줄)와 달라 수정

.gitignore
  ★ CLAUDE.md 를 더 이상 무시하지 않는다. 에이전트 지침은 팀과 모든
    에이전트가 공유하는 규약이라 커밋해야 한다 — 무시하면 클론한 사람이
    "배포 후 republish_all.py 필수" 같은 함정을 전달받지 못한다.
    개인용 오버라이드는 ~/.claude/CLAUDE.md 에 둔다.
2026-08-31 13:57:59 +09:00

211 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# API 사용 이력 · 비용 기록
**집계 시각: 2026-08-27 08:59 KST**
집계 대상: o2o-web4ai 프로젝트 전체 (backend · frontend/admin · frontend/site)
---
## 0. 먼저 — 섞으면 안 되는 두 가지 비용
| | 무엇 | 언제 나가나 | 현재 |
|---|---|---|---|
| **A. 제품 원가** | 사이트 **1건 만들 때** 나가는 외부 API 비용 (Perplexity·Kakao·Gemini) | 업소 1곳 처리할 때마다 **반복** | **0원** (아직 한 번도 호출 안 함) |
| **B. 개발비** | 이 코드를 **짜는 데** 쓴 Claude API 비용 | 개발 기간 동안 **한 번** | $70.07 |
**A 의 상한은 $1(1,400원)이다.** B 는 사이트를 만들 때마다 나가는 돈이 아니다 — 코드가 완성되면 끝나는 일회성 지출이다. 이 문서의 2절은 전부 **B** 얘기다.
---
## 0-1. 한 줄 요약
| 구분 | 실제 호출 | 비용 |
|---|---|---|
| **외부 유료 API** (Perplexity · Kakao · Gemini · TourAPI) | **0건** | **0원** |
| **개발 도구 API** (Claude Code / Anthropic Messages API) | 348 요청 | **$70.07** (≈ 98,100원, 1,400원/USD 가정) |
지금까지 나간 돈은 **전부 개발 과정의 Claude API 요금**이고, 제품이 쓸 외부 API는 아직 **한 번도 실제로 때리지 않았다.**
---
## 1. [A] 제품 원가 — 외부 API 계약 현황과 실호출 이력
### 1-1. 키 상태
| API | 용도 | 키 설정 | 어댑터 | 실호출 |
|---|---|---|---|---|
| Perplexity Sonar (`api.perplexity.ai`) | 채널 URL 발견 | ✅ 설정됨 | `backend/services/external/perplexity.py` 구현 완료 | **0건** |
| Kakao Local (`dapi.kakao.com`) | 동일 업소 검증 · 주변 정보 | ⬜ 비어 있음 | 미구현 (설계만) | **0건** |
| Gemini (Google AI Studio) | 사진 분류 · 카피 작성 | ✅ 설정됨 | 미구현 | **0건** |
| TourAPI (`data.go.kr`) | 축제 · 관광지 | ⬜ 비어 있음 | 미구현 | **0건** |
| Open-Meteo | 날씨 | 키 불필요 | `frontend/.../useWeather.ts` | **0건** (프론트 코드만 작성) |
> `.env` 의 키가 비어 있으면 해당 어댑터만 비활성되고 서버는 그대로 뜬다 — 설계상 의도된 동작.
### 1-2. 실호출이 0건인 근거
1. **코드 레벨** — 전체 세션 트랜스크립트에서 위 호스트로 나간 HTTP 요청이 없다. 등장하는 건 전부 소스 작성·문서 편집 텍스트다.
2. **테스트 레벨**`backend/tests/` 안에서 실제 네트워크를 타는 코드가 없다. `test_perplexity.py` · `test_collector.py` 는 전부 목(mock) 기반이고, `httpx.get/post` 직접 호출은 0건.
3. **설정 레벨**`DECISIONS.md` 의 결정대로 **`APP_ENV=test``.env` 를 읽지 않는다.** 실키가 테스트로 새어 외부 API 요금이 나가는 경로 자체를 막아 뒀다.
4. **DB 레벨** — 어떤 테이블에도 외부 API 응답이 적재된 흔적이 없다 (호출 로그 테이블도 아직 없다 — 3-2 참고).
### 1-3. 단가표 (호출 시작 시 적용될 요금)
| API | 과금 방식 | 단가 |
|---|---|---|
| Perplexity Sonar | **토큰 요금 + 검색 호출 요금이 별도로 붙는다** | `sonar` 기본, `sonar-pro` 는 더 비쌈. 호출당 검색 횟수는 이미 로그로 남기고 있음 (`perplexity.py` 의 `usage.num_search_queries`) |
| Kakao Local | 무료 쿼터 초과분 종량 | 키워드/카테고리 검색 **2원**, 좌표 변환 **0.5원** (키워드가 4배 비싸다) |
| Gemini | 토큰 종량 (Google AI Studio) | 모델·티어별 |
| TourAPI | 공공데이터 무료 (일 트래픽 제한) | 0원 |
| Open-Meteo | 무료 | 0원 |
> ⚠️ **Kakao 무료 쿼터 함정** — 무료 쿼터는 개발자 계정의 "첫 번째 활성 앱" 에만 붙는다. dev/stage/prod 앱을 따로 파면 **하나만 무료**다. 앱을 나누기 전에 확인할 것.
---
## 2. [B] 개발비 — Claude (Anthropic Messages API)
> 사이트 1건당 원가가 **아니다.** 코드를 짜는 데 든 일회성 비용이다.
### 2-1. 총계
| 항목 | 값 |
|---|---|
| 기간 | 2026-08-26 16:23 ~ 2026-08-27 08:58 KST (약 16.6시간) |
| 모델 | `claude-opus-5` (1M 컨텍스트) 단일 |
| 과금 요청 수 | **348건** (메시지 ID 기준 중복 제거) |
| 출력 토큰 | 450,509 |
| 캐시 쓰기 (1h TTL) | 2,020,375 |
| 캐시 쓰기 (5m TTL) | 96,213 |
| 캐시 읽기 | 76,004,125 |
| 캐시 미적용 입력 | 690 |
| 입력 환산 합계 | 78,121,403 |
| **총 비용** | **$70.07** |
### 2-2. 비용 내역
| 항목 | 토큰 | 요율 ($/MTok) | 금액 |
|---|---|---|---|
| 캐시 읽기 (기본 입력가 ×0.1) | 76,004,125 | $0.50 | **$38.00** |
| 캐시 쓰기 1h (기본 입력가 ×2) | 2,020,375 | $10.00 | **$20.20** |
| 출력 | 450,509 | $25.00 | **$11.26** |
| 캐시 쓰기 5m (기본 입력가 ×1.25) | 96,213 | $6.25 | **$0.60** |
| 캐시 미적용 입력 | 690 | $5.00 | $0.003 |
| | | **합계** | **$70.07** |
> 각 항목은 센트 단위 반올림이라, 눈으로 더하면 합계와 1센트 차이가 날 수 있다.
**캐시 효과** — 프롬프트 캐싱이 없었다면 같은 작업이 **$401.87** 이었다. 즉 **$331.80 (82.6%) 를 캐시로 아꼈다.**
동시에, 비용의 **54%($38.00)가 캐시 *읽기*** 다. 출력 토큰 값($11.26)의 3배가 넘는다 — 긴 컨텍스트를 반복해서 다시 읽는 패턴이 이 프로젝트 비용 구조를 지배한다는 뜻이다. 줄이려면 출력을 줄이는 게 아니라 **컨텍스트를 작게 유지**해야 한다.
### 2-3. 세션별 내역
| 세션 | 종류 | 시각 (KST) | 요청 | 출력 토큰 | 캐시 읽기 | 비용 |
|---|---|---|---|---|---|---|
| `4db7aca5` | 메인 (backend 주력) | 08-26 16:23 ~ 08-27 08:55 | 157 | 266,385 | 35,748,750 | **$32.65** |
| `99be2a6c` | 메인 | 08-26 17:24 ~ 08-27 08:58 | 92 | 124,641 | 16,660,246 | **$19.79** |
| `19be092e` | 메인 (frontend/admin) | 08-27 08:42 ~ 08:58 | 29 | 25,903 | 2,476,298 | $3.37 |
| `agent-a58…` | 서브에이전트 | 08-27 08:51 ~ 08:57 | 13 | 6,173 | 5,741,359 | $3.21 |
| `agent-afa…` | 서브에이전트 | 08-27 08:52 ~ 08:55 | 14 | 423 | 6,132,381 | $3.21 |
| `6052d29a` | 메인 (이 기록 작성 세션) | 08-27 08:52 ~ 08:58 | 21 | 23,936 | 1,678,581 | $2.92 |
| `agent-aa5…` | 서브에이전트 | 08-27 08:52 ~ 08:58 | 9 | 68 | 3,905,434 | $2.07 |
| `agent-ab1…` | 서브에이전트 | 08-27 08:52 ~ 08:57 | 8 | 165 | 3,466,831 | $1.90 |
| `d56c8333` | 메인 | 08-27 08:52 ~ 08:53 | 5 | 2,815 | 194,245 | $0.95 |
| `2ac02dde` | 메인 (중복 세션) | — | 0 | 0 | 0 | $0.00 |
**읽을 거리 두 가지:**
- **서브에이전트 4개 합계 $10.39** — Perplexity/Kakao 클라이언트를 병렬로 짠 fork 들이다. 출력 토큰은 다 합쳐 6,829개뿐인데 캐시 읽기가 **1,925만 토큰**이다. **fork 는 부모 컨텍스트를 통째로 들고 시작하기 때문에**, 실제 작업이 짧아도 부모 컨텍스트가 크면 읽기 요금이 그만큼 붙는다. 병렬 fork 를 쓸지 말지는 "일이 병렬인가" 보다 **"부모 컨텍스트가 큰가"** 로 판단하는 편이 비용상 맞다.
- **`2ac02dde` 는 `99be2a6c` 로 이어진 세션(fork/resume)이라 메시지가 전부 중복** — 0원으로 처리했다. 세션 간 중복을 제거하지 않으면 408건 / **$79.16** 이 나와 **$9.09 과대집계**된다. 재집계할 때 메시지 ID 중복 제거를 반드시 넣을 것.
### 2-4. 이 숫자를 읽을 때 주의할 점
- **구독제(Max/Pro)로 쓰고 있다면 이 금액이 그대로 청구되지는 않는다.** 토큰 사용량을 공개 API 요율로 환산한 "이만큼 썼다" 값이다. 종량제 API 키로 같은 작업을 돌렸다면 나왔을 금액.
- 요율 기준: Opus 5 입력 $5 / 출력 $25 per MTok, 캐시 읽기 ×0.1, 캐시 쓰기 5m ×1.25 · 1h ×2.
- 1M 컨텍스트 구간에 별도 프리미엄이 붙는 경우 실제 금액은 이보다 높을 수 있다 — **하한선으로 읽을 것.**
- 원화 환산은 1,400원/USD 가정이다. 실제 청구 환율과 다를 수 있다.
- **이 스냅샷은 찍는 순간에도 움직인다.** 집계 시각 기준으로 `4db7aca5`·`99be2a6c`·`19be092e`·`6052d29a` 등 여러 세션이 동시에 살아 있었다. 4절의 스크립트로 언제든 다시 뽑을 것.
---
## 3. 앞으로 할 일
### 3-1. 비용이 실제로 발생하기 시작하는 지점
수집 파이프라인이 도는 순간부터다:
```
상호명 → [Perplexity: 채널 URL 발견] ← 토큰 + 검색 호출 과금
→ [Kakao Local: 동일 업소 검증] ← 건당 2원
→ 크롤링(무료)
→ [Gemini: 사진 분류 + 카피] ← 토큰 과금
```
업소 1건당 Perplexity 1~2회 + Kakao 여러 건 + Gemini(사진 수만큼)가 붙는다. **업소당 원가를 먼저 실측한 뒤 배치를 키우는 순서**가 안전하다.
### 3-2. 사이트 1건당 예산: **$1 (1,400원)** — 코드로 강제한다
제품 제약이 정해졌다: **업소 1곳의 사이트를 만드는 외부 API 비용은 $1 을 넘으면 안 된다.**
문서에만 적으면 지켜지지 않으므로 가드를 구현했다 — `backend/common/cost.py` (`CostMeter`), 테스트 `backend/tests/test_cost.py` 8건.
```python
meter = CostMeter(place_id=42)
meter.guard(Provider.PERPLEXITY, searches=2, tokens=3000) # 호출 "전" 에 확인 → 넘으면 BudgetExceeded
...실제 호출...
meter.charge(Provider.PERPLEXITY, searches=2, tokens=3100) # 호출 "후" 에 실측 기록
```
- `guard()` 가 막으면 **그 호출을 하지 않는다.** 이미 쓴 돈은 못 돌려받지만 손실이 선형으로 늘어나는 걸 끊는다.
- 예산 70% 소진 시 경고 로그, 100% 초과 시 예외.
- 미터 1개 = 사이트 1건.
**⚠️ 단가표에 아직 추정치가 섞여 있다.** 확정된 건 카카오뿐이다(레포에 2원/0.5원이 적혀 있다). Perplexity·Gemini 는 공식 단가표를 확인해서 `RATES` 를 교체해야 한다. 그때까지 `assert_rates_confirmed()` 가 실배치를 막는다 — 추정치 위에 "예산 안에 들어온다" 는 결론을 세우지 않기 위해서다.
### 3-3. 지금 없는 것 — 호출 로그 테이블
현재 DB 스키마(`backend/common/database/model/models.py`)에 **API 호출·비용을 적재하는 테이블이 없다.** 지금은 애플리케이션 로그로만 남는다 (`perplexity.py` 가 `tokens=` 와 검색 횟수를 찍는 정도).
파이프라인을 돌리기 전에 `api_call_logs` 같은 테이블(공급자 · 엔드포인트 · 토큰/호출수 · 추정단가 · place_id · 시각)을 얹어두면, 이 문서를 손으로 갱신하지 않고 쿼리로 뽑을 수 있다.
---
## 4. 재집계 방법
Claude 사용액은 아래로 다시 뽑는다 (세션 트랜스크립트 기준, 메시지 ID로 중복 제거):
```bash
cd ~/.claude/projects && python3 - <<'PY'
import json,glob,collections,os
files=sorted(glob.glob("-Users-minachoi-projects-o2o-web4ai*/*.jsonl")
+ glob.glob("-Users-minachoi-projects-o2o-web4ai*/*/subagents/*.jsonl"),
key=lambda p:-os.path.getsize(p))
IN,OUT,CR,W5,W1H = 5.0/1e6, 25.0/1e6, 0.50/1e6, 6.25/1e6, 10.0/1e6 # Opus 5 요율
seen=set(); t=collections.Counter()
for f in files:
for line in open(f,encoding="utf-8",errors="replace"):
try: o=json.loads(line)
except: continue
m=o.get("message") or {}; u=m.get("usage")
if not u: continue
rid=m.get("id")
if rid:
if rid in seen: continue # ★ fork/resume 중복 제거 — 빠뜨리면 과대집계
seen.add(rid)
cc=u.get("cache_creation") or {}
w5=cc.get("ephemeral_5m_input_tokens",0); w1=cc.get("ephemeral_1h_input_tokens",0)
if w5+w1==0: w5=u.get("cache_creation_input_tokens",0)
t["in"]+=u.get("input_tokens",0); t["out"]+=u.get("output_tokens",0)
t["w5"]+=w5; t["w1h"]+=w1; t["cr"]+=u.get("cache_read_input_tokens",0); t["n"]+=1
cost=t["in"]*IN+t["out"]*OUT+t["w5"]*W5+t["w1h"]*W1H+t["cr"]*CR
print(dict(t)); print(f"요청 {t['n']}건 / ${cost:.2f}")
PY
```
외부 API 실호출 여부 확인:
```bash
cd ~/.claude/projects && grep -c "api.perplexity.ai\|generativelanguage.googleapis.com\|dapi.kakao.com" \
-- -Users-minachoi-projects-o2o-web4ai*/*.jsonl
# 매칭이 있어도 대부분 소스 작성 텍스트다 — tool_use 의 command/url 필드인지 확인할 것
```