# task-2907 보고서 — 소식지 검토 상세뷰 크래시 수정 (source_file_ref 객체 렌더, React #31)

- 작업 ID: task-2907
- 팀: dev2 (오딘) / 구현 담당: 프레이야(프론트엔드)
- 레벨: Lv.1 hotfix
- 프로젝트: InsuRo (webapp, src/ only)
- base: origin/main = `2ea5096` (배포됨)
- 작업 브랜치: `task/task-2907-dev2`
- worktree: `/home/jay/projects/InsuRo/.worktrees/task-2907-dev2`
- 커밋: `fdf1ccb`

## Situation (상황)
관리자 소식지 검토 화면에서 항목 클릭 → 상세뷰가 **React 최소화 에러 #31** ("Objects are not valid as a React child, object with keys {sha256, file_url}")로 크래시. 회장 실클릭 테스트에서 재현.

## Complication (문제)
- 백엔드(`server/routes/newsletter_review_v1.py:137`)는 `source_file_ref`를 **객체** `{"file_url", "sha256"}`(jsonb)로 반환.
- 프론트(`reviewApi.ts:55`)는 `source_file_ref: string | null`로 **문자열로 오인** → `ReviewDetailPanel.tsx:128`에서 `{detail.source_file_ref}`로 **객체를 그대로 렌더** → 크래시.
- 좌측 "원문" pane 설계 전제(source_file_ref = 원문 텍스트)가 틀림. source_file_ref는 **원본 파일 참조**(url+sha)이지 원문 텍스트가 아님.

## Question (핵심 질문)
객체형 source_file_ref를 크래시 없이 렌더하고, 리뷰어가 codex 플래그의 source_quote와 대조할 수 있도록 원본 파일을 표시하려면?

## Answer (해결) — surgical, src/ 3파일만
1. **타입 수정** (`reviewApi.ts`): `SourceFileRef` 타입 신규 추가(`{ file_url?: string; sha256?: string }`), `ReviewDetail.source_file_ref: string | null` → `SourceFileRef | null`. string 가정 제거.
2. **크래시 제거 + 원본 파일 표시** (`ReviewDetailPanel.tsx` 좌측 pane):
   - 객체 직접 렌더 제거. `source_file_ref?.file_url`이 있으면 **원본 파일 보기** 링크(새 탭, `rel=noopener noreferrer`).
   - file_url이 이미지 확장자면 `<img>` 인라인 표시 + `onError` 시 링크로 graceful fallback(`imgFailed` state, `load`에서 리셋).
   - sha256을 작게 병기. file_url 없으면 "원문 참조 없음".
3. **테스트 수정/추가** (`ReviewDetailPanel.test.tsx`): baseDetail을 객체형으로 교체 + 신규 3종(객체형 링크 렌더 / 이미지 img 렌더 / null "원문 참조 없음").

server·extension·.github 불변. 다른 검토 UI 기능(목록·2-pane·플래그 3열·게이트·전이 버튼) 회귀 0.

## 수정 파일 (정확히 3개, src/만)
- `src/components/newsletter/reviewApi.ts` — SourceFileRef 타입 추가, source_file_ref 타입 교체
- `src/components/newsletter/ReviewDetailPanel.tsx` — 좌측 pane 객체렌더 → 링크/이미지, imgFailed state
- `src/components/newsletter/ReviewDetailPanel.test.tsx` — mock 객체형 교체 + 신규 3 테스트
- 변경량: 3 files changed, 76 insertions(+), 9 deletions(-)

## 검증 결과
- **tsc** (`tsc --noEmit -p tsconfig.app.json`): 수정 3파일 신규 타입 에러 **0**. 저장소 전체 6건은 pre-existing(base 대조 동일: CommunityBoard, HistoryGroupCard.test, use-user-tokens.test×2, AdminSubscriptions, sw-bypass).
- **vitest** (`vitest run ReviewDetailPanel.test.tsx`): **7/7 PASS** (기존 4 + 신규 3). 팀장 독립 재실행 확인.
- **npm run build**: **PASS** (`✓ built in 11.80s`, dist/index.html 10:12 갱신). 500kB 청크 경고는 기존 벤더 번들 경고로 무관.

## L1 스모크테스트 결과 (실렌더)
- **서버 재시작**: 해당없음(정적 프로덕션 서버 아님) → 대신 실제 컴포넌트 vite 빌드 하베스트로 렌더(백엔드/인증만 stub, reviewApi를 rollup resolveId로 목킹하여 객체형 source_file_ref 주입).
- **렌더 방식**: `.harness/` 빌드 → 정적 서버(127.0.0.1:8267) → Playwright 접속.
- **결과**: 상세뷰 다이얼로그가 **크래시 없이 정상 렌더**. 콘솔 에러 **0건**.
  - 좌측 "원문 (원본 파일)" pane: `원본 파일 보기` 링크(href=이미지 URL) + `sha256: abc123def456` 표시. 이미지 URL 404 → onError → img graceful fallback(링크만) 동작 확인.
  - 우측 추출 필드(보험사명/제목/본문), 발행 전 확인 게이트 체크박스 2종, 제시 증거(HIGH 플래그) 정상 렌더 → 회귀 0.
- **스크린샷**: `/home/jay/workspace/memory/reports/task-2907-detail-render.png`
- (하베스트 파일 `.harness/`는 검증 후 삭제, git 트리는 커밋 3파일만 clean)

## 인지 검증 (표면 통과 ≠ 의도 동작)
- 팀장이 diff·grep을 직접 확인: 객체 직접 렌더(`{detail.source_file_ref}`) 완전 제거, `const ref = detail.source_file_ref`(할당) 만 잔존.
- vitest는 **실제 컴포넌트를 jsdom에서 객체형 입력으로 렌더**해 크래시 부재를 증명(정확한 회귀 입력). 추가로 Playwright 실렌더 스크린샷으로 육안 확인.

## 발견 이슈 및 해결
- L1 하베스트 dev 서버가 config 자기감지 재시작 루프로 자멸 → dev 서버 포기하고 **정적 vite build + python http.server** 방식으로 전환하여 안정적 실렌더 확보.
- reviewApi 목킹: dev 미들웨어 broad match("/review")가 `reviewApi.ts` 모듈 요청까지 가로챈 문제 → 빌드 시 rollup `resolveId` 플러그인으로 컴포넌트의 `./reviewApi` import만 정밀 리다이렉트.

## 머지 판단
- **머지 필요**: Yes
- **브랜치**: `task/task-2907-dev2`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2907-dev2`
- **머지 의견**: src/ 3파일 surgical, additive/버그픽스. tsc(신규0)·vitest(7/7)·build PASS, 실렌더 스크린샷 크래시 부재 확인. 회귀 위험 낮음. ★ 순서 B: ANU 독립검증(실렌더)·머지·재배포는 ANU 소유.

## 모델 사용 기록
- 프레이야(프론트): sonnet (general-purpose Task) — React/TS 로직 구현. haiku 미사용.
- 팀장(오딘, Opus): 설계·분배·검토·L1 실렌더 검증만. 직접 코딩 없음.

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

