# 画面設計書 10-チーム招待ログイン画面

## 概要

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

### 本画面の処理概要

チーム招待ログイン画面は、チームへの招待を受けた既存ユーザーがログインするための画面である。招待メールアドレスが固定表示され、パスワードを入力してログインとチーム参加を一括で行う。

**業務上の目的・背景**：本画面は、既にGiselleアカウントを持っているユーザーがチーム招待を受けた場合に、スムーズにログインとチーム参加を行えるようにするための専用画面である。招待メールアドレスを固定表示することで、正しいアカウントでログインすることを促し、メールアドレス不一致によるエラーを防止する。ログイン成功後は自動的にチーム参加処理が実行されるため、ユーザーの操作ステップを削減している。

**画面へのアクセス方法**：以下の方法でチーム招待ログイン画面にアクセスできる。
- チーム招待画面のWrongEmailErrorからサインアウト後、自動的にリダイレクト
- チーム招待サインアップ画面から「Log in」リンクをクリック
- ブラウザで直接 `/join/[token]/login` URLにアクセス（有効なトークンが必要）

**主要な操作・処理内容**：
1. パスワード入力：招待メールアドレスに対応するパスワードを入力
2. ログイン＆チーム参加：「Join to team」ボタンを押下してログインとチーム参加を一括実行
3. サインアップへの遷移：アカウントを持っていない場合のサインアップ画面への遷移
4. 招待の辞退：「Decline」リンクを押下して招待を辞退

**画面遷移**：
- 遷移元：チーム招待画面（WrongEmailErrorからサインアウト後）、チーム招待サインアップ画面
- 遷移先：チーム参加成功画面（ログイン＆参加成功時）、チーム招待サインアップ画面（サインアップ選択時）、サービスサイト（辞退時）

**権限による表示制御**：本画面は未ログインユーザーのみがアクセスすることを想定している。有効な招待トークンが必要である。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 96 | ユーザー認証 | 主機能 | 招待されたチームに参加するためのログイン処理 |
| 97 | チーム管理 | 遷移先機能 | ログイン後のチーム参加処理への遷移 |

## 画面種別

認証（ログインフォーム）

## URL/ルーティング

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

## 入出力項目

### 入力項目

| 項目名 | 項目ID | 型 | 必須 | 最大長 | 説明 |
|--------|--------|-----|------|--------|------|
| メールアドレス | email | string | Yes | - | 招待メールアドレス（読み取り専用） |
| パスワード | password | string | Yes | - | ログイン用パスワード |
| 招待トークン | token | string | Yes | - | URLパスから取得（hidden） |

### 出力項目

本画面での直接的な出力項目はない。ログイン＆参加成功時はチーム参加成功画面へリダイレクト、失敗時はエラーメッセージを表示する。

## 表示項目

| 項目名 | 表示内容 | 説明 |
|--------|----------|------|
| ページタイトル | "You have been invited" | ヘッダー部のメインタイトル |
| チーム名 | 動的（招待されたチームの名前） | 招待先チーム名の表示 |
| ログイン案内 | "Log in to join this team" | ログイン案内メッセージ |
| メールラベル | "Email" | メール表示フィールドのラベル |
| メールアドレス | 動的（招待メールアドレス） | 読み取り専用で表示 |
| パスワードラベル | "Password" | パスワード入力フィールドのラベル |
| ログインボタン | "Join to team" または "Joining..." | ログイン＆参加ボタン |
| サインアップ誘導 | "Don't have a Giselle account?" + "Sign up" | サインアップへの誘導 |
| 利用規約同意 | LegalConsent コンポーネント | 利用規約への同意表示 |
| 辞退リンク | "Decline" | 招待辞退リンク |

## イベント仕様

### 1-ログイン＆参加ボタン押下

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

**処理フロー**:
1. フォームデータから `email`, `password`, `token` を取得
2. `loginUser`関数が呼び出される（Server Action）
3. Supabase Auth クライアントを作成
4. `signInWithPassword` メソッドでログインを実行
5. ログイン成功後、`acceptInvitation`関数でチーム参加を実行
6. 参加成功時、`/join/success`（チーム参加成功画面）にリダイレクト

**例外処理**:
- ログイン失敗（無効な認証情報）：エラーメッセージを表示
- チーム参加失敗（JoinError）：チーム招待画面にリダイレクト
- その他のエラー：Sentryにキャプチャし、チーム招待画面にリダイレクト

### 2-サインアップリンク押下

**トリガー**: "Sign up" リンクをクリック

**処理フロー**:
1. Next.js Link コンポーネントによるクライアントサイドナビゲーション
2. `/join/[token]/signup` ページ（チーム招待サインアップ画面）に遷移

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

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

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

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

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

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

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

#### team_memberships

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

#### invitations

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

## メッセージ仕様

| メッセージ種別 | メッセージ内容 | 表示条件 |
|--------------|---------------|----------|
| エラー | Supabaseからのエラーメッセージ | ログイン失敗時 |
| 処理中 | "Joining..." | ログイン処理中 |

## 例外処理

| 例外種別 | 条件 | 処理内容 |
|---------|------|----------|
| トークン無効 | トークンが見つからない | 404 Not Foundを返却 |
| 期限切れ | 招待の有効期限が過ぎている | チーム招待画面へリダイレクト |
| ログイン失敗 | 無効なパスワード | エラーメッセージを表示 |
| 参加エラー | チーム参加処理で失敗 | チーム招待画面にリダイレクト、Sentryにキャプチャ |

## 備考

