# 機能設計書 72-Comments UI

## 概要

本ドキュメントは、Ghost CMSの記事コメント機能を提供するフロントエンドウィジェット「Comments UI」の機能設計書である。Comments UIはReact + TypeScriptで実装されており、記事へのコメント投稿・表示・返信・いいね・報告機能を提供する。

### 本機能の処理概要

Comments UIは、Ghostサイトのテーマに埋め込まれるコメントウィジェットで、読者が記事に対してコメントを投稿し、他の読者とディスカッションできる機能を提供する。Tiptapエディタを使用したリッチテキスト入力、階層的な返信構造、いいね機能、管理者によるモデレーション機能をサポートする。

**業務上の目的・背景**：読者のエンゲージメントを高め、コミュニティ形成を促進するために、コメント機能は不可欠である。Comments UIは、テーマに依存せず一貫したコメント体験を提供し、サイト運営者がコメントを効果的に管理できるようにする。スパムや不適切なコンテンツに対するモデレーション機能も備えている。

**機能の利用シーン**：
- 読者が記事の内容についてコメントを投稿する際
- 他の読者のコメントに返信する際
- コメントにいいねを付ける際
- 不適切なコメントを報告する際
- 管理者がコメントを非表示/表示する際
- 自分のコメントを編集・削除する際

**主要な処理内容**：
1. 記事に関連するコメントの取得・表示（ページネーション対応）
2. 新規コメントの投稿（Tiptapエディタによるリッチテキスト入力）
3. コメントへの返信投稿（階層構造をサポート）
4. コメントのいいね/いいね解除
5. コメントの編集・削除
6. 不適切なコメントの報告
7. 管理者によるコメントの非表示/表示（モデレーション）
8. コメントの並び替え（いいね数順、新着順）
9. コメントパーマリンクによる直接アクセス

**関連システム・外部連携**：
- Ghost Members API（`/members/api/comments/`）：コメントCRUD操作
- Ghost Admin API：管理者向けモデレーション機能
- Tiptap Editor：リッチテキスト編集機能

**権限による制御**：
- 未ログイン状態：コメント閲覧のみ、投稿不可
- 無料メンバー：コメント投稿可能（サイト設定による）
- 有料メンバー：コメント投稿可能
- 管理者（Owner/Administrator/Super Editor）：コメントの非表示/表示、すべての機能にアクセス可能

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 93 | コメント一覧 | 主機能 | 投稿へのコメント一覧表示 |
| 94 | コメント投稿フォーム | 主機能 | 新規コメントの投稿 |
| 95 | 返信フォーム | 主機能 | コメントへの返信投稿 |
| 96 | 編集フォーム | 主機能 | コメントの編集 |
| 97 | 削除確認ポップアップ | 主機能 | コメント削除の確認 |
| 98 | 通報ポップアップ | 主機能 | 不適切なコメントの報告 |
| 99 | 詳細追加ポップアップ | 主機能 | コメント投稿者の詳細情報追加 |

## 機能種別

CRUD操作 / UI表示 / リアルタイム更新

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| postId | string | Yes | コメント対象の記事ID | 有効な記事ID |
| html | string | Yes | コメント本文（HTML形式） | 空でないこと |
| parent_id | string | No | 返信先のコメントID | 有効なコメントID |
| in_reply_to_id | string | No | 直接返信先のコメントID（返信の返信用） | 有効なコメントID |
| order | string | No | 並び替え順（count__likes desc, created_at desc） | 有効な並び替えキー |
| page | number | No | ページ番号 | 正の整数 |
| limit | number | No | 1ページあたりの件数（デフォルト20） | 正の整数 |

### 入力データソース

- HTMLスクリプトタグ属性：`data-ghost-comments`, `data-post-id`, `data-api-key`, `data-admin-url`等
- Ghost Members API：メンバーセッション情報
- Tiptapエディタ：コメント本文入力

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| comments | Comment[] | コメント一覧（返信含む） |
| pagination | object | ページネーション情報 |
| commentCount | number | 総コメント数 |
| member | object | ログインメンバー情報 |
| admin | object | 管理者情報（管理者の場合） |
| popup | string | 現在表示中のポップアップ |

