# 機能設計書 26-組織管理

## 概要

本ドキュメントは、Liam DBシステムにおける組織管理機能の設計を記述するものである。本機能は、マルチテナント環境での組織単位のリソース管理とメンバーシップ管理を実現する。

### 本機能の処理概要

組織管理機能は、組織の作成・取得、メンバーの管理（招待・承認・削除）、組織の切り替えを提供する。Cookie ベースの組織ID管理により、ユーザーが複数の組織に所属している場合でも、現在操作対象の組織を明確に識別できる。

**業務上の目的・背景**：Liam DB はチーム向けのデータベース設計ツールであり、組織単位でプロジェクトやデザインセッションを管理する必要がある。組織管理により、適切なアクセス制御とリソースの分離を実現する。

**機能の利用シーン**：
- 初回ログイン時に自動で個人組織が作成される
- 設定画面からメンバーを招待し、チームで作業できるようにする
- 招待されたユーザーが招待リンクから組織に参加する
- 複数組織に所属するユーザーが組織を切り替える

**主要な処理内容**：
1. 組織の作成（createOrganization）
2. 組織一覧の取得（getOrganizations）
3. 組織IDの取得（Cookie優先、フォールバックでDB）
4. メンバー招待（inviteMember、RPCでアトミック処理）
5. 招待の承認（acceptInvitation）
6. メンバー・招待の削除（removeMember、removeInvitation）

**関連システム・外部連携**：Supabase PostgreSQL、Resend（招待メール送信）

**権限による制御**：組織メンバーのみが該当組織のリソースにアクセス可能。RLS（Row Level Security）で強制。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 3 | 組織一覧画面 | 主画面 | 所属組織の一覧表示、組織切り替え |
| 4 | 組織メンバー画面 | 主画面 | メンバー一覧表示、招待、削除 |
| 5 | 招待承認画面 | 主画面 | 招待の承認処理 |

## 機能種別

データ管理 / マルチテナント管理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes（組織作成時） | 組織名 | - |
| email | string | Yes（招待時） | 招待先メールアドレス | valibot email() |
| organizationId | string | Yes（各操作時） | 組織の識別子 | UUID形式 |
| memberId | string | Yes（メンバー削除時） | メンバーシップID | UUID形式 |
| invitationId | string | Yes（招待削除時） | 招待ID | UUID形式 |
| token | string | Yes（招待承認時） | 招待トークン | - |

### 入力データソース

- フォーム入力（組織名、メールアドレス）
- Cookie（organizationId）
- URL パラメータ（招待トークン）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| organizationId | string | 作成された組織のID |
| organizations | Organization[] | ユーザーが所属する組織一覧 |
| members | Member[] | 組織メンバー一覧 |
| invites | Invite[] | 保留中の招待一覧 |
| invitation_token | string | 生成された招待トークン |

### 出力先

- Cookie（organizationId）
- Supabase データベース（organizations、organization_members、invitations）
- メール（招待メール）

## 処理フロー

### 処理シーケンス

```
1. 組織作成
   └─ createOrganization(name)
   └─ organizations テーブルに INSERT
   └─ organization_members テーブルに INSERT
2. 組織ID取得
   └─ getOrganizationId()
   └─ Cookie から取得を試行（getOrganizationIdFromCookie）
   └─ 失敗時は DB から取得（getOrganizationIdFromDatabase）
3. メンバー招待
   └─ inviteMember(formData)
   └─ invite_organization_member RPC（アトミック）
   └─ sendInvitationEmail（Resend）
4. 招待承認
   └─ acceptInvitation(formData)
   └─ accept_invitation RPC（アトミック）
   └─ setOrganizationIdCookie
5. メンバー削除
   └─ removeMember(formData)
   └─ organization_members から DELETE
6. 招待削除
   └─ removeInvitation(formData)
   └─ invitations から DELETE
```

### フローチャート

