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

## 概要

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

### 本画面の処理概要

メンバー詳細画面は、個別のメンバー（購読者）情報を表示・編集するための画面である。メンバーの基本情報、ラベル、ニュースレター購読設定、サブスクリプション情報、アクティビティフィードを管理できる。新規メンバー作成画面としても使用される。

**業務上の目的・背景**：個々のメンバーの詳細情報を確認し、必要に応じて情報の更新、サブスクリプションの管理、メンバーのなりすまし（Impersonate）によるデバッグが可能。

**画面へのアクセス方法**：メンバー一覧画面でメンバー行をクリック、またはURL `/ghost/#/members/{member_id}` で直接アクセス可能。新規作成は `/ghost/#/members/new`。

**主要な操作・処理内容**：
1. メンバー基本情報の表示・編集（名前、メールアドレス、メモ）
2. ラベルの追加・削除
3. ニュースレター購読設定の管理
4. サブスクリプションの表示・管理（キャンセル、継続）
5. 無料コンプリメンタリーサブスクリプションの付与・削除
6. メンバーのなりすまし（Impersonate）
7. 全デバイスからのサインアウト
8. メンバー削除

**画面遷移**：
- 遷移元: メンバー一覧画面、投稿分析画面
- 遷移先: メンバー一覧画面、Stripeダッシュボード（外部）

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

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|-------------------|
| 4 | メンバー管理 | 主機能 | メンバーの詳細表示・編集 |
| 5 | ラベル管理 | 関連機能 | ラベルの追加・削除 |

## 画面種別

詳細/編集

## URL/ルーティング

| 項目 | 値 |
|------|-----|
| URL（既存） | `/ghost/#/members/{member_id}` |
| URL（新規） | `/ghost/#/members/new` |
| ルート名 | member, member.new |
| ルートファイル | `ghost/admin/app/routes/member.js` |
| コントローラファイル | `ghost/admin/app/controllers/member.js` |
| テンプレートファイル | `ghost/admin/app/templates/member.hbs` |

### クエリパラメータ

| パラメータ名 | 型 | デフォルト値 | 説明 |
|-------------|-----|-------------|------|
| post | string | null | 投稿分析からの遷移用投稿ID |

## 入出力項目

### 入力項目

| 項目名 | 項目ID | 必須 | 型 | 説明 |
|--------|--------|------|-----|------|
| 名前 | name | - | string | メンバー名 |
| メールアドレス | email | 必須 | string | メンバーのメールアドレス |
| メモ | note | - | string | 管理者用メモ（最大500文字） |
| ラベル | labels | - | array | メンバーに付与するラベル |
| ニュースレター | newsletters | - | array | 購読するニュースレター |

## 表示項目

### ヘッダー部

| 項目名 | 説明 |
|--------|------|
| パンくずリスト | 通常: Members > (New member / Edit member)、分析から: Posts > Analytics > Members > (Edit member) |
| タイトル | 新規: 「New member」、既存: メンバー名またはメールアドレス |
| アクションメニュー | Impersonate、Sign out、Delete（既存メンバーのみ） |
| 保存ボタン | saveTask実行ボタン |

### メンバー詳細セクション（GhMemberDetails）

| 項目名 | フィールド | 説明 |
|--------|-----------|------|
| アバター | avatarImage | メンバーアバター |
| 登録日 | createdAtUTC | 「D MMM YYYY (X ago)」形式 |
| 最終閲覧日 | lastSeenAtUTC | 最後にサイトを閲覧した日時 |
| メール開封率 | emailOpenRate | メール開封率（%） |
| 位置情報 | geolocation | 国・地域 |

### 基本情報フォーム（GhMemberSettingsForm）

| 項目名 | フィールド | 説明 |
|--------|-----------|------|
| 名前入力 | name | テキスト入力、新規時フォーカス |
| メールアドレス入力 | email | テキスト入力、必須 |
| ラベル選択 | labels | GhMemberLabelInputコンポーネント |
| メモ入力 | note | テキストエリア、500文字制限 |

### ニュースレター設定（Member::NewsletterPreference）

| 項目名 | 説明 |
|--------|------|
| ニュースレター一覧 | 購読可能なニュースレターのチェックリスト |

### サブスクリプション情報

| 項目名 | 説明 |
|--------|------|
| ティア名 | サブスクリプションのティア名 |
| 価格 | 通貨記号、金額、月次/年次 |
| ステータスバッジ | Active / Canceled |
| 有効期限/更新日 | validityDetails |
| アクションメニュー | View Stripe customer/subscription、Cancel/Continue |

### アクティビティフィード（Member::ActivityFeed）

| 項目名 | 説明 |
|--------|------|
| アクティビティ履歴 | メンバーのサイト上での活動履歴 |

## イベント仕様

### 1-保存（Save）

- 処理: saveTask.perform()
- scratchMemberからmemberへプロパティをコピーし、member.save()実行
- 新規作成時: membersCountCache.clear()、member.newルートからmemberルートへreplaceRoute
- API: `POST/PUT /members/{id}`

### 2-削除（Delete）

