지운 것 — 앞으로의 개발에 쓸 데가 없다. - 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 라우터 누락.
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.py 의 RATES 다. 아래는 그 요약이다.
| 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.py8건.
⚠️ 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 · 시각)를 얹어두면, 이 문서를 손으로 갱신하지 않고 쿼리로 뽑을 수 있다.