# 画面設計書 17-メンバー一覧画面

## 概要

本ドキュメントは、Ghost管理画面におけるメンバー一覧画面の設計仕様を定義するものである。

### 本画面の処理概要

メンバー一覧画面は、サイトに登録されているメンバー（購読者）を一覧表示し、管理するための画面である。高度なフィルタリング機能、検索機能、一括操作機能を備え、大量のメンバーを効率的に管理できる。

**業務上の目的・背景**：メンバーシップ機能はGhostの収益化の核となる機能である。本画面により、無料会員・有料会員の管理、ラベルによる分類、メールマーケティングのためのセグメント分析が可能となる。

**画面へのアクセス方法**：サイドバーの「Members」メニューをクリック、またはURL `/ghost/#/members` で直接アクセス可能。

**主要な操作・処理内容**：
1. メンバー一覧の表示（仮想スクロール対応）
2. 検索によるメンバーの絞り込み
3. 高度なフィルター機能（NQL対応）
4. メンバーのインポート/エクスポート
5. 一括ラベル追加/削除
6. 一括購読解除
7. 一括削除
8. 新規メンバー作成

**画面遷移**：
- 遷移元: サイドバーメニュー、投稿分析画面
- 遷移先: メンバー詳細画面、メンバー新規作成画面、メンバーインポート画面

**権限による表示制御**：
- Author/Contributor: 本画面にアクセス不可（ホームへリダイレクト）
- Editor/Administrator/Owner: 全機能利用可能

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|-------------------|
| 4 | メンバー管理 | 主機能 | メンバーの一覧表示・検索・フィルター |
| 5 | ラベル管理 | 関連機能 | ラベルによるフィルター、一括ラベル操作 |

## 画面種別

一覧

## URL/ルーティング

| 項目 | 値 |
|------|-----|
| URL | `/ghost/#/members` |
| ルート名 | members |
| ルートファイル | `ghost/admin/app/routes/members.js` |
| コントローラファイル | `ghost/admin/app/controllers/members.js` |
| テンプレートファイル | `ghost/admin/app/templates/members.hbs` |

### クエリパラメータ

| パラメータ名 | 型 | デフォルト値 | 説明 |
|-------------|-----|-------------|------|
| search | string | '' | 検索キーワード |
| paid | string | null | 支払い状態フィルター（null/true/false） |
| label | string | null | ラベルスラッグ |
| order | string | null | 並び順（null/email_open_rate） |
| filter | string | null | NQL形式のフィルター文字列 |
| post | string | null | 投稿分析からの遷移用投稿ID |

## 入出力項目

### 入力項目

| 項目名 | 項目ID | 必須 | 型 | 説明 |
|--------|--------|------|-----|------|
| 検索キーワード | search | - | string | メンバー名・メールアドレスで検索 |
| 支払い状態 | paid | - | select | All/Free/Paid |
| ラベル | label | - | select | ラベルによるフィルター |
| 並び順 | order | - | select | Newest/Open rate |
| フィルター | filter | - | NQL | 高度なフィルター条件 |

## 表示項目

### ヘッダー部

| 項目名 | 説明 |
|--------|------|
| パンくずリスト | 投稿分析からの遷移時のみ表示（Posts > Analytics > Members） |
| タイトル | 「Members」固定 |
| 検索バー | メンバー検索用入力欄 |
| フィルターボタン | 高度なフィルター設定 |
| アクションメニュー | インポート/エクスポート/一括操作 |
| 新規メンバーボタン | 「New member」ボタン |

### メンバー一覧テーブル

| 項目名 | フィールド | 説明 |
|--------|-----------|------|
| ヘッダー | listHeader | 件数表示または「Loading...」「Search result」 |
| ステータス | status | Free/Paid/Comped |
| 開封率 | emailOpenRate | メール開封率（%）※設定有効時のみ |
| 位置情報 | geolocation | 国・地域 |
| 作成日 | createdAtUTC | 登録日時 |
| 動的カラム | filterColumns | フィルターに応じた追加カラム（最大2列） |

### メンバー行項目

| 項目名 | フィールド | 説明 |
|--------|-----------|------|
| アバター | avatarImage | メンバーアバター画像 |
| 名前 | name | メンバー名（未設定時はメールアドレス） |
| メールアドレス | email | メールアドレス |
| ティア | tiers | 購読中のティア一覧 |

