# task-2915 완료 보고 — 소식지 검토 상세 UI 레이아웃 B + 플래그별 개별 확인(ack) 게이트

- **작업 ID**: task-2915 (Lv.3, 프론트엔드)
- **팀**: dev2-team (오딘) / 구현: 프레이야(Sonnet)
- **프로젝트**: InsuRo (`/home/jay/projects/InsuRo`)
- **브랜치**: `task/task-2915-dev2` (base=origin/main `42df0bd`)
- **일시**: 2026-08-06

---

## SCQA 요약

**S**: 소식지 "검토 상세" 모달은 좌측 제시증거 영역에 스크롤 래퍼가 없어(`min-h-0` 부재) HIGH 플래그가 4건 중 일부만 보이고 잘렸으며, 승인 게이트가 전역 2체크박스뿐이어서 "1개 보고 N개 서명" 안티패턴이었다.

**C**: 잘린 HIGH 플래그는 미검토 발행(금소법 §44/§45 입증책임) 위험이고, 전역 2체크박스는 서버 우회 및 형식적 확인을 유발했다.

**Q**: 회장 선택 레이아웃 B로 스크롤 골격을 재구성하고, HIGH 플래그별 개별 확인(ack)을 강제하면서 서버 fail-closed 재검증과 결정론 flag_key 계약을 프론트가 정확히 지킬 수 있는가?

**A**: 3개 파일(reviewApi/ValidationFlags/ReviewDetailPanel)을 surgical 재구성 — 모든 스크롤 노드 `min-h-0` + bounded(≤26vh) 독립 스크롤로 잘림을 원천 제거, HIGH당 개별 attestation 체크(전역 2체크박스 폐기), 미확인 HIGH = **전체 HIGH 총수**(접힌 플래그 포함) 기준, 승인 API에 백엔드와 100% 동일한 결정론 `flag_key`로 `acknowledgements` 전송. vitest 30/30 PASS, tsc(build) 0, build PASS, 실렌더 스크린샷 3종 육안 확인.

---

## 수정 파일 (allowed_resources 범위 내 5파일)

| 파일 | 변경 요지 |
|---|---|
| `src/components/newsletter/reviewApi.ts` (+25줄) | `computeFlagKey`(async sha256, 백엔드 동일 알고리즘), `flagHasGrounding`, `TransitionBody.acknowledgements` 추가 |
| `src/components/newsletter/ValidationFlags.tsx` (137줄 변경) | `flagKeys`/`attested`/`onAttest` props, HIGH 카드별 attestation 체크 + "⚠ 원문 근거 없음" RED 뱃지, 3열→세로 스택(`grid-cols-1 min-w-0 break-words tabular-nums`), `onLocate(query, sourceQuote)` |
| `src/components/newsletter/ReviewDetailPanel.tsx` (436줄 변경) | 레이아웃 B(sticky 요약스트립 + bounded 플래그 스크롤 + 하단 2-pane `flex-1 min-h-0` + sticky 액션바), 전역 게이트 제거, `unconfirmedHigh`/`canApprove`/`approveBlockReason`(aria-live), 교정시작 시 ack 무효화, 승인 시 `acknowledgements` 전송, `HighlightedBody`(뷰 하이라이트+fallback) |
| `ReviewDetailPanel.test.tsx` / `ValidationFlags.test.tsx` | 기존 갱신 + 신규 케이스 |

## 공유 계약 (백엔드 task-2914와 100% 동일)
- **flag_key** = `sha256(rule + "|" + field + "|" + (detail.source_quote‖summary_value‖summary_text‖"") + "|" + message).hex[:16]` — JS `||`(Python truthy) 정확 재현.
- **승인 바디**: `acknowledgements: [{flag_key, attested:true}]` (attested된 HIGH 전부). 서버가 HIGH 집합 자체 계산·fail-closed 대조.
- 고정 픽스처 3종 검증: `846808015ca14853` / `3d521d932c4404b9` / `e65d567c8c42b015` (팀장이 백엔드 알고리즘으로 독립 계산한 값과 일치).

## 검증 결과
- **tsc** (`tsconfig.json`, build 경로): exit 0 — newsletter 파일 타입에러 0.
  - (`tsconfig.app.json`은 CommunityBoard/AdminSubscriptions/use-user-tokens 등 **기존 무관 파일** 에러만 존재 — 이번 변경과 무관, base에도 동일.)
