# 画面設計書 9-チーム招待画面

## 概要

本ドキュメントは、Giselle Studio のチーム招待画面の設計仕様を記載したものである。

### 本画面の処理概要

チーム招待画面は、チームへの招待メールに含まれるリンクからアクセスし、招待されたチームへの参加を承諾または辞退するための画面である。ログイン済みユーザーの場合は直接参加を選択でき、未ログインユーザーの場合はログインまたはサインアップ画面へリダイレクトされる。

**業務上の目的・背景**：本画面は、チーム管理者が送信した招待メールを受けたユーザーが、Giselleプラットフォーム上でチームに参加するためのエントリーポイントである。招待トークンの検証、招待メールアドレスとログインユーザーのメールアドレスの一致確認、期限切れチェックなど、セキュアなチーム参加フローを提供する。これにより、不正なチーム参加を防止しつつ、正規のユーザーがスムーズにチームに参加できる。

**画面へのアクセス方法**：以下の方法でチーム招待画面にアクセスできる。
- チーム招待メールに含まれるリンクをクリック
- ブラウザで直接 `/join/[token]` URLにアクセス（有効なトークンが必要）

**主要な操作・処理内容**：
1. チーム参加の承諾：「Join to team」ボタンを押下してチームに参加
2. 招待の辞退：「Decline」リンクを押下して招待を辞退
3. 未ログインユーザーの場合：サインアップ画面へ自動リダイレクト

**画面遷移**：
- 遷移元：チーム招待メールのリンク（外部）
- 遷移先：チーム参加成功画面（参加成功時）、サービスサイト（辞退時）、サインアップ画面（未ログイン時）、チーム招待ログイン画面（メールアドレス不一致でサインアウト後）

**権限による表示制御**：
- ログイン済みユーザー：チーム参加または辞退の選択肢を表示
- 未ログインユーザー：サインアップ画面へ自動リダイレクト
- 招待メールアドレスと異なるユーザー：WrongEmailErrorコンポーネントを表示（サインアウトを促す）
- 招待期限切れ：ExpiredErrorコンポーネントを表示

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 97 | チーム管理 | 主機能 | チームへの招待を承諾または辞退する処理 |
| 96 | ユーザー認証 | 補助機能 | ログイン済みユーザーの認証状態確認 |

## 画面種別

認証（チーム招待処理）

## URL/ルーティング

| 項目 | 値 |
|------|-----|
| URL | `/join/[token]` |
| ルートファイル | `apps/studio.giselles.ai/app/(auth)/join/[token]/page.tsx` |
| 動的パラメータ | `token` - 招待トークン |

## 入出力項目

### 入力項目

| 項目名 | 項目ID | 型 | 必須 | 最大長 | 説明 |
|--------|--------|-----|------|--------|------|
| 招待トークン | token | string | Yes | - | URLパスから取得（hidden） |

### 出力項目

本画面での直接的な出力項目はない。参加成功時はチーム参加成功画面へリダイレクト、辞退時はサービスサイトへリダイレクトする。

## 表示項目

| 項目名 | 表示内容 | 説明 |
|--------|----------|------|
| ページタイトル | "You have been invited" | ヘッダー部のメインタイトル |
| チーム名 | 動的（招待されたチームの名前） | 招待先チーム名の表示 |
| 参加案内 | "You have been invited to join this team" | 参加案内メッセージ |
| 参加ボタン | "Join to team" | チーム参加ボタン |
| 利用規約同意 | LegalConsent コンポーネント | 利用規約への同意表示 |
| 辞退リンク | "Decline" | 招待辞退リンク |

### エラー時の表示

#### 招待期限切れ時

| 項目名 | 表示内容 | 説明 |
|--------|----------|------|
| エラーメッセージ | "This invitation has expired." | 期限切れエラー |
| 案内文 | "Please ask the team administrator to send you a new invitation." | 対処方法の案内 |
| ログインリンク | "Already have an account?" + "Log in" | ログインへの誘導 |
| サインアップリンク | "or" + "Create an account" | サインアップへの誘導 |

#### メールアドレス不一致時

| 項目名 | 表示内容 | 説明 |
|--------|----------|------|
| チーム名 | 動的（招待されたチームの名前） | 招待先チーム名 |
| エラーメッセージ | "The email address you're currently using doesn't match..." | メール不一致エラー |
| サインアウトボタン | "Sign out" | サインアウトボタン |

## イベント仕様

### 1-参加ボタン押下

**トリガー**: "Join to team" ボタンをクリック

**処理フロー**:
1. フォームデータから `token` を取得
2. Server Action `joinTeam` が呼び出される
3. `acceptInvitation`関数でトークンを検証しチームに参加
4. トランザクション内で以下を実行：
   - 招待トークンの有効性確認（期限切れ、メールアドレス一致）
   - `team_memberships`テーブルへINSERT
   - 招待を無効化（`revokedAt`を設定）
