# 画面設計書 11-投稿一覧画面

## 概要

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

### 本画面の処理概要

投稿一覧画面は、サイトに投稿された記事（Post）を一覧表示し、管理・編集するための中心的な画面である。

**業務上の目的・背景**：コンテンツ管理システム（CMS）の中核機能として、編集者・管理者が効率的に記事を管理できる環境を提供する。複数の記事を俯瞰し、公開状況の把握、フィルタリングによる絞り込み、一括操作による効率的な管理を実現する。特に大量の記事を抱えるサイトにおいて、適切なフィルタリング・ソート機能は運用効率を大きく左右する重要な機能である。

**画面へのアクセス方法**：サイドバーの「Posts」メニューをクリック、またはホーム画面からのナビゲーションによりアクセスする。URL `/ghost/#/posts` で直接アクセスも可能。

**主要な操作・処理内容**：
1. 記事一覧の表示・無限スクロールによる追加読み込み
2. ステータス別フィルタリング（下書き・公開済み・予約・送信済み・おすすめ）
3. 公開範囲別フィルタリング（全公開・パブリック・メンバー限定・有料メンバー限定）
4. 著者・タグによるフィルタリング
5. ソート順の変更（新しい順・古い順・更新順）
6. 新規投稿作成への遷移
7. 複数選択による一括操作（タグ追加・アクセス権変更・削除など）
8. 個別記事のアナリティクス表示（訪問者数・メンバー獲得数）
9. エディタ画面への遷移

**画面遷移**：
- 遷移元: ホーム画面、サイドバーメニュー、セットアップ完了画面
- 遷移先: エディタ画面（lexical-editor）、投稿分析画面（posts-x）、メンバー一覧画面（members）

**権限による表示制御**：
- Contributor: 自分の投稿のみ表示、著者フィルタ非表示、一括選択機能無効
- Author: 自分の投稿のみ表示、著者フィルタ非表示
- Editor/Administrator/Owner: 全投稿表示、全機能利用可能

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 1 | 記事管理 | 主機能 | 記事の一覧表示・フィルタリング・検索 |
| 52 | 記事分析 | 補助機能 | 記事ごとの閲覧数・エンゲージメント表示 |
| 3 | タグ管理 | 補助機能 | タグによるフィルタリング |
| 40 | スタッフ管理 | 補助機能 | 著者によるフィルタリング |

## 画面種別

一覧

## URL/ルーティング

| 項目 | 値 |
|------|-----|
| URL | `/ghost/#/posts` |
| ルート名 | posts |
| ルートファイル | `ghost/admin/app/routes/posts.js` |

### クエリパラメータ

| パラメータ名 | 型 | デフォルト値 | 説明 |
|-------------|-----|-------------|------|
| type | string | null | ステータスフィルタ（draft/published/scheduled/sent/featured） |
| visibility | string | null | 公開範囲フィルタ（public/members/[paid,tiers]） |
| author | string | null | 著者スラッグによるフィルタ |
| tag | string | null | タグスラッグによるフィルタ |
| order | string | null | ソート順（published_at asc/updated_at desc） |

## 入出力項目

### 入力項目

| 項目名 | 項目ID | 必須 | 型 | 説明 |
|--------|--------|------|-----|------|
| ステータスフィルタ | type | - | select | 投稿ステータスによる絞り込み |
| 公開範囲フィルタ | visibility | - | select | アクセス権限による絞り込み |
| 著者フィルタ | author | - | select | 著者による絞り込み |
| タグフィルタ | tag | - | select | タグによる絞り込み |
| ソート順 | order | - | select | 表示順の指定 |

### フィルタ選択肢

#### ステータスフィルタ（type）

| 表示名 | 値 |
|--------|-----|
| All posts | null |
| Draft posts | draft |
| Published posts | published |
| Email only posts | sent |
| Scheduled posts | scheduled |
| Featured posts | featured |

#### 公開範囲フィルタ（visibility）

| 表示名 | 値 |
|--------|-----|
| All access | null |
| Public | public |
| Members-only | members |
| Paid members-only | [paid,tiers] |

