# task-2136 완료 보고서: 3단 하이브리드 키워드 소싱 Phase 1 MVP

## SCQA

**S**: InsuRo의 구글 트렌드 파이프라인이 task-2133에서 구축 완료(고정 30개 + pytrends + DB). 에이전트 미팅 19인 합의로 3단 하이브리드 키워드 소싱 Phase 1이 확정되었다.

**C**: 현재 키워드 소싱이 pytrends 단일 소스에 의존하여 Google 429 rate limit 반복 발생. 네이버 검색광고 API의 검색량 데이터를 활용하지 못하고 있으며, 키워드 정규화/필터링이 없어 중복·법적 리스크 키워드가 혼입될 수 있다.

**Q**: BaseCollector 패턴으로 수집 소스를 확장하고, 네이버 검색량 기반 스코어링 + TOP 20 리스트 뷰를 구현할 수 있는가?

**A**: 8개 모듈 전체 구현 완료. BaseCollector(Template Method) + FixedCollector + NaverCollector 패턴 구축, KeywordNormalizer(동의어 30쌍) + NegativeFilter(50개) 구현, 스코어링 v1 + REST API 3개 + 프론트엔드 키워드 순위 탭 추가. pytest 30/30 PASS, 서버 기동 확인, 3개 신규 API 엔드포인트 정상 등록.

---

## 작업 내용

### 백엔드 (루/Lugh)

#### 모듈 1-3: Collector 패턴
- `server/collectors/base.py` — BaseCollector ABC (collect→normalize→filter_negative→save 템플릿 메서드)
- `server/collectors/fixed.py` — FixedCollector (config/fixed_keywords.txt에서 30개 로드)
- `server/collectors/naver.py` — NaverCollector (HMAC-SHA256 인증, 배치 5개씩, 상위 70개 선별)

#### 모듈 4-5: 정규화 + 필터
- `server/utils/normalizer.py` — KeywordNormalizer (strip→공백→특수문자→동의어 4단계)
- `server/utils/negative_filter.py` — NegativeFilter (50개 법적 리스크/무관 키워드)

#### 모듈 6-7: DB + 스코어링
- `server/migrations/003_keyword_hybrid.sql` — source/status/naver_search_volume/score/last_collected_at 컬럼 추가, pg_trgm 확장
- `server/utils/scoring.py` — 검색량 기반 0~100 정규화 스코어링

#### Config 파일
- `config/fixed_keywords.txt` — 보험 키워드 30개
- `config/synonyms.json` — 동의어 30쌍
- `config/negative_words.json` — 네거티브 50개
- `config/keyword_config.yaml` — 수집 주기/스코어링/신뢰도 설정

#### API 엔드포인트
- `GET /api/insuro/keywords/top?limit=20` — TOP N 키워드 (score DESC, freshness/layer 태그)
- `GET /api/insuro/keywords/search?q=암보험` — 정규화 검색 + 유사 3개 추천
- `GET /api/insuro/keywords/{keyword_id}` — 키워드 상세 (시계열 + 메타)

### 프론트엔드 (브리짓/Brigid)
- `src/pages/KeywordAnalysis.tsx` — "키워드 순위" 탭 추가 (grid-cols-6)
  - TOP 20 카드형 리스트: 순위 | 키워드명 | 검색량 바 | 스코어
  - Layer 컬러태그: L1 파랑, L2 초록
  - 신뢰도 3단계: 초록(24h), 노랑(7일), 빨강(7일+)
  - 미등록 키워드: "트렌드 데이터를 준비 중입니다" + 유사 3개 + "알림 받기" CTA
  - inactive: opacity 50% + "마지막 갱신: YYYY-MM-DD"

### 테스트 (모리건/Morrigan)
- `server/tests/test_normalizer_filter.py` — 14개 테스트
- `server/tests/test_scoring.py` — 8개 테스트
- `server/tests/test_naver_collector.py` — 8개 테스트