### 空状態表示

| 条件 | 表示内容 |
|------|---------|
| メンバーなし | 「Get started with memberships」ヘルプセクション |
| フィルター結果なし | 「No members match the current filter」メッセージ |

### ヘルプセクション（メンバー6件未満時）

| 項目 | 説明 |
|------|------|
| ガイド1 | 「Building your audience with subscriber signups」 |
| ガイド2 | 「Get your first 100 email subscribers」 |

## イベント仕様

### 1-検索

- 処理: searchTask（250msデバウンス）でsearchParamを更新
- API: `GET /members?search={keyword}`

### 2-フィルター適用

- 処理: applyFilter()でfilterParamを更新し、fetchMembersTask実行
- フィルターグループ: Basic, Newsletters, Subscription, Email
- 対応フィルター: name, email, label, subscribed, last_seen, created_at, tier, status等

### 3-並び順変更

- 処理: changeOrder()でorderParamを更新
- オプション: Newest（created_at desc）, Open rate（email_open_rate desc）

### 4-メンバーインポート

- 処理: members.importルートへ遷移
- 条件: membersSignupAccess !== 'none' の場合のみ表示

### 5-メンバーエクスポート

- 処理: exportData()でCSVダウンロード
- API: `GET /members/upload?{currentFilters}&limit=all`
- ファイル名: `members.{YYYY-MM-DD}.csv`

### 6-一括ラベル追加

- 処理: bulkAddLabel()でBulkAddMembersLabelModalを開く
- 条件: フィルター適用中かつメンバーが存在する場合

### 7-一括ラベル削除

- 処理: bulkRemoveLabel()でBulkRemoveMembersLabelModalを開く
- 条件: フィルター適用中かつメンバーが存在する場合

### 8-一括購読解除

- 処理: bulkUnsubscribe()でBulkUnsubscribeMembersModalを開く
- 条件: フィルター適用中かつmembersSignupAccess !== 'none'

### 9-一括削除

- 処理: bulkDelete()でBulkDeleteMembersModalを開く
- 条件: isBulkDeletePermitted = true（特定のStripeフィルター未使用時）
- 削除後: フィルターリセット、メンバーリロード、統計更新

### 10-新規メンバー作成

- 処理: member.newルートへ遷移
- 条件: membersSignupAccess !== 'none' の場合のみ表示

### 11-メンバー行クリック

- 処理: memberルートへ遷移
- 遷移先URL: `#/members/{member_id}`

### 12-仮想スクロール

- 処理: ellaSparseによる仮想スクロールとページネーション
- 設定: estimateHeight=75, staticHeight=true, bufferSize=5, limit=50

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

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| メンバー一覧表示 | members | SELECT | メンバーデータの取得 |
| メンバーエクスポート | members | SELECT | 全メンバーのCSV出力 |
| 一括ラベル追加 | members_labels | INSERT | ラベル関連付け追加 |
| 一括ラベル削除 | members_labels | DELETE | ラベル関連付け削除 |
| 一括購読解除 | members_newsletters | DELETE | ニュースレター購読解除 |
| 一括削除 | members | DELETE | メンバー削除 |

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

#### members

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, name, email, status, created_at等 | フィルター条件 | include=labels,tiers |

## メッセージ仕様

| メッセージID | 種別 | 条件 | メッセージ内容 |
|-------------|------|------|---------------|
| MSG-01 | 表示 | ローディング中 | Loading... |
| MSG-02 | 表示 | 検索中 | Search result |
| MSG-03 | 表示 | フィルター結果なし | No members match the current filter |
| MSG-04 | 表示 | ヘルプタイトル | Get started with memberships |

## 例外処理

| 例外ケース | 処理内容 |
|-----------|---------|
| 権限不足（canManageMembers=false） | ホーム画面へリダイレクト |
| API通信エラー | エラー通知表示 |
| 一括削除不可（Stripeフィルター使用時） | 削除ボタン非表示 |

## 備考

- ellaSparseサービスによる仮想スクロールで大量データに対応
- fetchMembersTaskは1分以内の再アクセス時はキャッシュを使用
- NQLフィルターはURLパラメータとして保存され、共有可能
- 一括削除は特定のStripeサブスクリプション関連フィルター使用時は無効化される
- 投稿分析画面（postAnalytics）からの遷移時はパンくずリストが表示される