- 処理: confirmDeleteMember()でDeleteMemberModalを開く
- 削除後: membersStats.invalidate()、members.refreshData()、membersルートへ遷移
- API: `DELETE /members/{id}`

### 3-なりすまし（Impersonate）

- 処理: toggleImpersonateMemberModal()でimpersonate-memberモーダルを開く
- member.fetchSigninUrl()でサインインURLを取得

### 4-全デバイスサインアウト

- 処理: confirmLogoutMember()でLogoutMemberModalを開く
- member.logoutAllDevices()でAPIを呼び出し
- API: `POST /members/{id}/signout`

### 5-サブスクリプションキャンセル

- 処理: cancelSubscription()でcancelSubscriptionTask実行
- API: `PUT /members/{id}/subscriptions/{subscription_id}` with `{cancel_at_period_end: true}`

### 6-サブスクリプション継続

- 処理: continueSubscription()でcontinueSubscriptionTask実行
- API: `PUT /members/{id}/subscriptions/{subscription_id}` with `{cancel_at_period_end: false}`

### 7-コンプリメンタリーサブスクリプション追加

- 処理: ModalMemberTierでティアを選択し、addTier()実行
- メンバーのtiersを更新

### 8-コンプリメンタリーサブスクリプション削除

- 処理: removeComplimentary()でremoveComplimentaryTask実行
- メンバーのtiersから該当ティアを削除
- API: `PUT /members/{id}`

### 9-未保存変更の確認（willTransition）

- 処理: dirtyAttributesがある場合、ConfirmUnsavedChangesModalを表示
- 確認後: model.rollbackAttributes()してtransition.retry()

### 10-ラベル編集

- 処理: editLabel()でmembers-label-formモーダルを開く

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

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| メンバー表示 | members | SELECT | メンバーデータ取得（include=tiers） |
| メンバー保存 | members | INSERT/UPDATE | メンバー情報の保存 |
| メンバー削除 | members | DELETE | メンバーの削除 |
| ラベル更新 | members_labels | UPDATE | ラベル関連付けの更新 |
| ニュースレター更新 | members_newsletters | UPDATE | ニュースレター購読の更新 |

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

#### members

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, name, email, note, status等 | member_id | include=tiers |
| INSERT/UPDATE | name, email, note | 入力値 | |

## メッセージ仕様

| メッセージID | 種別 | 条件 | メッセージ内容 |
|-------------|------|------|---------------|
| MSG-01 | タイトル | 新規作成 | New member |
| MSG-02 | タイトル | 既存編集 | Edit member |
| MSG-03 | ラベル | メモ説明 | (not visible to member) |
| MSG-04 | 表示 | サブスクリプションなし | No subscriptions |

## 例外処理

| 例外ケース | 処理内容 |
|-----------|---------|
| 権限不足 | ホーム画面へリダイレクト |
| バリデーションエラー | member.errorsにエラー追加、フォームにエラー表示 |
| 未保存変更あり遷移 | ConfirmUnsavedChangesModal表示 |
| 保存中遷移 | saveTask完了を待ってからtransition.retry() |

## 備考

- scratchMemberパターンを使用し、フォーカスアウト時にプロパティを更新
- SCRATCH_PROPS = ['name', 'email', 'note'] の3プロパティがスクラッチ対象
- ラベルとニュースレターの変更検知はカスタム実装（_hasDirtyAttributes）
- 投稿分析画面（postAnalytics）からの遷移時はパンくずリストが変化
- directlyFromAnalyticsフラグで分析画面からの直接遷移を判定

---

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

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

### 推奨読解順序

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

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

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

**主要処理フロー**:
- **9-28行目**: メンバーの属性定義
- **29行目**: labels hasMany関連
- **46-52行目**: fetchSigninUrl タスク
- **54-57行目**: logoutAllDevices タスク

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

データフェッチと遷移制御の仕組みを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | member.js | `ghost/admin/app/routes/member.js` | ルート定義 |

**主要処理フロー**:
- **25-33行目**: model() - queryRecordまたはcreateRecord
- **35-48行目**: setupController - setInitialRelationshipValues、fetchMemberTask
- **67-70行目**: save アクション
- **72-97行目**: willTransition - 未保存変更確認
- **99-108行目**: confirmUnsavedChanges - モーダル表示

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

詳細表示と編集ロジックを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | member.js | `ghost/admin/app/controllers/member.js` | コントローラー定義 |

**主要処理フロー**:
- **11行目**: SCRATCH_PROPS定義
- **61-63行目**: dirtyAttributes getter
- **95-99行目**: scratchMember computed
- **101-105行目**: subscribedAt computed
- **137-147行目**: confirmDeleteMember - 削除モーダル
- **176-220行目**: saveTask - 保存処理
- **253-283行目**: _hasDirtyAttributes - 変更検知

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

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | member.hbs | `ghost/admin/app/templates/member.hbs` | メインテンプレート |

**主要処理フロー**:
- **4-30行目**: パンくずリスト（postAnalytics対応）
- **31-37行目**: タイトル（新規/既存で分岐）
- **41-94行目**: アクションメニュー
- **96行目**: GhTaskButton - 保存ボタン
- **102-109行目**: GhMemberSettingsFormコンポーネント

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

