# 작업 보고: task-2998 — InsuRo 메뉴 일원화 · 용어 완결

- 팀: dev5-team (마르둑)
- 레벨: Lv.2 (UI 정보구조)
- base: `origin/main` = `36ac664` (task-2997 PR #244 머지본)
- 워크트리: `/home/jay/workspace/projects/insuro/.worktrees/task-2998-dev5`
- 브랜치: `task/task-2998-dev5`
- 커밋: `05aebe0`(구현) · `6f30091`(봉인테스트) · `129ee9b`(E2E)

---

## S — 상황

task-2997(PR #244)이 머지됐으나 회장님 화면에는 4건 중 2건만 반영됐다.
원인은 ANU 명세의 전제 오류였다. `src/config/routes.ts` 의 `section` 필드는
유일한 소비자인 `AppSidebar` 가 **어디에서도 import 되지 않는 죽은 코드**라
화면 메뉴를 좌우하지 않는다. 실제 메뉴 소스는 `navigationConfig.ts` 다.

본 태스크에서 **실측으로 재확인**했다 (base `36ac664`):

| 항목 | 명세 주장 | 실측 | 일치 |
|---|---|---|---|
| navigationConfig 내 new-design-comparison | 0건 | 0건 | 일치 |
| 약관 업로드 위치 | navigationConfig.ts:57 analytics-tools | 동일 | 일치 |
| "신규 견적" 잔존 | Table 264 · 303 (2곳) | 정확히 2곳 | 일치 |
| routes.ts section 소비자 | AppSidebar 뿐 (죽음) | AppSidebar 선언만 존재, import 0건 | 일치 |

명세와 실측이 어긋난 항목은 **없었다.**

## C — 복잡성

명세 4건을 그대로 적용하면 **명세에 없던 새 결함이 하나 생긴다.**

`getActiveTabFromPath()` (navigationConfig.ts:98)는 `domainTabs` 만 순회한다.
②로 `약관 업로드` 를 `analytics-tools` 에서 빼면 `/tools/policy-terms-upload` 가
어떤 탭에도 매칭되지 않아 **기본값 `"crm"`** 으로 떨어진다(navigationConfig.ts:120).
→ 관리자가 그 페이지에 들어가면 좌측에 **고객관리 사이드바**가 뜬다.

호출처를 전수 조사해 영향 범위를 확정했다: 호출처는 `DashboardLayout.tsx:22` **단 1곳**이며
그 반환값이 상단 탭 하이라이트·좌측 서브메뉴·모바일 하단탭을 모두 결정한다.

## Q — 질문

명세에 없는 이 파생 수정을 해야 하는가, 아니면 명세대로만 하고 보고할 것인가?

## A — 답변 / 조치

**했다.** 근거: (1) 이 결함은 명세가 지시한 변경의 **불가피한 파생**이지 별도 기능이 아니다.
(2) 수정 위치가 `navigationConfig.ts` 로 **허용 경로 안**이다.
(3) 방치하면 회장님 화면에 새 오작동이 생겨 태스크 목적에 반한다.
`adminMenuItems` 를 조회해 `"admin"` 을 반환하도록 **10줄 추가**했고, 기존 분기
(`/mypage`·`/settings`·`/pricing`·`/admin/*`·일반 탭)가 안 깨졌음을 회귀 단언 5건으로 봉인했다.
★ 이 판단은 ANU 확인 대상으로 아래 「ANU 판단 요청」에 올린다.

---

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|------|-----------|-----------|------|
| /home/jay/projects/InsuRo/.worktrees/task-2998-dev5/src/components/navigation/navigationConfig.ts | navigationConfig.ts:41 — 신규설계 비교 항목 추가 (고객 증권분석 다음) | grep "new-design-comparison" 1건 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2998-dev5/src/components/navigation/navigationConfig.ts | navigationConfig.ts:8 — GitCompare import 추가 | grep "GitCompare" 1건 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2998-dev5/src/components/navigation/navigationConfig.ts | navigationConfig.ts:57 — 약관 업로드를 analytics-tools 에서 제거 | analytics-tools 내 0건 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2998-dev5/src/components/navigation/navigationConfig.ts | navigationConfig.ts:153 — 약관 업로드를 adminMenuItems 에 추가 | grep "policy-terms-upload" 1건 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2998-dev5/src/components/navigation/navigationConfig.ts | navigationConfig.ts:104 — getActiveTabFromPath adminMenuItems 귀속 보정 | 테스트 단언 PASS | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2998-dev5/src/components/policy/NewDesignComparisonTable.tsx | NewDesignComparisonTable.tsx:264 — 경고배너 신규 견적 → 신규 설계 | grep "신규 견적" 0건 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2998-dev5/src/components/policy/NewDesignComparisonTable.tsx | NewDesignComparisonTable.tsx:303 — 빈상태 문구 신규 견적 → 신규 설계 | grep "신규 견적" 0건 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2998-dev5/src/config/routes.ts | routes.ts:44 — section 죽은 필드 주석 1줄 (필드 무삭제) | grep "실제 메뉴 =" 1건 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2998-dev5/tests/navigation.menu.task-2998.test.ts | 봉인 회귀 테스트 15건 | vitest 15 passed | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2998-dev5/tests/e2e/task-2998-menu-consolidation.spec.ts | 실브라우저 E2E 4건 | playwright 4 passed | verified |

planned 항목 0건.

★ **본 브랜치가 실제로 바꾼 파일은 위 5개뿐이다** (`git diff --name-only 36ac664..HEAD` = 5건).
QC `file_touch_ratio_check` 는 `HEAD~5` 를 base 로 잡아 17건을 세는데, 그 차이분
(`src/App.tsx`·`ContextSubMenu.tsx`·`supabase/migrations/*` 등)은 **이미 main 에 있던
task-2996/2997 커밋**이지 본 태스크의 변경이 아니다. forbidden_paths 침범은 0건이며
`git diff --name-only 36ac664..HEAD | grep -E '<forbidden>' | wc -l` = 0 으로 실측했다.

---

## 검증 결과

### 기준선 재측정 (명세 수치 아님 — base `36ac664` 실측)

| 게이트 | base 36ac664 | 변경 후 | 판정 |
|---|---|---|---|
| npm run build | EXIT=0 | EXIT=0 | 유지 |
| npx vitest run | 91 files / 1299 tests | 92 files / 1314 tests | +1 file/+15 tests, 기존 0건 파손 |
| npx tsc --noEmit | 에러 0 | 에러 0 | 유지 |
| 기존 E2E 회귀 | — | 2 passed | 유지 |

### 봉인 증명 — 변이 테스트 (Mutation Proof)

"N passed" 재현만으로는 테스트에 이빨이 있는지 알 수 없다. 4개 변이를 각각 독립 수행하고 원복했다.

| 변이 | 내용 | 실제 FAIL 출력 | 원복 |
|---|---|---|---|
| M1 | crm 탭에서 신규설계 비교 삭제 | 3건 FAIL — expected undefined to be defined / expected -1 to be 4 | 완료 |
| M2 | 순서만 변경(맨 끝으로 이동, 존재는 유지) | 순서 단언만 FAIL — expected 6 to be 4 (나머지 14건 PASS) | 완료 |
| M3 | 약관 업로드를 analytics-tools 로 원복 | 4건 FAIL — expected +0 to be 1 / expected 'analytics-tools' to be 'admin' | 완료 |
| M4 | 배너 1곳만 신규 견적으로 되돌림 | 2건 FAIL — expected 1 to be +0 | 완료 |

★ M2 가 핵심이다. **존재**가 아니라 **순서**만 깨뜨렸는데 정확히 순서 단언 1건만 FAIL 하고
나머지 14건은 PASS 했다 — 순서 봉인이 존재 봉인과 독립적으로 이빨을 가진다는 뜻이다.
원복 후 제품 소스를 원본 백업과 바이트 단위 대조해 동일함을 확인했고, 최종 `git diff --stat` 은
제품 소스에 대해 출력이 없다.

### 배포 산출물(dist) 실측

minified 1-line 번들에서 `grep -c` 는 항상 1을 반환하므로 `grep -o | wc -l` 로 셌다.

| 문자열 | dist/assets/*.js | 기대 |
|---|---|---|
| 신규 견적 | 0 | 0 |
| 신규설계 비교 | 3 | 존재 |
| 신규 설계 단독 | 3 | 존재 |
| /tools/policy-terms-upload | 2 | URL 무변경 |

---

## L1 스모크테스트

- **서버 재시작**: 해당없음 (프론트엔드 전용 변경. 서버 코드 무변경 — `server/**` 는 forbidden path 로 일절 미접근)
- **API 응답 확인**: 해당없음 (API 계약 무변경)
- **스크린샷**: 아래 11장. **실제 렌더이며 픽스처가 아니다.** 전부 팀장이 직접 이미지를 열어 눈으로 확인했고, `/login` 리다이렉트가 아님을 URL 단언과 육안으로 이중 확인했다.

Playwright(vite dev :8080) 실브라우저 4/4 PASS. 스크린샷 디렉토리:
`/home/jay/workspace/projects/insuro/.worktrees/task-2998-dev5/tests/e2e/screenshots/task-2998/`

| 파일 | 팀장이 육안 확인한 내용 |
|---|---|
| 1a-admin-crm-sidebar.png | 고객관리 서브메뉴에 대시보드/고객목록/파이프라인/고객 증권분석/**신규설계 비교**/메신저/디지털명함 — 증권분석 바로 다음 위치 확인 |
| 1a-admin-after-click-new-design-comparison.png | 클릭 후 /new-design-comparison 이동 |
| 1b-admin-analytics-tools-no-policy-upload.png | fullPage 캡처로 분석&도구 12개 항목 **전체** 노출, **약관 업로드 없음** (스크롤 누락 아님) |
| 1c-admin-menu-has-policy-upload.png | 관리자 서브메뉴 13번째 맨 아래 **약관 업로드** 존재 |
| 2a-nonadmin-policy-terms-upload-no-admin-submenu.png | 비관리자: 관리자 aside 자체가 DOM 에 없음(count=0) |
| 2b/2c-nonadmin-*.png | 비관리자 고객관리/분석&도구에도 약관 업로드 부재 |
| 3a-mobile-bottom-nav.png | 390x844 하단 탭 4개 정상, 데스크탑 aside 없음 |
| 3b-mobile-crm-hub-new-design-comparison.png | 모바일 고객관리 허브 카드에 **신규설계 비교** 노출 |
| 3c-mobile-after-hub-click-*.png | ★ 로딩 스켈레톤 상태로 캡처됨 — 시각 확인 불완전(URL 단언은 PASS) |
| 4-existing-unlinked-banner.png | 경고배너 실렌더. "**신규 설계** 단독 목록이므로…" 확인, "신규 견적" 없음 |

★ 2b 와 1a 는 md5 가 동일하다(`64a168ca…`). 이는 위조가 아니라 **고객관리 사이드바는 관리자/비관리자가 동일**하기 때문이며, 예상된 결과다.

---

## ANU 판단 요청 (임의 판단하지 않고 보고)

### 1. 파생 수정 `getActiveTabFromPath` 보정 (사후 승인 요청)
명세에 없던 10줄을 추가했다. 사유는 위 「A — 답변」 참조. 허용 경로 안이며 회귀 5건으로 봉인했다.
불승인 시 해당 hunk 만 되돌리면 되고, 그 경우 `/tools/policy-terms-upload` 에서 고객관리
사이드바가 뜨는 상태를 수용하는 것이 된다.

### 2. "견적" 이 실제로 견적을 뜻하는 잔존 3곳 — 고치지 않고 보고 (명세 지시대로)
명세가 "실제로 견적을 뜻하는 곳이 있으면 두고 보고하라. 기계적 치환 금지" 라고 했다. 해당 사례:

| 위치 | 문구 | 판단 |
|---|---|---|
| NewDesignComparisonTable.tsx:142 | PREMIUM_MISSING: "견적 보험료 없음" | 보험료 견적이 맞음 → 유지 (허용 경로지만 의도적 미변경) |
| server/new_design_comparison/engine.py:198 | "신규 보험료는 견적 기준금액 대비 선형 환산…" | 견적 기준금액이 맞음 + forbidden path → 미변경 |
| server/new_design_comparison/engine.py:137,254 | "**신규 견적** 보험료가 없어 비교할 수 없습니다" | 아래 ★ 참조 |

★ **정직한 유보 — "화면 전체 신규 견적 0건"은 정상 경로에 한정된다.**
`engine.py:254` 가 던지는 `NO_PREMIUM_DATA` 문구는 `NewDesignComparison.tsx:121-125` 가
`detail.detail` 로 꺼내 `setError` → 212행 `AlertDescription` 에 렌더한다. 즉 **전 담보에
보험료가 비어있는 에러 경로에서는 "신규 견적" 문자열이 화면에 노출될 수 있다.**
- 이 문구는 실제로 **보험료 견적**을 뜻하므로 용어 오류가 아니다.
- `server/**` 는 forbidden path 이고 `NewDesignComparison.tsx` 도 수정 금지 대상이다.
→ 고치지 않고 보고한다. 정상 경로(배너 포함)에서 "신규 견적" 0건은 실브라우저로 실증했다.

### 3. `/tools/policy-terms-upload` 라우트 가드 부재 (범위 밖 — 보고만)
메뉴에서는 감췄으나 **라우트 자체에 `adminOnly` 가 없어** URL 을 직접 아는 비관리자는
페이지 본문에 접근 가능함을 E2E 중 관찰했다. 명세가 "라우트 `adminOnly` 변경은 범위 밖,
필요하면 보고하고 멈춰라" 라고 했으므로 **건드리지 않았다.** 별도 태스크 판단 요청.
(참고: task-2997 에서 봇이 같은 판단으로 보류한 것과 동일한 사안)

---

## trip-wire 5종 실측

| 항목 | 실측 | 판정 |
|---|---|---|
| Critical7 | 0 | PASS |
| PII net-new | 0 (UI 문자열·메뉴 설정만 변경) | PASS |
| 회귀실패 | 0 (기존 1299건 전량 유지) | PASS |
| forbidden_paths 침범 | 0 (App.tsx·ContextSubMenu.tsx·NewDesignComparison.tsx·PolicyAnalysis.tsx·server/**·supabase/** 무변경) | PASS |
| nonce=task_id | task-2998 일치 | PASS |

---

## 모델 사용 기록

| 팀원 | 역할 | 모델 | 비고 |
|---|---|---|---|
| (Explore) | 구조 선행 조사 | 기본 | 호출처·소비자 전수 |
| 이쉬타르 | 프론트엔드 구현 | sonnet | 코딩 |
| 닌기르수 | 테스트/QA | sonnet | 봉인테스트+변이실증 |
| 아르고스 | E2E 검증 | sonnet | 실브라우저 |

팀장(Opus)은 직접 코딩하지 않았다. 설계·분배·독립검증·통합만 수행했다.
haiku 미사용(판단·검증 비중이 커 sonnet 이상 필요).

---

## 3 Step Why

- **1st Why**: 왜 2997 이 화면에 반영되지 않았나? → `routes.ts` 의 `section` 을 고쳤는데 그 필드의 유일한 소비자 `AppSidebar` 가 import 0건인 죽은 코드라서.
- **2nd Why**: 왜 죽은 필드를 고치는 명세가 나왔나? → 명세 작성 시 "라우트 정의가 메뉴 소스"라고 전제했고, 실제 소스(`navigationConfig.ts`)와 대조하는 단계가 없었다. 이번엔 소비자 전수조사를 **구현 전에** 수행해 같은 실수를 막았다.
- **3rd Why**: 왜 다음 사람이 또 헤매지 않나? → ④로 `routes.ts:44` 에 진짜 소스를 가리키는 주석을 남겼고, 순서까지 봉인한 회귀 테스트 15건이 변이 실증으로 이빨을 증명했다.

---

## 비고

- **URL 무변경**: `/new-design-comparison`, `/tools/policy-terms-upload` 리터럴 유지 (테스트로 봉인)
- **`adminOnly` 무변경**: 기존 `tests/routes.section.test.ts` 가 `undefined` 를 계속 단언
- 아이콘은 `GitCompare` 선택 — `routes.ts:333`·`NewDesignComparison.tsx:157` 이 같은 기능에 이미 쓰는 공식 아이콘이라 일관성 목적. navigationConfig 의 기존 import 30개는 전부 사용 중이라 여유분이 없어 1줄 추가함.
- 스크린샷은 저장소 `.gitignore:42` 가 `/tests/e2e/screenshots/` 를 제외하므로 커밋되지 않는다(디스크 증거로 보존).
- **★ PR 생성까지가 범위. 머지는 ANU 가 한다. 직접 머지하지 않았다.**

---

## PR (머지 안 함 — ANU 판단 대기)

| 항목 | 값 |
|---|---|
| PR | #245 |
| state | open · merged=false |
| head sha | 129ee9b0989108bc22c09bc20d7bc190959d4887 |
| base | main |
| changed_files | 5 |

PR `files` API 실측 파일 목록 (5건, 보고서 표와 일치):
`src/components/navigation/navigationConfig.ts` · `src/components/policy/NewDesignComparisonTable.tsx` ·
`src/config/routes.ts` · `tests/e2e/task-2998-menu-consolidation.spec.ts` · `tests/navigation.menu.task-2998.test.ts`

★ **직접 머지하지 않았다.** `FINALIZE_ONLY=1` 로 종결했고 `.finalize-only` 마커가 생성됐다
(`.merge-done` 미생성). 머지는 ANU 가 배포 시점과 함께 판단한다.

### 종결 게이트 실측
- QC: **8 PASS / 0 FAIL** / 11 SKIP / 4 WARN (overall WARN)
- `file_touch_ratio_check`: **PASS · ratio 1.00** (보고서 5건 ↔ 교집합 5건)
- `scope-guard`: **PASS** (SCOPE_BASE=36ac664, `.scope-guard-done` 생성)
- `git_evidence`: PASS (task-2998 커밋 3건, uncommitted 0)
- `l1_smoketest_check`: PASS

### 1차 종결 실패와 원인 (숨기지 않고 기록)
1회차 `finish-task.sh` 는 **EXIT=1** 이었다. 원인은 코드 결함이 아니라 **보고서 경로 표기**였다.
`file_touch_ratio_check` 는 보고서 표의 경로에서 report-root/CODE_ROOT 접두만 strip 한 뒤
`git diff` 결과와 **정확일치 교집합**을 계산한다. 그런데
- 보고서가 심볼릭 경로(`/home/jay/workspace/projects/insuro/...`)를 썼고 실제 CODE_ROOT 는
  realpath(`/home/jay/projects/InsuRo/...`) 라 접두 strip 이 실패했고,
- 파일열에 `:라인번호` 가 붙어 있어 `git diff` 의 순수 파일명과 매칭되지 않았다.

→ 표 파일열을 **realpath 절대경로 + 라인번호 제거**로 고쳐 ratio 0.00 → **1.00** 이 됐다.
게이트를 우회한 것이 아니라 **보고서를 검증 가능한 형태로 정정**한 것이다.

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


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

