# 画面設計書 4-招待承認画面

## 概要

本ドキュメントは、Liam DBアプリケーションの組織への招待を承認するための画面について定義する。

### 本画面の処理概要

招待承認画面は、組織管理者から送信された招待メールのリンクをクリックしたユーザーが、招待を承認して組織に参加するための画面である。招待トークンを検証し、有効な場合は組織名と承認ボタンを表示する。

**業務上の目的・背景**：Liam DBでは複数のユーザーが同じ組織に所属してプロジェクトを共同管理できる。新規メンバーを組織に追加する際、管理者が招待メールを送信し、招待されたユーザーがこの画面で承認操作を行うことで、セキュアにメンバー追加を実現する。

**画面へのアクセス方法**：
- 招待メールに記載されたリンク（`/invitations/tokens/{token}`）をクリック
- 招待リンクを直接ブラウザに入力

**主要な操作・処理内容**：
1. 招待トークンを検証し、招待情報（組織名）を取得
2. 有効な招待の場合、組織名と「Accept Invite」ボタンを表示
3. 無効または期限切れの招待の場合、エラーメッセージを表示
4. 「Accept Invite」ボタンクリックで招待を承認
5. 承認成功時、プロジェクト一覧画面へリダイレクト

**画面遷移**：
- 未認証時 → ログイン画面（`/login`）
- 招待承認成功時 → プロジェクト一覧画面（`/projects`）
- 承認エラー時 → 同画面でエラーメッセージ表示

**権限による表示制御**：認証必須。未認証の場合はログイン画面へリダイレクト。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 31 | 招待機能 | 主機能 | 組織への招待トークンを検証し、招待承認フォームを表示 |
| 26 | 組織管理 | 補助機能 | 招待された組織情報の取得と表示 |
| 39 | Supabaseクライアント | API連携 | 招待データの取得とユーザー認証情報の確認 |

## 画面種別

フォーム（承認）

## URL/ルーティング

- URL: `/invitations/tokens/[token]`
- ファイル: `frontend/apps/app/app/invitations/tokens/[token]/page.tsx`
- レンダリング: サーバーコンポーネント
- 動的パラメータ: `token`（招待トークン）

## 入出力項目

### 入力項目

| 項目名 | 項目ID | データ型 | 必須 | 備考 |
|--------|--------|----------|------|------|
| 招待トークン | token | string | はい | URLパスパラメータ |

### 出力項目

| 項目名 | 説明 | 備考 |
|--------|------|------|
| 組織名 | 招待元の組織名 | 有効な招待の場合のみ表示 |
| ユーザーメール | 現在ログインしているユーザーのメールアドレス | 挨拶文に使用 |

## 表示項目

| 項目名 | 説明 | 備考 |
|--------|------|------|
| Liam ロゴマーク | アプリケーションロゴ | @liam-hq/uiのLiamLogoMarkコンポーネント |
| 挨拶メッセージ | "Hi, {email}!" | 有効な招待時のみ |
| 招待メッセージ | "You've been invited to join {organizationName} organization on Liam DB." | 有効な招待時のみ |
| Accept Inviteボタン | 招待承認ボタン | 有効な招待時のみ |
| エラーメッセージ | "This invitation link is invalid or has expired." | 無効な招待時 |
| 再招待依頼メッセージ | "Please request a new invitation from your organization administrator." | 無効な招待時 |

## イベント仕様

### 1-招待承認（Accept Invite）

招待を承認して組織に参加する処理。

**トリガー**：「Accept Invite」ボタンクリック

**処理フロー**：
1. フォームデータ（token）をバリデーション
2. 現在のユーザー情報を取得
3. Supabase RPC関数`accept_invitation`を呼び出し
4. 招待承認処理をアトミックに実行（招待検証、メンバー追加、招待削除）
5. 成功時：組織IDをCookieに設定し、`/projects`へリダイレクト
6. 失敗時：エラーメッセージを表示

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

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| ページ読み込み | invitations | SELECT | 招待トークンの検証と組織情報取得 |
| 招待承認 | organization_members | INSERT | ユーザーを組織メンバーとして追加 |
| 招待承認 | invitations | DELETE | 使用済み招待トークンを削除 |

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

#### organization_members

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | user_id | 現在の認証ユーザーID | - |
| INSERT | organization_id | 招待に紐づく組織ID | - |

#### invitations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | token | URLパラメータと照合 | 有効期限も確認 |
| DELETE | - | 承認成功後に削除 | トークンの再利用防止 |

## メッセージ仕様

| メッセージID | メッセージ内容 | 表示タイミング | 種別 |
|--------------|---------------|----------------|------|
| MSG-001 | "Hi, {email}!" | 有効な招待で画面表示時 | 情報 |
| MSG-002 | "You've been invited to join {organizationName} organization on Liam DB." | 有効な招待で画面表示時 | 情報 |
| MSG-003 | "Please accept the invitation below to join." | 有効な招待で画面表示時 | 情報 |
| ERR-001 | "This invitation link is invalid or has expired." | 無効な招待で画面表示時 | エラー |
| ERR-002 | "Please request a new invitation from your organization administrator." | 無効な招待で画面表示時 | エラー |
| ERR-003 | "Failed to accept invitation. Please try again." | 承認処理失敗時 | エラー |