#### ソート順（order）

| 表示名 | 値 |
|--------|-----|
| Newest first | null（デフォルト: published_at desc） |
| Oldest first | published_at asc |
| Recently updated | updated_at desc |

## 表示項目

### 投稿リスト項目

| 項目名 | フィールド | 説明 |
|--------|-----------|------|
| タイトル | title | 投稿のタイトル |
| おすすめアイコン | featured | おすすめ投稿の場合はスターアイコン表示 |
| 著者名 | authors | 「By {著者名}」形式で表示 |
| プライマリタグ | primaryTag | 「in {タグ名}」形式で表示 |
| 日付 | publishedAtUTC/updatedAtUTC | 下書き・予約は更新日、公開済みは公開日 |
| ステータス | status | Draft/Scheduled/Published/Sent |
| メール配信情報 | email | 配信済みの場合はメンバー数を表示 |
| 開封率 | email.openRate | メール開封率（%） |
| クリック率 | clickRate | メールリンククリック率（%） |
| 訪問者数 | visitorCount | Webアナリティクス有効時に表示 |
| メンバー獲得数 | memberCounts | 無料/有料メンバー獲得数 |

### 空状態表示

| 条件 | 表示内容 |
|------|---------|
| 投稿なし（フィルタなし） | 「Start creating content.」メッセージと新規作成ボタン |
| 投稿なし（フィルタあり） | 「No posts match the current filter」と全表示ボタン |

## イベント仕様

### 1-新規投稿ボタン押下

- 処理: Lexicalエディタの新規投稿作成画面へ遷移
- 遷移先ルート: `lexical-editor.new`
- 遷移パラメータ: `post`

### 2-投稿行クリック

- 処理: 該当投稿のエディタ画面へ遷移
- 遷移先ルート: `lexical-editor.edit`
- 遷移パラメータ: `[post.displayName, post.id]`
- 例外: Contributorかつ公開済みの場合は外部リンクとして投稿URLを開く

### 3-フィルタ変更

- 処理: クエリパラメータを更新し、投稿一覧を再取得
- データ再取得: InfinityModelをリセットして再フェッチ
- スクロール位置: 一覧の先頭にリセット

### 4-無限スクロール

- 処理: スクロール位置がトリガーオフセット（1000px）に達したら次ページを読み込み
- ページサイズ: 30件/ページ
- 読み込み順序: scheduled → draft → published/sent

### 5-アナリティクスボタンクリック

- 条件: 管理者権限かつhasAnalyticsPage=true
- 遷移先ルート: `posts-x`
- 遷移パラメータ: `post.id`

### 6-コンテキストメニュー操作

- 右クリックまたは複数選択時にコンテキストメニュー表示
- 利用可能操作: タグ追加、アクセス権変更、非公開化、予約解除、削除

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

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| 投稿一覧表示 | posts | SELECT | 投稿データの取得 |
| 投稿一覧表示 | users | SELECT | 著者情報の取得 |
| 投稿一覧表示 | tags | SELECT | タグ情報の取得 |
| 投稿一覧表示 | emails | SELECT | メール配信情報の取得 |
| フィルタ用タグ取得 | tags | SELECT | フィルタ選択肢用タグ一覧取得 |
| フィルタ用著者取得 | users | SELECT | フィルタ選択肢用ユーザー一覧取得 |

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

#### posts

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, title, status, visibility, featured, slug, uuid | filter条件に基づく | 無限スクロール対応 |
| SELECT | published_at, updated_at, created_at | ソート順に使用 | |
| SELECT | authors, tags, email | リレーション含む | embedded: always |

## メッセージ仕様

| メッセージID | 種別 | 条件 | メッセージ内容 |
|-------------|------|------|---------------|
| MSG-01 | 情報 | 投稿が0件（フィルタなし） | Start creating content. |
| MSG-02 | 情報 | 投稿が0件（フィルタあり） | No posts match the current filter |
| MSG-03 | 情報 | 公開成功後 | 公開成功モーダル表示 |
| MSG-04 | 情報 | 予約成功後 | 予約成功モーダル表示 |

