o2o-site-AEO/docs/DATA_SOURCE_RESEARCH.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

499 lines
33 KiB
Markdown
Raw Permalink 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.

# 콘텐츠 소스 리서치 — AEO/SEO/GEO 관점
조사일: 2026-08-28. 모든 HTTP 응답은 이 날짜에 이 개발 머신에서 직접 찍은 실측값이다.
`DECISIONS.md` 1-1(크롤링 약관·법적 검토), 1-2(이미지 재게시 권리)에 대한 답을 포함한다.
---
## 0. 한 줄 결론
**네이버·카카오 생태계는 AI 검색엔진에게 통째로 닫혀 있다. 그래서 우리가 네이버 플레이스를 복제할 이유가 없어졌고, 오히려 복제하면 안 된다.**
AI 인용을 노린다면 소스 전략은 "OTA에서 긁어오기" → **"공공데이터(인용 가능한 1차 출처) + 사장님 인증 채널(고유 콘텐츠)"** 로 바꿔야 한다.
---
## 1. ★ 결정적 발견 — AI 크롤러 차단 지도
각 사이트 `robots.txt` 실측. `전면차단` = `Disallow: /`.
| 소스 | GPTBot | OAI-SearchBot | ClaudeBot | PerplexityBot | Google-Extended | Googlebot | Yeti(네이버) |
|---|---|---|---|---|---|---|---|
| **m.place.naver.com** (현재 어댑터) | 전면차단 | 전면차단 | 전면차단 | 전면차단 | 전면차단 | **전면차단** | 전면차단 |
| **pcmap.place.naver.com** | 전면차단 | 전면차단 | 전면차단 | 전면차단 | 전면차단 | **전면차단** | 전면차단 |
| map.naver.com | 전면차단 | 전면차단 | 전면차단 | 전면차단 | 전면차단 | 거의차단 | 거의차단 |
| blog.naver.com | 전면차단 | 전면차단 | 전면차단 | 전면차단 | 전면차단 | 거의차단 | **전면차단** |
| booking.naver.com | 전면차단 | 전면차단 | 전면차단 | 전면차단 | 전면차단 | 전면차단 | 전면차단 |
| smartstore.naver.com | 전면차단 | 전면차단 | 전면차단 | 전면차단 | 전면차단 | 부분제한 | 부분제한 |
| place.map.kakao.com | 전면차단 | 전면차단 | 전면차단 | 전면차단 | 전면차단 | 거의차단 | 거의차단 |
| app.catchtable.co.kr | 전면차단 | 전면차단 | 전면차단 | 전면차단 | 전면차단 | 허용 | 허용 |
| www.goodchoice.kr (여기어때) | 전면차단 | 허용 | 전면차단 | 허용 | 전면차단 | 부분제한 | 부분제한 |
| www.diningcode.com | 전면차단 | 부분제한 | **허용** | **허용** | 전면차단 | 부분제한 | 부분제한 |
| www.yanolja.com | 별도규칙 없음(`*`만) | " | " | " | " | " | " |
### 이게 왜 사업의 핵심인가
`m.place.naver.com/robots.txt` 원문 (2026-08-28, 3회 재시도 모두 동일):
```
User-agent: *
Disallow: /
```
즉 **네이버 플레이스는 예외 없이 모든 크롤러를 금지한다.** 그 결과:
- 사용자가 ChatGPT/Claude/Perplexity/Gemini에 `"강릉 ○○펜션 반려동물 되나요?"` 를 물으면 → **인용할 소스가 존재하지 않는다.** 네이버 플레이스에 답이 있어도 AI는 그 페이지를 읽을 수 없다.
- 네이버 블로그·카페 후기도 똑같이 막혀 있다. 사장님 블로그를 아무리 잘 써도 글로벌 AI 검색에는 안 잡힌다.
- 캐치테이블·카카오맵도 AI 봇 전면 차단.
- **한국 로컬 비즈니스 정보는 AI 검색 입장에서 통째로 빈 구멍이다.**
→ 사장님 자체 도메인의 정적 사이트가 그 구멍을 채우면 **경쟁자 없이 독점 인용**된다. 이 제품의 해자가 데이터로 확인됐다.
### 채널 이원화가 필요하다
| 목표 | 인용 소스 특성 | 우리 전략 |
|---|---|---|
| **네이버 AI 브리핑** | 네이버 생태계 편중. 인용 272건 분석 시 블로그가 158건으로 1위. 검색순위와 별개(인용의 49.3%가 검색 10위 밖) | 자체 도메인으로는 뚫기 어렵다. 사장님 블로그 채널이 필요 (현재 미구현 — §11) |
| **Google AI Overview / ChatGPT / Perplexity** | 웹 전체 대상, 자사 도메인 유리. 경쟁 소스(네이버·카카오)가 전부 차단 상태 | **여기가 우리 주전장.** 현재 구조(정적 HTML + JSON-LD + llms.txt)가 정확히 맞다 |
---
## 2. 현재 상태 실측
| 항목 | 결과 |
|---|---|
| `m.place.naver.com/{kind}/{id}/home` | **HTTP 200**, `__APOLLO_STATE__` 존재 (restaurant 347KB / accommodation 612KB). 기능적으로는 정상 동작 |
| `pcmap.place.naver.com/restaurant/{id}/home` | HTTP 200, `__APOLLO_STATE__` 존재 |
| **robots.txt 준수 여부** | ❌ **위반 중.** `Disallow: /` 를 무시하고 있다 |
| 네이버 지역검색 API (`openapi.naver.com/v1/search/local`) | ✅ 정상 (2026-08-28 확인). 단 **`telephone`·`description`은 전 표본 100% 빈 문자열** |
| TourAPI `KorService2` | ❌ **HTTP 403** — 키는 유효하나 해당 API 미신청 상태 (같은 키로 축제 API는 200) |
| `KAKAO_REST_API_KEY` | 미설정(빈 값) |
> **기능이 되는 것과 해도 되는 것은 다르다.** 지금 네이버 플레이스 어댑터는 200을 받지만 robots.txt를 명시적으로 위반한다. `DECISIONS.md` 1-1의 "약관·robots.txt 기준 허용 범위" 질문에 대한 답은 **"robots.txt 기준으로는 명백히 불허"** 다.
---
## 3. OTA 크롤링 — 이미 막혀 있다
풀 브라우저 헤더(Chrome 128 UA + sec-ch-ua + Sec-Fetch-* 전체)로 재시도한 결과:
| URL | 결과 |
|---|---|
| `www.yanolja.com/pension/3013306` | **HTTP 403** + Cloudflare 챌린지 |
| `nol.yanolja.com/hotels/{id}` | HTTP 404/403 + Cloudflare 챌린지 |
| `www.goodchoice.kr/product/detail?ano=...` | **HTTP 403** + Cloudflare 챌린지 (홈페이지는 200) |
| `place.map.kakao.com/{id}` | HTTP 200이나 **3.6KB SPA 셸** — 내용 없음 |
| `place-api.map.kakao.com/places/panel3/{id}` (내부 API) | **HTTP 406** 차단 |
**야놀자·여기어때 어댑터는 만들어도 못 쓴다.** 뚫으려면 헤드리스 브라우저 + 봇 탐지 우회가 필요한데, 그건 `registry.py`와 `DECISIONS.md`가 **결론과 무관하게 영구 금지**로 못 박은 영역이다.
### 법적으로도 하면 안 된다 — 야놀자 v 여기어때
같은 행위(OTA 숙박정보 크롤링)에 대한 국내 유일 대형 선례다. **형사·민사 결론이 갈렸다.**
| 구분 | 결론 |
|---|---|
| 형사 (대법원 2022-05-12 확정) | **무죄.** 정보통신망침해·업무방해·저작권법 위반 모두 무죄. "수집한 정보 대부분이 이용자에게 공개된 것", DB의 통상적 이용을 방해한 정도가 아니라고 판단 |
| 민사 (서울중앙지법 2021-08) | **10억 원 배상 + 사용·복제·저장 금지.** 성과물 무단사용(부정경쟁행위) 인정. 야놀자가 영업조직 인건비만 26억 투입한 점을 배상액 산정에 반영 |
**"형사는 무죄니까 괜찮다"가 아니라 "민사로 10억 물었다"가 우리에게 유효한 신호다.** 우리는 그 정보를 **재게시**까지 하므로 여기어때보다 노출이 크다.
---
## 4. Google Places API — 우리 제품 구조와 약관이 충돌한다
| 항목 | 내용 |
|---|---|
| 캐싱 | `place_id`만 무기한 저장 가능. 좌표는 최대 30일. **이름·영업시간·사진·리뷰·전화번호는 캐싱 금지 — 실시간 조회 후 표시해야 한다** |
| 표시 조건 | Google 지도 위에 표시 + Google 로고 및 제3자 제공자 귀속 표기 의무 |
우리는 **DB를 실시간 조회하지 않는 정적 HTML**을 발행한다(§7). 정적 빌드에 값을 굽는 순간 캐싱 금지 조항 위반이다.
→ **Places API는 콘텐츠 소스로 배제.** (Place ID를 엔티티 식별자로만 쓰는 건 가능)
---
## 5. 업종별 실측: 사장님이 실제로 어디에 콘텐츠를 두는가
네이버 지역검색 API `link` 필드 충전율. 업종별 5개 쿼리 × 상위 5건 = **표본 25건씩** (작은 표본이니 방향만 참고).
| 업종 | link 있음 | 인스타그램 | 자체 도메인·기타 | 시사점 |
|---|---|---|---|---|
| **카페** | 23/25 (**92%**) | **17** | 6 (자체 도메인 2, 스마트스토어 2, 캐치테이블 1) | 카페는 사실상 **인스타가 공식 홈페이지**다 |
| **숙박** | 24/25 (**96%**) | 5 | **19** (개별 도메인 다수: `gangmunstay.kr`, `offinghouse.com` 등) | 숙박은 **자체 홈페이지 보유율이 압도적** |
| **음식점** | 16/25 (64%) | 8 | 8 (네이버블로그 4, 자체 도메인 4) | 절반씩 갈린다. 충전율도 가장 낮다 |
**이게 어댑터 우선순위를 정해준다:**
- 숙박 → 사장님 **자체 홈페이지 크롤링**(`StaticHtmlAdapter`)이 최고 효율. 사장님이 URL을 확정해주므로 법적으로도 가장 깨끗하다.
- 카페 → **인스타그램**이 유일한 실질 콘텐츠 소스.
- 음식점 → 커버리지가 낮아 공공데이터·사장님 직접입력 비중을 높여야 한다.
---
## 5-1. ★ 업종별로 결론이 다르다
세 업종은 **경쟁 상황이 서로 다르다.** "AI가 인용할 대체 소스가 이미 있는가"가 갈림점이다.
### 숙박 — 야놀자·여기어때는 답이 아니다
| 소스 | 실측 | 판정 |
|---|---|---|
| **야놀자** | HTTP **403** + Cloudflare 챌린지 (풀 브라우저 헤더로도 동일) | ✗ 기술적 차단 + 민사 10억 선례 |
| **여기어때** | HTTP **403** + Cloudflare 챌린지 | ✗ 동일 |
| **TourAPI 숙박(32)** | 무료·재게시 자유. 체크인/아웃, 객실 수·유형, 취사, 주차, 픽업, 부대시설 | ◎ **1순위** |
| **사장님 자체 홈페이지** | 표본 보유율 **76%** (숙박이 3업종 중 최고) | ◎ **2순위** |
| LOCALDATA 숙박업 | 객실 수, 시설면적, 영업상태(인허가 기준) | ○ 신뢰 근거 |
| 스테이폴리오 | SSR + JSON-LD 있으나 감성숙소 한정 | △ 커버리지 좁음 |
> **덧붙일 중요한 사실:** 야놀자·여기어때는 **AI 크롤러에게도 닫혀 있다.** 즉 긁어봐야 "AI가 못 읽는 정보를 위법하게 옮겨오는" 것에 불과하다. GEO 관점에서 얻는 게 없다.
**단, 숙박만 예외가 하나 있다.** 해외 OTA는 AI에 열려 있다:
| 소스 | 상세페이지 | GPTBot | ClaudeBot | PerplexityBot |
|---|---|---|---|---|
| 에어비앤비 | HTTP 200 (446KB, SSR) | 허용 | 허용 | 허용 |
| 부킹닷컴 | 접근 가능 | 허용 | 허용 | 허용 |
| 아고다 | 접근 가능 | 허용 | 허용 | 허용 |
→ **호텔·리조트급은 AI 검색 공백이 아니다.** 부킹닷컴·아고다 페이지가 이미 인용된다. 우리가 긁을 대상이 아니라 **경쟁 소스**로 인식해야 한다.
→ 반대로 **펜션·게스트하우스·독채는 해외 OTA 커버리지가 낮고 국내 채널은 전부 차단** → **공백이 그대로 남아 있다. 여기가 숙박의 진짜 기회다.**
### 카페 — AI 검색 공백은 최대. 소스는 생각보다 있다
> **정정 (2026-08-31).** 초판에서 "TourAPI 카페 커버리지 거의 없음"이라고 썼는데 **틀렸다.**
> 카페는 별도 콘텐츠 타입이 아니라 **음식점(39)의 소분류**(`cat3=A05020900` 카페/전통찻집)로 들어 있고,
> 전국 **1,979건**이 등록돼 있다. 실측으로 확인했다.
| 소스 | 실측 | 판정 |
|---|---|---|
| **TourAPI (39 / A05020900)** | 전국 1,979건. 영업시간·휴무일·대표메뉴·취급메뉴·주차·사진 | ○ **관광지 인근 편중이지만 실재한다** |
| **인스타그램** | 표본의 **68%**가 인스타를 공식 채널로 등록 (3업종 중 최고) | ◎ 그 외 카페의 주 소스. 사장님 OAuth 필요 |
| LOCALDATA 휴게음식점 | 영업상태·시설면적만 | △ 검증용 |
| 네이버 플레이스 | robots 전면 금지 | ✗ |
→ 카페는 **AI 검색 공백이 가장 크다**(네이버·카카오·인스타 전부 AI 차단). TourAPI에 등록된 카페는 바로 채워지고, 나머지는 **인스타 OAuth + 사장님 입력**으로 간다.
### 음식점 — 소스는 가장 많은데 쓸 만한 게 가장 적다
| 소스 | 실측 | 판정 |
|---|---|---|
| 배민 / 요기요 / 쿠팡이츠 | 공식 API 없음, 앱 중심 | ✗ |
| 캐치테이블 | SPA 셸(8KB) + **AI봇 전면차단** | ✗ |
| 다이닝코드 | SPA 셸(23KB, JS 필요). 단 **ClaudeBot·PerplexityBot 허용** | △ **경쟁 소스** |
| 식신 | SSR + `LocalBusiness` JSON-LD (상호·전화·주소) | △ 얕음 |
| 미쉐린가이드 | AI봇 허용 | △ 경쟁 소스(해당 업소 한정) |
| 블루리본서베이 | AI봇 전면차단 | ✗ |
| **TourAPI 음식점(39)** | 대표메뉴·취급메뉴·영업시간·쉬는날·포장·예약·좌석 수 | ○ **단 관광지 인근 편중 우려** |
| **LOCALDATA 일반음식점** | 업태(한식/중식…), 시설면적, 영업상태 | ○ |
| 식품안전나라 위생등급 | 공공 인증 | ○ **E-E-A-T 신호로 유용** |
| 사장님 자체 홈페이지 | 표본 보유율 32%, `link` 충전율 **64%로 3업종 최저** | △ |
→ 음식점은 **자체 채널 보유율이 가장 낮아** 공공데이터와 사장님 직접 입력 비중을 가장 크게 잡아야 한다.
### 요약
| | 대체 소스 | AI 검색 공백 | 주력 소스 | 난이도 |
|---|---|---|---|---|
| **숙박(펜션·독채)** | 야놀자·여기어때 모두 차단 | **완전 공백** | TourAPI + 자체 홈페이지(76%) | **낮음** ← 먼저 공략 |
| 숙박(호텔) | 해외 OTA가 AI에 노출 중 | 부분 공백 | TourAPI + 자체 홈페이지 | 중간 |
| **카페** | 사실상 없음 | **완전 공백** | 인스타 OAuth + 사장님 입력 | 높음 |
| **음식점** | 다이닝코드·미쉐린 일부 노출 | 대부분 공백 | LOCALDATA + TourAPI + 사장님 입력 | 높음 |
---
## 6. 소스 카탈로그
### Tier 1 — 공공 API (재게시 자유, GEO에 가장 강함)
| 소스 | 상태 | 숙박 | 카페 | 음식점 | 라이선스 |
|---|---|---|---|---|---|
| **TourAPI (한국관광공사 KorService2)** | 키는 있으나 **활용신청 필요**(무료·즉시) | ◎ 체크인/아웃, 객실 수·유형, 취사, 주차, 픽업, 부대시설 | ✕ 커버리지 거의 없음 | ○ 대표메뉴, 취급메뉴, 영업시간, 쉬는날, 포장·예약, 좌석 수 | **이용허락범위 제한없음 / 무료.** 개발계정 1,000건/일 → 운영계정 확장 가능 |
| **LOCALDATA (지방행정 인허가)** | 미도입. 무료 신청 | ○ 숙박업 인허가 — 객실 수, 시설면적, 영업상태 | ○ 휴게음식점 | ○ 일반음식점 — 업태(한식/중식…), 시설면적, 영업상태 | 공공데이터. 208개 업종 전체, 좌표 포함(EPSG:5174) |
| **네이버 지역검색 API** | ✅ 사용 중 | △ | △ | △ | 상호·주소·좌표·카테고리·`link`만. **전화·설명 필드는 빈 값** |
| **카카오 로컬 API** | 키 미설정 | △ | △ | △ | 네이버 지역검색과 동급. 콘텐츠 없음 |
> **TourAPI의 필드 목록은 활용신청 후 `detailIntro2`로 실측 확정 필요.** 현재 403이라 응답 스키마를 직접 확인하지 못했다.
### Tier 2 — 사장님 인증 채널 (고유 콘텐츠, 법적으로 가장 안전)
| 소스 | 방식 | 숙박 | 카페 | 음식점 | 비고 |
|---|---|---|---|---|---|
| **사장님 자체 홈페이지** | 사장님이 URL 확정 → `StaticHtmlAdapter` | ◎ (보유율 76%) | △ | ○ | **가장 권장.** 사장님 동의 = 법적 리스크 0. 이미 `LinkChannel.OFFICIAL_SITE` 존재 |
| **인스타그램 Graph API** | 사장님 OAuth (Business/Creator 계정 필요) | △ | ◎ (**68%**) | ○ | 사진 + 캡션. **저작권자가 사장님이라 1-2 미결이 해소된다** |
| **Google Business Profile API** | 사장님 OAuth + Google 접근 승인 필요 | ○ | ○ | ○ | 영업시간·속성·사진 전부. 단 **국내 소상공인 GBP 관리율이 낮아 커버리지 문제**. 신규 프로젝트는 쿼터 0에서 시작, 별도 심사 필요 |
| **사장님 직접 입력** | 관리자 화면 | ◎ | ◎ | ◎ | 커버리지 100%. 이탈률이 유일한 비용 |
### Tier 3 — 기술적으로 가능하나 권장하지 않음
| 소스 | 상태 |
|---|---|
| 식신 (`siksinhot.com`) | HTTP 200, SSR, `LocalBusiness` JSON-LD 제공(상호·전화·주소). 콘텐츠 깊이는 네이버보다 얕고 업소 매칭이 어렵다 |
| 다이닝코드 | HTTP 200이나 **SPA 셸(23KB)** — JS 렌더링 필요. `rid` 매칭 문제 |
### Tier 4 — 배제
야놀자 / 여기어때 (Cloudflare 403 + 민사 10억 선례) · 카카오맵 (406, AI봇 전면차단) · 캐치테이블 (SPA + AI봇 전면차단) · Google Places API (캐싱 금지) · 네이버 블로그·카페 후기 (이미 정책상 배제, robots 전면차단)
---
## 7. GEO 관점에서 소스를 다시 줄 세우면
Princeton·Georgia Tech·IIT Delhi 공동연구(KDD 2024)가 확인한 **AI 인용률을 최대 40% 끌어올리는 요인**은 키워드가 아니라 이 셋이다:
1. **통계·수치 추가 (Statistics Addition)**
2. **출처 인용 (Cite Sources)**
3. **인용문 추가 (Quotation Addition)**
그리고 ChatGPT·Gemini·Copilot이 인용한 소스의 **10% 미만만 해당 질의 구글 상위 10위에 든다.** 랭킹 신호와 인용 신호는 다른 게임이다.
이 기준으로 소스를 평가하면 순위가 뒤집힌다:
| 소스 | 수치 밀도 | **출처로 명시 가능?** | GEO 가치 |
|---|---|---|---|
| **LOCALDATA 인허가** | 높음 (객실 수, 시설면적, 인허가일) | ✅ "○○시 인허가 정보 기준" | **최상** |
| **TourAPI** | 높음 (체크인 시각, 객실 수, 좌석 수) | ✅ "한국관광공사 TourAPI 기준" | **최상** |
| 사장님 확인 정보 | 중간 | ✅ "사업주 확인, 2026-08-28" | 높음 |
| **네이버 플레이스 크롤링** | 높음 | ❌ **밝힐 수 없다** (robots 위반 + 재게시 근거 없음) | **낮음** |
**핵심 역설: 지금 유일한 소스인 네이버 플레이스가 GEO 관점에서 가장 가치가 낮다.**
출처를 명시할 수 없는 사실은 AI가 신뢰하지 않고, 우리 `AnswerBlock`·`llms.txt`의 "출처와 검증 시각" 신호도 채우지 못한다. 반면 공공데이터는 그 자체가 **인용 가능한 1차 출처**라 GEO 신호를 직접 만든다.
여기에 하나 더 — **복제 콘텐츠는 GEO에서 마이너스다.** 네이버 플레이스와 같은 문장이 실린 페이지는 `PUBLISH_NO_UNIQUE_CONTENT` 게이트가 이미 경계하는 그 상태다. AI가 굳이 인용할 이유가 없다.
---
## 8. 권고 로드맵
| 순위 | 작업 | 근거 | 비용 |
|---|---|---|---|
| **1** | **TourAPI KorService2 활용신청** 후 `tour_api.py` 확장 (`searchStay2`, `detailIntro2`) | 숙박·음식점 fact를 출처 명시 가능한 형태로 확보. 무료, 신청 즉시 승인 | 1일 |
| **2** | **`StaticHtmlAdapter` 등록** (사장님 확정 URL 전용) | 숙박 76%·음식점 32%가 자체 도메인 보유. 사장님 동의 기반이라 법적 리스크 0. `DECISIONS.md` 1-1이 "결론 후 등록"으로 남겨둔 바로 그 어댑터 | 3일 |
| **3** | **LOCALDATA 연동** | 인허가 기반 영업상태·객실수·시설면적. `PLACE_NOT_VERIFIED` 검증 근거로도 사용 가능 | 2일 |
| **4** | **인스타그램 Graph API (사장님 OAuth)** | 카페 68%의 유일한 콘텐츠·사진 소스. **1-2(이미지 재게시 권리) 미결을 구조적으로 해소** | 5일 |
| **5** | 네이버 플레이스 어댑터 **역할 축소** — 사장님 화면의 "대조용 참고값"으로만 사용하고 발행 payload에서 제외 | robots 위반 해소 + 복제 콘텐츠 제거. `COLLECT_ADAPTERS` 환경변수로 코드 수정 없이 즉시 가능 | 0.5일 |
| 6 | 네이버 블로그 채널 (§11 미구현분) | 네이버 AI 브리핑 인용의 58%가 블로그. 글로벌 AI와 별개 전장 | 별도 검토 |
`registry.py`가 이미 `COLLECT_ADAPTERS` 환경변수로 채널별 on/off를 지원하므로, 5번은 **배포 없이 즉시 실행 가능**하다.
### 업종 출시 순서 제안
소스 확보 난이도와 AI 검색 공백 크기가 같은 방향을 가리킨다.
1. **숙박(펜션·게스트하우스·독채)** — TourAPI 32 + 자체 홈페이지(보유율 76%)로 fact가 가장 잘 차고, 경쟁 소스가 전부 차단 상태다. **1·2번 작업만으로 출시 가능.**
2. **음식점** — LOCALDATA + TourAPI 39 + 위생등급으로 공공 fact를 채우고 나머지는 사장님 입력. 3번 작업 필요.
3. **카페** — 인스타 OAuth(4번)가 선행되지 않으면 사장님 입력 100% 의존이라 이탈률이 높다. 가장 마지막.
---
## 8-1. 구현됨 (2026-08-28) — 사이트별 파서 대신 LLM 추출
조사 도중 방향을 바꿨다. **사장님 홈페이지마다 파서를 짜는 것은 끝이 없다** — 실측으로 확인됐다:
| 사이트 | 결과 |
|---|---|
| `mulhoe.co.kr` | 본문 2,841자 + JSON-LD 1블록 — **쓸 만함** |
| `gangmunstay.kr` | 8.5KB HTML인데 **본문 9자** — SPA, JS 렌더링 필요 |
| `cheomdangukbap.com` | 834바이트, **본문 0자** — SPA |
| `offinghouse.com` | SSL 자체서명 인증서 오류 |
4곳 중 1곳만 정적 HTML로 내용이 나온다. 그래서 **입력이 무엇이든(URL 크롤링 본문 / 사장님 붙여넣기 텍스트) 원문 문자열 하나로 모아 Gemini가 업종 스키마로 추출**하는 구조로 만들었다.
```text
URL → StaticHtmlAdapter (robots 준수) ─┐
붙여넣기 ────────────────────────────────────┴→ 원문 텍스트 → Gemini 구조화 추출
→ evidence 원문 대조 → fact 후보(UNVERIFIED)
```
**핵심 안전장치는 프롬프트가 아니라 코드다.** 모델이 적어 낸 근거 문장이 원문에 실제로 있는지 대조하고, 없으면 버린다. 실측 검증:
| 공격 케이스 | 결과 |
|---|---|
| 원문에 없는 문장을 근거로 지어냄 | 🚫 차단 |
| 근거는 "반려동물 불가"인데 `pet_allowed=true` | 🚫 차단 (부호 뒤집기) |
| 원문 "오후 3시"를 "15:00"으로 환산 | 🚫 차단 (표기 변경) |
| 업종 스키마에 없는 key | 🚫 차단 |
| unit 스코프인데 `unit_name` 없음 | 🚫 차단 |
실제 결과 — 파서 한 줄 없이:
- `mulhoe.co.kr` (음식점): 영업시간·주차·포장·예약채널 + 메뉴 5종의 이름/가격/설명 = **fact 19건**
- 붙여넣기 238자 (펜션): 체크인/아웃·주차·와이파이·반려동물(false)·바비큐요금 + 객실 2종의 인원/침대/주중·주말요금 = **fact 15건**
| 파일 | 역할 |
|---|---|
| `solution/backend/services/collector/static_html_adapter.py` | robots.txt 준수 fetch + JSON-LD/OG 추출. `_DENY_HOSTS` 로 플랫폼 차단 |
| `solution/backend/services/prompts/extract.py` | 추출 프롬프트·응답 스키마 (업종 스키마에서 자동 생성) |
| `solution/backend/services/grounding/extract.py` | **evidence 원문 대조 게이트** |
| `solution/backend/services/external/gemini_extract.py` | 엮는 자리 |
| `solution/backend/tests/test_extract_grounding.py` | 게이트 회귀 테스트 17건 |
비용: 호출당 약 $0.006 (gemini-3.7-flash, temperature 0.0).
**남은 작업:** `collect_service` 에서 이 추출 경로를 호출하도록 배선 + 관리자 화면에 "텍스트 붙여넣기" 입력창. 컨테이너는 소스 마운트가 없으므로 `docker compose up -d --build` 로 이미지를 다시 빌드해야 반영된다.
---
## 8-2. 지금 실제로 가져올 수 있는 정보 (2026-08-31 실측)
TourAPI 활용신청이 승인돼 어댑터를 붙였다. 아래는 **표본 8곳씩 실제로 돌려서 센 것**이지 문서 기준 추정이 아니다.
### 숙박 — 객실 단위까지 채워진다
| 필드 | TourAPI 충전율 | 비고 |
|---|---|---|
| ★ 체크인 / 체크아웃 | **8/8곳** | critical |
| 숙소 소개 | 8/8곳 | |
| 바비큐 이용 | 8/8곳 | |
| 주차 | 6/8곳 | |
| ★ 취사 가능 | 4/8곳 | critical |
| ★ 취소·환불 규정 | 3/8곳 | critical |
| 픽업 서비스 | 3/8곳 | |
| **객실 단위** — 타입·★기준인원·★최대인원·★주중요금·★주말요금·에어컨 | **객실 20개분** | 표본 8곳에서 객실 20개 |
| 객실 면적(㎡) | 객실 17개분 | |
| ★ 성수기 요금 | 객실 14개분 | |
| ★ 주방 여부 | 객실 13개분 | |
| 사진 | 4/8곳 | 객실별로 붙는다 |
**TourAPI가 못 채우는 13개:** `pet_allowed` `smoking` `extra_person_fee` `reception_hours` `parking_capacity` `wifi` `bbq_fee` `breakfast` `baby_amenities` `bed_type` `bathroom_count` `view` `room_intro` → 자체 홈페이지 추출 또는 사장님 입력.
### 음식점 · 카페 — 영업정보와 메뉴 이름까지
| 필드 | TourAPI 충전율 |
|---|---|
| 가게 소개 · ★영업시간 · ★휴무일 · 대표메뉴 · ★주차 | **8/8곳** |
| 메뉴 이름(단위) | 메뉴 25건 |
| 포장 | 4/8곳 |
| ★ 예약 필수 | 2/8곳 |
| 사진 | **8/8곳** |
**못 채우는 16개:** `break_time` `last_order` `reservation_channel` `pet_allowed` `kids_allowed` `corkage` `room_available` `group_seat_max` `parking_capacity` `delivery` `payment_methods` `price_range` `wheelchair_accessible` `menu_price` `menu_min_order` `menu_intro`
→ **메뉴 가격이 없는 것이 가장 아프다.** 자체 홈페이지 추출(속초항아리물회에서 메뉴 5종 가격 확보)이나 사장님 입력으로 메운다.
### 네이버 플레이스와 비교 (같은 날 실측)
| | 네이버 플레이스 | TourAPI |
|---|---|---|
| 숙박 사업장 fact | **2건** (`intro`, `wifi`) | **8종** (체크인/아웃·취사·주차·픽업·바비큐·환불규정·소개) |
| 숙박 객실 정보 | 객실명(`room_type`)**만** 18개 | 객실 20개의 **인원·요금·면적·주방·에어컨** |
| 음식점 | **수집 실패** (`PlaceDetailBase` 없음) | 영업시간·휴무일·대표메뉴·주차·메뉴명·사진 |
| 출처 표기 | **불가** (robots 위반) | 가능 — 대한민국구석구석 공개 URL |
**네이버 플레이스를 유지할 이유가 사실상 사라졌다.** 정보량도 적고 출처도 못 밝힌다.
### 이미지 — 저작권까지 확정
TourAPI 숙박 사진 167장 표본의 공공누리 유형: **Type3 161장 · Type1 6장 → 100% 상업적 이용 가능.**
다만 **Type3가 96%이고 제3유형은 '변경금지'** 다.
> ⚠️ **렌더러 제약:** Type3 사진은 **크롭·리사이즈하면 조건 위반**이다. 썸네일 생성 경로가 이 사진들을 건드리면 안 된다.
> `CollectedMedia.license` 에 유형을 담아 내보내므로 발행 단계에서 판정할 수 있다.
> 상업적 이용이 막힌 Type2·Type4와 유형 미상은 **수집 단계에서 버린다**.
---
## 8-3. API별 수집 카탈로그 — 어디서 무엇을 가져오는가
코드에서 뽑은 것이다(2026-08-31). `→` 왼쪽이 API 원본 필드, 오른쪽이 우리 fact key.
### A. 한국관광공사 TourAPI · `apis.data.go.kr/B551011/KorService2`
무료 · 이용허락범위 제한없음 · 개발계정 1,000건/일. **fact 의 주력 공급원.**
| 오퍼레이션 | 원본 → fact key |
|---|---|
| `detailCommon2` | `overview` → **intro** · `firstimage`+`cpyrhtDivCd` → 대표사진<br>*참고용으로만 읽음:* `title` `addr1` `mapx/mapy` `homepage` `cat1/2/3` `modifiedtime` |
| `detailIntro2` **숙박(32)** | `checkintime` → **check_in_time** · `checkouttime` → **check_out_time**<br>`refundregulation` → **cancel_policy** · `chkcooking` → **cooking_allowed**<br>`parkinglodging` → **parking** · `pickup` → **pickup_service** · `barbecue` → **bbq_available** |
| `detailIntro2` **음식점·카페(39)** | `opentimefood` → **business_hours** · `restdatefood` → **closed_days**<br>`firstmenu` → **signature_menu** · `treatmenu` → **menu_name**(단위, `/`로 분리)<br>`parkingfood` → **parking** · `packing` → **takeout** · `reservationfood` → **reservation_required** |
| `detailInfo2` **숙박(32)** = 객실 행 | `roomtitle` → **room_type**(+ unit_name) · `roombasecount` → **standard_capacity**<br>`roommaxcount` → **max_capacity** · `roomsize2` → **room_size**(㎡)<br>`roomoffseasonminfee1/2` → **weekday_price / weekend_price**<br>`roompeakseasonminfee1` → **peak_price** · `roomcook` → **has_kitchen** · `roomaircondition` → **has_aircon**<br>`roomimg1~5` + `cpyrhtDivCd1~5` → 객실 사진 |
| `detailImage2` | `originimgurl` `imgname` `cpyrhtDivCd` → 추가 사진 |
**일부러 안 쓰는 필드** — 스키마에 자리가 없거나 뜻이 다르다:
`roomcount` `scalelodging` `accomcountlodging` `foodplace` `reservationlodging` `infocenter*` `subfacility`
`seminar/sports/sauna/beauty/beverage/karaoke/campfire/bicycle/fitness/publicpc/publicbath`
`kidsfacility`(놀이방 유무 ≠ 아동 입장) · `chkcreditcardfood`(가능/없음 ≠ 결제수단 목록) · `smoking` · `lcnsno`
`roomsize1`(평 — ㎡와 섞으면 3배 틀린다) · `roombathfacility/tv/cable/internet/refrigerator/toiletries/sofa/table/hairdryer`
### B. 네이버 지역검색 API · `openapi.naver.com/v1/search/local.json`
**fact 를 만들지 않는다.** 사업장 후보 검색과 채널 URL 발견에만 쓴다.
`title`→상호 · `roadAddress`/`address`→주소 · `mapx`/`mapy`→좌표(1e7 스케일) · `category`→업종명 · `link`→**공식 채널 URL 후보**
`telephone`·`description` → **실측 100% 빈 문자열.** 있는 셈 치면 안 된다.
### C. 카카오 로컬 API · `dapi.kakao.com` *(키 미설정 — 현재 미사용)*
`id`→kakao_place_id · `place_name` · `road_address_name` · `address_name` · `phone` · `x`/`y`→좌표 · `category_name` · `place_url`
행정구역: `code` · `region_1~3depth_name` · `region_type` → **지역 콘텐츠 캐시 키**
### D. 네이버 플레이스 크롤링 · `m.place.naver.com` `__APOLLO_STATE__`
> ⚠️ robots.txt `Disallow: /` 위반 상태. **출처를 밝힐 수 없어 GEO 가치가 없다.** 대조용으로만 남긴다.
`description`/`microReviews` → **intro** · `Naverhotel3HotelData.checkInStr/checkOutStr` → **check_in_time/check_out_time**
`BusinessHour*` → **business_hours · operating_hours · reception_hours**
`conveniences` + `InformationFacilities*` → **parking wifi bbq_available cooking_allowed breakfast pet_allowed pickup_service baby_amenities has_aircon terrace takeout delivery power_outlet wheelchair_accessible**
`Menu:*` → **room_type**(단위) · `PlaceDetailTopPhotoItem*` → 사진
### E. 사장님 자체 홈페이지 · `static_html` 어댑터 *(사장님 확정 URL 한정, robots 준수)*
**① JSON-LD(schema.org) / OpenGraph 직접 매핑**
`description`/`og:description` → **intro** · `openingHours(Specification)` → **business_hours 등**
`checkinTime`/`checkoutTime` → **check_in_time/check_out_time** · `priceRange` → **price_range** · `paymentAccepted` → **payment_methods**
`petsAllowed` → **pet_allowed** · `smokingAllowed` → **smoking**
`hasMenu.hasMenuItem` → **menu_name · menu_price · menu_intro**(단위)
`amenityFeature` → 편의시설 bool 15종 · `image`/`og:image` → 사진
**② Gemini 추출** — ①로 안 잡히면 본문 텍스트에서 **업종 스키마 전 필드**를 뽑는다(근거 원문 대조 통과분만)
### F. 사장님 붙여넣기 텍스트 · `gemini_extract`
원문 문자열 하나 → **업종 스키마 전 필드.** E-②와 같은 엔진, 입력만 다르다.
### G. Perplexity Sonar
**채널 URL 후보만** (`title`, `url`). ★ 응답 본문의 영업시간·가격 같은 정보는 **쓰지 않는다.**
### H·I. Gemini
| | 산출 |
|---|---|
| Vision | `ref` · `label`(업종별 사진 라벨) · `alt_text` · `confidence` |
| Text | `intro` · `meta_description` · FAQ(question/answer/fact_keys) — **확보된 fact 로만** |
### J·K. 지역 콘텐츠 (사업장 fact 아님 — 주변 정보 블록용)
| API | 산출 |
|---|---|
| Open-Meteo `api.open-meteo.com/v1/forecast` | `temperature` `weather_code` `wind_speed` `observed_at` `timezone` |
| 공공데이터포털 축제 `tn_pubr_public_cltur_fstvl_api` | `title` `eventstartdate` `eventenddate` `mapx/mapy` `tel` `homepage` `organizer` `overview` |
---
## 9. `DECISIONS.md` 미결 항목에 대한 답
### 1-1. OTA·플레이스 크롤링의 약관·법적 검토 → **결론 가능**
| 대상 | 판정 | 근거 |
|---|---|---|
| 야놀자·여기어때 | **불가** | Cloudflare 403 실측 + 민사 10억 배상 선례. 기술적으로도 법적으로도 막혔다 |
| 네이버 플레이스 | **robots.txt 기준 불허** | `Disallow: /` 실측. 기능은 되지만 준수 상태가 아니다 |
| 카카오맵 | **불가** | 내부 API 406 차단 |
| 사장님 확정 URL(자체 홈페이지) | **가능** | 사장님 동의 기반. `StaticHtmlAdapter` 등록 권고 |
→ **폴백 3단계(① 공식 API → ② 사장님 직접 붙여넣기 → ③ 최소 정보 생성 + 보완 요청)로 간다**는 기존 설계가 맞았다. 이번 조사는 ①의 실체를 TourAPI·LOCALDATA로 특정한 것이다.
### 1-2. 크롤링 이미지 재게시 권리 → **회피 경로 확보**
권리 관계를 다투는 대신 **입력원을 바꾸면 문제가 사라진다.**
- 인스타그램 Graph API (사장님 OAuth) → 저작권자 = 사장님
- 사장님 직접 업로드 → 동일
- TourAPI 이미지 → 공공누리
`media.source_type`(owner/crawl) 컬럼이 이미 있으므로, 발행 시 `CRAWL` 제외 전환은 준비되어 있다.
---
## 10. 확인하지 못한 것
- **TourAPI `detailIntro2` 실제 응답 필드** — 403이라 미확인. 활용신청 후 실측 필요. §6의 필드 목록은 공개 문서 기준 추정치다.
- **업종별 link 충전율 표본이 25건씩**이라 통계적 신뢰구간이 넓다. 방향성 판단용으로만 사용할 것.
- **GBP 국내 소상공인 실제 관리율** — 정량 자료를 찾지 못했다. 도입 전 표본 조사 필요.
- **TourAPI 음식점(39) 커버리지** — 관광지 인근 위주라 도심 일반 음식점 커버리지가 낮을 것으로 보이나 미측정.