o2o-site-AEO/docs/API_USAGE.md
Mina Choi 4871e50327 문서: 앱을 가른 뒤 낡아진 서술을 고치고, 개발과 무관해진 기록을 지운다
지운 것 — 앞으로의 개발에 쓸 데가 없다.
- solution/backend/demo_site.html: 어떤 스크립트도 만들지 않는 고아 산출물이고,
  손으로 쓴 HTML 이라 "백엔드는 HTML 을 만들지 않는다" 와도 어긋난다.
- solution/README.md: front/ · admin/.env · 사이트당 rooms/index.html·sitemap.xml 처럼
  지금은 전부 틀린 서술이었다. 살아 있는 두 가지(빌더 CSR vs 발행물 SSG 대비표,
  하이드레이션 블롭에 미검증 값이 샜던 실측)는 ARCHITECTURE 로 옮겼다.
- docs/API_USAGE.md 의 Claude 개발비 집계: 2026-08-27 스냅샷과 재집계 스크립트는
  일회성 지출 기록이라 제품 원가와 성격이 다르다. 문서를 외부 API 원가 하나로 좁혔다.

고친 것 — 코드를 따라가지 못하던 서술.
- 코드 경로가 solution/backend 로 옮겨진 뒤 `backend/...` 로 남아 있던 포인터 전부.
  가리키는 자리가 없는 경로는 문서가 아니라 함정이다.
- ARCHITECTURE: 트리의 front→frontend, 컨테이너 표에 api-admin(:9801)·admin(:3002) 추가.
- ★ ARCHITECTURE·AGENTS 의 "admin 전용 라우터가 0개" 는 사실이 아니었다.
  /v1/admin/local-content 가 admin 전용인데 :9800 에도 마운트돼 있다 —
  포트를 가른 논리에 아직 남은 구멍이라 그렇게 적었다.
- DECISIONS: 결론난 것을 미결로 두면 함정이 된다. 작업 큐(2026-08-27 결론),
  날씨 캐시 TTL 1시간, jobs 테이블, media 조회 API, 수집 체인을 결론으로 옮기고
  네이버 플레이스 대 TourAPI 실측(2026-08-31)을 1-1 에 이었다.
- API_USAGE: 어댑터가 다 붙고 TourAPI 키도 나왔다. "실호출 0건" 은 낡은 서술이었다.
- backend/README: 16→17 테이블(jobs), 없어진 alters/, MockAdapter 만이라는 서술,
  cd backend 경로, media·local 라우터 누락.
2026-08-31 16:58:09 +09:00

4.5 KiB

외부 API 원가

사이트 1건을 만드는 데 나가는 외부 API 비용만 다룬다. 상한은 $1(1,400원) 이고, 문서가 아니라 코드가 강제한다(3절). 제품 제약으로서의 근거는 PRODUCT.md 8절.

⚠️ 이 상한에 개발비를 섞지 않는다. 코드를 짜는 데 든 LLM 요금은 일회성 지출이고, 사이트를 만들 때마다 반복해서 나가는 원가와는 성격이 다르다.


1. 공급자별 상태 (2026-08-31)

API 용도 붙은 자리 과금
TourAPI (apis.data.go.kr) fact 주력 공급원 — 숙박·음식점 collector/tour_api_adapter.py 무료 (개발계정 1,000건/일)
네이버 지역검색 (openapi.naver.com) 동일 업소 검증 후보 external/naver.py 무료 쿼터
Perplexity Sonar 채널 URL 발견 external/perplexity.py 요청 + 토큰
Gemini (AI Studio) 사진 분류 · 소개문/FAQ · 붙여넣기 추출 external/gemini.py gemini_text.py gemini_extract.py 토큰
Kakao Local (dapi.kakao.com) 주변 정보 미발급 external/kakao.py 건당 2원
Open-Meteo 날씨 불필요 external/open_meteo.py 무료

.env 의 키가 비어 있으면 해당 어댑터만 비활성되고 서버는 그대로 뜬다 — 부팅이 외부 계약에 묶이면 안 되기 때문이다. 수집 어댑터는 COLLECT_ADAPTERS 로 배포 없이 개별로 끌 수 있다.

무엇을 어디서 가져오는지(필드 단위 카탈로그와 실측 충전율)는 DATA_SOURCE_RESEARCH.md 8-3.

2. 단가표

코드의 단일 출처는 solution/backend/common/cost.pyRATES 다. 아래는 그 요약이다.

API 단가 확정
Kakao Local 키워드/카테고리 검색 2원, 좌표 변환 0.5원 .env.example
Perplexity Sonar 요청 7원 + 1K 토큰당 1.4원 (low context, USD_KRW 1,400) 공식 단가표
Gemini 이미지 1원 · 1K 토큰 0.5원 추정치
TourAPI · Open-Meteo 0원

좌표 변환이 키워드 검색보다 4배 싸다. 같은 공급자인데 단가가 다르므로 호출측이 kakao_coord 로 구분해 넘긴다. 지역 정보를 place_id 가 아니라 행정구역 코드로 캐싱하는 이유도 이것이다 — 같은 지역에 사이트 50개가 생겨도 외부 조회는 1회다.

⚠️ Kakao 무료 쿼터 함정 — 무료 쿼터는 개발자 계정의 "첫 번째 활성 앱" 에만 붙는다. dev/stage/prod 앱을 따로 파면 하나만 무료다. 앱을 나누기 전에 확인할 것.

3. 상한을 코드가 강제한다

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건.
  • solution/backend/common/cost.py · 테스트 tests/test_cost.py 8건.

⚠️ Gemini 단가가 아직 추정치다. 그래서 assert_rates_confirmed() 가 실배치를 막는다 — 추정치 위에 "예산 안에 들어온다" 는 결론을 세우지 않기 위해서다. 공식 단가표로 RATES 를 교체하면서 confirmed=True 로 바꾼다.

비용이 실제로 붙는 순서:

상호명 → [Perplexity: 채널 URL 발견]   ← 요청 + 토큰
       → [TourAPI: fact 수집]          ← 무료
       → 크롤링(무료)
       → [Gemini: 사진 분류 + 카피]     ← 토큰 (사진 수에 비례)

업소당 원가를 먼저 실측한 뒤 배치를 키우는 순서가 안전하다.

4. 아직 없는 것 — 호출 로그 테이블

DB(solution/backend/common/database/model/models.py, 17테이블)에 API 호출·비용을 적재하는 테이블이 없다. 지금은 애플리케이션 로그로만 남는다(perplexity.py 가 토큰과 검색 횟수를 찍는 정도).

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