## 例外処理

| 例外ケース | 処理内容 |
|-----------|---------|
| API通信エラー | 標準のエラーハンドリングによるエラー表示 |
| 権限不足 | 権限に応じたフィルタ・機能の非表示 |
| 不明なフィルタ値 | 「Unknown type/visibility/author/tag」と赤字で表示 |

## 備考

- 投稿一覧は3つのInfinityModelに分割されている（scheduled, draft, published/sent）
- 各InfinityModelは順番に読み込まれ、前のモデルが完了後に次のモデルの読み込みが開始される
- Webアナリティクス有効時は、公開済み投稿の訪問者数が非同期で取得・表示される
- メンバートラッキング有効時は、投稿経由のメンバー獲得数が表示される
- 公開/予約成功後はlocalStorageに情報が保存され、一覧表示時に成功モーダルが表示される

---

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

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

### 推奨読解順序

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

まず、投稿データのモデル定義を理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | post.js | `ghost/admin/app/models/post.js` | Postモデルの属性定義、リレーション、computed properties |

**読解のコツ**: Ember Dataのモデル定義。`attr()`で属性、`belongsTo()`/`hasMany()`でリレーションを定義。`computed()`プロパティで派生値を計算。

**主要処理フロー**:
- **69-82行目**: Postモデルの基本定義とサービス注入
- **83-128行目**: 属性定義（title, status, visibility, featuredなど）
- **124-129行目**: リレーション定義（authors, tags, email, newsletter）
- **161-168行目**: ステータス判定用computed（isPublished, isDraft, isScheduled, isSent）
- **220-228行目**: アナリティクスページ表示条件

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

ルートファイルがデータ取得の起点となる。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | posts.js | `ghost/admin/app/routes/posts.js` | ルート定義、model()フック、クエリパラメータ処理 |

**主要処理フロー**:
- **43-56行目**: ルートクラス定義とqueryParams設定（type, visibility, author, tag, orderでモデル再取得）
- **78-133行目**: model()フック - フィルタ条件構築と3つのInfinityModel生成
- **136-159行目**: setupController() - 著者・タグの事前ロードと選択リスト設定
- **221-242行目**: _getTypeFilters() - ステータスフィルタのマッピング

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

画面の状態管理とユーザーアクションの処理を担当。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | posts.js | `ghost/admin/app/controllers/posts.js` | フィルタ状態管理、選択リスト、アクション定義 |

**主要処理フロー**:
- **11-54行目**: フィルタ選択肢の定義（TYPES, VISIBILITIES, ORDERS）
- **66-77行目**: クエリパラメータとtrackedプロパティの定義
- **98-129行目**: フィルタ選択値の取得（selectedType, selectedVisibility, availableTags等）
- **202-225行目**: フィルタ変更アクション（changeType, changeVisibility, changeAuthor, changeTag, changeOrder）
- **227-230行目**: エディタへの遷移アクション

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

画面構造とコンポーネントの配置を理解。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | posts.hbs | `ghost/admin/app/templates/posts.hbs` | 画面レイアウト、フィルタUIとリストの配置 |

**主要処理フロー**:
- **1-32行目**: ヘッダー部（タイトル、フィルタUI、新規作成ボタン）
- **6-26行目**: ContentFilterコンポーネントへのプロパティ渡し
- **34-55行目**: PostsList::Listコンポーネントと空状態表示
- **57-75行目**: 3つのInfinityLoaderの条件付き表示

#### Step 5: リストコンポーネントを理解する

投稿リストの表示ロジック。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | list.js | `ghost/admin/app/components/posts-list/list.js` | リストコンポーネントのロジック、公開成功モーダル |
| 5-2 | list.hbs | `ghost/admin/app/components/posts-list/list.hbs` | リスト描画、ステータス別の表示順制御 |
| 5-3 | list-item.js | `ghost/admin/app/components/posts-list/list-item.js` | リストアイテムのロジック、アナリティクスデータ取得 |
| 5-4 | list-item.hbs | `ghost/admin/app/components/posts-list/list-item.hbs` | 個別投稿の表示、ステータス表示、アナリティクス表示 |

