# 작업 보고: task-3061

- 팀: dev3-team (다그다)
- 저장소: `Jeon-Jonghyuk/InsuRo` · base `b19b324` · 브랜치 `task/task-3061-dev3`
- **PR #274 (draft)** — https://github.com/Jeon-Jonghyuk/InsuRo/pull/274
- 작업 내용: 상위노출 분석 수치 오류 2건 수정 — 본문 텍스트에 제목이 혼입되던 결함, 이미지 배지 없음을 파싱 실패로 오판하던 결함

## S — 상황
회장 제보 2건: ①"본문키워드 카운팅이 제목꺼까지 합해서 되는 것 같다" ②"노란색 'blog 이미지 배지 미검출' 메시지가 왜 나오는가". 추가로 회장이 이미지 규칙을 직접 알려줬다(배지없음=1장, 썸네일없음=0장, 오류 없어야 함). 두 건 모두 ANU 실측으로 사실 확정된 상태에서 착수했다.

## C — 문제
- ① `analyzer.py:201 _extract_blog_text` 가 `soup.select("p.se-text-paragraph span")` 로 **문서 전체**를 훑었다. 네이버 SE3 는 제목도 `p.se-text-paragraph` 로 감싸므로 제목 문단이 본문에 섞였다. `body_keyword_count` 와 `char_count` 가 함께 부풀려졌다.
- ② `collector.py _parse_image_badge` 가 "배지 없음"과 "파싱 실패"를 구분 불가로 단정하고 `None` 을 반환 → `badge_missing` 집계 → 노란 경고가 상시 노출됐다.

## Q — 핵심 판단
**"기존 스코프 상수를 쓰라"는 명세를 순진하게 따르면 상태가 더 나빠진다.** `_BODY_SCOPE_SELECTORS` 는 `div.se-main-container` 와 `div.se-viewer` 를 **둘 다** 매칭하는데 전자는 후자의 **자손**이다. 콤마로 묶어 한 번에 select 한 뒤 매칭 루트를 전부 순회하면 본문이 **거의 2배로 이중 계상**된다(실측 3643자 → **7249자**). 따라서 **셀렉터 우선순위 첫 매칭 하나만** 사용하도록 구현했다.

## A — 조치
1. `_extract_blog_text` 를 `_BODY_SCOPE_SELECTORS` **우선순위 첫 매칭** 방식으로 재작성(새 상수 신설 없음)
2. `_resolve_blog_image_count()` 신설 — 배지숫자→그 값 / 배지없음+`RectangleImage`→1 / 썸네일없음(sds 마크업은 존재)→0 / **sds 마크업 전무→None(진짜 실패)**
3. `PostItem.image_count_source` 추가(`badge`/`single_thumbnail`/`no_thumbnail`), analyzer 소비부 반영
4. `badge_missing` 노란 경고 제거 → **진짜 파싱 실패 전용 경고**로 교체(fail-loud 유지)

## 수정 파일별 검증 상태

| 파일 | 변경 | 검증 상태 |
|---|---|---|
| server/naver_benchmark/analyzer.py | 본문 스코프 한정 + image_count_source 소비 | PASS — 실측 2글 전후 수치 일치 |
| server/naver_benchmark/collector.py | 이미지 수 판정 신설 + 경고 교체 | PASS — 라이브 30건 재현, 경고 소멸 확인 |
| server/tests/test_naver_benchmark_task3061.py | 신규 12건 | PASS — 봉인 변이 3종 전부 FAIL 유도 |

## ① 제목 혼입 — 수정 전후 실측 (검색어 "대장암 초기")

| 대상 | char 전 | char 후 | Δ | '대장암' 전 | 후 | Δ |
|---|---|---|---|---|---|---|
| blog bajiworld/224388255346 | 3643 | **3605** | −38 | 32 | **30** | −2 |
| blog infinitespace44/224382173209 | 2699 | **2646** | −53 | 29 | **28** | −1 |
| cafe uikl/12808 | 568 | 568 | 0 | 5 | 5 | 0 |

제거된 글자수가 **제목 길이+개행과 정확히 일치**한다(37+1=38 / 52+1=53). 근거 있는 감소다.

> post_0 는 본문 첫 문단이 제목과 거의 같은 문구("…증상까지")를 정상적으로 반복한다. 수정 후에도 유사 문자열이 남는 것은 버그가 아니다 — 제거 대상은 제목 **컴포넌트**이지 제목과 비슷한 본문 문장이 아니다.

