# task-2990 — InsuRo 개인정보 동의 게이트 집행 (A: DDL 불요분)

**레벨**: Lv.2 (보안·법정) · **팀**: dev3-team (다그다) · **level=security** · **회장 승인 완료** (2026-08-20)

## 이 명세의 작성 방식 (읽기 전 유의)
직전 task-2985 에서 ANU 가 **파일 위치를 틀리게 적어** 봇이 착수 전 정정 보고만 하고 멈췄다.
그 실패를 반복하지 않기 위해, 이 명세의 모든 지목은 **ANU 가 직접 실측한 근거**를 붙였다.
그럼에도 **착수 전에 각 지목이 실제와 일치하는지 먼저 확인**하라. 불일치하면 구현하지 말고 정정 보고하라.
줄 번호는 바뀔 수 있으니 **심볼명을 우선 신뢰**하라.

## 배경
task-2983 이 개인정보처리방침을 게시했고 동의 체크박스도 넣었다.
그러나 **동의 여부를 판정할 서버 측 상태가 존재하지 않아** 게이트가 집행되지 않는다.

## ANU 실측 근거 (2026-08-20, main=`deeaf84` 기준)

**증거 1 — 동의 상태가 로컬 state 로만 존재한다**
```
src/components/AuthForm.tsx:37   const [consentChecked, setConsentChecked] = useState(false);
```

**증거 2 — 인증 호출 어디에도 동의가 전달되지 않는다**
```
AuthForm.tsx:63-66   supabase.auth.signUp({ email, password, options: { emailRedirectTo } })
AuthForm.tsx:93-96   supabase.auth.signInWithOAuth({ provider: "google", options: { redirectTo } })
```
두 호출 모두 `consentChecked` 를 넘기지 않는다. 동의는 화면을 벗어나는 순간 사라진다.

**증거 3 — 체크박스 강제는 회원가입 모드에만 걸려 있다**
```
AuthForm.tsx:49   if (mode === "signup" && !consentChecked) { ... }
AuthForm.tsx:86   if (mode === "signup" && !consentChecked) { ... }
AuthForm.tsx:145  disabled={loading || (mode === "signup" && !consentChecked)}
AuthForm.tsx:260  disabled={loading || (mode === "signup" && !consentChecked)}
```
즉 **로그인 모드에서는 동의 없이 통과**한다.

**증거 4 — 게이트가 세션 유무만 본다 (동의 검사 0건)**
```
src/components/AuthGuard.tsx:41   setAuthenticated(!!session);
src/components/AuthGuard.tsx:47   setAuthenticated(!!session);
```

**증거 5 — 그래서 우회가 성립한다**
브라우저 콘솔에서 `supabase.auth.signInWithOAuth({provider:'google'})` 를 직접 호출하면
체크박스를 건드리지 않고 세션이 생성되고, `AuthGuard` 가 통과시켜 본 화면에 진입한다.

**증거 6 — 수정 지점은 한 곳이다**
```
src/App.tsx:54   <AuthGuard>   ← 전 라우트를 감싸는 단일 최상위 래퍼
```
`AuthGuard.tsx` 내부만 고치면 전 경로에 적용된다. **`App.tsx` 는 수정할 필요가 없다.**

**증거 7 — ★ ANU 신규 발견: 동의 테이블이 이미 존재한다**
```
src/integrations/supabase/types.ts:546   customer_consents: {
                                   549     consent_date: string
                                   550     consent_method: string | null
                                   551     consent_type: string
```
봇 보고는 "`profiles` 에 동의 컬럼이 없다"고만 했으나, **별도 동의 테이블이 실재**한다.
다만 `customer_` 접두라 상담 고객용인지 로그인 사용자(설계사)용인지 불명이다. **착수 전 확인하라.**

## 목표 (A 범위 — DDL 없이 가능한 것만)
**"동의 화면을 보지 않고 서비스에 진입하는 것"을 막는다.**

1. 동의 여부를 **인증 세션에 붙어 다니는 상태**로 만든다. DDL 없이 가능한 수단을 쓴다.
   - 1순위: **증거 7의 `customer_consents` 재사용 가능 여부를 먼저 확인**하라. 스키마·RLS·용도를 보고 판단한다.
   - 2순위: Supabase auth 의 **user metadata**(`supabase.auth.updateUser({ data: {...} })`). 스키마 변경이 필요 없다.
   - 어느 쪽을 택하든 **근거를 보고서에 남긴다.**
