# task-3077 — 벤치마킹 SEO 노하우 모드 3분기

담당: dev1 · 레벨: Lv.3 · repo: InsuRo · 2026-08-31 KST
브랜치 `task/task-3077-dev1` · head `0890169e8212bae08ade035a435853cceba1899c` · base `02c493a`
PR: https://github.com/Jeon-Jonghyuk/InsuRo/pull/289 (**OPEN · 머지·배포 미실행**)

**S**: 벤치마킹 글쓰기가 항상 네이버 SEO 최적화를 걸었다. 블로그 지수가 낮은 계정에는 어뷰징 판정 위험이다.
**C**: SEO 를 뺄 때 스킬 프롬프트만 손보면 채널 프롬프트(더 강한 위치)가 살아 SEO 가 그대로 적용된다.
**Q**: 어떻게 하면 SEO 가 **결과물에서** 실제로 사라지고, 환각금지·금소법·분량 목표는 그대로인가?
**A**: 서버가 채널 프롬프트를 모드별로 변환하고 최종 지시에서 재확인한다. 실호출 6/6 으로 확인했다.

## S — 상황

벤치마킹 글쓰기는 1위 글을 근거로 글을 쓰지만, 네이버 SEO 노하우가 **항상** 적용됐다.
회장 지시(verbatim): **"지수작업은 다 제외. 아마추어처럼 보이는게 목적."**
블로그 지수가 낮을 때 SEO 를 과하게 걸면 네이버가 어뷰징으로 판정할 수 있다.
성공 기준은 "잘 쓴 글"이 아니라 **"최적화한 티가 안 나는 글"** 이다.

## C — 복잡성 (이 태스크의 함정)

프롬프트 조립 순서는
`① 스킬프롬프트 → ② 벤치마킹 블록[맨끝 원문 최대 20,000자] → ③ 채널프롬프트 → ④ 타겟팅 → ⑤ 금소법 → ⑥ 프로필 → ⑦ 최종지시`
이고 ①②는 원문 2만 자 **앞에 묻히고** ③~⑦은 뒤에 온다. **뒤가 이긴다.**
→ `naver_seo.txt`/`geo.txt` 만 빼고 ③의 C-Rank/DIA 블록을 두면 SEO 가 그대로 적용된다.
게다가 ③은 **프론트(`src/data/generateOptions.ts`)가 만들어 보낸다** — 서버만 고치면 효과 0(t3014 실증).

## Q — 질문

SEO 를 **결과물 수준에서** 실제로 없애면서, 환각 금지·금소법·원문복제금지·글자수/이미지수 목표를 하나도 잃지 않을 수 있는가?

## A — 답 (구현)

### 3개 모드

| 모드 | 값 | 화면 표기 | SEO | 구조 |
|---|---|---|---|---|
| 1 | `seo` | 상위노출용 | 전부 적용(현행과 **글자 하나 다르지 않다**) | 전부 |
| 2 | `organic_structured` | **지수작업(구조)** ← **기본값** | **전부 제외** | 소제목–본문만 |
| 3 | `organic_prose` | 지수작업(줄글) | **전부 제외** | **소제목 없음** |

### 배선

- 프론트는 **모드 값만** 싣는다(`seoMode`). 벤치마킹 payload 가 있을 때만 싣는다.
- 서버가 받은 `channelPrompt` 에서 SEO 구획을 들어내고 자연스러운 글쓰기 블록으로 갈아 끼운다
  (t3074 `neutralize_channel_image_guide` 와 같은 방식 — 프론트/서버/하네스 3중 복제 회피).
- 최종 지시(⑦)에서 한 번 더 못박되, **이미지 지시가 계속 맨 끝**이도록 그 **앞에** 넣는다(t3070 B-1 보존).
- 모드 효과는 **벤치마킹 근거가 있을 때만** 발동한다(`seo_mode_is_organic`) → 일반 생성 영향 0.

