# task-2926 완료 보고 — InfoKeyword 홍보성 판단 로직 개선

## Situation
치매 키워드 블로그 TOP10 홍보성 진단에서 회장 피드백 2건:
- **Top2 "뉴히어로"**: 이미지에 외부 광고링크(`m.site.naver.com/2dtWW` 단축URL → `bohumclick.com` 광고)가 걸렸는데 정보성으로 오판.
- **Top7 "보험디테일"**: 이미지 속 전화번호 미감지 + 걸린 signal 일부만 표시.

## Complication
1. **단축URL 위장광고 미감지**: `_is_naver_internal`이 `.naver.com` 전체를 내부로 제외 → 네이버 단축URL로 위장한 광고링크를 놓침. 회장 기준(네이버 예외=딱 3종)과 불일치.
2. **이중 필터**: 단축URL이 크롤 단계(`blog_content.py`)와 분석 단계(`external_links.py`) **양쪽**에서 걸러져 analyzer까지 도달조차 못함.
3. **이미지 전화번호 미감지**: 스크린샷을 4096px에서 crop → 하단(전화번호 흔한 위치) 통째 손실.
4. **signal 부분 표시**: reasons가 `phone_or_address`로 통합되어 걸린 항목 누락.

## Question
회장 기준(`memory/specs/infor-keyword/requirements-v1.md` §i·§ii·§iii)을 정확히 반영하여, 단축URL 위장광고를 감지하고 이미지 전화번호를 강화하며 걸린 모든 signal을 누락 없이 표시하되 — **정상 정보성 블로그에 과도한 오탐을 만들지 않으려면?**

## Answer (수정 내용)

### 회장 기준 (단일 소스, verbatim)
네이버 링크 예외 = **딱 3종**: ①작성자 본인 블로그(`blog.naver.com/<작성자>`) ②`expert.naver.com` ③공익(`*.go.kr`/`*.or.kr`). 그 외 모든 `*.naver.com`/`*.naver.net`(단축URL·쇼핑·카페·뉴스 등)은 홍보성. 톡톡/플레이스는 홍보성 유지.

### 수정 파일/함수

**1. `worker/analyzer/external_links.py`**
- `_is_naver_internal(domain)`: **변경 전** `.naver.com`/`.naver.net` 전체 내부 → **변경 후** `expert.naver.com` + `*.go.kr`/`*.or.kr`만 예외로 narrowing.
- 신규 `is_naver_shortener(url_or_domain)`: `naver.me`/`m.site.naver.com`/`me2.do`.
- 신규 `async resolve_short_url(url, *, timeout=5.0, max_redirects=5)`: httpx HEAD→GET 폴백, follow_redirects, 리다이렉트 상한 5, 타임아웃 5s. **실패/타임아웃 시 원본 URL 반환(graceful)** — 원본이 단축URL로 남으면 3예외에 안 걸려 external로 처리(보수적 정책, 주석 명시).
- 신규 `async resolve_naver_shorteners(links, ...)`: 리스트 일괄 해소.
- `detect_external_links(links, blog_author_id="")`: 3예외만 제외, **톡톡/플레이스 도메인 제외**(전용 signal, 이중집계 방지), blog_author_id 빈값이면 모든 `blog.naver.com` 예외(하위호환).

**2. `worker/crawler/blog_content.py`**
- `_INTERNAL_DOMAINS` 정규식 폐기 → `_is_internal_link()`이 external_links의 3예외 기준 재사용.
- **본문 스코프 도입**(`_get_body_scope`, `_BODY_SCOPE_SELECTORS`): `_extract_external_links`가 문서 전체가 아닌 **게시글 본문 컨테이너**(SE3 `div.se-main-container` 등) 내부에서만 링크 수집. → PostView 페이지 nav/로그인/RSS/admin/help/security 링크 오탐 제거. 스코프 실패 시 폴백에서 `_NAVER_PLATFORM_CHROME` denylist 방어.
- 단축URL 해소(`resolve_naver_shorteners`) 후 저장 + 반환 dict에 `blog_author_id`(blogId)·`image_link_urls`(이미지 출신 external) 추가.

**3. `worker/pipeline/analyzer.py` (`_analyze_single_blog`)**
- `blog_author_id` 전달, `detect_external_links(..., blog_author_id=...)`.
- **reasons 세분화·누락 없이**: `phone`/`address`/`external_link`/`image_link`/`naver_talktalk`/`naver_place`/`attachment`/`phone_in_image`/`address_in_image`.
- 신규 `signals` 구조화 필드(`{"type","detail"}`) 추가.

**4. `worker/analyzer/image_analysis.py`**
- `_resize_screenshot`: **crop→비율축소/타일 분할**로 교정(하단 손실 0). 매우 긴 페이지는 최대 3장 세로 타일, 각 타일 분석 후 OR 병합.
- 프롬프트 강화(명함·배너·대표번호 1588/휴대폰/지역번호·라벨 옆 숫자·주소), 스크롤로 lazy-load 로딩, JSON 코드펜스 파싱 견고화.

## 검증 결과

