# 画面設計書 19-メンバーインポート画面

## 概要

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

### 本画面の処理概要

メンバーインポート画面は、CSVファイルからメンバーを一括インポートするためのモーダル画面である。ファイル選択、カラムマッピング、アップロード、結果表示のステップで構成されるウィザード形式の画面。

**業務上の目的・背景**：既存のメーリングリストや他のサービスからメンバーを移行する際に使用。CSVファイルの各カラムをGhostのメンバー属性にマッピングし、一括でメンバーを登録できる。

**画面へのアクセス方法**：メンバー一覧画面のアクションメニューから「Import members」をクリック、またはURL `/ghost/#/members/import` で直接アクセス可能。

**主要な操作・処理内容**：
1. CSVファイルの選択
2. カラムマッピングの設定
3. ラベルの追加
4. インポートの実行
5. 結果の確認（成功件数、エラー件数）
6. エラーファイルのダウンロード

**画面遷移**：
- 遷移元: メンバー一覧画面
- 遷移先: メンバー一覧画面（インポート完了後、ラベルフィルター適用）

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

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|-------------------|
| 4 | メンバー管理 | 主機能 | メンバーの一括インポート |
| 5 | ラベル管理 | 関連機能 | インポート時のラベル付与 |

## 画面種別

モーダル（ウィザード）

## URL/ルーティング

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

## 入出力項目

### 入力項目

| 項目名 | 項目ID | 必須 | 型 | 説明 |
|--------|--------|------|-----|------|
| CSVファイル | file | 必須 | file | インポートするCSVファイル |
| カラムマッピング | mapping | - | object | CSVカラムとメンバー属性の対応 |
| ラベル | labels | - | array | インポートメンバーに付与するラベル |

## 表示項目

### 状態別表示（state）

#### INIT（初期状態）

| 項目名 | 説明 |
|--------|------|
| ヘッダー | 「Import members」 |
| ヘルプボックス | ヘルプリンク、サンプルCSVダウンロードリンク |
| ファイル選択 | CsvFileSelectコンポーネント |
| フッター | Closeボタン |

#### MAPPING（マッピング状態）

| 項目名 | 説明 |
|--------|------|
| ヘッダー | 「Import members」 |
| マッピングフォーム | CsvFileMappingコンポーネント |
| フッター | Start overボタン、Importボタン（件数表示） |

#### UPLOADING（アップロード中）

| 項目名 | 説明 |
|--------|------|
| ヘッダー | 「Import members」 |
| マッピングフォーム | 無効化状態 |
| フッター | Start over（無効）、Uploading...（スピナー） |

#### PROCESSING（処理中）

| 項目名 | 説明 |
|--------|------|
| ヘッダー | 「Import in progress」アイコン付き |
| メッセージ | 処理中メッセージ、完了時メール通知の説明 |
| フッター | Upload another file、Got itボタン |

#### COMPLETE（完了）

| 項目名 | 説明 |
|--------|------|
| ヘッダー | 「Import complete」（成功時は紙吹雪アイコン） |
| 結果サマリー | インポート成功件数 |
| エラーリスト | エラーの種類と件数 |
| フッター | Download error file、View members / Try again |

#### ERROR（エラー）

| 項目名 | 説明 |
|--------|------|
| ヘッダー | エラーヘッダー（動的） |
| エラーメッセージ | 詳細エラーメッセージ |
| フッター | Try again、OKボタン |

## イベント仕様

### 1-ファイル選択（setFile）

- 処理: ファイルを設定し、state を MAPPING に遷移
- ファイル: CSVファイルのみ対応

### 2-マッピング設定（setMappingResult）

- 処理: CSVカラムとメンバー属性のマッピング結果を保存
- バリデーション: emailカラムは必須

### 3-アップロード実行（upload）

- 処理: generateRequest()でAPIにPOST
- API: `POST /members/upload/`
- FormData: file, labels[], mapping[key]=value

### 4-アップロード成功（_uploadSuccess）

- 処理: インポート結果の解析と表示
- import_labelの自動作成と保存
- エラーCSVの生成（Blob URL）
- confirm()でメンバーリスト更新

### 5-アップロードエラー（_uploadError）

- 処理: エラー種別に応じたメッセージ表示
- エラー種別: UnsupportedMediaType, RequestEntityTooLarge, DataImportError, HostLimitError