5. 参加成功時、`/join/success`（チーム参加成功画面）にリダイレクト
6. 使用量ベース課金の場合、`handleMemberChange`で課金情報を更新

**例外処理**:
- トークン無効/期限切れ：JoinErrorをスローし、チーム招待画面にリダイレクト
- メールアドレス不一致：JoinErrorをスローし、チーム招待画面にリダイレクト
- その他のエラー：Sentryにキャプチャし、チーム招待画面にリダイレクト

### 2-辞退リンク押下

**トリガー**: "Decline" リンクをクリック

**処理フロー**:
1. フォームデータから `token` を取得
2. Server Action `declineInvitation` が呼び出される
3. `revokeInvitation`関数で招待を無効化
4. サービスサイト（`NEXT_PUBLIC_SERVICE_SITE_URL`）にリダイレクト

### 3-サインアウトボタン押下（WrongEmailError時）

**トリガー**: "Sign out" ボタンをクリック

**処理フロー**:
1. Server Action `signoutUser` が呼び出される
2. Supabase Authでサインアウト実行
3. チーム招待ログイン画面（`/join/[token]/login`）にリダイレクト

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

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| チーム参加 | team_memberships | INSERT | チームメンバーシップ作成 |
| チーム参加 | invitations | UPDATE | 招待を無効化（revokedAt設定） |
| 招待辞退 | invitations | UPDATE | 招待を無効化（revokedAt設定） |

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

#### team_memberships

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | userDbId | ログインユーザーのDBID | usersテーブルから取得 |
| INSERT | teamDbId | 招待先チームのDBID | 招待トークンから取得 |
| INSERT | role | 招待時に指定されたロール | 招待トークンから取得 |

#### invitations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | revokedAt | 現在日時 | 招待承諾または辞退時 |

## メッセージ仕様

| メッセージ種別 | メッセージ内容 | 表示条件 |
|--------------|---------------|----------|
| エラー | "This invitation has expired." | 招待トークンが期限切れの場合 |
| エラー | "The email address you're currently using doesn't match the email this invitation was intended for..." | ログインユーザーのメールアドレスが招待メールアドレスと異なる場合 |

## 例外処理

| 例外種別 | 条件 | 処理内容 |
|---------|------|----------|
| トークン無効 | トークンが見つからない | 404 Not Foundを返却 |
| 期限切れ | 招待の有効期限が過ぎている | ExpiredErrorコンポーネントを表示 |
| メールアドレス不一致 | ログインユーザーのメールと招待メールが異なる | WrongEmailErrorコンポーネントを表示 |
| 未ログイン | ユーザーがログインしていない | `/join/[token]/signup`へリダイレクト |
| 参加エラー | チーム参加処理で失敗 | チーム招待画面にリダイレクト、Sentryにキャプチャ |

## 備考

- 招待トークンはURLのパスパラメータとして渡される
- `fetchInvitationToken`関数でトークンの有効性を検証
- `getUser`関数でログイン状態を確認し、未ログインの場合はサインアップ画面へリダイレクト
- チーム参加処理はトランザクション内で実行され、整合性が保証される
- 既にチームメンバーの場合は`onConflictDoNothing`で無視される
- 使用量ベース課金のチームでは、`handleMemberChange`で課金情報が更新される

---

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

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

### 推奨読解順序

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

まず、招待トークンの型とエラーコードを理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | invitation.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/invitation.ts` | `InvitationToken`型の定義（行21-29）、`fetchInvitationToken`関数（行31-75） |
| 1-2 | errors.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/errors.ts` | `ErrorCode`型と`JoinError`クラス（行1-9） |

**読解のコツ**: `InvitationToken`にはトークン、チームID、招待メール、有効期限、ロールが含まれる。`fetchInvitationToken`はデータベースから招待情報を取得し、`revokedAt`がnullのもののみを返す。

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

処理の起点となるページコンポーネントを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | page.tsx | `apps/studio.giselles.ai/app/(auth)/join/[token]/page.tsx` | トークン検証、ユーザー認証確認、条件分岐（行11-81） |

**主要処理フロー**:
1. **行16-21**: URLパラメータからトークンを取得し、`fetchInvitationToken`で検証
2. **行22-24**: トークンが期限切れの場合、`ExpiredError`コンポーネントを表示
3. **行26-32**: `getUser`でログイン状態を確認、未ログインの場合はサインアップ画面へリダイレクト
4. **行34-36**: メールアドレス不一致の場合、`WrongEmailError`コンポーネントを表示
5. **行38-79**: 正常時のチーム招待UIを表示

#### Step 3: エラーコンポーネントを理解する