---

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

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

### 推奨読解順序

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

メンバーモデルの属性定義を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | member.js | `ghost/admin/app/models/member.js` | Ember Dataモデル定義 |

**主要処理フロー**:
- **9-28行目**: メンバーの属性定義（name, email, status, subscriptions, geolocation等）
- **29行目**: labels hasMany関連
- **46-52行目**: fetchSigninUrl - サインインURL取得タスク
- **54-57行目**: logoutAllDevices - 全デバイスログアウトタスク

#### Step 2: ルートを理解する

権限チェックとデータフェッチの仕組みを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | members-management.js | `ghost/admin/app/routes/members-management.js` | 権限チェック基底クラス |
| 2-2 | members.js | `ghost/admin/app/routes/members.js` | クエリパラメータ、model() |

**主要処理フロー（members-management.js）**:
- **6-8行目**: canManageMembersチェック、不可時はhomeへリダイレクト

**主要処理フロー（members.js）**:
- **9-16行目**: queryParams定義（label, search, paid, order, filter, post）
- **18-21行目**: model() - resetFiltersとfetchMembersTask実行
- **24-37行目**: setupController - fetchLabelsTask実行
- **39-50行目**: resetController - postAnalytics時のフィルタークリア
- **52-57行目**: buildRouteInfoMetadata - titleToken='Members'

#### Step 3: コントローラーを理解する

一覧表示と各種アクションの実装を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | members.js | `ghost/admin/app/controllers/members.js` | メインロジック |

**主要処理フロー**:
- **18-27行目**: PAID_PARAMS定義（All/Free/Paid）
- **52-73行目**: tracked状態（members, searchParam, filters等）
- **91-113行目**: listHeader computed - 件数表示ロジック
- **125-140行目**: availableOrders - 並び順オプション
- **177-179行目**: isFiltered - フィルター適用判定
- **227-246行目**: isBulkDeletePermitted - 一括削除可否判定
- **255-291行目**: getApiQueryObject - APIクエリ生成
- **374-409行目**: exportData - CSVエクスポート
- **437-458行目**: bulk操作（ラベル追加/削除、購読解除）
- **467-478行目**: bulkDelete - 一括削除
- **487-491行目**: searchTask - 250msデバウンス検索
- **498-551行目**: fetchMembersTask - ellaSparseによるページネーション

#### Step 4: テンプレートを理解する

UI構造とデータバインディングを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | members.hbs | `ghost/admin/app/templates/members.hbs` | メインテンプレート |
| 4-2 | list-item.hbs | `ghost/admin/app/components/members/list-item.hbs` | 行コンポーネント |

**主要処理フロー（members.hbs）**:
- **4-15行目**: パンくずリスト（postAnalytics時）
- **22-34行目**: 検索バー
- **38-48行目**: Members::Filterコンポーネント
- **49-118行目**: アクションドロップダウン
- **126-165行目**: メンバーテーブル（VerticalCollection使用）
- **168-178行目**: 空状態表示

**主要処理フロー（list-item.hbs）**:
- **2-12行目**: 名前・アバター表示
- **31-38行目**: 開封率表示（newsletterEnabled時）
- **41-55行目**: 位置情報表示
- **57-62行目**: 作成日表示

#### Step 5: フィルターコンポーネントを理解する

高度なフィルター機能の実装を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | filter.js | `ghost/admin/app/components/members/filter.js` | フィルターロジック |

**主要処理フロー**:
- **15-58行目**: FILTER_GROUPS定義（Basic, Newsletters, Subscription, Email）
- **74-150行目**: Filter class - フィルター条件オブジェクト
- **295-339行目**: generateNqlFilter - NQLクエリ生成
- **341-351行目**: parseNqlFilterString - NQLパース

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