- メールアドレスは招待メールアドレスが固定表示され、ユーザーは編集できない
- ログインとチーム参加が一括で実行されるため、ユーザーの操作ステップが削減される
- `useTransition`フックで処理中状態を管理
- ボタンテキストは処理中に「Joining...」に変化
- メールアドレス入力フィールドには`readOnly`属性が設定されている

---

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

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

### 推奨読解順序

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

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | invitation.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/invitation.ts` | `InvitationToken`型の定義（行21-29）、`invitedEmail`プロパティ |

**読解のコツ**: 招待トークンには`invitedEmail`が含まれており、これがログインフォームのメールアドレスとして固定表示される。

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

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | page.tsx | `apps/studio.giselles.ai/app/(auth)/join/[token]/login/page.tsx` | トークン検証、期限切れチェック、UIレンダリング（行11-77） |

**主要処理フロー**:
1. **行16-21**: URLパラメータからトークンを取得し、`fetchInvitationToken`で検証
2. **行22-24**: トークンが期限切れの場合、チーム招待画面へリダイレクト
3. **行28-41**: チーム名とログイン案内の表示
4. **行43-45**: `LoginForm`でログインフォームを表示（email, token をpropsで渡す）
5. **行47-56**: サインアップ画面へのリンク
6. **行58-60**: LegalConsentコンポーネント
7. **行62-74**: 辞退リンク

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

ログインフォームの状態管理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | form.tsx | `apps/studio.giselles.ai/app/(auth)/join/[token]/login/form.tsx` | `useTransition`の使用（行18）、`handleSubmit`（行20-30）、読み取り専用メールフィールド（行54-62） |

**主要処理フロー**:
- **行16-17**: `password`と`error`の状態管理
- **行18**: `useTransition`で処理中状態を管理
- **行20-30**: フォームサブミット時に`loginUser`を呼び出し、エラーがあれば`setError`
- **行54-62**: メールアドレスを読み取り専用で表示（`readOnly`属性）
- **行86-92**: 処理中は「Joining...」、それ以外は「Join to team」を表示

#### Step 4: Server Action（ログイン＆参加処理）を理解する

ログインとチーム参加の一括処理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | actions.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/login/actions.ts` | `loginUser`関数（行9-30） |

**主要処理フロー**:
- **行10-12**: フォームデータから`email`, `password`, `token`を取得
- **行13-14**: Supabaseクライアント作成、`signInWithPassword`でログイン
- **行15-17**: ログインエラー時はエラーメッセージを返却
- **行20-21**: `acceptInvitation`でチーム参加
- **行22-28**: JoinError時はチーム招待画面へリダイレクト、その他はSentryにキャプチャ
- **行29**: 成功時に`/join/success`へリダイレクト

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

```
page.tsx (Server Component)
    │
    ├─ fetchInvitationToken(token)
    │      └─ DB SELECT (invitations + teams)
    │
    ├─ [トークン無効] ───▶ notFound()
    │
    ├─ [期限切れ] ───▶ redirect(/join/[token])
    │
    └─ [正常] ───▶ AuthContainer
                      │
                      ├─ LoginForm (Client Component)
                      │      │
                      │      ├─ useTransition
                      │      │
                      │      ├─ loginUser (Server Action)
                      │      │      │
                      │      │      ├─ supabase.auth.signInWithPassword
                      │      │      │      │
                      │      │      │      ├─▶ 失敗 ───▶ エラーメッセージ返却
                      │      │      │      │
                      │      │      │      └─▶ 成功
                      │      │      │
                      │      │      └─ acceptInvitation
                      │      │             ├─ DB INSERT (team_memberships)
                      │      │             ├─ DB UPDATE (invitations.revokedAt)
                      │      │             └─ handleMemberChange (課金更新)
                      │      │
                      │      └─ redirect(/join/success)
                      │
                      ├─ ActionPrompt (サインアップ誘導)
                      │      └─ Link (/join/[token]/signup)
                      │
                      ├─ LegalConsent (利用規約同意)
                      │
                      └─ declineInvitation (Server Action)
                             └─ revokeInvitation
                                    └─ DB UPDATE (invitations.revokedAt)
```

### データフロー図

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

パスワード入力 ───▶ LoginForm
                      │
                      └─▶ handleSubmit
                               │
                               └─▶ loginUser (Server Action)
                                        │
                                        ├─▶ signInWithPassword
                                        │         │
                                        │         ├─▶ 失敗 ───▶ エラーメッセージ ───▶ エラー表示
                                        │         │
                                        │         └─▶ 成功
                                        │
                                        └─▶ acceptInvitation
                                                  │
                                                  ├─▶ 成功 ───▶ redirect(/join/success)
                                                  │
                                                  └─▶ 失敗 (JoinError) ───▶ redirect(/join/[token])


"Sign up" ───▶ Link ───▶ /join/[token]/signup


"Decline" ───▶ declineInvitation ───▶ redirect(サービスサイト)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| page.tsx | `apps/studio.giselles.ai/app/(auth)/join/[token]/login/page.tsx` | ソース | チーム招待ログイン画面のメインページコンポーネント |
| form.tsx | `apps/studio.giselles.ai/app/(auth)/join/[token]/login/form.tsx` | ソース | ログインフォームのClient Component |
| actions.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/login/actions.ts` | ソース | ログイン＆チーム参加のServer Action |
| invitation.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/invitation.ts` | ソース | 招待トークン検証・参加処理 |
| actions.ts | `apps/studio.giselles.ai/app/(auth)/join/[token]/actions.ts` | ソース | declineInvitationのServer Action |
| 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` | ソース | 利用規約同意表示コンポーネント |
| action-prompt.tsx | `apps/studio.giselles.ai/app/(auth)/components/action-prompt.tsx` | ソース | アクション誘導コンポーネント |