**主要処理フロー（list-item.hbs）**:
- **9-48行目**: Contributor用の公開済み投稿表示（外部リンク）
- **49-146行目**: 通常ユーザー用の投稿表示（エディタリンク）
- **149-258行目**: メトリクス列（開封率、クリック率、訪問者数、メンバー獲得数）
- **261-283行目**: アクションボタン（アナリティクス/編集/表示）

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

```
PostsRoute (ghost/admin/app/routes/posts.js)
    │
    ├─ model() - データ取得
    │      ├─ infinity.model('post', params) × 3
    │      │      └─ PostsWithAnalytics.afterInfinityModel()
    │      │             └─ postAnalytics.loadVisitorCounts()
    │      │             └─ postAnalytics.loadMemberCounts()
    │      └─ RSVP.hash(models)
    │
    └─ setupController()
           ├─ store.query('user') - 著者フィルタ用
           ├─ store.queryRecord('tag') - タグフィルタ用
           └─ selectionList設定

PostsController (ghost/admin/app/controllers/posts.js)
    │
    ├─ queryParams処理
    ├─ availableTypes/Visibilities/Orders - フィルタ選択肢
    ├─ loadMoreTagsTask - タグ追加読み込み
    └─ changeType/Visibility/Author/Tag/Order - フィルタ変更

posts.hbs (テンプレート)
    │
    ├─ GhCanvasHeader
    │      └─ PostsList::ContentFilter
    │             └─ PowerSelect × 5 (type, visibility, author, tag, order)
    │
    ├─ PostsList::List
    │      └─ MultiList::List
    │             └─ PostsList::ListItemAnalytics × N
    │                    └─ postAnalytics service
    │
    └─ GhInfinityLoader × 3 (scheduled, draft, published)
```

### データフロー図

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

URLクエリパラメータ ───────▶ PostsRoute.model() ─────────────▶ InfinityModel × 3
(?type=draft&tag=news)            │
                                  ├─ フィルタ条件構築
                                  ├─ infinity.model()呼び出し
                                  └─ PostsWithAnalytics

フィルタUI選択 ─────────────▶ PostsController ──────────────▶ クエリパラメータ更新
                                  │                              │
                                  └─ changeType/Visibility等      └─ ルート再実行

InfinityModel ─────────────▶ list.hbs ────────────────────▶ PostsList::ListItem
(posts配列)                       │
                                  └─ 各投稿をループ表示

postAnalytics ─────────────▶ list-item.js ────────────────▶ 訪問者数/メンバー数
(サービス)                        │
                                  └─ getVisitorCount()
                                  └─ getMemberCounts()
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| posts.js | `ghost/admin/app/routes/posts.js` | ルート | データ取得、クエリパラメータ処理 |
| posts.js | `ghost/admin/app/controllers/posts.js` | コントローラー | 状態管理、フィルタ処理 |
| posts.hbs | `ghost/admin/app/templates/posts.hbs` | テンプレート | 画面レイアウト |
| post.js | `ghost/admin/app/models/post.js` | モデル | 投稿データ構造定義 |
| list.js | `ghost/admin/app/components/posts-list/list.js` | コンポーネント | リストロジック |
| list.hbs | `ghost/admin/app/components/posts-list/list.hbs` | テンプレート | リスト描画 |
| list-item.js | `ghost/admin/app/components/posts-list/list-item.js` | コンポーネント | アイテムロジック |
| list-item.hbs | `ghost/admin/app/components/posts-list/list-item.hbs` | テンプレート | アイテム描画 |
| content-filter.js | `ghost/admin/app/components/posts-list/content-filter.js` | コンポーネント | フィルタUIロジック |
| content-filter.hbs | `ghost/admin/app/components/posts-list/content-filter.hbs` | テンプレート | フィルタUI描画 |
| context-menu.js | `ghost/admin/app/components/posts-list/context-menu.js` | コンポーネント | 右クリックメニュー |
| selection-list.js | `ghost/admin/app/components/posts-list/selection-list.js` | コンポーネント | 複数選択管理 |