2. `AuthGuard` 가 **세션 + 동의** 두 가지를 검사하도록 바꾼다. 동의 미완료면 본 화면 대신 동의 화면으로 보낸다.
3. **로그인 모드에도 동의 게이트가 걸리게** 한다(증거 3). 기존 사용자는 최초 1회만 요구하고, 이후에는 다시 묻지 않는다.
4. 동의 시점을 기록한다. 법정 고지 성격상 "언제 동의했는가"가 남아야 한다.

## ★ 한계를 반드시 명시하라
user metadata 를 택할 경우 **클라이언트가 수정할 수 있으므로 이것은 보안 경계가 아니다.**
이 태스크의 목적은 **동의 절차의 집행**이지 위변조 방지가 아니다.
- 코드에 주석으로 이 한계와 "정식 영속화는 후속 DDL 배치(B)에서 컬럼으로 이전"을 남긴다.
- 보고서에도 명시한다. **"보안 경계가 세워졌다"고 서술하지 말 것.**

## 범위 (엄수)
**수정 허용**: `src/components/AuthGuard.tsx` · `src/components/AuthForm.tsx` · 관련 테스트

**수정 금지**:
- `src/App.tsx` — 증거 6대로 불필요하다
- `supabase/migrations/**` — **DDL 은 이 태스크 범위가 아니다.** DB 비밀번호가 없어 적용·검증이 불가능하고,
  검증하지 못한 보안 SQL 을 머지하면 무효한 안심만 남는다. 후속 B 배치에서 처리한다.
- `src/pages/CustomerChat.tsx` · `src/pages/DigitalNamecard.tsx` — 후속 태스크 범위
- 진행 중 태스크 파일: `scripts/deploy/**`(task-2988)

## 검증
1. **우회 재현 테스트를 먼저 작성**하라. 수정 전에 FAIL 해야 한다.
   시나리오: 동의 없이 세션만 생성된 상태에서 보호 라우트 접근 → 진입이 **차단**되어야 한다.
2. 수정 후 위 테스트가 PASS 로 전환되는 것을 증거로 남긴다.
3. **실브라우저 3종**(스크린샷 첨부, 픽스처 아님을 명시):
   - 신규 가입 → 동의 없이는 진행 불가
   - **동의 없이 세션만 만든 상태**(콘솔에서 직접 OAuth 호출) → 본 화면 진입 차단
   - 이미 동의한 사용자 → 재로그인 시 **다시 묻지 않음**(회귀 없음)
4. `npm run build` EXIT=0 · `npx vitest run` 회귀 유지(기준선 1226. 불일치 시 근거와 함께 보고)
5. 기존 로그인 사용자 흐름이 깨지지 않는지 확인한다.

## allowed_resources (본 task의 capability)

```yaml
allowed_resources:
  paths:
    - "src/components/AuthGuard.tsx"
    - "src/components/AuthForm.tsx"
    - "src/components/__tests__/**"
    - "tests/**"
    - "e2e/**"
    - "memory/reports/task-2990.md"
  forbidden_paths:
    - "src/App.tsx"
    - "supabase/migrations/**"
    - "src/pages/CustomerChat.tsx"
    - "src/pages/DigitalNamecard.tsx"
    - "scripts/deploy/**"
    - ".github/**"
  commands:
    - "npm run build"
    - "npx vitest run"
    - "npx playwright test"
  merge_policy: "tiered"
  ttl_hours: 48
```

## 선행 조건
`origin/main = deeaf84` 기준으로 `git pull --ff-only` 후 시작한다.
**주의**: 이 저장소는 서비스 워킹트리와 별개다. 서비스 재기동을 유발하는 조작을 하지 말 것.

## 완료 조건
우회 재현 테스트 FAIL→PASS · 실브라우저 3종 증거 · 빌드 0 · 회귀 유지 · 기존 사용자 무회귀 · **한계 명시**

## 보고
**PR 생성까지가 범위다. 머지는 ANU 가 한다. 직접 머지하지 말 것.**
(task-2986 에서 봇이 이 지시를 어기고 직접 머지한 사례가 있다. 반복하지 말 것.)
`memory/reports/task-2990.md` 작성 후 표준 완료 콜백 등록. 콜백 프롬프트 **UTF-8 3900 bytes 이하**.

## goal_assertions (auto-generated)
- `npm run build`
- `npx vitest run`
