# 画面設計書 18-設定-メンバー画面

## 概要

本ドキュメントは、Liam DBアプリケーションにおける設定-メンバー画面の設計仕様を定義する。

### 本画面の処理概要

この画面では、組織メンバーの管理を行う。メンバー一覧の表示、新規メンバーの招待、既存メンバーの削除が可能である。

**業務上の目的・背景**：
組織のコラボレーションを実現するため、メンバー管理機能が必要である。この画面では、組織管理者がチームメンバーを管理し、新しいメンバーを招待してチームを拡大できる。また、不要になったメンバーや保留中の招待を削除することで、アクセス権限を適切に管理する。

**画面へのアクセス方法**：
- グローバルナビゲーションの設定メニューから「Members」を選択
- 設定画面のタブ切り替えでアクセス
- URLパターンは `/settings/members`

**主要な操作・処理内容**：
1. 組織メンバーの一覧表示（検索機能付き）
2. 保留中の招待一覧表示
3. 新規メンバーの招待（メール送信）
4. 既存メンバーの削除
5. 保留中の招待のキャンセル

**画面遷移**：
- 遷移元: 設定-一般画面、設定-プロジェクト画面、設定-請求画面（タブ切り替え）
- 遷移先: 設定-一般画面、設定-プロジェクト画面、設定-請求画面（タブ切り替え）
- 自分自身を削除した場合: 組織一覧画面（`/organizations`）へリダイレクト

**権限による表示制御**：
認証済みユーザーのみアクセス可能。未認証の場合はログイン画面へリダイレクトされる。自分自身かどうかで削除ボタンの表示/動作が変わる。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 33 | 設定画面 | 主機能 | 組織メンバーの管理画面 |
| 26 | 組織管理 | 主機能 | メンバー一覧の取得と表示 |
| 31 | 招待機能 | 補助機能 | 新規メンバーの招待処理 |
| 39 | Supabaseクライアント | API連携 | メンバーおよび招待データの取得・更新 |

## 画面種別

一覧 / 管理

## URL/ルーティング

```
/settings/members
```

## 入出力項目

### 入力項目

| 項目名 | 型 | 必須 | 説明 |
|--------|-----|------|------|
| searchQuery | string | No | メンバー検索クエリ |
| inviteEmail | string | Yes | 招待時のメールアドレス（モーダル内） |

### 出力項目

| 項目名 | 型 | 説明 |
|--------|-----|------|
| members | Member[] | 組織メンバー一覧 |
| invites | Invite[] | 保留中の招待一覧 |
| currentUserId | string | 現在のユーザーID |

## 表示項目

### 検索ヘッダー

| 項目名 | 型 | 説明 |
|--------|-----|------|
| searchInput | SearchInput | "Search Members.." プレースホルダー |
| inviteButton | Button | "Invite" ボタン |

### 組織メンバーセクション

| 項目名 | 型 | 説明 |
|--------|-----|------|
| sectionTitle | h2 | "Organization Members" |
| membersList | MemberItem[] | メンバーアイテム一覧 |

### メンバーアイテム

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | メンバーシップID |
| name | string | ユーザー名 |
| email | string | メールアドレス |
| initial | string | 名前のイニシャル |
| avatarColor | number | アバターの背景色（1-7） |
| avatarUrl | string | アバター画像URL |
| isSelf | boolean | 現在のユーザーかどうか |

### 保留中招待セクション

| 項目名 | 型 | 説明 |
|--------|-----|------|
| sectionTitle | h2 | "Pending Invitations" |
| invitesList | InvitationItem[] | 招待アイテム一覧 |

### 招待アイテム

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | 招待ID |
| email | string | 招待先メールアドレス |
| initial | string | メールアドレスの先頭文字 |
| avatarColor | number | アバターの背景色（1-7） |

## イベント仕様

### 1-メンバー検索

検索入力フィールドにテキストを入力する。

**処理フロー**:
1. `handleSearch`関数がトリガー
2. `searchQuery`状態を更新
3. `filteredMembers`と`filteredInvites`を再計算
4. フィルタリングされた一覧を表示

### 2-招待モーダル表示

「Invite」ボタンをクリックして招待モーダルを開く。

**処理フロー**:
1. `handleInvite`関数がトリガー
2. `isInviteModalOpen`をtrueに設定
3. InviteMemberModalを表示

### 3-メンバー招待

招待モーダルでメールアドレスを入力して送信する。

