# task-2983 — 인슈로 개인정보처리방침 신설 + 공개폼 고지 + 회원가입 동의 (P0)

- **팀**: dev3-team (다그다)
- **레벨/중요도**: Lv.3 / critical
- **프로젝트 경로**: `/home/jay/projects/InsuRo`
- **worktree**: `/home/jay/projects/InsuRo/.worktrees/task-2983-dev3`
- **브랜치**: `task/task-2983-dev3` · **base**: `60c3871` · **HEAD**: `74809a1`
- **PR**: **#235 OPEN** (base `main`, head `74809a1`, 17 커밋 / 18 파일) — ★ **머지 HOLD**

---

## S — 상황

InsuRo 는 공개 상담폼(디지털명함·고객채팅)으로 고객 PII 를 수집하고 있으나, 개인정보처리방침 페이지가 **부재**했다. `https://insuro.biz/privacy` 는 루트와 md5 가 완전 동일한(`7ea9d99e769e3c34ff8e3a6327dbcef3`, 2909 bytes) SPA fallback 이었고, 260807 초안은 빈칸 채로 멈춰 있었으며 문서 2건은 git 미추적(유실 위험) 상태였다.

## C — 문제

회장이 빈칸 6종과 동의 모델(InsuRo = **수탁자**)을 확정했으나, 이를 코드로 옮기는 과정에서 **처리방침 본문의 사실 주장이 실제 코드와 일치하는지** 검증되지 않으면 법적 문서의 허위 기재가 된다. 실제로 초안 제9조는 "성명·생년월일 등 주요 개인정보는 암호화하여 저장합니다"라고 **과대 기재**되어 있었고, 반대로 제5조 Anthropic 행은 실제 이전 데이터보다 **과소 기재**되어 있었다.

## Q — 핵심 질문

빈칸 6종을 채운 `/privacy` 실페이지를 띄우고 링크·고지·동의를 배선하되, **처리방침에 적힌 모든 사실 주장이 코드로 뒷받침되는가**를 어떻게 보증할 것인가?

## A — 답변

조항 문구를 `SECTIONS` 상수로 구조화해 단일 소스화하고, 각 사실 주장에 **코드 근거 각주**를 붙인 뒤 팀장·Codex·마아트 3중 검증으로 반증을 시도했다. 그 결과 **과대 기재 1건(제9조)·과소 기재 1건(제5조)·공개 요건 미충족 1건(경로 정규화)** 을 게시 전에 잡아 교정했다. 최종: 전체 회귀 **85 files / 1226 tests / 0 failed** (base 81/1194 전부 통과 대비 실패 증가 0), 빌드 성공, `/privacy`·`/privacy/`·`/Privacy` 3경로 실브라우저 렌더 확인(md5 `82cf2b54e1a2909f4af1e237f0d795ed` 3경로 동일). 머지·배포는 HOLD.

---

## 완료조건 대조

| 완료조건 | 결과 | 실측 근거 |
|---|---|---|
| /privacy 실페이지 렌더 (SPA fallback 아님) | 충족 | 작업 전 md5 루트와 동일 → 작업 후 실브라우저 h1=개인정보처리방침, 본문 3944자 |
| 빈칸 6종 실표기 | 충족 | grep 전건 1건 이상 + 스크린샷 육안 확인 |
| 로그인·가입·푸터 처리방침 링크 | 충족 | 로그인 푸터 실브라우저 확인, 앱 레이아웃은 코드 근거 |
| 공개폼 2종 고지 문구 + 링크 | 충족 | 고객채팅 실브라우저 확인 / 디지털명함은 렌더 회귀 테스트로 보완 |
| 회원가입 동의 체크박스 + 미체크 차단 | 충족(단서 있음) | 강제 제출 시 signUp 미호출 실증. ★ login 모드 Google 최초 로그인은 우회 가능 — 아래 미해결 참조 |
| privacy 문서 2건 git 커밋 | 충족 | 커밋 901e1d6, 2 files 279 insertions |

---

## 수정 파일별 검증 상태