```mermaid
flowchart TD
    A[ユーザー操作] --> B{操作種別}
    B -->|組織作成| C[createOrganization]
    B -->|メンバー招待| D[inviteMember]
    B -->|招待承認| E[acceptInvitation]
    B -->|メンバー削除| F[removeMember]
    B -->|招待削除| G[removeInvitation]
    C --> H[organizations INSERT]
    H --> I[organization_members INSERT]
    D --> J[invite_organization_member RPC]
    J --> K[sendInvitationEmail]
    E --> L[accept_invitation RPC]
    L --> M[setOrganizationIdCookie]
    M --> N[redirect /projects]
    F --> O[organization_members DELETE]
    G --> P[invitations DELETE]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-26-01 | 組織自動作成 | 初回ログイン時に自動で組織を作成 | 所属組織がない場合 |
| BR-26-02 | 組織名生成 | ユーザー名から「{name}'s Organization」形式で生成 | 自動作成時 |
| BR-26-03 | 招待アトミック処理 | 招待処理は RPC でアトミックに実行 | inviteMember |
| BR-26-04 | 組織ID優先順位 | Cookie > Database の順で取得 | getOrganizationId |

### 計算ロジック

- 組織名生成: `generateOrganizationNameFromUser`
  - full_name > name > email.split('@')[0] > 'User' の優先順位
  - `${username}'s Organization` 形式

## データベース操作仕様

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| createOrganization | organizations | INSERT | 組織作成 |
| createOrganization | organization_members | INSERT | メンバー追加 |
| getOrganizations | organization_members | SELECT | 所属組織取得 |
| getOrganizationId | organization_members | SELECT | 組織ID取得 |
| getOrganizationMembers | organization_members | SELECT | メンバー一覧取得 |
| getOrganizationInvites | invitations | SELECT | 招待一覧取得 |
| inviteMember | - | RPC | invite_organization_member |
| acceptInvitation | - | RPC | accept_invitation |
| removeMember | organization_members | DELETE | メンバー削除 |
| removeInvitation | invitations | DELETE | 招待削除 |

### テーブル別操作詳細

#### organizations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | name | フォーム入力値 | createOrganization |

#### organization_members

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | user_id, organization_id | 認証ユーザーID、組織ID | 作成時 |
| SELECT | organization_id | user_id = 認証ユーザーID | 所属確認 |
| DELETE | id, organization_id | フォーム入力値 | メンバー削除 |

#### invitations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, email, invited_at | organization_id | 招待一覧 |
| DELETE | id | フォーム入力値 | 招待削除 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| create_org_failed | 作成エラー | 組織作成 INSERT 失敗 | エラーメッセージ返却 |
| add_member_failed | メンバー追加エラー | organization_members INSERT 失敗 | エラーメッセージ返却 |
| invite_failed | 招待エラー | invite_organization_member RPC 失敗 | エラーメッセージ返却 |
| email_failed | メール送信エラー | Resend API 失敗 | エラーメッセージ返却 |
| accept_failed | 承認エラー | accept_invitation RPC 失敗 | エラーメッセージ返却 |

### リトライ仕様

- 招待メール送信失敗時は成功ステータスを返さない
- ユーザーは再度招待操作を行う

## トランザクション仕様

- inviteMember: `invite_organization_member` RPC でアトミック処理
- acceptInvitation: `accept_invitation` RPC でアトミック処理
- createOrganization: 現時点では個別 INSERT（要改善点としてコメント記載）

## パフォーマンス要件

- getOrganizationId: Cookie から取得できれば DB アクセス不要
- getOrganizationMembers/Invites: 組織ID でインデックス検索

## セキュリティ考慮事項

- Row Level Security (RLS) で組織単位のアクセス制御
- 招待トークンはランダム生成
- 招待メールは Resend API で送信（RESEND_API_KEY 必須）

## 備考

組織作成時のトランザクション処理は現状未実装。要件確定後に DB 側のトリガーでの制約実装を検討。

---

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

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

### 推奨読解順序

