From f1164b63c57880e8cd22539ace4590e0975f5328 Mon Sep 17 00:00:00 2001 From: joungmin Date: Mon, 15 Jun 2026 20:11:52 +0900 Subject: [PATCH] =?UTF-8?q?docs(design):=20#357=20=EC=A0=95=EC=8B=9D=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20API=20=EC=A0=84=ED=99=98=20=EC=84=A4?= =?UTF-8?q?=EA=B3=84=EC=84=9C=20(Architect)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WebSearchService 신규: Naver Search webkr.json 우선, DDG 폴백 - searchTabling/searchCatchtable 내부 호출만 교체 (시그니처 유지) - application.yml + k8s secrets에 NAVER_CLIENT_ID/SECRET 추가 Refs: #357 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 --- docs/design/357-web-search-api/README.md | 114 +++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 docs/design/357-web-search-api/README.md diff --git a/docs/design/357-web-search-api/README.md b/docs/design/357-web-search-api/README.md new file mode 100644 index 0000000..df36a5e --- /dev/null +++ b/docs/design/357-web-search-api/README.md @@ -0,0 +1,114 @@ +# 설계서: DDG HTML 파싱 → 정식 검색 API 전환 (#357) + +> **상태**: Approved +> **작성**: [AI] Architect · **최종수정**: 2026-06-15 +> **추적성** — Redmine: #357 · 부모: #348(이름 유사도, 09-Done) · 관련: searchTabling/searchCatchtable +> · 구현 파일: `backend-java/src/main/java/com/tasteby/service/WebSearchService.java`(신규), `backend-java/src/main/java/com/tasteby/controller/RestaurantController.java`, `backend-java/src/main/resources/application.yml`, `k8s/secrets.yaml.template` +> · 테스트: 본 범위 밖 (수동 — Tabling/Catchtable 매핑 SSE에서 확인) + +## 1. 목적 (Why) + +`searchDuckDuckGo`는 html.duckduckgo.com을 정규식으로 긁어 Tabling/Catchtable URL을 추출. 봇 차단/HTML 구조 변경 시 대량 `NONE` 마킹 위험. 정식 검색 API로 교체해 안정성/정확도 확보. + +## 2. 범위 (Scope) + +- **포함** + - `WebSearchService` 신규 — Naver Search webkr.json 우선, 미설정/실패 시 DDG 폴백. + - `RestaurantController.searchTabling/searchCatchtable` 내부 호출을 새 서비스로 교체. + - `application.yml` + `k8s/secrets.yaml.template`에 `NAVER_CLIENT_ID/SECRET` 항목 추가. +- **제외 (별도 후속)** + - Kakao Local API 검색 (식당 검색 전용이라 사이트 URL 매칭 부적합). + - Google Custom Search Engine (비용 + 무료 100/day 제약). + - 결과 캐시/메트릭 — 후속. + +## 3. 인수조건 + +- [ ] `NAVER_CLIENT_ID/SECRET` 환경변수 등록 시 Naver Search webkr.json 호출. +- [ ] 미설정 또는 5xx/timeout 시 DDG로 자동 폴백 — 회귀 없음. +- [ ] 응답 형식: 기존 `List>` 유지 — `{title, url}` 키. +- [ ] URL 패턴 필터 동일 동작 (예: `tabling.co.kr/restaurant/`, `tabling.co.kr/place/`). +- [ ] 빌드/배포 회귀 없음. + +## 4. 컨텍스트 & 제약 + +- Naver Search 무료 한도: 일 25,000건. Tabling/Catchtable 매핑 백필이 1,200 식당 × 2회 ≈ 2,400건 1회성 → 한도 내. +- Naver는 `site:` 연산자 미지원 — 결과 URL 패턴 필터링으로 대체. +- `WebClient` 또는 `HttpClient` — 기존 `HttpClient`(static) 재사용. +- 키 미설정 환경(dev local 일부)에서도 DDG 폴백으로 동작 보장. + +## 5. 아키텍처 개요 + +``` +RestaurantController.searchTabling(name) + │ + ▼ +WebSearchService.search(query, urlPatterns) + │ + ├─ NAVER_CLIENT_ID 설정 시 + │ └─ Naver webkr.json → URL 패턴 필터 → 결과 + │ (실패/0건 시 ▼) + └─ DDG html.duckduckgo.com → 정규식 파싱 → URL 패턴 필터 +``` + +## 6. 데이터 모델 + +### 응답 (기존 유지) +```json +[ { "title": "스타벅스 강남대로점", "url": "https://app.catchtable.co.kr/dining/12345" } ] +``` + +### Naver 응답 → 변환 +```json +{ "items": [ { "title": "스타벅스 강남점", "link": "https://..." } ] } +``` +- `title`은 `` 태그 제거 후 사용. +- `link`가 urlPatterns 중 하나에 매칭되면 결과에 포함. + +## 7. 함수 명세 + +| 함수 | 책임 | 비고 | +|---|---|---| +| `WebSearchService.search(query, urlPatterns)` | 외부 진입점 | Naver 우선 → DDG 폴백 | +| `WebSearchService.searchNaver(...)` | Naver Search webkr.json 호출 + 필터 | 키 미설정 시 즉시 빈 결과 | +| `WebSearchService.searchDdg(...)` | 기존 DDG 로직 이관 | RestaurantController에서 옮김 | +| `WebSearchService.stripTags(s)` | `...` 제거 | | +| `WebSearchService.matchesPattern(url, patterns)` | URL 패턴 매칭 | `urlPatterns.length == 0` 면 모두 통과 | + +## 8. 흐름 + +### 정상 +1. `searchTabling(name)` → `webSearchService.search("스타벅스", ["tabling.co.kr/restaurant/", "tabling.co.kr/place/"])`. +2. Naver 호출 → 200, items 30 → URL 패턴 매칭 0~N건 추출. +3. 0건이면 DDG 폴백, 그래도 0건이면 빈 리스트. + +### 폴백 +- Naver 5xx / IOException / 키 미설정 → DDG. + +### 키 등록 (운영자 작업) +1. https://developers.naver.com/apps/#/list 검색 앱 등록. +2. `NAVER_CLIENT_ID`, `NAVER_CLIENT_SECRET` 발급. +3. dev: `.env`에 추가, prod: `k8s/secrets.yaml`에 추가 + `kubectl apply`. + +## 9. 엣지케이스 + +- **키 미설정**: searchNaver 즉시 빈 리스트 → DDG 폴백 (회귀 없음). +- **Naver rate limit (429)**: DDG 폴백. +- **DDG도 막힘**: 빈 리스트 반환 → 호출자(매핑 SSE)에서 `NONE` 처리 — 기존 동작 동일. +- **URL 패턴 빈 배열**: 패턴 매칭 스킵, 모든 결과 반환 (API 일반화 대비). + +## 10. 테스트 (수동) + +- Dev: `NAVER_CLIENT_ID/SECRET` 등록 → 어드민 `bulkTabling` SSE 실행 → 매칭율 비교 (이전 DDG 대비 ≥). +- 키 일부 제거 → DDG 폴백 동작 확인. + +## 11. 리스크 & 대안 + +- **선택**: Naver primary + DDG fallback. 한국 식당 정확도 + 무료 한도 + 폴백 안정성. +- **대안 A**: Kakao Local Search — 식당 정보 직접 검색은 가능하지만 Tabling/Catchtable URL 매핑 부적합. +- **대안 B**: Google CSE — 비용/한도 제약. +- **트레이드오프**: Naver 응답 정확도가 압도적이지만 키 발급 운영자 작업 필요. 폴백으로 회귀 0 보장. + +## 12. 미해결 질문 + +- Naver의 인덱스 신선도 vs Tabling/Catchtable 최신 입점 식당 미반영 가능성 — 백필 주기 후속. +- 결과 캐시(같은 식당 재호출) — 후속.