# 설계서: 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<Map<String, Object>>` 유지 — `{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": "<b>스타벅스</b> 강남점", "link": "https://..." } ] }
```
- `title`은 `<b>` 태그 제거 후 사용.
- `link`가 urlPatterns 중 하나에 매칭되면 결과에 포함.

## 7. 함수 명세

| 함수 | 책임 | 비고 |
|---|---|---|
| `WebSearchService.search(query, urlPatterns)` | 외부 진입점 | Naver 우선 → DDG 폴백 |
| `WebSearchService.searchNaver(...)` | Naver Search webkr.json 호출 + 필터 | 키 미설정 시 즉시 빈 결과 |
| `WebSearchService.searchDdg(...)` | 기존 DDG 로직 이관 | RestaurantController에서 옮김 |
| `WebSearchService.stripTags(s)` | `<b>...</b>` 제거 | |
| `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 최신 입점 식당 미반영 가능성 — 백필 주기 후속.
- 결과 캐시(같은 식당 재호출) — 후속.