### 제외 대상 (모드 2·3)

`naver_seo.txt` 전체 · `geo.txt` 전체 · 채널 프롬프트 C-Rank/DIA·상위노출 구획 전체 ·
`base.txt`(정답박스·FAQ 5개·중간요약·질문형 소제목·정답→이유→예시·리스트/표 적극활용) ·
`pro_blog.txt`("체크리스트, 비교표, 단계별 가이드") · 본문 키워드 **배치 횟수 강제**.

**분량(글자수)·이미지수 목표는 유지**했다 — 분량은 SEO 노하우가 아니다.

## 검증 — grep 이 아니라 실호출 결과물

sonnet 실호출 **3모드 × 2회 = 6/6 성공**(`naver_blog_prod` 픽스처, `--compliance full`, 히든 기본 모델 sonnet).

| 지표 | ①상위노출 | ②지수(구조) | ③지수(줄글) |
|---|---|---|---|
| 소제목수(■/▶) | 8 / 9 | 5 / 5 | **0 / 0** |
| 질문형 소제목 | 5 / 5 | 0 / 0 | 0 / 0 |
| FAQ 구획 | 1 / 1 | 0 / 0 | 0 / 0 |
| Q&A 쌍 | 5 / 5 | 0 / 0 | 0 / 0 |
| 꿀팁·잠깐 박스 | 2 / 2 | 0 / 0 | 0 / 0 |
| 볼드 개수 | 4 / 3 | 0 / 0 | 0 / 0 |
| 정의문(GEO) | 1 / 0 | 0 / 0 | 0 / 0 |
| 해시태그 | 1 / 1 | 0 / 0 | 0 / 0 |
| 키워드 반복 | 7 / 11 | 3 / 2 | 1 / 3 |
| 본문 자수 | 3115 / 3713 | 2347 / 2453 | 2721 / 2696 |

**전 모드 불가침 — 실생성물 확인**: 금소법 고지문 3/3 · 이미지 프롬프트 구획 **정확히 6개** 3/3 ·
본문 위치표시 6개 3/3 · 금지어("무조건"/"100%") 0건.

★ **정직 고지**: `정답/요약박스`·`표 구분선` 지표는 **모드 1 에서도 0** 이라 이번 표본에서 판별력이 없다.
이 두 지표는 개선 근거로 인용하지 않는다.

원문 보존 위치: `/tmp/t3077_bench/raw/{seo,organic_structured,organic_prose}.{prompt,output}.txt`
(모드 1 = 숫자형 제목 "5가지" + `■ 오늘 결론부터 말씀드립니다` + 볼드 / 모드 3 = 소제목 없는 자연스러운 줄글)

## 봉인 — 변이 14/14 KILLED · SURVIVED 0 · ERROR 0

**대조군 선행 GREEN**(py 149 · harness 114 · ts) 확인 후 실행. git worktree **제자리** 주입.
주입 전 `원문 in 소스` + `count==1` 단언으로 **no-op(거짓 SURVIVED) 배제**. 파이프 없이 returncode 직접 판정.

M1 모드분기 전면무력화 · **M2 채널 프롬프트 변환 무력화(최대 함정)** · M3 SEO 스킬 제외 무력화 ·
M4 줄필터 no-op · M5 기본값 변경 · M6 최종지시 리마인더 제거 · M7 모드3=모드2 ·
**M8 환각가드 제거(불가침)** · M9 키워드 횟수 강제 복귀 · M10 카페 도입문 오탐 회귀 ·
M11 볼드결함 되돌리기 · M12 프론트 기본값 변경 · **M13 payload 에서 seoMode 제거(t3014 유형)** ·
M14 프론트–서버 값 계약 파손 → **전건 KILLED**.

하네스: `/home/jay/workspace/memory/scratch/t3077_mutation.py` · 결과 `/tmp/t3077_mutation_result.json`

## 회귀 — 0건