## ② 이미지 수 — 수정 전후 (광고제외 상위 10)

| rank | 배지 | Rect | 수정 전 | 수정 후 | source |
|---|---|---|---|---|---|
| 1 | 3 | 2 | 3 | 3 | badge |
| 2~4 | 5/9/6 | 6 | 동일 | 동일 | badge |
| **5** | 없음 | 2 | **None(미검출)** | **1** | **single_thumbnail** |
| 6~10 | 8/5/2/5/3 | 2~6 | 동일 | 동일 | badge |

수정 전 미검출 **1건 → 수정 후 미해결 0건**. 전체 30건 기준 badge 25 / single_thumbnail 5.

## ③ 경고 — 소멸 및 fail-loud 유지

- 정상 케이스(라이브 10건 종단): **errors 0건** — 노란 경고 소멸 확인
- `data-sds-comp` 속성을 전면 제거한 실패 시뮬: **"sds 마크업 미검출 10건/10건" 경고 발생** — 실패 은폐 아님

판별자 근거: 정상 카드는 `data-sds-comp` 요소를 반드시 가진다(실측 분포 RectangleImage 44 / Profile 10 / ProfileThumbnail 10 / DotDivider 10 / IconSingleButton 10). 이 마크업이 **하나도 없으면** 구조 자체를 못 읽은 것이므로 진짜 실패다.

## 오판 방지 확인

| 입력 | 결과 |
|---|---|
| ProfileThumbnail 만 존재 | `(0, no_thumbnail)` — 썸네일로 **오인 안 함** |
| 썸네일없음(sds 존재) | `(0, no_thumbnail)` — 근거 있는 0 |
| sds 마크업 전무 | `(None, None)` — 진짜 실패 |

`RectangleImage` 와 `ProfileThumbnail` 은 별개 식별자임을 실측 분포로 확인했다.

## 테스트 결과

- 회귀: base **2956 passed / 5 skipped / 0 failed** → 수정 후 **2968 / 5 / 0** (`server/` 에서 실행). 델타 +12 = 신규 테스트 수와 정확히 일치, 회귀 0
- 봉인 변이 3종 — 전부 **no-op 아님을 assert 로 선증명한 뒤** FAIL 확인·복원:

| 변이 | no-op 아님 증명 | 결과 |
|---|---|---|
| 본문 스코프 제한 제거 | char 3605→**3643**(결함값 정확 재현) | **3 failed** |
| 배지없음 → None 회귀 | `(1,single_thumbnail)`→`(None,None)` | **3 failed** |
| 썸네일없음 → 1 처리 | `(0,no_thumbnail)`→`(1,no_thumbnail)` | **2 failed** |

복원 후 12 passed · `git diff` 0.

## 명세 전제 정정 3건 (반증)

1. **"배지 없는 7건"** → 실측은 상위 10건 중 **1건**(전체 30건 중 5건). 명세 표는 패턴 B 미반영 수치로 보인다. 경고 자체는 실제로 떴으므로 결함 판정은 유효하다.
2. **"카페도 동시 해결"** → 카페 `contentHtml` 에는 **제목 컴포넌트 자체가 없어** 애초에 혼입이 없었다(568→568 무변화). 이 수정은 카페에 **무회귀**이지 "동시 해결"은 아니다.
3. **SE2/구버전 폴백** → `postViewArea` 등은 **원래부터 컨테이너로 스코프**돼 있어 같은 결함이 없었다. 결함은 SE3 분기의 무스코프 `soup.select()` 하나뿐이다.

## 미해결 / ANU 판단 필요

- ★ **썸네일 없는 글을 라이브에서 찾지 못했다**(30건 전부 썸네일 보유). `no_thumbnail` 경로는 **픽스처 테스트로만** 검증됐다. 실제 사례 확인은 ANU 판단 영역.
- CI: base 에서도 적색인 선재 실패(`ci`=러너 디스크 Errno 28, `e2e-test`=task-2998)와의 교집합 여부는 완주 후 확인 필요.

## 비고

- 네이버 요청 **총 5회**로 제한(블로그 검색 1 + 카페 검색 1 + 블로그 본문 2 + 카페 본문 1). IP 차단 위험 회피를 위해 이후 검증은 전부 저장 HTML 오프라인 재생.
- worktree(`/home/jay/projects/.worktrees/InsuRo-task-3061`)에서 작업 — 메인 저장소 cron(06:00·08:00) 미간섭.
- 변경 파일 3개 전부 `allowed_resources` 내부. forbidden 경로(`src/**`, `server/main.py` 등) 변경 0.
- **PR 생성까지가 범위. 머지·배포는 ANU.** 자동 머지 없음.