エラー時の表示UIを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | error-components.tsx | `apps/studio.giselles.ai/app/(auth)/join/[token]/error-components.tsx` | `ExpiredError`（行24-59）、`WrongEmailError`（行61-92）の実装 |

**主要処理フロー**:
- **行24-59**: 期限切れ時のUIと代替オプション（ログイン/サインアップ）
- **行61-92**: メール不一致時のUIとサインアウトボタン

#### Step 4: Server Action（参加・辞退処理）を理解する

チーム参加と辞退のロジックを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | actions.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/actions.ts` | `joinTeam`（行17-30）、`declineInvitation`（行32-41）の実装 |
| 4-2 | invitation.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/invitation.ts` | `acceptInvitation`関数（行108-163） |

**主要処理フロー**:
- **actions.ts 行17-30**: `joinTeam`で`acceptInvitation`を呼び出し、成功時に`/join/success`へリダイレクト
- **actions.ts 行32-41**: `declineInvitation`で`revokeInvitation`を呼び出し、サービスサイトへリダイレクト
- **invitation.ts 行108-163**: トランザクション内でメンバーシップ追加と招待無効化を実行

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

```
page.tsx (Server Component)
    │
    ├─ fetchInvitationToken(token)
    │      └─ DB SELECT (invitations + teams)
    │
    ├─ [トークン無効] ───▶ notFound()
    │
    ├─ [期限切れ] ───▶ ExpiredError
    │                     ├─ Link (/login)
    │                     └─ Link (/signup)
    │
    ├─ getUser()
    │      │
    │      └─ [未ログイン] ───▶ redirect(/join/[token]/signup)
    │
    ├─ [メール不一致] ───▶ WrongEmailError
    │                          └─ signoutUser (Server Action)
    │                                 └─ redirect(/join/[token]/login)
    │
    └─ [正常] ───▶ AuthContainer
                      │
                      ├─ joinTeam (Server Action)
                      │      └─ acceptInvitation
                      │             ├─ DB INSERT (team_memberships)
                      │             ├─ DB UPDATE (invitations.revokedAt)
                      │             └─ handleMemberChange (課金更新)
                      │
                      └─ declineInvitation (Server Action)
                             └─ revokeInvitation
                                    └─ DB UPDATE (invitations.revokedAt)
```

### データフロー図

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

URLパラメータ(token) ───▶ page.tsx
                              │
                              ├─▶ fetchInvitationToken
                              │         │
                              │         ├─▶ トークン無効 ───▶ 404 Not Found
                              │         │
                              │         └─▶ 有効 ───▶ InvitationToken
                              │
                              ├─▶ 期限切れチェック
                              │         │
                              │         └─▶ 期限切れ ───▶ ExpiredError
                              │
                              ├─▶ getUser
                              │         │
                              │         └─▶ 未ログイン ───▶ redirect(/join/[token]/signup)
                              │
                              └─▶ メールアドレス比較
                                        │
                                        ├─▶ 不一致 ───▶ WrongEmailError
                                        │
                                        └─▶ 一致 ───▶ チーム招待UI
                                                        │
                                        ┌───────────────┼───────────────┐
                                        ▼               ▼               ▼
                                  "Join to team"   "Decline"      その他
                                        │               │
                                        ▼               ▼
                                  acceptInvitation  revokeInvitation
                                        │               │
                                        ▼               ▼
                                  DB INSERT/UPDATE  DB UPDATE
                                        │               │
                                        ▼               ▼
                                  /join/success   サービスサイト
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| page.tsx | `apps/studio.giselles.ai/app/(auth)/join/[token]/page.tsx` | ソース | チーム招待画面のメインページコンポーネント |
| actions.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/actions.ts` | ソース | チーム参加・辞退・サインアウトのServer Actions |
| invitation.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/invitation.ts` | ソース | 招待トークン検証・参加処理 |
| errors.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/errors.ts` | ソース | エラーコードとJoinErrorクラス |
| error-components.tsx | `apps/studio.giselles.ai/app/(auth)/join/[token]/error-components.tsx` | ソース | エラー表示コンポーネント |
| auth-container.tsx | `apps/studio.giselles.ai/app/(auth)/components/auth-container.tsx` | ソース | 認証画面共通のレイアウトコンポーネント |
| auth-button.tsx | `apps/studio.giselles.ai/app/(auth)/components/auth-button.tsx` | ソース | 認証画面共通のボタンコンポーネント |
| legal-consent.tsx | `apps/studio.giselles.ai/app/(auth)/components/legal-consent.tsx` | ソース | 利用規約同意表示コンポーネント |
| member-change.ts | `apps/studio.giselles.ai/services/teams/member-change.ts` | ソース | メンバー変更時の課金処理 |