- **vitest** `src/components/newsletter/`: **30/30 PASS** (3 files).
- **build** `npm run build`: PASS (✓ built ~12s, PWA precache 175 entries).
- 신규 통과 케이스: 개별 ack 전 승인 disabled→전부 ack 후 enable / 미확인 HIGH=전체 HIGH수(접힌 노이즈 포함) / 교정시작→ack 무효화 / 근거없는 HIGH RED뱃지 / flag_key 고정 픽스처 3종 일치 / onLocate 2-인자.

## L1 스모크테스트 (실렌더 — Playwright + Vite 하네스, 목업 flags)
- **서버 재시작**: 해당없음(프론트 컴포넌트). 대신 실제 Vite dev(5199)로 real-render 하네스 구동 후 임시 하네스 파일 삭제·서버 종료.
- **API 응답 확인**: fetch 인터셉트로 목업 detail(HIGH 4·MED 2·INFO 1) 주입. 콘솔 에러 **0건**(경고 2=Radix Dialog aria, 무해).
- **스크린샷**(경로: `memory/reports/assets/`):
  - `t2915-01-initial.png` — 레이아웃 B: 얇은 요약스트립(🔴숫자1·🟠의미1·미확인 HIGH 4)+메타바 / bounded 독립스크롤 플래그영역(세로 대조·"문제" 헤드라인) / 하단 2-pane(원본|마크다운) / sticky 액션바 승인 **disabled** + "미확인 HIGH 4건 — 모두 확인 후 승인 가능".
  - `t2915-02-all-acked.png` — HIGH 4건 모두 attestation 체크 → 미확인 HIGH **0**, 승인 **enabled**(파란 버튼).
  - `t2915-03-locate-highlight.png` — "본문에서 찾기" → 좌측 "원문 발췌 대조: 1,900만원 한도로 보장" 배너 + 우측 마크다운 "1,000만원" 노란 `<mark>` 하이라이트. RED 뱃지("⚠ 원문 근거 없음 — 육안 필수") ai_suspect HIGH에 표시.
- 프로그램 검증: attest 체크박스 4개(HIGH당 1), RED 뱃지 1개, flags-scroll `max-h-[26vh] overflow-y-auto overscroll-contain min-h-0`(잘림 원천 제거).

## 설계 결정 / 제약 대응
- **container-query 미사용**: allowed_resources가 4파일뿐이라 `@tailwindcss/container-queries` 플러그인 추가 불가(tailwind.config·package.json 수정 금지). 세로 스택 기본으로 잘림을 원천 제거(작업지시 "좁으면 세로 스택" 충족). 넓은 화면 3열 복원은 의도적 미적용.
- **sha256 동기화**: 앱 기존 패턴 `crypto.subtle.digest`(async) 재사용, `load()`에서 전 flag key를 `Promise.all` 선계산 후 렌더 → 카운트/ack 동기 렌더.
- **회귀 0**: 보험사명/제목/본문 편집·전이(반려/재추출/발행)·source_file_ref 렌더·기존 테스트 전부 유지.

## 발견 이슈 및 해결
1. **테스트 픽스처 타입에러**(LSP 감지): flag_key 픽스처에 `level` 누락 → `as Flag` 실패. `level: "HIGH"` 추가(알고리즘은 level 미사용 → 해시 불변). 팀장이 직접 수정·재검증.
2. **모듈 목 파편화**: `vi.mock("./reviewApi")`가 `flagHasGrounding` 미제공 → `importOriginal` 스프레드로 해결(프레이야).
3. **jsdom scrollIntoView 미구현**: optional chaining best-effort 처리.

## 머지 판단
- **머지 필요**: Yes (Lv.3, worktree PR)
- **브랜치**: `task/task-2915-dev2` / **워크트리**: `/home/jay/projects/InsuRo/.worktrees/task-2915-dev2`
- **의견**: build/tsc/vitest·실렌더 3종 통과, scope 4파일 준수. 백엔드 task-2914(서버 fail-closed 게이트)와 계약 정합(flag_key 픽스처 일치). ANU 독립검증(실렌더 재확인)·머지·배포 권고.

## 모델 사용 기록
- 프레이야(프론트 구현): **Sonnet** — 일반 코딩/로직. haiku 미사용.
- 팀장(오딘, Opus): 설계·구조파악·독립검증·L1 실렌더·보고서.

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