★ 첫 base 측정은 **무효 처리**했다. 메인 repo(`.env` 있음)와 워크트리(`.env` 없음)는 환경이 달라
skip 이 4→8 로 갈렸다. **동일 워크트리 환경**(`/home/jay/workspace/.worktrees/task-3077-base`)에서 재측정했다.

| | passed | failed | skipped |
|---|---|---|---|
| base `02c493a` | 3676 | 1 | 8 |
| head `0890169` | **3767** | 1 | 8 |

신규 +91 = `test_task3077_seo_mode.py` 91건과 정확히 일치. **실패 집합 동일**
(`TestRateLimiterNegativeControl::test_rate_limit_still_enforced_after_reset_fixture` — 기존 픽스처 결함, 본 PR 무관).
프론트 `tsc --noEmit` **0건** · vitest 신규 10건 통과.
비벤치마킹 일반 생성 프롬프트는 **어떤 모드를 줘도 바이트 동일**(테스트로 봉인).

## 계측 하네스 (t3073 재사용)

- `--seo-mode` 추가. 조립 로직을 **복제하지 않고** `main.py` 함수를 호출한다
  (기존 "재구성 == 실제 조립" assert 가 드리프트를 즉시 잡는다).
- ★ **볼드 미처리 결함 수정**(`memory/events/task-3073.followup-defect.json`).
  `**■ …**` 를 소제목으로 세지 못해 **모드 3 의 "소제목 0개" 판정을 거짓 성공시킬 수 있었다** —
  측정 도구가 제 결론을 위조하는 상태였다. 대조군(평서문 볼드 소제목) 포함 봉인 테스트 추가.
- SEO 흔적 지표 9종 추가. **게이트 아님** — 임계값 없이 수치만 기록한다.

## 수정 파일별 검증 상태

| 절대경로 | 변경 | 검증 |
|---|---|---|
| `/home/jay/projects/InsuRo/server/main.py` | 모드 상수·정규화·채널변환·스킬필터·최종지시·요청모델·호출부 | pytest 91건 신규 + 변이 M1~M10 KILLED |
| `/home/jay/projects/InsuRo/server/tests/test_task3077_seo_mode.py` | 신규 91건 | 전건 통과 |
| `/home/jay/projects/InsuRo/server/scripts/bench_harness/metrics.py` | 볼드결함 수정 + SEO 흔적 9종 | harness 114건 + 변이 M11 KILLED |
| `/home/jay/projects/InsuRo/server/scripts/bench_harness/prompt_build.py` | `seo_mode` 관통 | 동일성 assert 통과(3모드) |
| `/home/jay/projects/InsuRo/server/scripts/bench_harness/run_bench.py` | `--seo-mode` + 지표 기록 | 실호출 6/6 |
| `/home/jay/projects/InsuRo/server/scripts/bench_harness/tests/test_metrics_indicators.py` | 봉인 테스트 +9 | 통과 |
| `/home/jay/projects/InsuRo/src/data/generateOptions.ts` | 모드 정의·기본값 | vitest + 변이 M12/M14 KILLED |
| `/home/jay/projects/InsuRo/src/components/generate/SeoModeSelector.tsx` | 3택 UI 신규 | vitest 6건 |
| `/home/jay/projects/InsuRo/src/components/generate/__tests__/SeoModeSelector.test.tsx` | 신규 | 통과 |
| `/home/jay/projects/InsuRo/src/pages/Generate.tsx` | 상태·payload·UI 마운트 | payload 테스트 + 변이 M13 KILLED |
| `/home/jay/projects/InsuRo/src/pages/__tests__/Generate.seoMode.test.tsx` | 신규 4건 | 통과 |

## 건드리지 않은 것 (지시 준수)

이미지 프롬프트 구획 로직(t3074) · 플랜 게이트(t3076) · 분량 지시 구조 ·
`server/skill_prompts/**` 파일 자체(제외는 **런타임 필터**로만 한다 — 파일을 고치면 모드 1 이 함께 죽는다).