### 出力先

- DOM（iframeベースのコメントセクション）
- Ghost Members API（コメントデータ保存）

## 処理フロー

### 処理シーケンス

```
1. 初期化（initSetup）
   └─ IntersectionObserverでビューポート内に入ったら初期化開始
   └─ メンバーセッションデータ取得
   └─ コメント一覧取得（ページネーション込み）
   └─ コメント数取得
   └─ パーマリンクがある場合は対象コメントまでスクロール

2. 管理者認証（initAdminAuth）
   └─ Admin APIでユーザー情報取得
   └─ Owner/Administrator/Super Editorのみ管理者として認識
   └─ 管理者の場合はAdmin API経由でコメント再取得

3. コメント操作
   └─ 投稿：addComment / addReply
   └─ 編集：editComment
   └─ 削除：deleteComment
   └─ いいね：likeComment / unlikeComment
   └─ 報告：reportComment

4. モデレーション（管理者のみ）
   └─ 非表示：hideComment（Admin API経由）
   └─ 表示：showComment（Admin API経由）

5. ページネーション
   └─ loadMoreComments：追加コメント読み込み
   └─ loadMoreReplies：追加返信読み込み
```

### フローチャート

```mermaid
flowchart TD
    A[Comments UI初期化] --> B{ビューポート内?}
    B -->|No| B
    B -->|Yes| C[APIデータ取得]
    C --> D{メンバーログイン?}
    D -->|Yes| E[メンバー情報設定]
    D -->|No| F[閲覧専用モード]
    E --> G{管理者チェック}
    G -->|Yes| H[Admin API初期化]
    G -->|No| I[コメント表示]
    H --> I
    F --> I

    I --> J{ユーザー操作}
    J -->|新規コメント| K[コメントフォーム表示]
    J -->|返信| L[返信フォーム表示]
    J -->|いいね| M[いいね処理]
    J -->|編集| N[編集フォーム表示]
    J -->|削除| O[削除確認ポップアップ]
    J -->|報告| P[報告ポップアップ]
    J -->|非表示| Q{管理者?}

    K --> R[API: addComment]
    L --> S[API: addReply]
    M --> T[API: likeComment/unlikeComment]
    N --> U[API: editComment]
    O --> V[API: deleteComment]
    P --> W[API: reportComment]
    Q -->|Yes| X[Admin API: hideComment]
    Q -->|No| I

    R --> Y[コメント一覧更新]
    S --> Y
    T --> Y
    U --> Y
    V --> Y
    X --> Y
    Y --> I
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-72-01 | コメント投稿権限 | コメント投稿はメンバーのみ可能 | member !== null |
| BR-72-02 | 有料メンバー限定 | サイト設定で有料メンバーのみコメント可能に制限可能 | commentsEnabled === 'paid' |
| BR-72-03 | 自己コメント編集 | 自分のコメントのみ編集・削除可能 | comment.member.uuid === member.uuid |
| BR-72-04 | 管理者モデレーション | 管理者はすべてのコメントを非表示/表示可能 | admin !== null |
| BR-72-05 | 削除済みコメント表示 | 返信があるコメントは削除後もプレースホルダー表示 | comment.status === 'deleted' && replies.length > 0 |
| BR-72-06 | 非表示コメント表示 | 非表示コメントは管理者のみ閲覧可能 | comment.status === 'hidden' && isAdmin |
| BR-72-07 | いいねカウント | 楽観的更新でいいね数を即座に反映 | likeComment/unlikeComment呼び出し時 |

### 計算ロジック

- 並び替えデフォルト順：`count__likes desc, created_at desc`（いいね数降順、作成日時降順）
- ページネーション：1ページ20件、追加読み込みは次ページから

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| コメント追加 | comments | INSERT | 新規コメントレコード作成 |
| コメント編集 | comments | UPDATE | コメント本文の更新 |
| コメント削除 | comments | UPDATE | statusを'deleted'に更新 |
| コメント非表示 | comments | UPDATE | statusを'hidden'に更新 |
| いいね追加 | comment_likes | INSERT | いいねレコード作成 |
| いいね削除 | comment_likes | DELETE | いいねレコード削除 |
| 報告追加 | comment_reports | INSERT | 報告レコード作成 |

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

#### comments

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | post_id, member_id, html, parent_id, in_reply_to_id | フォーム入力値 | コメント投稿時 |
| UPDATE | html, edited_at | フォーム入力値、現在時刻 | 編集時 |
| UPDATE | status | 'deleted' | 削除時 |
| UPDATE | status | 'hidden'/'published' | モデレーション時 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| Failed to add comment | 投稿エラー | コメントAPI呼び出し失敗 | エラーログ出力、UI上で再試行促進 |
| Failed to edit comment | 編集エラー | 編集API呼び出し失敗 | エラーログ出力 |
| Failed to fetch comments | 取得エラー | コメント一覧取得失敗 | initStatus: 'failed'に設定 |
| Failed to like comment | いいねエラー | いいねAPI呼び出し失敗 | 楽観的更新をロールバック |

### リトライ仕様

- いいね操作：失敗時に楽観的更新をロールバック
- その他のAPI呼び出し：自動リトライなし、エラーログ出力

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

- Comments UIはクライアントサイドアプリケーションのため、直接的なDBトランザクション制御は行わない
- サーバーサイド（Ghost Core）のComments APIがトランザクション管理を担当

## パフォーマンス要件

- 遅延初期化：IntersectionObserverでビューポート内に入るまで初期化を遅延
- ページネーション：1ページ20件、追加読み込みで負荷分散
- 楽観的更新：いいね操作は即座にUI反映し、API呼び出しは非同期で実行

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

- same-origin credentials：セッション認証にsame-origin設定を使用
- HTMLサニタイズ：コメント本文はサーバーサイドでサニタイズ
- 管理者認証：Admin APIアクセスは認証済み管理者のみ
- CORS対策：同一オリジンからのリクエストのみ許可

## 備考

- Comments UIは`apps/comments-ui/`ディレクトリに配置されたReact + TypeScriptアプリケーション
- ビルド成果物はUMD形式で`umd/`ディレクトリに出力
- Tailwind CSSでスタイリング
- Tiptapエディタでリッチテキスト入力をサポート
- 多言語対応は`@tryghost/i18n`パッケージの'comments'名前空間を使用

---

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

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

### 推奨読解順序

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

まず、Comments UIで使用される主要なデータ構造を理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | app-context.ts | `apps/comments-ui/src/app-context.ts` | Comment型、EditableAppContext型等の定義 |
| 1-2 | constants.ts | `apps/comments-ui/src/utils/constants.ts` | 定数定義 |

**読解のコツ**: app-context.tsにはComment型の定義があり、id、html、status、member、replies、count（likes、replies）等のプロパティを持つ。EditableAppContextが状態管理の中心となる。

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

処理の起点となるファイル・関数を特定する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | index.tsx | `apps/comments-ui/src/index.tsx` | アプリケーションのエントリーポイント |
| 2-2 | app.tsx | `apps/comments-ui/src/app.tsx` | メインアプリケーションコンポーネント |

**主要処理フロー（app.tsx）**:
1. **30-54行目**: App関数コンポーネントの初期state設定
2. **58-64行目**: Ghost API初期化（setupGhostApi）
3. **121-165行目**: initAdminAuth()で管理者認証処理
4. **168-179行目**: fetchComments()でコメント一覧取得
5. **286-336行目**: initSetup()でアプリ初期化
6. **338-370行目**: IntersectionObserverで遅延初期化

#### Step 3: アクションハンドラを理解する

ユーザー操作に対する処理ロジックを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | actions.ts | `apps/comments-ui/src/actions.ts` | 全アクションハンドラの定義 |

**主要処理フロー**:
- **6-26行目**: loadMoreComments - ページネーション処理
- **58-105行目**: loadMoreReplies - 返信追加読み込み
- **107-115行目**: addComment - 新規コメント追加
- **117-145行目**: addReply - 返信追加
- **147-179行目**: hideComment - コメント非表示（管理者）
- **181-217行目**: showComment - コメント表示（管理者）
- **257-276行目**: likeComment/unlikeComment - いいね処理
- **278-282行目**: reportComment - コメント報告
- **284-334行目**: deleteComment - コメント削除
- **336-362行目**: editComment - コメント編集

#### Step 4: API通信層を理解する

バックエンドとの通信処理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | api.ts | `apps/comments-ui/src/utils/api.ts` | Ghost Members API通信ラッパー |
| 4-2 | admin-api.ts | `apps/comments-ui/src/utils/admin-api.ts` | Ghost Admin API通信ラッパー |

**主要処理フロー（api.ts）**:
- **57-106行目**: api.member - メンバー関連操作
- **108-300行目**: api.comments - コメント関連操作（browse, replies, add, edit, like, unlike, report）
- **305-324行目**: api.init - 初期化処理

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

各画面のUI実装を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | content-box.tsx | `apps/comments-ui/src/components/content-box.tsx` | メインコンテンツコンテナ |
| 5-2 | comment.tsx | `apps/comments-ui/src/components/content/comment.tsx` | コメントコンポーネント |
| 5-3 | form.tsx | `apps/comments-ui/src/components/content/forms/form.tsx` | 入力フォームベース |
| 5-4 | main-form.tsx | `apps/comments-ui/src/components/content/forms/main-form.tsx` | メインコメントフォーム |
| 5-5 | reply-form.tsx | `apps/comments-ui/src/components/content/forms/reply-form.tsx` | 返信フォーム |
| 5-6 | edit-form.tsx | `apps/comments-ui/src/components/content/forms/edit-form.tsx` | 編集フォーム |

**読解のコツ（comment.tsx）**:
- **20-40行目**: AnimatedComment - トランジション付きコメント表示
- **42-64行目**: CommentComponent - コメント表示の分岐ロジック
- **67-80行目**: useCommentVisibility - 表示状態の判定フック
- **85-156行目**: PublishedComment - 公開コメントの表示
- **162-203行目**: UnpublishedComment - 削除/非表示コメントの表示

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

```
index.tsx (init)
    │
    └─ App (app.tsx)
           │
           ├─ useEffect (遅延初期化)
           │      └─ initSetup()
           │             ├─ api.init()
           │             │      ├─ api.member.sessionData()
           │             │      └─ api.site.settings()
           │             │
           │             ├─ fetchComments()
           │             │      ├─ api.comments.browse()
           │             │      └─ api.comments.count()
           │             │
           │             └─ loadScrollTarget() (パーマリンク時)
           │
           ├─ initAdminAuth()
           │      └─ adminApi.getUser()
           │             └─ adminApi.browse() (管理者の場合)
           │
           ├─ dispatchAction(action, data)
           │      └─ ActionHandler (actions.ts)
           │             ├─ addComment() → api.comments.add()
           │             ├─ addReply() → api.comments.add()
           │             ├─ editComment() → api.comments.edit()
           │             ├─ deleteComment() → api.comments.edit()
           │             ├─ likeComment() → api.comments.like()
           │             ├─ unlikeComment() → api.comments.unlike()
           │             ├─ reportComment() → api.comments.report()
           │             ├─ hideComment() → adminApi.hideComment()
           │             └─ showComment() → adminApi.showComment()
           │
           └─ render()
                  ├─ CommentsFrame
                  │      └─ ContentBox
                  │             ├─ Content
                  │             │      ├─ MainForm
                  │             │      └─ Comment[]
                  │             │             ├─ Avatar
                  │             │             ├─ CommentHeader
                  │             │             ├─ CommentBody
                  │             │             ├─ CommentMenu
                  │             │             │      ├─ LikeButton
                  │             │             │      ├─ ReplyButton
                  │             │             │      └─ MoreButton
                  │             │             ├─ Replies
                  │             │             └─ ReplyForm
                  │             └─ Pagination
                  ├─ AuthFrame
                  └─ PopupBox
                         ├─ DeletePopup
                         ├─ ReportPopup
                         └─ AddDetailsPopup