## dev3-team 추가 항목
- GLM 코드 품질: 미사용(팀원 위임 경로로 진행)
- 버그 발견/수정: ★ 명세를 그대로 따랐을 때 발생하는 **중첩 스코프 이중 계상**(3643→7249) 을 구현 전 실측으로 사전 발견해 회피. 이 함정이 봉인 테스트(중첩 픽스처)로 고정됐다.

## L1 스모크테스트

실서버를 별도 포트(8099)로 기동해 검증했다(pytest GREEN 이 감추는 import/500 결함 포착 목적).

| 항목 | 결과 |
|---|---|
| 서버 기동 + sha 일치 | HTTP 200, sha=082910a (PR head 와 동일) |
| analyze 엔드포인트 결선 | HTTP 401 (인증 게이트 정상, 500 아님) |
| 수정 코드 런타임 로드 | _extract_blog_text 우선순위 첫매칭 True, _resolve_blog_image_count 존재 |
| import 오류/트레이스백 | 0건 |

```
$ curl -s -w ' -> %{http_code}' http://127.0.0.1:8099/api/status      # curl 결과: 200 OK
{"status":"ok","sha":"082910a7451fe44765da5ad459a8042eb907d2a1","ready":true,...} -> 200 OK

$ curl -s -X POST .../api/v1/naver-benchmark/analyze -d '{"keyword":"대장암 초기"}'
{"detail":"Missing or invalid authorization"} -> 401 (인증 게이트 정상, 500 아님)
```

curl `/api/status` 응답 코드 **200 OK** 실측. 회귀 스위트는 **2968 passed**.

추가로 `_collect_blog` 종단을 저장 HTML 로 재생해 **errors 0건**(노란 경고 소멸)과
source 분포(badge 9 / single_thumbnail 1)를 확인했다.

**결과: 통과 (PASS)** — 서버 기동·라우터 결선·수정 코드 런타임 반영 모두 정상, 500 계열 0건.

## CI 판정

| 체크 | 결과 | 판정 |
|---|---|---|
| ci | failure | 선재 — Errno 28 No space left on device (pip install 단계 러너 디스크). 명세 기재와 일치 |
| e2e-test | failure | 선재 — 3건 전부 phase3/task-2998-menu-consolidation·new-design-comparison (프론트). 명세 기재의 task-2998 과 일치 |
| 그 외 9종 | success | gemini-review-gate·merge-safety-check·qc-check·hidden-path-audit·guard·lock-in-check·diagnostic·ci/guard·cancel-kill-switch |

교집합 0: 본 PR 변경은 server/naver_benchmark/** + server/tests/** 뿐이며, e2e 실패 3건은 프론트 라우트 스펙이고 ci 실패는 우리 테스트 실행 **이전** pip 설치 단계에서 발생했다.

## scope-guard FAIL — 오탐 판정 (자가해소 금지, ANU 판단 필요)

`finish-task.sh` 가 `SCOPE_GUARD_FAIL` 로 종료했다. **범위 위반이 아니라 capabilities 스냅샷 부재**다.

```
[scope-guard] ERROR: snapshot 없음 — dispatch 미수행?
  (/home/jay/workspace/memory/capabilities/task-3061.json)
```

판별 근거 4종:

| 검사 | 결과 |
|---|---|
| capabilities/task-3061.json 존재 | **없음** — 본 건은 dispatch.py 경유가 아닌 스케줄 실행이라 스냅샷이 생성된 적 없음 |
| task md 의 allowed_resources | **존재**(2건) — 명세 결함 아님 |
| 실제 변경 파일 | analyzer.py · collector.py · tests/test_naver_benchmark_task3061.py — **3건 전부 허용 경로** |
| forbidden 경로 침범 | **0건** (src/·extension/·server/main.py·silson/·policy_grouping/·.github/) |

선례와 동일 유형(스냅샷 부재 오탐: t3049·t3054·t3060). **자가해소하지 않고 근거만 박제**한다.
`.escalate` / `failure-envelope.json` 이 생성됐으며, 봉투의 `team=dev4-team`·`bot=vishnu` 는
스케줄 실행 경로의 기본값 오염으로 보인다(실제 수행 팀은 dev3). **ANU 판단 영역.**

QC 자체는 최종 실행에서 **6 PASS / 15 SKIP / 2 WARN / 0 FAIL** 로 통과했다.