| 파일 | 변경 | 검증 방법 | status |
|---|---|---|---|
| /home/jay/projects/InsuRo/.worktrees/task-2983-dev3/src/pages/Privacy.tsx | 신설 428줄, 제1~9조+부칙 | grep 빈칸 6종 각 1건 + 실브라우저 육안 + 단위테스트 11건 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2983-dev3/src/config/routes.ts | /privacy 라우트 등록 | grep 1건 + 실브라우저 렌더 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2983-dev3/src/components/AuthGuard.tsx | 공개경로 추가 + 경로 정규화 | 회귀 테스트 16건 + /privacy/ 및 /Privacy 실브라우저 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2983-dev3/src/components/Footer.tsx | 신설 48줄 | 로그인 화면 실브라우저 육안 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2983-dev3/src/pages/Login.tsx | 인라인 푸터를 Footer 로 교체 | grep + 실브라우저 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2983-dev3/src/components/DashboardLayout.tsx | main 하단 Footer 삽입 | grep 2건 (인증 필요로 실브라우저 미도달) | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2983-dev3/src/pages/DigitalNamecard.tsx | 상담신청 폼에 고지 + 링크 | 렌더 회귀 테스트 4건 (설계사명 치환 assert) | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2983-dev3/src/pages/CustomerChat.tsx | 게이트 고지 + 설계사명 조회 + 폴백 | 실브라우저 육안 + 렌더 테스트 2건 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2983-dev3/src/components/AuthForm.tsx | 동의 체크박스 + 이중 가드 | 강제 제출 우회 테스트 6건 + 실브라우저 Enter 강제 제출 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2983-dev3/src/config/legalEntity.ts | 신설, 법인 고지정보 단일 상수 | 하드코딩 잔존 grep 0건 + 상수 경유 렌더 육안 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2983-dev3/docs/privacy-ux-spec-260820.md | 신설 325줄 문구/IA 스펙 | 조항별 코드 근거 각주 + 미확인 항목 목록화 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2983-dev3/docs/privacy-p0-p2-roadmap-260807.md | git 추적 편입 | 커밋 901e1d6 | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2983-dev3/docs/privacy-policy-audit-and-draft-260807.md | git 추적 편입 | 커밋 901e1d6 | verified |

테스트 파일 5종(AuthForm.test.tsx, AuthGuard.test.tsx, Privacy.test.tsx, DigitalNamecard.test.tsx, CustomerChat.gateNotice.test.tsx) 신설/증설. 총 18 files changed, 1800 insertions.

---

## L1 스모크테스트

- **서버 재시작**: 성공 — `npm run build` 후 `npx vite preview --port 4183` 로 **프로덕션 빌드** 기동 (dev 서버 아님. 상수 번들링이 실제 빌드에서 깨지지 않는지 확인 목적)
- **API 응답 확인**: 해당없음 (프론트 전용 변경). 대신 작업 전 대조군으로 `curl https://insuro.biz/privacy` 와 루트 응답 md5 동일함을 실측
- **스크린샷**: 9장, 전부 Read 도구로 **이미지를 직접 열어 육안 확인**
  - `/home/jay/workspace/memory/reports/screenshots/task-2983/f_privacy_final.png` — `/privacy`
  - `/home/jay/workspace/memory/reports/screenshots/task-2983/g_privacy_trailing_slash.png` — `/privacy/` (교정 검증 핵심)
  - `/home/jay/workspace/memory/reports/screenshots/task-2983/h_privacy_uppercase.png` — `/Privacy`
  - `/home/jay/workspace/memory/reports/screenshots/task-2983/i_login_footer_final.png` — 로그인 푸터
  - `/home/jay/workspace/memory/reports/screenshots/task-2983/a_privacy.png` · `b_login_footer.png` · `c_signup_consent.png` · `d_namecard.png` · `e_customerchat.png` — 교정 전 1차 L1

**육안 확인 내용**: f/g/h 3장 md5 완전 동일(`82cf2b54e1a2909f4af1e237f0d795ed`)이며 **로그인 폼이 아니라 처리방침 본문**이 보임. 상수화 후에도 회사명이 `undefined` 로 깨지지 않음. 제5조 Anthropic 행에 "상담 통화 전문" 표기, 제9조가 범위한정 문구로 교체됨, 부칙 시행일 정상.

**추가 라우트 실측**: `/privacyfoo` → `/login` 리다이렉트(과잉매칭 없음). `/chat/dummy-token-12345` → 공개 유지 + 고지 노출.

**콘솔 에러**: 6개 라우트 순회 통틀어 **0건** (경고 1종: `apple-mobile-web-app-capable` deprecation, 무해).

**정리**: `browser_close` 완료, preview 서버 종료 확인.

