# 画面設計書 12-チーム招待メール確認画面

## 概要

本ドキュメントは、Giselle Studio の「チーム招待メール確認画面」に関する画面設計書である。チーム招待サインアップ後のメールアドレス確認を行い、確認完了後に自動的にチームへ参加する処理を定義する。

### 本画面の処理概要

**業務上の目的・背景**：新規サインアップしたユーザーのメールアドレスが実在し、本人が所有していることを確認する。これによりスパムアカウントの作成を防ぎ、招待メールアドレスと登録アドレスの整合性を保証する。メール確認が成功すると自動的にチームへの参加処理が実行される。

**画面へのアクセス方法**：チーム招待サインアップ画面（No.11）でサインアップを実行すると、自動的にこの画面にリダイレクトされる。

**主要な操作・処理内容**：
1. 確認コード（6桁OTP）の入力
2. 「Verify」ボタンによる確認コード検証
3. 「Resend code」ボタンによる確認コードの再送信
4. 検証成功時の自動チーム参加処理

**画面遷移**：
- 遷移元：チーム招待サインアップ画面（/join/[token]/signup）
- 遷移先：チーム参加成功画面（/join/success）、チーム招待画面（/join/[token] - エラー時）

**権限による表示制御**：招待トークンが有効で、有効期限内である必要がある。有効期限切れの場合はチーム招待画面にリダイレクトされる。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 96 | ユーザー認証 | 主機能 | チーム招待サインアップ後のメール確認処理 |

## 画面種別

確認画面（入力フォーム）

## URL/ルーティング

`/join/[token]/signup/verify-email`

- `[token]`: 招待トークン（動的パラメータ）

## 入出力項目

| 項目名 | 項目ID | 入出力 | 必須 | データ型 | 制約・バリデーション |
|--------|--------|--------|------|----------|---------------------|
| 招待トークン | invitationToken | 入力（URLパラメータ・hidden） | 必須 | string | 有効な招待トークンであること |
| 招待メールアドレス | invitedEmail | hidden | 必須 | string | 招待トークンに紐づくメールアドレス |
| 確認コード | token | 入力 | 必須 | string | 6桁の数字 |

## 表示項目

| 項目名 | 説明 | データソース |
|--------|------|-------------|
| タイトル | 「Verify your email」 | 固定テキスト |
| 説明文 | 確認コード送信済みの旨 | 固定テキスト |
| OTP入力フィールド | 6桁の確認コード入力UI | InputOTPコンポーネント |
| 再送リンク | 「Resend code」 | 固定テキスト |
| エラーメッセージ | 検証エラーの内容 | verifyJoinEmail/resendJoinOtp関数の戻り値 |
| 成功メッセージ | コード再送成功の旨 | resendJoinOtp関数の戻り値 |

## イベント仕様

### 1-確認コード検証（Verify）

**トリガー**: 「Verify」ボタンクリック

**前提条件**:
- 6桁の確認コードが入力されていること

**処理フロー**:
1. verifyJoinEmail Server Actionを呼び出し
2. Supabase Auth verifyOtpでコードを検証
3. 検証成功時、initializeAccountでアカウント初期化
4. acceptInvitationでチーム参加処理を実行
5. チーム参加成功画面（/join/success）へリダイレクト
6. エラー時、エラーメッセージを表示

### 2-確認コード再送信（Resend code）

**トリガー**: 「Resend code」リンクをクリック

**処理フロー**:
1. resendJoinOtp Server Actionを呼び出し
2. Supabase Auth resendで確認コードを再送
3. 成功時、成功メッセージを表示
4. エラー時、エラーメッセージを表示

## データベース更新仕様

### 操作別データベース影響一覧

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| Verify成功 | users | INSERT | 新規ユーザーレコード作成 |
| Verify成功 | supabaseUserMappings | INSERT | Supabaseユーザーマッピング作成 |
| Verify成功 | teamMemberships | INSERT | チームメンバーシップ作成 |
| Verify成功 | invitations | UPDATE | 招待トークン無効化 |

### テーブル別更新項目詳細

#### users

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | email | Supabase認証から取得 | initializeAccount関数で実行 |
| INSERT | supabaseId | Supabase user.id | Supabase認証から取得 |

#### supabaseUserMappings

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | userDbId | 新規作成されたユーザーのdbId | users テーブルと紐づけ |
| INSERT | supabaseUserId | Supabase user.id | Supabase認証から取得 |

#### teamMemberships

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | userDbId | 認証済みユーザーのdbId | supabaseUserMappingsから取得 |
| INSERT | teamDbId | 招待元チームのdbId | 招待トークンから取得 |
| INSERT | role | 招待時に指定されたロール | invitation.roleから取得 |

#### invitations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | revokedAt | 現在日時 (new Date()) | 招待トークン無効化 |

## メッセージ仕様

| 種別 | メッセージID | メッセージ内容 | 表示条件 |
|------|-------------|---------------|----------|
| 成功 | resend_success | A new confirmation code has been sent to your email address. | コード再送成功時 |
| エラー | auth_error | Authentication Error: {エラー内容} | OTP検証失敗時 |
| エラー | invalid_payload | Invalid verification payload. Please try again. | フォームデータ不正時 |
| エラー | no_user | No user returned | ユーザー取得失敗時 |