---

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| server/collectors/base.py | BaseCollector ABC | grep "class BaseCollector" OK | verified |
| server/collectors/fixed.py | FixedCollector 구현 | grep "class FixedCollector" OK | verified |
| server/collectors/naver.py | NaverCollector 구현 | grep "NaverCollector" OK | verified |
| server/utils/normalizer.py | KeywordNormalizer | grep "class KeywordNormalizer" OK | verified |
| server/utils/negative_filter.py | NegativeFilter | grep "class NegativeFilter" OK | verified |
| server/utils/scoring.py | calculate_score/normalize_scores | grep "calculate_score" OK | verified |
| server/migrations/003_keyword_hybrid.sql | ALTER TABLE 7컬럼 추가 | grep "ADD COLUMN" OK | verified |
| config/fixed_keywords.txt | 키워드 30개 | wc -l = 30 OK | verified |
| config/synonyms.json | 동의어 30쌍 | grep "실비" OK | verified |
| config/negative_words.json | 네거티브 50개 | grep "보험사기" OK | verified |
| config/keyword_config.yaml | 수집/스코어링 설정 | grep "batch_size" OK | verified |
| server/main.py | API 3개 엔드포인트 추가 | grep "keywords/top" OK | verified |
| src/pages/KeywordAnalysis.tsx | 키워드 순위 탭 | grep "keyword-ranking" OK | verified |
| server/tests/test_normalizer_filter.py | 14개 테스트 | pytest PASS | verified |
| server/tests/test_scoring.py | 8개 테스트 | pytest PASS | verified |
| server/tests/test_naver_collector.py | 8개 테스트 | pytest PASS | verified |

---

## L1 스모크테스트 결과

- 서버 재시작: 성공 (uvicorn port 8099 기동 확인)
- API 응답 확인:
  - `GET /api/status` → 200 `{"status":"ok"}`
  - `GET /api/insuro/keywords/top?limit=5` → 401 (인증 필요 — 정상)
  - `GET /api/insuro/keywords/search?q=암보험` → 401 (인증 필요 — 정상)
  - `GET /api/insuro/keywords/{id}` → 401 (인증 필요 — 정상)
  - OpenAPI: 3개 신규 엔드포인트 모두 등록 확인
- 스크린샷: 해당없음 (서버 API 작업, 프론트 빌드는 프로덕션 배포 시 확인)

---

## 발견 이슈 및 해결

### 이슈 1: Codex 게이트 is_active ↔ status 호환 (critical)
- **발견**: Codex 사전 검증에서 기존 `is_active` 필드와 새 `status` 컬럼의 호환 문제 지적
- **해결**: `is_active` 필드 유지 + `status` 컬럼 이중 운영. 마이그레이션에서 기존 데이터 동기화 (`is_active=TRUE → status='active'`). BaseCollector.save()에서 양쪽 동시 갱신.

### 이슈 2: last_collected_at 누락 (high)
- **발견**: Codex가 inactive/freshness 산출에 필요한 `last_collected_at` 필드 누락 지적
- **해결**: ALTER TABLE에 `last_collected_at TIMESTAMPTZ` 컬럼 추가. 수집 시 갱신하도록 BaseCollector.save()에 반영.

### 이슈 3: pyright import 경로 (medium)
- **발견**: worktree에서 `collectors.base`, `utils.normalizer` import가 pyright LSP에서 resolve 실패
- **해결**: `# type: ignore[import-not-found]` 추가. pyrightconfig.json의 extraPaths에 `.`이 이미 포함되어 있어 실제 실행 시 문제없음 (pyright 실행 에러 0건).

---

## 모델 사용 기록

| 팀원 | 역할 | 모델 | 작업 |
|------|------|------|------|
| 루(Lugh) | 백엔드 | sonnet | config+utils+migration, collectors+API |
| 브리짓(Brigid) | 프론트엔드 | sonnet | 키워드 순위 탭 UI |
| 모리건(Morrigan) | 테스터 | sonnet | 단위 테스트 30개 |
| 다그다(팀장) | 설계/검토 | opus | 설계/분배/검토/통합 |

---

## 머지 판단

- **머지 필요**: Yes
- **브랜치**: task/task-2136-dev3
- **워크트리 경로**: /home/jay/projects/InsuRo/.worktrees/task-2136-dev3
- **머지 의견**: 30/30 테스트 PASS, 서버 기동 확인, 3개 API 엔드포인트 정상 등록. is_active 호환 유지로 기존 코드 영향 없음. Gemini PR 리뷰 후 머지 권장.

---

## 3문서 상태

- plan.md: status → completed
- context-notes.md: 3 Step Why + Codex 리스크 대응 기록 완료
- checklist.md: 13/15 항목 체크 (87% — QC/최종보고 제외)

## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회