**L1 미통과 항목 (정직 표기, SKIP 아님)**
- 디지털명함 `/namecard/<id>` 고지 문구 **실브라우저 미도달** — 유효한 설계사 프로필 시드가 없어 "프로필을 찾을 수 없습니다" 화면에서 멈춤. → 프로필 mock 렌더 회귀 테스트 4건으로 보완(설계사명이 플레이스홀더가 아닌 실제 값으로 치환되는지까지 assert).
- 앱 내부 레이아웃 푸터 — 인증 필요로 실브라우저 미도달, 코드 근거(import + 사용처)로 갈음.

---

## 테스트 결과

| 항목 | base (60c3871) | HEAD (74809a1) |
|---|---|---|
| vitest 파일 | 81 | 85 |
| vitest 테스트 | 1194 passed / 0 failed | 1226 passed / 0 failed |
| tsc --noEmit | - | 에러 0건 |
| npm run build | 성공 | 성공, dist/assets/Privacy-B148r4es.js 생성 |

회귀 실패 증가 **0건**. 증가분 +32 테스트는 이번에 추가한 5개 테스트 파일 수와 정합. base 수치는 마아트가 clean worktree 에서 **독립 재현**했고, HEAD 수치는 팀장이 별도로 재실행해 팀원 보고와 일치함을 확인.

---

## 게이트 결과

- **Codex 사전 검증**: 1차는 `affected_files` 미지정으로 입력 부족 아티팩트(HIGH 등급 지적 1건이 "확인할 코드가 없다"는 사유). **실제 diff 재투입 후 재실행**하여 유효 지적 4건 확보. PII 마스킹 5건 자동 적용(sanitize 게이트 충족).
- **마아트 독립 검증**: **CONDITIONAL PASS** — Critical 7종 0, HIGH 등급 지적 3건. 스스로 고안한 우회 11종 중 2종이 뚫림. 동의 우회 5종(B-1~B-5)은 전부 막힘.
- **Gemini**: sunset 상태로 리뷰 없음 → `--action pr` 자동 머지 차단(머지 HOLD 요건과 일치).

## trip-wire 5종 실측

| 항목 | 실측 | 비고 |
|---|---|---|
| Critical7 (net-new) | 0 | 아래 CRITICAL 1건은 base 에 동일 존재 확인 |
| PII net-new | 0 | 주민번호 0 · 실이메일 0 · API키 0. 휴대폰 1종은 법정 공개 대상인 보호책임자 연락처 |
| 회귀 실패 | 0 | 1226 passed / 0 failed |
| forbidden_paths 침범 | 0 | 18 파일 전부 InsuRo src/ 및 docs/ 내부 |
| nonce = task_id | 일치 | task-2983 |

API키 패턴 스캔에서 10건이 걸렸으나 전수 확인 결과 `sk-2983`·`sk-2356` = **"task-2983"·"task-2356" 문자열이 `sk-` 패턴에 걸린 오탐**. 실제 키 0건.

---

## 발견 이슈 및 해결

### 자체 해결 (6건)

1. **제9조 안전조치 과대 기재** — "성명·생년월일 등 주요 개인정보는 암호화하여 저장합니다"를 적용범위 한정 문구로 교정
   - 반증: `server/customer_match.py` 의 `insert_row` 가 주석 그대로 "평문 컬럼 — 기존 customers 흐름 호환" + "신규 암호화 컬럼 (dual-write)" → **평문 병존**. 게다가 그 경로는 오마이매니저 캡처 전용. 일반 CRM 등록 `src/pages/CrmCustomers.tsx:176` 은 전 항목 평문이며 `src/` 전체에 암호화 컬럼 참조 0건.
2. **제5조 Anthropic 과소 기재** — 이전 정보에 상담 통화 전문·상담 메모 추가
   - 반증: `server/main.py` 의 transcribe-call·evaluate-consultation 이 `req.transcript` 를 프롬프트에 삽입 → `anu_provider.generate()` → `_run_claude_cli_async` → `claude` CLI → Anthropic(미국) 전송.
3. **`/privacy` 경로 변형이 로그인월로 튕김** — `AuthGuard` 에 경로 정규화(끝 슬래시 제거·소문자화) 도입 + 회귀 테스트 7건
   - 처리방침 URL 은 외부 문서에 복붙 유통되고 링크 정규화기가 슬래시를 자동 부가한다. 과잉매칭 방지를 위해 `/privacyfoo` 음성 케이스도 테스트에 포함.