#### Step 1: 組織ID管理の仕組みを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | constants.ts | `frontend/apps/app/features/organizations/constants.ts` | ORGANIZATION_ID_KEY 定数 |
| 1-2 | getOrganizationIdFromCookie.ts | `frontend/apps/app/features/organizations/services/getOrganizationIdFromCookie.ts` | Cookie からの取得 |
| 1-3 | getOrganizationId.ts | `frontend/apps/app/features/organizations/services/getOrganizationId.ts` | Cookie 優先、DB フォールバック |

**読解のコツ**: neverthrow の ResultAsync パターンを理解すると読みやすい。

#### Step 2: 組織作成フローを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | generateOrganizationNameFromUser.ts | `frontend/apps/app/features/organizations/services/generateOrganizationNameFromUser.ts` | 組織名自動生成 |
| 2-2 | createOrganizations.ts | `frontend/apps/app/components/OrganizationNewPage/actions/createOrganizations.ts` | 組織作成処理 |
| 2-3 | setOrganizationIdCookie.ts | `frontend/apps/app/features/organizations/services/setOrganizationIdCookie.ts` | Cookie 設定 |

**主要処理フロー**:
- **9-28行目（createOrganizations.ts）**: organizations INSERT → organization_members INSERT
- **17-27行目（generateOrganizationNameFromUser.ts）**: ユーザーメタデータから名前取得

#### Step 3: メンバー管理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | getMembersAndInvites.ts | `frontend/apps/app/components/OrganizationMembersPage/services/getMembersAndInvites.ts` | メンバー・招待一覧取得 |
| 3-2 | OrganizationMembersPage.tsx | `frontend/apps/app/components/OrganizationMembersPage/OrganizationMembersPage.tsx` | メンバー画面 |

**主要処理フロー**:
- **3-33行目（getMembersAndInvites.ts）**: getOrganizationMembers でメンバー取得
- **35-67行目**: getOrganizationInvites で招待取得

#### Step 4: 招待フローを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | inviteMember.ts | `frontend/apps/app/components/OrganizationMembersPage/components/InviteMemberModal/actions/inviteMember.ts` | 招待処理 |
| 4-2 | sendInvitationEmail.ts | `frontend/apps/app/components/OrganizationMembersPage/components/InviteMemberModal/actions/sendInvitationEmail.ts` | 招待メール送信 |
| 4-3 | acceptInvitation.ts | `frontend/apps/app/components/InvitationPage/components/InvitationCard/actions/acceptInvitation.ts` | 招待承認 |
| 4-4 | getInvitationData.ts | `frontend/apps/app/components/InvitationPage/services/getInvitationData.ts` | 招待データ取得 |

**主要処理フロー**:
- **34-96行目（inviteMember.ts）**: RPC invite_organization_member 呼び出し → メール送信
- **33-97行目（acceptInvitation.ts）**: RPC accept_invitation 呼び出し → Cookie 設定 → リダイレクト

#### Step 5: 削除処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | removeMember.ts | `frontend/apps/app/components/OrganizationMembersPage/components/DeleteMemberModal/actions/removeMember.ts` | メンバー削除 |
| 5-2 | removeInvitation.ts | `frontend/apps/app/components/OrganizationMembersPage/components/DeleteInvitationModal/actions/removeInvitation.ts` | 招待削除 |

**主要処理フロー**:
- **12-50行目（removeMember.ts）**: organization_members から DELETE
- **12-48行目（removeInvitation.ts）**: invitations から DELETE

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

```
[組織ID取得]
getOrganizationId
    │
    ├─ getOrganizationIdFromCookie
    │      └─ cookies().get(ORGANIZATION_ID_KEY)
    │
    └─ getOrganizationIdFromDatabase (フォールバック)
           ├─ supabase.auth.getUser
           └─ supabase.from('organization_members').select

[組織作成]
createOrganization
    │
    ├─ supabase.from('organizations').insert
    │
    ├─ supabase.auth.getUser
    │
    └─ supabase.from('organization_members').insert

[メンバー招待]
inviteMember
    │
    ├─ v.safeParse (validation)
    │
    ├─ supabase.rpc('invite_organization_member')
    │
    └─ sendInvitationEmail
           ├─ supabase.from('organizations').select
           └─ resend.emails.send

[招待承認]
acceptInvitation
    │
    ├─ v.safeParse (validation)
    │
    ├─ supabase.auth.getUser
    │
    ├─ supabase.rpc('accept_invitation')
    │
    ├─ setOrganizationIdCookie
    │
    └─ redirect(/projects)
```