```

### データフロー図

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

HTMLスクリプトタグ属性 ─┐
  (postId, apiKey)       │
                         │
メンバーセッション ─────┼─▶ App (app.tsx)
                         │   ├─ initSetup()           ─▶ state更新
Tiptapエディタ入力 ─────┤   ├─ dispatchAction()      ─▶ action実行
  (html)                 │   └─ ActionHandler()       ─▶ API呼び出し
                         │
いいね/報告ボタン ──────┘
                               │
                               ▼
                          api.ts / admin-api.ts
                               │
                               ▼
                    Ghost Members API ───▶ DB更新
                    Ghost Admin API
                               │
                               ▼
                         state更新 ───▶ UI再レンダリング
                               │
                               ▼
                         CommentsFrame (iframe)
                               │
                               ▼
                         [コメント表示]
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.tsx | `apps/comments-ui/src/index.tsx` | ソース | アプリエントリーポイント |
| app.tsx | `apps/comments-ui/src/app.tsx` | ソース | メインアプリコンポーネント |
| app-context.ts | `apps/comments-ui/src/app-context.ts` | ソース | コンテキスト・型定義 |
| actions.ts | `apps/comments-ui/src/actions.ts` | ソース | アクションハンドラ |
| api.ts | `apps/comments-ui/src/utils/api.ts` | ソース | Members API通信 |
| admin-api.ts | `apps/comments-ui/src/utils/admin-api.ts` | ソース | Admin API通信 |
| pages.ts | `apps/comments-ui/src/pages.ts` | ソース | ページ定義 |
| helpers.ts | `apps/comments-ui/src/utils/helpers.ts` | ソース | ユーティリティ関数 |
| hooks.ts | `apps/comments-ui/src/utils/hooks.ts` | ソース | カスタムフック |
| options.ts | `apps/comments-ui/src/utils/options.ts` | ソース | オプション管理 |
| content-box.tsx | `apps/comments-ui/src/components/content-box.tsx` | ソース | メインコンテナ |
| comment.tsx | `apps/comments-ui/src/components/content/comment.tsx` | ソース | コメントコンポーネント |
| replies.tsx | `apps/comments-ui/src/components/content/replies.tsx` | ソース | 返信一覧コンポーネント |
| avatar.tsx | `apps/comments-ui/src/components/content/avatar.tsx` | ソース | アバターコンポーネント |
| form.tsx | `apps/comments-ui/src/components/content/forms/form.tsx` | ソース | フォームベース |
| main-form.tsx | `apps/comments-ui/src/components/content/forms/main-form.tsx` | ソース | メインフォーム |
| reply-form.tsx | `apps/comments-ui/src/components/content/forms/reply-form.tsx` | ソース | 返信フォーム |
| edit-form.tsx | `apps/comments-ui/src/components/content/forms/edit-form.tsx` | ソース | 編集フォーム |
| like-button.tsx | `apps/comments-ui/src/components/content/buttons/like-button.tsx` | ソース | いいねボタン |
| reply-button.tsx | `apps/comments-ui/src/components/content/buttons/reply-button.tsx` | ソース | 返信ボタン |
| more-button.tsx | `apps/comments-ui/src/components/content/buttons/more-button.tsx` | ソース | その他ボタン |
| pagination.tsx | `apps/comments-ui/src/components/content/pagination.tsx` | ソース | ページネーション |
| popup-box.tsx | `apps/comments-ui/src/components/popup-box.tsx` | ソース | ポップアップコンテナ |
| delete-popup.tsx | `apps/comments-ui/src/components/popups/delete-popup.tsx` | ソース | 削除確認ポップアップ |
| report-popup.tsx | `apps/comments-ui/src/components/popups/report-popup.tsx` | ソース | 報告ポップアップ |
| add-details-popup.tsx | `apps/comments-ui/src/components/popups/add-details-popup.tsx` | ソース | 詳細追加ポップアップ |
| frame.tsx | `apps/comments-ui/src/components/frame.tsx` | ソース | iframeラッパー |
| auth-frame.tsx | `apps/comments-ui/src/auth-frame.tsx` | ソース | 認証用iframe |
| package.json | `apps/comments-ui/package.json` | 設定 | パッケージ定義 |
| vite.config.ts | `apps/comments-ui/vite.config.ts` | 設定 | ビルド設定 |
| tailwind.config.cjs | `apps/comments-ui/tailwind.config.cjs` | 設定 | Tailwind CSS設定 |
