o2o-site-AEO/docs/API_USAGE.md
Mina Choi c85c577349 이름: solution/front → solution/frontend
`backend` 옆에 `front` 가 있을 이유가 없었다. negosium 의 negodata/front 를 그대로
베꼈고 그게 왜 front 인지는 따져보지 않았다 — 근거 없이 들여온 이름이라 바로잡는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
2026-08-31 15:27:16 +09:00

12 KiB
Raw Blame History

API 사용 이력 · 비용 기록

집계 시각: 2026-08-27 08:59 KST 집계 대상: o2o-web4ai 프로젝트 전체 (backend · solution/frontend · solution/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.pyusage.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 메인 (solution/frontend) 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 를 쓸지 말지는 "일이 병렬인가" 보다 "부모 컨텍스트가 큰가" 로 판단하는 편이 비용상 맞다.
  • 2ac02dde99be2a6c 로 이어진 세션(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건.

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.pytokens= 와 검색 횟수를 찍는 정도).

파이프라인을 돌리기 전에 api_call_logs 같은 테이블(공급자 · 엔드포인트 · 토큰/호출수 · 추정단가 · place_id · 시각)을 얹어두면, 이 문서를 손으로 갱신하지 않고 쿼리로 뽑을 수 있다.


4. 재집계 방법

Claude 사용액은 아래로 다시 뽑는다 (세션 트랜스크립트 기준, 메시지 ID로 중복 제거):

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 실호출 여부 확인:

cd ~/.claude/projects && grep -c "api.perplexity.ai\|generativelanguage.googleapis.com\|dapi.kakao.com" \
  -- -Users-minachoi-projects-o2o-web4ai*/*.jsonl
# 매칭이 있어도 대부분 소스 작성 텍스트다 — tool_use 의 command/url 필드인지 확인할 것