## 例外処理

| 例外ケース | 処理内容 |
|------------|----------|
| URLパラメータ不正 | 404 Not Found を返す |
| 未認証ユーザー | `/login`へリダイレクト |
| 無効な招待トークン | エラーメッセージを表示（Accept Invite ボタンは非表示） |
| 期限切れの招待 | エラーメッセージを表示（Accept Invite ボタンは非表示） |
| 承認処理失敗 | 画面上にエラーメッセージを表示 |

## 備考

- 招待トークンはValibotでバリデーション
- 招待承認処理はSupabase RPC関数でアトミックに実行
- 承認成功後、組織IDがCookieに設定される
- 招待が無効または期限切れの場合、Accept Invite ボタンは表示されない

---

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

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

### 推奨読解順序

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

招待データと認証ユーザー情報の構造を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | getInvitationData.ts | `frontend/apps/app/components/InvitationPage/services/getInvitationData.ts` | 招待データ取得のRPC呼び出しとスキーマ定義 |

**読解のコツ**: `get_invitation_data` RPC関数が返すデータ構造（`organizationName`）を確認する。

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

招待ページのエントリーポイントを確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | page.tsx | `frontend/apps/app/app/invitations/tokens/[token]/page.tsx` | 動的ルートパラメータの処理、Valibotバリデーション |
| 2-2 | InvitationPage.tsx | `frontend/apps/app/components/InvitationPage/InvitationPage.tsx` | メインコンポーネント、getInvitationDataの呼び出し |

**主要処理フロー（page.tsx）**:
- **6-8行目**: Valibotでパラメータスキーマ定義
- **10-12行目**: パラメータをパースし、失敗時は404
- **16行目**: InvitationPageコンポーネントにtokenを渡す

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

招待カードUIの実装を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | InvitationCard.tsx | `frontend/apps/app/components/InvitationPage/components/InvitationCard/InvitationCard.tsx` | 招待UI、条件分岐による表示切り替え |

**主要処理フロー**:
- **22-23行目**: useTransitionでローディング状態管理
- **25-40行目**: handleAcceptで承認処理を実行
- **46-76行目**: 有効/無効な招待による表示分岐
- **79-98行目**: 承認ボタンとエラー表示

#### Step 4: 承認処理を理解する

サーバーアクションとして実装された承認処理。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | acceptInvitation.ts | `frontend/apps/app/components/InvitationPage/components/InvitationCard/actions/acceptInvitation.ts` | 招待承認のサーバーアクション |

**主要処理フロー**:
- **33-45行目**: フォームデータのバリデーション
- **50-60行目**: ユーザー認証確認
- **65-75行目**: `accept_invitation` RPC呼び出し
- **88-96行目**: 成功時のCookie設定とリダイレクト

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

```
app/invitations/tokens/[token]/page.tsx
    │
    └─ components/InvitationPage/InvitationPage.tsx
           │
           ├─ services/getInvitationData.ts [Server Action]
           │      ├─ supabase.auth.getUser()
           │      └─ supabase.rpc('get_invitation_data')
           │
           └─ components/InvitationCard/InvitationCard.tsx
                  │
                  └─ actions/acceptInvitation.ts [Server Action]
                         ├─ supabase.auth.getUser()
                         ├─ supabase.rpc('accept_invitation')
                         ├─ setOrganizationIdCookie()
                         └─ redirect('/projects')
```

### データフロー図

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

URLパラメータ(token) ───▶ page.tsx                    ───▶ 招待画面
     │                      │
     │                      └─ Valibotバリデーション
     │
     └─────────────────▶ getInvitationData            ───▶ 組織名/ユーザー情報
                            │
                            └─ RPC: get_invitation_data
     │
Accept Inviteクリック ───▶ acceptInvitation           ───▶ /projects
                            │
                            ├─ RPC: accept_invitation
                            └─ setOrganizationIdCookie
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| page.tsx | `frontend/apps/app/app/invitations/tokens/[token]/page.tsx` | ソース | 招待ページのエントリーポイント |
| InvitationPage.tsx | `frontend/apps/app/components/InvitationPage/InvitationPage.tsx` | ソース | 招待ページのメインコンポーネント |
| InvitationCard.tsx | `frontend/apps/app/components/InvitationPage/components/InvitationCard/InvitationCard.tsx` | ソース | 招待カードUIコンポーネント |
| getInvitationData.ts | `frontend/apps/app/components/InvitationPage/services/getInvitationData.ts` | ソース | 招待データ取得サービス |
| acceptInvitation.ts | `frontend/apps/app/components/InvitationPage/components/InvitationCard/actions/acceptInvitation.ts` | ソース | 招待承認サーバーアクション |
| InvitationCard.module.css | `frontend/apps/app/components/InvitationPage/components/InvitationCard/InvitationCard.module.css` | スタイル | 招待カードのスタイル |
| server.ts | `frontend/apps/app/libs/db/server.ts` | ソース | Supabaseクライアント生成 |
| setOrganizationIdCookie.ts | `frontend/apps/app/features/organizations/services/setOrganizationIdCookie.ts` | ソース | 組織IDのCookie設定 |