## 例外処理

| 例外状況 | 処理内容 | ユーザーへの通知 |
|---------|---------|----------------|
| 招待トークンが無効 | notFound()を返す | 404ページを表示 |
| 招待期限切れ | チーム招待画面へリダイレクト | リダイレクト |
| OTP検証失敗 | エラーメッセージを表示 | Alertコンポーネントで表示 |
| チーム参加処理エラー（JoinError） | チーム招待画面へリダイレクト | リダイレクト |
| その他のエラー | Sentryへ報告、チーム招待画面へリダイレクト | リダイレクト |

## 備考

- OTP入力は6桁の数字で、3桁ずつセパレータで区切られたUI
- 確認コードの有効期限はSupabase Authの設定に依存
- メール確認成功後、自動的にチーム参加処理が実行される
- チーム参加処理ではusage-based billingのメンバー変更ハンドリングも実行される

---

## コードリーディングガイド

本画面を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

招待トークンのデータ構造は前画面と同様。OTP検証のフローを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | invitation.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/invitation.ts` | acceptInvitation関数（108-163行目）でのチーム参加処理 |

**読解のコツ**: acceptInvitationはトランザクション内でteamMembershipsへの挿入とinvitationsの無効化を行う。

#### Step 2: エントリーポイントを理解する

ページコンポーネントの構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | page.tsx | `apps/studio.giselles.ai/app/(auth)/join/[token]/signup/verify-email/page.tsx` | 招待トークン検証（10-18行目）、フォーム表示 |

**主要処理フロー**:
1. **10-11行目**: URLパラメータから招待トークンを取得
2. **12-14行目**: fetchInvitationTokenで招待情報取得、無効なら404
3. **16-18行目**: 有効期限チェック
4. **35-38行目**: JoinVerifyFormコンポーネントにメールアドレスと招待トークンを渡す

#### Step 3: フォームコンポーネントを理解する

OTP入力とバリデーション、再送機能を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | form.tsx | `apps/studio.giselles.ai/app/(auth)/join/[token]/signup/verify-email/form.tsx` | OTP入力UI（83-95行目）、検証処理（29-42行目）、再送処理（44-60行目） |

**主要処理フロー**:
- **29-42行目**: handleSubmit関数でverifyJoinEmailを呼び出し
- **44-60行目**: handleResend関数でresendJoinOtpを呼び出し
- **83-95行目**: InputOTPコンポーネントで6桁入力UI

#### Step 4: Server Actionを理解する

OTP検証とチーム参加処理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | actions.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/signup/verify-email/actions.ts` | verifyJoinEmail（10-51行目）、resendJoinOtp（53-84行目） |

**主要処理フロー**:
- **27-31行目**: supabase.auth.verifyOtpでコード検証
- **38行目**: initializeAccountでアカウント初期化
- **42行目**: acceptInvitationでチーム参加
- **50行目**: 成功時に/join/successへリダイレクト

### プログラム呼び出し階層図

```
page.tsx (Server Component)
    │
    ├─ fetchInvitationToken() ─── invitation.ts
    │
    └─ JoinVerifyForm (Client Component) ─── form.tsx
           │
           ├─ verifyJoinEmail() ─── actions.ts
           │      │
           │      ├─ supabase.auth.verifyOtp()
           │      ├─ initializeAccount() ─── services/accounts
           │      └─ acceptInvitation() ─── invitation.ts
           │             │
           │             ├─ db.transaction()
           │             │      ├─ INSERT teamMemberships
           │             │      └─ UPDATE invitations
           │             │
           │             └─ handleMemberChange() ─── usage billing
           │
           └─ resendJoinOtp() ─── actions.ts
                  └─ supabase.auth.resend()
```

### データフロー図

```
[入力]                    [処理]                         [出力]

OTPコード(6桁) ───────▶ verifyJoinEmail() ───────────▶ 検証結果
                             │
                             ▼
                       supabase.auth.verifyOtp()
                             │
                             ▼
                       initializeAccount() ───────────▶ users, mappings
                             │
                             ▼
                       acceptInvitation() ────────────▶ teamMemberships
                             │
                             ▼
                       リダイレクト ──────────────────▶ /join/success
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| page.tsx | `apps/studio.giselles.ai/app/(auth)/join/[token]/signup/verify-email/page.tsx` | ソース | ページエントリーポイント（Server Component） |
| form.tsx | `apps/studio.giselles.ai/app/(auth)/join/[token]/signup/verify-email/form.tsx` | ソース | OTP入力フォーム（Client Component） |
| actions.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/signup/verify-email/actions.ts` | ソース | OTP検証・再送 Server Action |
| invitation.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/invitation.ts` | ソース | 招待トークン処理・チーム参加処理 |
| input-otp.tsx | `apps/studio.giselles.ai/components/ui/input-otp.tsx` | ソース | OTP入力UIコンポーネント |
| alert.tsx | `apps/studio.giselles.ai/components/ui/alert.tsx` | ソース | エラー・成功メッセージ表示 |