### 6-リセット（reset）

- 処理: 全状態を初期化し、state を INIT に戻す

### 7-モーダルクローズ（closeModal）

- 処理: UPLOADING 状態以外で遷移許可
- コントローラのclose()でmembersルートへ遷移

### 8-メンバー更新（refreshMembers）

- 処理: インポート完了後のメンバーリスト更新
- import_labelがある場合はフィルター適用して遷移

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

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| メンバーインポート | members | INSERT/UPDATE | メンバーの一括登録・更新 |
| ラベル作成 | labels | INSERT | import_labelの自動作成 |
| ラベル関連付け | members_labels | INSERT | インポートメンバーへのラベル付与 |

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

#### members

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT/UPDATE | name, email, note等 | CSVから取得 | マッピングに従う |

## メッセージ仕様

| メッセージID | 種別 | 条件 | メッセージ内容 |
|-------------|------|------|---------------|
| MSG-01 | ヘルプ | 初期状態 | Need some help? Learn more about importing members... |
| MSG-02 | 処理中 | PROCESSING | Your import is being processed, and you'll receive a confirmation email... |
| MSG-03 | 成功 | COMPLETE | A total of {n} person(s) were successfully added or updated... |
| MSG-04 | エラー | メール不正 | Invalid email address |
| MSG-05 | エラー | メールなし | Missing email address |
| MSG-06 | エラー | ノート長すぎ | Note is too long |
| MSG-07 | エラー | ファイル種別 | The file type you uploaded is not supported. |
| MSG-08 | エラー | ファイルサイズ | The file you uploaded was larger than the maximum file size... |
| MSG-09 | エラー | 汎用 | An unexpected error occurred, please try again |
| MSG-10 | エラー | 制限 | Woah there cowboy, that's a big list |

## 例外処理

| 例外ケース | 処理内容 |
|-----------|---------|
| 権限不足 | ホーム画面へリダイレクト |
| 未サポートファイル形式 | UnsupportedMediaTypeError メッセージ表示 |
| ファイルサイズ超過 | RequestEntityTooLargeError メッセージ表示 |
| データエラー | DataImportError メッセージ表示 |
| ホスト制限 | HostLimitError メッセージ表示、Try again非表示 |
| バージョン不一致 | VersionMismatchError 通知表示 |

## 備考

- CSVファイルはemail列が必須
- インポート時に自動でimport_labelが作成される
- 大量インポート時はAcceptedResponseが返り、PROCESSING状態になる
- エラーが発生したメンバーはエラーCSVとしてダウンロード可能
- エラーメッセージは人間が読みやすい形式に変換される
- サンプルCSVファイルは https://static.ghost.org/v4.0.0/files/member-import-template.csv

---

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

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

### 推奨読解順序

#### Step 1: ルートとコントローラーを理解する

シンプルなルート構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | import.js | `ghost/admin/app/routes/members/import.js` | 権限チェック継承のみ |
| 1-2 | import.js | `ghost/admin/app/controllers/members/import.js` | refreshMembers、close |

**主要処理フロー（コントローラー）**:
- **10-17行目**: refreshMembers - ラベルフィルター付きでメンバーリスト更新
- **19-26行目**: close - backgroundからでなければmembersへ遷移

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

モーダル呼び出し構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | import.hbs | `ghost/admin/app/templates/members/import.hbs` | GhFullscreenModal呼び出し |

**主要処理フロー**:
- **1-4行目**: import-membersモーダル呼び出し

#### Step 3: モーダルコンポーネントを理解する

インポート処理のメインロジックを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | modal-import-members.js | `ghost/admin/app/components/modal-import-members.js` | インポートロジック |
| 3-2 | modal-import-members.hbs | `ghost/admin/app/components/modal-import-members.hbs` | UI（状態別表示） |

**主要処理フロー（JS）**:
- **23-34行目**: 状態管理プロパティ（state, file, mappingResult等）
- **40-42行目**: uploadUrl - /members/upload/ エンドポイント
- **44-63行目**: formData computed - マルチパートフォームデータ生成
- **65-106行目**: actions（setFile, setMappingResult, upload, reset, closeModal）
- **108-130行目**: generateRequest - API呼び出し、AcceptedResponse対応
- **132-200行目**: _uploadSuccess - 成功時処理、エラーCSV生成
- **202-235行目**: _uploadError - エラー種別判定とメッセージ設定