4. **고객채팅 고지가 "어느 설계사"를 충족 못함** — 게이트 화면에서 토큰으로 설계사 표시명만 조회해 치환, 실패 시 기존 "담당 설계사" 폴백
   - 표시명 외 어떤 정보도 조회하지 않음. 본인확인 로직은 무변경.
5. **법인 고지정보 6곳 하드코딩** — `src/config/legalEntity.ts` 단일 상수로 통합(잔존 grep 0건)
   - 법인명이 바뀌면 법적 고지 문구가 파일마다 드리프트하는 구조였음. 워크스페이스 "단일 소스" 규칙 위반 해소.
6. **privacy 문서 2건 git 미추적** — worktree 브랜치에 커밋(유실 방지). PII 스캔 결과 실제 PII 0건.

### 범위 외 미해결 (회장·아누 판단 필요)

1. **[CRITICAL·후속 P0] 공개 디지털명함이 이름만으로 기존 CRM 고객 레코드에 연결됨**
   - `src/pages/DigitalNamecard.tsx` 의 `handleSubmitChat` 이 `agent_id + name` 만으로 기존 고객을 조회하고, 그 레코드로 채팅 토큰을 발급한다. 기존 고객의 전화번호가 비어 있으면 방문자 입력값으로 덮어쓴다.
   - **결과**: 공개 명함 URL 을 아는 사람이 고객 이름만 추측하면 **타인의 상담 이력에 접근**할 수 있다.
   - **범위 외 사유**: base `60c3871` 에 **동일 코드가 존재**함을 확인(이번 작업의 diff 는 고지 문단 추가뿐). CRM 매칭 로직 변경은 기존 상담 흐름에 영향이 커 별도 승인이 필요.
   - **권고**: 항상 신규 리드로 생성하거나, 최소한 이름+전화번호가 모두 일치할 때만 연결.
2. **[HIGH·후속 P0] Google OAuth 최초 로그인이 동의 게이트를 우회**
   - login 모드의 1차 CTA 가 "Google로 시작하기"인데, Supabase 는 미존재 사용자를 자동 생성한다. 동의 가드는 signup 모드에만 걸려 있어 **신규 설계사 다수가 무동의 경로**를 탄다.
   - **범위 외 사유**: 기존 로그인 흐름 파손 위험이 있어 별도 설계 판단 필요.
   - **권고**: login 모드 Google 버튼에도 동의 문구 상시 노출, 또는 OAuth 콜백 후 동의 인터스티셜.
3. **[HIGH] 동의 증적이 저장되지 않음** — 체크박스는 UI 에서만 강제되고 동의 시각·방침 버전·가입 방식이 어디에도 기록되지 않는다. `privacy_consent_at TIMESTAMPTZ` 컬럼이 마이그레이션 `20260308182257` 에 **이미 존재하나 write 0건인 휴면 상태**. 분쟁 시 동의 입증 불가.
   - **범위 외 사유**: task 지시 범위는 "동의 체크박스"이며 증적 저장은 요구되지 않음. 인증 흐름에 쓰기를 추가하는 변경이라 마지막 단계에서 임의 확대하지 않음.
4. **[HIGH] 보유기간 정책과 실장치의 간극** — "설계사 탈퇴 시까지"로 고지했으나 회원 탈퇴/계정삭제 기능이 코드에 없다(로드맵 P1-1 미구현). 자동 파기 배치도 없음(P2-2).
   - 마아트 판정: **게시 차단 사유 아님** — "처리방침 미게시 상태를 유지하는 쪽이 훨씬 큰 위반". 단 P1 기한을 회장 보고에 명시할 것.
5. **[MEDIUM] 미기재 수탁/전송처** — 공개 고객 페이지에서 `fonts.googleapis.com`/`fonts.gstatic.com`, `t1.kakaocdn.net` 로드 200 실측(고객 IP·UA 전송). Cloudflare 경유 개연성(코드 근거 추론, 인프라 미확인). 제5조·제8조 미기재.
6. **[MEDIUM] PostHog 가 공개 고객 페이지에서 고객 단말에 영구 device UUID 를 심고 autocapture 가 활성** — 제1조 3항의 "비식별 정보만 수집됩니다" 단정이 위태로울 수 있다. 다만 마아트의 12초 관측 창에서 캡처 페이로드는 **미관측**이므로 "토큰이 전송된다"고 단정하지 않는다. 후속 실측 필요.
7. **[LOW] 게이트 설계사명 조회가 토큰 유효성 오라클** — 본인확인 전 anon 조회라 무효 토큰과 유효 토큰의 화면 문구가 다르다. UUIDv4 라 실질 위험은 낮으나 rate limit 밖에서 동작.
8. **[LOW] PWA 설치 프롬프트가 공개 처리방침 본문 일부를 가림** — 1차 캡처에서 "회사의 지위 안내" 문단 위에 배너가 겹침. 공개 법정 고지 화면에서는 억제 검토 권고.