## ANU 판단 요청 (분리 명시)

1. **`pro_blog.txt` 의 "체크리스트, 비교표, 단계별 가이드 형식 활용" 제외** —
   계획서 §4-3 열거에 **없던 줄**이다. 그러나 이 줄을 두면 `naver_seo` 를 빼도 비교표가 그대로 나와
   검증 항목(비교표 유무)과 정면으로 부딪힌다. 회장 기준("SEO 공부했네 인상")으로 제외 판단했다.
2. **`viral_hook.txt` / `cro_copy.txt` 는 손대지 않았다** —
   훅·CTA·사회적 증거는 SEO 최적화가 아니라 설득 기법으로 보아 회장 지시의 사정거리 밖으로 판단했다.
   (다만 `viral_hook.txt` 의 "충격적 통계"·"~% 사람들이 모르는"은 환각 유발 소지가 있다 — **기존 결함**이며 본 태스크 범위 밖.)
3. **`base.txt` 의 "첫 줄에 결론을 쓴다"는 유지**했다. AEO 와 인접하나 일반 글쓰기 원칙이고,
   빼면 "안녕하세요" 인사말로 되돌아갈 위험이 있다.
4. **`[원문]` 블록의 "1위 글 실측 본문 키워드 삽입 횟수"는 유지**했다(실측 사실이고 환각가드 영역).
   지시로는 "맞추지 마라"가 별도로 들어간다.
5. **판별력 없는 지표 2종**(정답/요약박스·표 구분선)은 모드 1 에서도 0 이었다. 표본 4건(모드당 2회)의 한계다.
   더 큰 표본이 필요하면 지시해 달라.
6. **머지·배포는 지시대로 하지 않았다.** ANU 소관이다.

## 미해결 / 한계

- 표본은 모드당 2회(+원문 보존용 1회)다. 통계적 주장이 아니라 **방향 확인**이다.
- "환각이 없다"는 증명하지 않았다 — 환각 **가드가 프롬프트에 살아있음**만 확인했다(전 모드 30 케이스).
- CI 결과는 `CI_WATCH_HANDOFF` 에 별도 기록한다.

## 워크트리 위치 (ANU 재현용)

- PR 브랜치 체크아웃: `/home/jay/projects/InsuRo/.worktrees/task-3077-dev1` (push 경로 요건)
- 변이 하네스 대상 + `node_modules` 링크: `/home/jay/workspace/.worktrees/task-3077-dev1` (detached `0890169`)
- 회귀 기준선: `/home/jay/workspace/.worktrees/task-3077-base` (detached `02c493a`) — **환경 동등 대조용, 지우지 말 것**

## 작업 시간 고지

타이머를 작업 **시작 시점에 걸지 못했다**(스크립트 경로를 늦게 찾았다). 종료 시 함께 기록하면서
`start_time` 을 실제 첫 도구 호출 시각(17:26 KST)으로 정정했다 — 자동 기록된 "0초"는 허위였다.
`task-timers.json` 의 `note` 필드에 같은 내용을 남겼다.

## ANU 콜백

- 봉투: `/home/jay/workspace/memory/events/anu_callback/task-3077-normal-completion.json`
- `schedule_id` **F276731F** · `owner_key` `c119085addb0f8b7`(ANU 독립키, self-key 아님)
- 프롬프트 UTF-8 **3,883 bytes** (상한 3,900)
- 발사 예정 **2026-08-31 18:20:00 KST** (라이브 런타임이 `10s` 형태를 거부하므로 절대시각으로 등록)
- `registration_result_status=REGISTERED` · `callback_delivery_status=DELIVERED`
- ★ `--cron-list` 는 내 키 스코프만 보여주므로 이 스케줄은 목록에 안 나온다(ANU 키 소유). 실재 판정은 `schedule_history/F276731F.log` 로 할 것.