### データフロー図

```
[組織作成フロー]
ユーザー入力(name) ───────▶ createOrganization
       │                        │
       │                        ├─ organizations INSERT
       │                        │
       │                        └─ organization_members INSERT
       │                               │
       └───────────────────────────────▶ organizationId

[招待フロー]
email, organizationId ──────▶ inviteMember
       │                        │
       │                        ├─ invite_organization_member RPC
       │                        │        │
       │                        │        └─ invitation_token
       │                        │
       │                        └─ sendInvitationEmail
       │                               │
       └───────────────────────────────▶ 招待メール

[承認フロー]
token ─────────────────────▶ acceptInvitation
       │                        │
       │                        ├─ accept_invitation RPC
       │                        │        │
       │                        │        └─ organization_members INSERT
       │                        │
       │                        ├─ setOrganizationIdCookie
       │                        │
       └───────────────────────▶ redirect /projects
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| constants.ts | `frontend/apps/app/features/organizations/constants.ts` | ソース | 定数定義 |
| getOrganizationId.ts | `frontend/apps/app/features/organizations/services/getOrganizationId.ts` | ソース | 組織ID取得 |
| getOrganizationIdFromCookie.ts | `frontend/apps/app/features/organizations/services/getOrganizationIdFromCookie.ts` | ソース | Cookie取得 |
| setOrganizationIdCookie.ts | `frontend/apps/app/features/organizations/services/setOrganizationIdCookie.ts` | ソース | Cookie設定 |
| clearOrganizationIdCookie.ts | `frontend/apps/app/features/organizations/services/clearOrganizationIdCookie.ts` | ソース | Cookieクリア |
| generateOrganizationNameFromUser.ts | `frontend/apps/app/features/organizations/services/generateOrganizationNameFromUser.ts` | ソース | 組織名生成 |
| createOrganizations.ts | `frontend/apps/app/components/OrganizationNewPage/actions/createOrganizations.ts` | ソース | 組織作成 |
| getOrganizations.ts | `frontend/apps/app/components/OrganizationsPage/getOrganizations.ts` | ソース | 組織一覧取得 |
| getMembersAndInvites.ts | `frontend/apps/app/components/OrganizationMembersPage/services/getMembersAndInvites.ts` | ソース | メンバー・招待取得 |
| inviteMember.ts | `frontend/apps/app/components/OrganizationMembersPage/components/InviteMemberModal/actions/inviteMember.ts` | ソース | メンバー招待 |
| sendInvitationEmail.ts | `frontend/apps/app/components/OrganizationMembersPage/components/InviteMemberModal/actions/sendInvitationEmail.ts` | ソース | 招待メール送信 |
| removeMember.ts | `frontend/apps/app/components/OrganizationMembersPage/components/DeleteMemberModal/actions/removeMember.ts` | ソース | メンバー削除 |
| removeInvitation.ts | `frontend/apps/app/components/OrganizationMembersPage/components/DeleteInvitationModal/actions/removeInvitation.ts` | ソース | 招待削除 |
| acceptInvitation.ts | `frontend/apps/app/components/InvitationPage/components/InvitationCard/actions/acceptInvitation.ts` | ソース | 招待承認 |
| getInvitationData.ts | `frontend/apps/app/components/InvitationPage/services/getInvitationData.ts` | ソース | 招待データ取得 |
| OrganizationMembersPage.tsx | `frontend/apps/app/components/OrganizationMembersPage/OrganizationMembersPage.tsx` | ソース | メンバー画面 |