```
MembersManagementRoute (ghost/admin/app/routes/members-management.js)
    │
    └─ beforeModel() - 権限チェック
           └─ !canManageMembers → home へリダイレクト

MembersRoute (ghost/admin/app/routes/members.js)
    │
    ├─ model(params)
    │      ├─ controller.resetFilters(params)
    │      └─ controller.fetchMembersTask.perform(params)
    │
    └─ setupController()
           └─ controller.fetchLabelsTask.perform()

MembersController (ghost/admin/app/controllers/members.js)
    │
    ├─ fetchMembersTask
    │      └─ ellaSparse.array()
    │             └─ store.query('member', {include: 'labels,tiers', ...})
    │
    ├─ searchTask
    │      └─ 250ms debounce → searchParam更新
    │
    ├─ exportData()
    │      └─ fetch(/members/upload?...) → Blob → ダウンロード
    │
    ├─ bulkAddLabel()
    │      └─ modals.open(BulkAddMembersLabelModal)
    │
    ├─ bulkRemoveLabel()
    │      └─ modals.open(BulkRemoveMembersLabelModal)
    │
    ├─ bulkUnsubscribe()
    │      └─ modals.open(BulkUnsubscribeMembersModal)
    │
    └─ bulkDelete()
           └─ modals.open(BulkDeleteMembersModal)
                  └─ onComplete: store.unloadAll, router.transitionTo

members.hbs (テンプレート)
    │
    ├─ Members::Filter
    │      └─ フィルター条件の編集・適用
    │
    ├─ GhDropdown (アクションメニュー)
    │      ├─ Import members → members.import
    │      ├─ Export members → exportData()
    │      ├─ Add label → bulkAddLabel()
    │      ├─ Remove label → bulkRemoveLabel()
    │      ├─ Unsubscribe → bulkUnsubscribe()
    │      └─ Delete → bulkDelete()
    │
    └─ VerticalCollection
           └─ Members::ListItem
                  └─ LinkTo member route
```

### データフロー図

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

URLクエリパラメータ ───────▶ queryParams定義 ─────────────────▶ コントローラープロパティ
(?search=&paid=&filter=)              │
                                      ▼
                            resetFilters(params)
                                      │
                                      ▼
                            fetchMembersTask.perform()
                                      │
                                      ▼
                            getApiQueryObject()
                                      │
                                      ├─ label → filter: label:'{slug}'
                                      ├─ paidParam → filter: status:-free or status:free
                                      └─ filterParam → filter: {nql}
                                      │
                                      ▼
                            ellaSparse.array()
                                      │
                                      ▼
                            store.query('member', {
                                include: 'labels,tiers',
                                order: 'created_at desc',
                                limit: 50,
                                page: {n},
                                filter: {combined}
                            })
                                      │
                                      ▼
                            API GET /members ──────────────────▶ メンバー配列
                                                                     │
                                                                     ▼
                                                              VerticalCollection
                                                                     │
                                                                     ▼
                                                              Members::ListItem
                                                                     │
                                                                     ▼
                                                              テーブル行表示
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| members-management.js | `ghost/admin/app/routes/members-management.js` | ルート | 権限チェック基底クラス |
| members.js | `ghost/admin/app/routes/members.js` | ルート | メンバー一覧ルート |
| members.js | `ghost/admin/app/controllers/members.js` | コントローラー | メインロジック |
| members.hbs | `ghost/admin/app/templates/members.hbs` | テンプレート | UIテンプレート |
| member.js | `ghost/admin/app/models/member.js` | モデル | メンバーモデル |
| list-item.hbs | `ghost/admin/app/components/members/list-item.hbs` | コンポーネント | 行コンポーネント |
| list-item.js | `ghost/admin/app/components/members/list-item.js` | コンポーネント | 行ロジック |
| filter.js | `ghost/admin/app/components/members/filter.js` | コンポーネント | フィルターロジック |
| filter.hbs | `ghost/admin/app/components/members/filter.hbs` | コンポーネント | フィルターUI |
| bulk-add-label.js | `ghost/admin/app/components/members/modals/bulk-add-label.js` | モーダル | 一括ラベル追加 |
| bulk-remove-label.js | `ghost/admin/app/components/members/modals/bulk-remove-label.js` | モーダル | 一括ラベル削除 |
| bulk-unsubscribe.js | `ghost/admin/app/components/members/modals/bulk-unsubscribe.js` | モーダル | 一括購読解除 |
| bulk-delete.js | `ghost/admin/app/components/members/modals/bulk-delete.js` | モーダル | 一括削除 |