---

## 3 Step Why

- **1st Why (설계)**: 왜 고객 직접 동의 UI 가 아닌 "고지 + 링크"인가 → InsuRo 는 고객 데이터를 자기 목적으로 쓰는 경로가 코드상 없는 **수탁자**이며, 수탁자가 정보주체 동의를 직접 받으면 책임 구조를 왜곡한다. 반면 설계사는 위탁의 당사자이므로 가입 시 체크박스가 필요하다.
- **2nd Why (대안 검토)**: SSR/정적 HTML(문구 이중관리 드리프트로 기각), JSX 직접 작성(상수화로 단일 소스 확보하여 기각), 공개폼 고객 동의 체크박스(수탁 구조와 모순으로 기각), 제9조 원문 유지(실측 반증으로 허위 기재가 되어 기각).
- **3rd Why (적대적)**: 이 설계가 실패하는 시나리오 5종을 사전 도출 — AuthGuard 누락, 코드엔 있고 화면엔 없는 고지, disabled 만으로 막힌 동의, 정책과 실장치의 괴리, 고지를 붙였지만 그 화면 자체가 유출 벡터. **5번째 시나리오가 Codex 검증에서 실제로 적발됨**(위 CRITICAL).

상세: `/home/jay/workspace/memory/plans/tasks/task-2983/context-notes.md`

---

## 모델 사용 기록

| 역할 | 담당 | 모델 | 비고 |
|---|---|---|---|
| 팀장 | 다그다 | opus | 설계·분배·검토·통합. 직접 코딩 없음 |
| UX/UI | 아네 | sonnet | 문구/IA 스펙. 법률성 카피라 haiku 미사용 |
| 백엔드 | 루 | sonnet | 서빙 경로 조사·문서 커밋·빌드 baseline |
| 프론트 | 브리짓 | sonnet | 페이지/라우트/푸터/고지/동의/교정 5회 |
| QA | 모리건 | sonnet | 회귀 테스트·L1 2회 |
| 독립 검증 | 마아트 | opus | 적대적 재현. critical 필수 게이트 |

haiku 사용 0건.

---

## 머지 판단

- **머지 필요**: Yes — 단 ★ **이번 task 는 머지 HOLD**. 아누 독립 검증 후 판단.
- **브랜치**: `task/task-2983-dev3`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2983-dev3`
- **PR**: #235 (OPEN, head `74809a1`)
- **배포**: 미실행. InsuRo 프론트는 Cloudflare Pages 이며 `npm run build` 후 `wrangler pages deploy dist` 수동 실행이 필요하다. 회장 승인 전까지 배포하지 않음.
- **머지 의견**: 코드 품질·회귀 관점에서는 머지 가능(회귀 실패 0, 빌드 성공, 3중 검증 통과). 다만 이 PR 은 **대외 법적 고지문**을 게시하는 변경이므로, 위 미해결 1~4번에 대한 회장 판단을 받은 뒤 게시하는 것이 안전하다. 특히 미해결 1번(CRITICAL)은 처리방침을 게시해도 해소되지 않는 **실제 유출 벡터**이므로 후속 P0 로 즉시 착수를 권고한다.

## 비고

- task 지시서는 main 을 `49c17a92` 로 기재했으나 실제 `origin/main` 은 `60c3871` 로 2커밋 진행되어 있었다. stale base 회피를 위해 최신을 base 로 사용했다.
- task 지시는 재위탁 "3사 명시"였으나 실측상 **PostHog 가 프로덕션에서 활성**(라이브 번들에 키·호스트 문자열 존재)이어서 누락 시 허위 고지가 된다. 4번째 수탁사로 포함했으며, 이는 **회장 재판단 사항**으로 보고한다.
- `worktree_manager.py finish --action pr` 은 외부 repo 에서 taskctl state 초기화 + state machine 전이 + `TASKCTL_CWD` 명시가 모두 필요하다. 함정을 메모리에 기록해 두었다.

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