### L1 스모크테스트
- **서버 재시작**: worktree 코드를 **임시 포트 8101**로 기동(프로덕션 8100 무중단·무영향). `/health` → **200** `{"status":"ok","service":"inforkeyword-worker"}`. 임시 인스턴스 정상 종료.
- **API 응답 확인**: `curl http://127.0.0.1:8101/health` 200 OK (부팅+서빙 확인).
- **재현 케이스 라이브(실네트워크)**: `resolve_short_url('https://m.site.naver.com/2dtWW')` → `https://dementiac.bohumclick.com/?adins_no=12101`. `get_blog_content('https://blog.naver.com/newhero101/224368161108')` → `external_links=['...bohumclick...']`, `image_link_urls` 동일, `detect_external_links` 결과에 bohumclick 포함 → **REPRO_PASS**.
- **스크린샷**: 해당없음(모듈/API 레벨 검증).

### 재현 케이스 (verification 기준 #1) — PASS
`newhero101/224368161108` 분석 시 `m.site.naver.com/2dtWW`→`bohumclick.com`이 **홍보성 외부링크로 감지**, signal에 external_link+image_link 포함. `is_promotional` 판정 근거 확보.

### 오탐 방지 (verification 기준 #2) — 중대 버그 발견·수정
QA(아르고스)가 발견: narrowing 후 `_extract_external_links`가 문서 전체를 스캔 → PostView 페이지 플랫폼 chrome(로그인/RSS/admin/section/help/security) 12건까지 external로 오판 → **거의 모든 네이버 블로그가 external_link=True 오판 위험**. 본문 스코프 도입으로 수정: newhero101 external_links **13건→1건(bohumclick만)**, chrome 링크 전부 제거 확인.

### 회귀 테스트 (verification 기준 #3) — 회귀 0
- worktree 회귀: **92 passed** (`worker/tests/` + 로컬 `tests/test_external_links.py`·`test_phone_address.py`·`test_llm_promotional_integration.py`).
- 신규 `worker/tests/test_external_links_promo.py` **23 케이스**(단축URL 판별/리다이렉트 mock/narrowing 분류/analyzer signal 전체/오탐방지) 전부 통과.
- 낡은(교정 전 잘못된 동작 단정) 테스트 2건은 회장 기준에 맞게 **정답 교정**(shopping/news.naver.com·static.naver.net → external). `phone_or_address`→`phone` 세분화 반영 1건 교정.
- **사전 존재 무관 실패**(task-2926과 무관, base에서 동일 재현): `tests/test_claude_integration.py` 3건(task-2898 CLI 인자 순서), `_parse_count("<10")` 1건(search_ad.py 사전버그). → 별도 이슈.

## 발견 이슈 및 해결
1. **[해결] 본문 스코프 오탐**: 위 verification #2 참조. `_get_body_scope`로 근본 수정.
2. **[해결] 이중 필터**: 크롤·분석 양쪽 narrowing으로 단축URL이 analyzer까지 도달.
3. **[환경, 정리완료] stray `tests/tests/`**: 팀장의 회귀 준비용 `cp -rn` 아티팩트 → 제거.
4. **[무관, 미해결] pre-existing 4 테스트 실패**: task-2898/search_ad 사전버그. 범위 외 → 별도 task 권장.

## 모델 사용 기록
- 불칸(백엔드, sonnet): Part A(external_links/blog_content) + Part C(analyzer) + 본문스코프 오탐수정 후속.
- 불칸(백엔드, sonnet): Part B(image_analysis).
- 아르고스(테스터, sonnet): 회귀/신규 테스트 + 재현 라이브 검증.
- 팀장(헤르메스, opus): 설계/분배/통합/L1 스모크/버그 판정. haiku 미사용.

## 머지 판단
- **머지 필요**: Yes
- **브랜치**: `task/task-2926-dev1` (base=`origin/master`, InfoKeyword 기본 브랜치는 master)
- **워크트리 경로**: `/home/jay/projects/InfoKeyword/.worktrees/task-2926-dev1`
- **커밋**: `5d4b84a` (6파일: 소스 4 + 신규 테스트 1 + 추적 테스트 교정 1)
- **머지 의견**: 회귀 0, 재현 REPRO_PASS, 오탐버그 수정 완료. ⚠️ **InfoKeyword repo는 과거 PR ruleset 데드락 이력**(task-2898). 머지 판정=ANU 독립검증. PR 머지가 ruleset로 막히면 **로컬 커밋까지 완료 + ANU 보고**(회장 결정 경로), 강제 우회 금지.
- **배포 주의**: worker 재기동(uvicorn worker.main:app :8100, InsuRo·InfoKeyword 양쪽 반영)은 **프로덕션 배포**이므로 회장/ANU 조율 후 무중단 재기동. 이 작업은 배포 미수행(worktree 검증만).

## 완료 보고 필수 항목 체크
- [x] 수정 파일/함수 diff (위)
- [x] `_is_naver_internal` 변경 전후
- [x] 단축URL 추적 구현(타임아웃 5s/리다이렉트 상한 5/graceful)
- [x] 이미지 전화번호 개선(crop→비율축소/타일)
- [x] signal 전체표시 방식(reasons 세분화 + signals)
- [x] 재현 케이스 결과(newhero101 → REPRO_PASS 증거)
- [x] 회귀 테스트 결과(92 passed, 신규 23)
- [x] worker 재기동/실동작 확인(8101 /health 200 + 라이브 재현)
- [x] PR/로컬커밋 상태(커밋 5d4b84a, PR은 finish 단계)

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