メンバー設定フォームの実装を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | gh-member-settings-form.hbs | `ghost/admin/app/components/gh-member-settings-form.hbs` | フォームテンプレート |
| 5-2 | gh-member-settings-form.js | `ghost/admin/app/components/gh-member-settings-form.js` | フォームロジック |

**主要処理フロー（テンプレート）**:
- **8-40行目**: 名前・メールアドレス入力
- **42-54行目**: ラベル選択
- **56-70行目**: メモ入力（500文字制限）
- **74-80行目**: ニュースレター設定
- **82-280行目**: サブスクリプション情報

**主要処理フロー（JS）**:
- **27-46行目**: isAddComplimentaryAllowed computed
- **52-75行目**: tiers computed - サブスクリプションからティア生成
- **164-180行目**: cancelSubscriptionTask
- **209-225行目**: continueSubscriptionTask

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

```
MemberRoute (ghost/admin/app/routes/member.js)
    │
    ├─ model(params)
    │      ├─ member_id あり → store.queryRecord('member', {include: 'tiers'})
    │      └─ member_id なし → store.createRecord('member')
    │
    ├─ setupController()
    │      ├─ controller.setInitialRelationshipValues()
    │      └─ _requiresBackgroundRefresh → controller.fetchMemberTask.perform()
    │
    └─ willTransition()
           └─ dirtyAttributes → confirmUnsavedChanges()
                  └─ ConfirmUnsavedChangesModal

MemberController (ghost/admin/app/controllers/member.js)
    │
    ├─ saveTask.perform()
    │      ├─ scratchProps → member
    │      ├─ member.save()
    │      ├─ member.updateLabels()
    │      └─ replaceRoute('member', member)
    │
    ├─ confirmDeleteMember()
    │      └─ modals.open(DeleteMemberModal)
    │             └─ afterDelete → transitionToRoute('members')
    │
    └─ confirmLogoutMember()
           └─ modals.open(LogoutMemberModal)

member.hbs (テンプレート)
    │
    ├─ GhDropdown (アクションメニュー)
    │      ├─ Impersonate → toggleImpersonateMemberModal()
    │      ├─ Sign out → confirmLogoutMember()
    │      └─ Delete → confirmDeleteMember()
    │
    └─ GhMemberSettingsForm
           ├─ GhMemberDetails
           ├─ 基本情報フォーム（name, email, labels, note）
           ├─ Member::NewsletterPreference
           ├─ サブスクリプション情報
           │      ├─ cancelSubscription()
           │      ├─ continueSubscription()
           │      └─ removeComplimentary()
           └─ Member::ActivityFeed
```

### データフロー図

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

URLパラメータ ─────────────▶ model() ────────────────────────▶ memberモデル
(member_id)                      │
                                 ├─ queryRecord('member', {include: 'tiers'})
                                 └─ createRecord('member')

フォーム入力 ─────────────▶ scratchMember ───────────────────▶ 一時的な値保持
                                 │
                                 ▼
フォーカスアウト ─────────▶ setProperty() ───────────────────▶ member更新
                                 │
                                 ▼
保存ボタン ───────────────▶ saveTask.perform() ─────────────▶ API呼び出し
                                 │
                                 ├─ scratchProps → member
                                 ├─ member.save()
                                 │      └─ POST/PUT /members/{id}
                                 ├─ member.updateLabels()
                                 └─ replaceRoute('member', member)

サブスクリプション操作 ────▶ cancelSubscriptionTask ────────▶ API呼び出し
                                 │
                                 └─ PUT /members/{id}/subscriptions/{sub_id}
                                        └─ cancel_at_period_end: true/false
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| member.js | `ghost/admin/app/routes/member.js` | ルート | メンバー詳細ルート |
| member.js | `ghost/admin/app/controllers/member.js` | コントローラー | メインロジック |
| member.hbs | `ghost/admin/app/templates/member.hbs` | テンプレート | UIテンプレート |
| member.js | `ghost/admin/app/models/member.js` | モデル | メンバーモデル |
| gh-member-settings-form.hbs | `ghost/admin/app/components/gh-member-settings-form.hbs` | コンポーネント | 設定フォームUI |
| gh-member-settings-form.js | `ghost/admin/app/components/gh-member-settings-form.js` | コンポーネント | 設定フォームロジック |
| gh-member-details.hbs | `ghost/admin/app/components/gh-member-details.hbs` | コンポーネント | メンバー詳細表示 |
| newsletter-preference.hbs | `ghost/admin/app/components/member/newsletter-preference.hbs` | コンポーネント | ニュースレター設定 |
| activity-feed.hbs | `ghost/admin/app/components/member/activity-feed.hbs` | コンポーネント | アクティビティ |
| delete-member.js | `ghost/admin/app/components/members/modals/delete-member.js` | モーダル | 削除確認 |
| logout-member.js | `ghost/admin/app/components/members/modals/logout-member.js` | モーダル | ログアウト確認 |
| impersonate-member.js | `ghost/admin/app/components/modal-impersonate-member.js` | モーダル | なりすまし |