**処理フロー**:
1. メールアドレスを入力
2. 送信ボタンをクリック
3. Server Actionで招待レコード作成
4. 招待メール送信
5. モーダルを閉じる
6. 一覧を更新

### 4-メンバー削除

メンバーアイテムの削除ボタンをクリックする。

**処理フロー**:
1. 削除ボタンをクリック
2. 確認ダイアログ表示（自分自身の場合は警告付き）
3. `onRemoveSuccess`コールバック
4. 自分自身の場合: `/organizations`へリダイレクト
5. それ以外: `router.refresh()`で一覧更新

### 5-招待キャンセル

招待アイテムの削除ボタンをクリックする。

**処理フロー**:
1. 削除ボタンをクリック
2. invitationsテーブルから削除
3. 一覧を更新

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

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| 画面表示 | organization_members | SELECT | メンバー一覧取得（users JOIN） |
| 画面表示 | invitations | SELECT | 招待一覧取得（users JOIN） |
| メンバー招待 | invitations | INSERT | 新規招待レコード作成 |
| メンバー削除 | organization_members | DELETE | メンバーシップ削除 |
| 招待キャンセル | invitations | DELETE | 招待レコード削除 |

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

#### organization_members（取得）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id | メンバーシップID | - |
| SELECT | joined_at | 参加日時 | - |
| SELECT | users.id | ユーザーID | JOIN |
| SELECT | users.name | ユーザー名 | JOIN |
| SELECT | users.email | メールアドレス | JOIN |
| SELECT | users.avatar_url | アバターURL | JOIN |

取得条件:
- `organization_id = :organizationId`

#### invitations（取得）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id | 招待ID | - |
| SELECT | email | 招待先メールアドレス | - |
| SELECT | invited_at | 招待日時 | - |
| SELECT | invite_by_user_id.id | 招待者ユーザーID | JOIN |
| SELECT | invite_by_user_id.name | 招待者名 | JOIN |
| SELECT | invite_by_user_id.email | 招待者メールアドレス | JOIN |

取得条件:
- `organization_id = :organizationId`

## メッセージ仕様

| メッセージID | 種別 | メッセージ | 表示条件 |
|------------|------|-----------|---------|
| MSG-001 | 情報 | No members found. | メンバー検索結果が空の場合 |
| MSG-002 | 情報 | No pending invitations. | 招待一覧が空の場合 |
| MSG-003 | プレースホルダー | Search Members.. | 検索入力フィールド |

## 例外処理

| 例外条件 | 処理内容 |
|---------|---------|
| 組織ID取得失敗 | ログイン画面へリダイレクト |
| 組織情報取得失敗 | notFound()を呼び出し、404ページを表示 |
| ユーザー情報取得失敗 | ログイン画面へリダイレクト |
| メンバー取得エラー | 空配列を返却（エラーログ出力） |
| 招待取得エラー | 空配列を返却（エラーログ出力） |

## 備考

- サーバーコンポーネントとクライアントコンポーネントのハイブリッド実装
- ToastProviderでラップしてToast通知を有効化
- アバターカラーはユーザーID/メールアドレスからハッシュ計算（1-7の範囲）
- イニシャルは名前またはメールアドレスの先頭文字を大文字化
- 自分自身を削除した場合は組織から離脱し、組織一覧へリダイレクト
- 検索はクライアントサイドでリアルタイムフィルタリング

---

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

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

### 推奨読解順序

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

まず、プログラム間で受け渡されるデータ構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | getMembersAndInvites.ts | `frontend/apps/app/components/OrganizationMembersPage/services/getMembersAndInvites.ts` | Member型とInvite型の定義 |

**読解のコツ**: MemberはjoinedAtとuserオブジェクトを持つ。Inviteはemail, invitedAt, inviteByを持つ。

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

処理の起点となるファイル・関数を特定。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | page.tsx | `frontend/apps/app/app/settings/members/page.tsx` | 認証チェックとOrganizationMembersPage呼び出し |

**主要処理フロー**:
1. **行8-11**: getOrganizationIdで組織ID取得、失敗時はログイン画面へリダイレクト
2. **行17-21**: Supabaseでorganizations情報取得
3. **行23-26**: 組織情報がなければnotFound()
4. **行28**: OrganizationMembersPageをレンダリング

#### Step 3: メインサーバーコンポーネントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | OrganizationMembersPage.tsx | `frontend/apps/app/components/OrganizationMembersPage/OrganizationMembersPage.tsx` | データ取得とクライアントコンポーネント呼び出し |