**主要処理フロー（テンプレート）**:
- **2-7行目**: INIT状態 - ファイル選択
- **9-13行目**: MAPPING状態 - カラムマッピング
- **15-20行目**: PROCESSING状態 - 処理中表示
- **22-35行目**: COMPLETE状態 - 結果表示（成功/エラー分岐）
- **37-41行目**: ERROR状態 - エラー表示
- **108-189行目**: フッターボタン（状態別）

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

```
MembersImportRoute (ghost/admin/app/routes/members/import.js)
    │
    └─ extends MembersManagementRoute
           └─ beforeModel() - canManageMembersチェック

import.hbs (テンプレート)
    │
    └─ GhFullscreenModal @modal="import-members"
           │
           ├─ @confirm={{this.refreshMembers}}
           └─ @close={{this.close}}

modal-import-members.js (モーダルコンポーネント)
    │
    ├─ state管理
    │      ├─ INIT → ファイル選択
    │      ├─ MAPPING → カラムマッピング
    │      ├─ UPLOADING → アップロード中
    │      ├─ PROCESSING → サーバー処理中
    │      ├─ COMPLETE → 完了
    │      └─ ERROR → エラー
    │
    ├─ actions.setFile(file)
    │      └─ state → MAPPING
    │
    ├─ actions.upload()
    │      └─ generateRequest()
    │             │
    │             ├─ ajax.post('/members/upload/', formData)
    │             │
    │             ├─ AcceptedResponse → state = PROCESSING
    │             ├─ 成功 → _uploadSuccess() → state = COMPLETE
    │             └─ エラー → _uploadError() → state = ERROR
    │
    └─ _uploadSuccess()
           ├─ インポート結果解析
           ├─ エラーCSV生成（Blob URL）
           ├─ import_label保存
           └─ confirm({label}) → refreshMembers()
```

### データフロー図

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

CSVファイル ───────────────▶ setFile() ─────────────────────▶ state = MAPPING
                                 │
                                 ▼
CSVパース ────────────────▶ CsvFileMapping ────────────────▶ mappingResult
                                 │
                                 ├─ membersCount
                                 ├─ mapping (カラム対応)
                                 └─ labels
                                 │
                                 ▼
upload() ─────────────────▶ generateRequest() ─────────────▶ API呼び出し
                                 │
                                 └─ ajax.post('/members/upload/', formData)
                                        │
                                        ├─ FormData
                                        │      ├─ membersfile: file
                                        │      ├─ labels[]: label.name
                                        │      └─ mapping[key]: value
                                        │
                                        ▼
                                 API Response
                                        │
                                        ├─ AcceptedResponse → PROCESSING
                                        │
                                        └─ JSON Response
                                               │
                                               ├─ meta.stats.imported → importedCount
                                               ├─ meta.stats.invalid → erroredMembers
                                               └─ meta.import_label → ラベル
                                               │
                                               ▼
                                        _uploadSuccess()
                                               │
                                               ├─ エラーCSV生成（Blob URL）
                                               ├─ store.pushPayload(label)
                                               └─ confirm({label})
                                                      │
                                                      ▼
                                               refreshMembers()
                                                      │
                                                      └─ router.transitionTo({filter: `label:[${slug}]`})
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| import.js | `ghost/admin/app/routes/members/import.js` | ルート | 権限チェック |
| import.js | `ghost/admin/app/controllers/members/import.js` | コントローラー | 遷移制御 |
| import.hbs | `ghost/admin/app/templates/members/import.hbs` | テンプレート | モーダル呼び出し |
| modal-import-members.js | `ghost/admin/app/components/modal-import-members.js` | コンポーネント | インポートロジック |
| modal-import-members.hbs | `ghost/admin/app/components/modal-import-members.hbs` | コンポーネント | モーダルUI |
| csv-file-select.js | `ghost/admin/app/components/modal-import-members/csv-file-select.js` | コンポーネント | ファイル選択 |
| csv-file-mapping.js | `ghost/admin/app/components/modal-import-members/csv-file-mapping.js` | コンポーネント | カラムマッピング |
| modal-base.js | `ghost/admin/app/components/modal-base.js` | コンポーネント | モーダル基底クラス |
| unparse.js | `@tryghost/members-csv/lib/unparse` | ライブラリ | CSV生成 |