**主要処理フロー**:
- **行21**: getOrganizationMembersでメンバー一覧取得
- **行22**: getOrganizationInvitesで招待一覧取得
- **行25-26**: supabase.auth.getUser()で現在のユーザー取得
- **行28-30**: ユーザー情報がなければログイン画面へリダイレクト
- **行32**: currentUserIdを抽出
- **行34-40**: ClientSearchWrapperをレンダリング

#### Step 4: クライアントコンポーネントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | ClientSearchWrapper.tsx | `frontend/apps/app/components/OrganizationMembersPage/components/ClientSearchWrapper/ClientSearchWrapper.tsx` | 検索とメンバー表示ロジック |

**主要処理フロー**:
- **行47-49**: 状態管理（searchQuery, loading, isInviteModalOpen）
- **行53-57**: filteredMembers - 名前/メールで検索フィルタ
- **行60-64**: filteredInvites - メール/招待者名で検索フィルタ
- **行66-68**: handleSearch - 検索クエリ更新
- **行70-72**: handleInvite - モーダル表示
- **行74-80**: handleRemoveSuccess - 削除後の処理
- **行83-90**: getAvatarColor - アバター色のハッシュ計算
- **行93-95**: getInitial - イニシャル取得

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

```
page.tsx (エントリーポイント)
    │
    ├─ getOrganizationId()
    │      └─ 認証チェック
    │
    ├─ Supabase (organizations SELECT)
    │
    └─ OrganizationMembersPage.tsx
           │
           ├─ getOrganizationMembers()
           │      └─ Supabase (organization_members + users SELECT)
           │
           ├─ getOrganizationInvites()
           │      └─ Supabase (invitations + users SELECT)
           │
           ├─ supabase.auth.getUser()
           │
           └─ ClientSearchWrapper.tsx
                  │
                  ├─ SearchInput
                  │
                  ├─ InviteMemberModal
                  │
                  ├─ MemberItem (複数)
                  │      └─ 削除機能
                  │
                  └─ InvitationItem (複数)
                         └─ キャンセル機能
```

### データフロー図

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

認証情報 ─────────▶ getOrganizationId ─────▶ organizationId / redirect
                   (認証チェック)

組織ID ───────────▶ getOrganizationMembers ─▶ members[]
                   (Supabase SELECT)

組織ID ───────────▶ getOrganizationInvites ─▶ invites[]
                   (Supabase SELECT)

認証情報 ─────────▶ getUser ──────────────▶ currentUserId
                   (Supabase Auth)

検索クエリ ───────▶ フィルタリング ─────────▶ filteredMembers
                   (クライアント処理)         filteredInvites

削除操作 ─────────▶ handleRemoveSuccess ───▶ refresh / redirect
                   (コールバック)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| page.tsx | `frontend/apps/app/app/settings/members/page.tsx` | ソース | ページエントリーポイント |
| OrganizationMembersPage.tsx | `frontend/apps/app/components/OrganizationMembersPage/OrganizationMembersPage.tsx` | ソース | サーバーコンポーネント |
| ClientSearchWrapper.tsx | `frontend/apps/app/components/OrganizationMembersPage/components/ClientSearchWrapper/ClientSearchWrapper.tsx` | ソース | クライアントコンポーネント |
| getMembersAndInvites.ts | `frontend/apps/app/components/OrganizationMembersPage/services/getMembersAndInvites.ts` | ソース | データ取得サービス |
| MemberItem.tsx | `frontend/apps/app/components/OrganizationMembersPage/components/MemberItem/MemberItem.tsx` | ソース | メンバー表示コンポーネント |
| InvitationItem.tsx | `frontend/apps/app/components/OrganizationMembersPage/components/InvitationItem/InvitationItem.tsx` | ソース | 招待表示コンポーネント |
| InviteMemberModal.tsx | `frontend/apps/app/components/OrganizationMembersPage/components/InviteMemberModal/InviteMemberModal.tsx` | ソース | 招待モーダル |
| SearchInput.tsx | `frontend/apps/app/components/OrganizationMembersPage/components/SearchInput/SearchInput.tsx` | ソース | 検索入力コンポーネント |
| ClientSearchWrapper.module.css | `frontend/apps/app/components/OrganizationMembersPage/components/ClientSearchWrapper/ClientSearchWrapper.module.css` | スタイル | スタイル定義 |
