# 機能設計書 27-コメントいいね

## 概要

本ドキュメントは、Ghostのコメントいいね機能の設計仕様を記述する。コメントいいね機能は、メンバーがコメントに対して「いいね」を付けることで、コミュニティ内での評価やエンゲージメントを促進する。

### 本機能の処理概要

**業務上の目的・背景**：コメントへの「いいね」機能は、読者がコメントに対する賛同や共感を簡単に表現できる手段を提供する。これにより、質の高いコメントが可視化され、コミュニティの活性化と建設的な議論の促進に寄与する。

**機能の利用シーン**：
- メンバーがコメントに「いいね」を付ける
- メンバーが「いいね」を取り消す
- コメント一覧でいいね数を確認
- 自分がいいね済みかどうかを確認

**主要な処理内容**：
1. いいねの追加（CommentLike レコード作成）
2. いいねの取り消し（CommentLike レコード削除）
3. いいね数のカウント表示
4. いいね状態の確認（liked フラグ）

**関連システム・外部連携**：
- Members API
- DomainEvents（comment_like.added イベント）

**権限による制御**：
- いいね操作：認証済みメンバーのみ（コメント設定によりfree/paid制限可）
- いいね数の閲覧：全員可能

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 83 | コメントセクション（埋め込み） | 主機能 | いいねボタン操作・いいね数表示 |

## 機能種別

CRUD / カウント集計

## 入力仕様

### 入力パラメータ（いいね追加）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | string | Yes | 対象コメントID | 存在するコメント |

### 入力パラメータ（いいね取り消し）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | string | Yes | 対象コメントID | 存在するコメント |

### 入力データソース

- **comments テーブル**: コメント存在確認
- **comment_likes テーブル**: 重複チェック
- **members テーブル**: メンバー認証

## 出力仕様

### 出力データ（いいね追加）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | CommentLike レコードID |
| comment_id | string | 対象コメントID |
| member_id | string | いいねしたメンバーID |
| created_at | datetime | いいね日時 |

### 出力データ（コメント表示時）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| count.likes | number | いいね総数 |
| liked | boolean | 現在のメンバーがいいね済みか |

### 出力先

- **comment_likes テーブル**: いいねデータの永続化
- **Members API レスポンス**: いいね数・状態

## 処理フロー

### 処理シーケンス（いいね追加）

```
1. コメント機能の有効性確認
   └─ checkEnabled() で comments_enabled をチェック

2. メンバー権限確認
   └─ メンバーのステータス（free/paid）とコメント設定を照合

3. 重複チェック
   └─ comment_likes テーブルで既存レコードを検索
   └─ 既にいいね済みの場合は BadRequestError

4. いいね追加
   └─ comment_likes テーブルに INSERT

5. キャッシュ無効化
   └─ X-Cache-Invalidate ヘッダー設定
```

### フローチャート

```mermaid
flowchart TD
    A[いいねリクエスト] --> B{メンバー認証?}
    B -->|No| C[UnauthorizedError]
    B -->|Yes| D{コメント機能有効?}
    D -->|No| E[MethodNotAllowedError]
    D -->|Yes| F{メンバー権限OK?}
    F -->|No| G[NoPermissionError]
    F -->|Yes| H{既にいいね済み?}
    H -->|Yes| I[BadRequestError: already liked]
    H -->|No| J[comment_likes INSERT]
    J --> K[キャッシュ無効化]
    K --> L[204 No Content]

    subgraph いいね取り消し
    M[いいね取り消しリクエスト] --> N{メンバー認証?}
    N -->|No| O[UnauthorizedError]
    N -->|Yes| P{コメント機能有効?}
    P -->|No| Q[MethodNotAllowedError]
    P -->|Yes| R[comment_likes DELETE]
    R --> S{レコード存在?}
    S -->|No| T[NotFoundError: like not found]
    S -->|Yes| U[キャッシュ無効化]
    U --> V[204 No Content]
    end
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-27-01 | 認証必須 | いいね操作はログインメンバーのみ可能 | 全操作 |
| BR-27-02 | 有料メンバー限定 | comments_enabled が 'paid' の場合、freeメンバーは操作不可 | いいね追加時 |
| BR-27-03 | 重複禁止 | 同一メンバーは同一コメントに1回のみいいね可能 | いいね追加時 |
| BR-27-04 | 自己いいね許可 | 自分のコメントにいいねすることは可能 | いいね追加時 |
| BR-27-05 | 物理削除 | いいね取り消しは物理削除（論理削除ではない） | いいね取り消し時 |

### 計算ロジック

**いいね数のカウント**：
```sql
SELECT COUNT(comment_likes.id)
FROM comment_likes
WHERE comment_likes.comment_id = comments.id
```

**いいね済み判定**：
```sql
SELECT COUNT(comment_likes.id)
FROM comment_likes
WHERE comment_likes.comment_id = comments.id
  AND comment_likes.member_id = ?
```
- count > 0 の場合、liked = true

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| いいね追加 | comment_likes | INSERT | 新規いいねレコード作成 |
| いいね取り消し | comment_likes | DELETE | いいねレコード削除 |
| 重複チェック | comment_likes | SELECT | 既存レコード検索 |
| いいね数取得 | comment_likes | SELECT | COUNT集計 |

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

#### comment_likes

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | 自動生成 (24文字) | ObjectID |
| INSERT | comment_id | リクエストパラメータ | 外部キー制約 |
| INSERT | member_id | ログインメンバーID | 外部キー制約 |
| INSERT | created_at | 現在日時 | 自動設定 |
| INSERT | updated_at | 現在日時 | 自動設定 |
| DELETE | - | comment_id AND member_id | destroyBy 使用 |
| SELECT | - | comment_id AND member_id | 重複チェック |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | UnauthorizedError | メンバー未認証 | 'Unable to find member' |
| - | MethodNotAllowedError | コメント機能が無効 | 'Comments are not enabled for this site.' |
| - | NoPermissionError | メンバー権限不足（paid限定時） | 'You do not have permission to comment on this post.' |
| - | BadRequestError | 既にいいね済み | 'This comment was liked already' |
| - | NotFoundError | いいねレコードが存在しない（取り消し時） | 'Unable to find like' |

### リトライ仕様

- 特になし

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

- いいね追加・削除は単一レコード操作のため、暗黙的トランザクション

## パフォーマンス要件

- いいね操作: 即時応答
- いいね数カウント: コメント取得時にサブクエリで効率的に取得

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

- メンバー認証による操作制限
- 自分のいいね状態のみ取得可能（他人のいいね一覧は非公開）
- キャッシュ無効化によるデータ整合性確保

## 備考

- いいねは comment_like.added イベントを発行
- count.liked は現在のメンバーがいいね済みかを示すカウント（0 or 1）
- いいねの通知機能は現時点では未実装

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | schema.js | `ghost/core/core/server/data/schema/schema.js` | comment_likes テーブル（985-991行）のスキーマ定義 |
| 1-2 | comment-like.js | `ghost/core/core/server/models/comment-like.js` | CommentLike モデルのリレーション定義 |

**読解のコツ**: comment_likes テーブルは comment_id, member_id の複合で一意。cascadeDelete により、コメント削除時に自動削除される。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | comments-members.js | `ghost/core/core/server/api/endpoints/comments-members.js` | like/unlike エンドポイント定義（161-190行） |
| 2-2 | comments-controller.js | `ghost/core/core/server/services/comments/comments-controller.js` | like/unlike コントローラー（290-339行） |

**主要処理フロー**:
1. **161-175行目** (endpoints): like - いいね追加エンドポイント
2. **177-190行目** (endpoints): unlike - いいね取り消しエンドポイント
3. **290-312行目** (controller): like - コントローラー処理
4. **317-339行目** (controller): unlike - コントローラー処理

#### Step 3: サービスロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | comments-service.js | `ghost/core/core/server/services/comments/comments-service.js` | likeComment, unlikeComment メソッド |

**主要処理フロー**:
- **94-121行目**: likeComment() - いいね追加のビジネスロジック
- **123-144行目**: unlikeComment() - いいね取り消しのビジネスロジック

#### Step 4: カウント処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | comment.js | `ghost/core/core/server/models/comment.js` | countRelations でのいいね数カウント（264-286行） |

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

```
Members API
    │
    ├─ comments-members.like
    │      │
    │      ├─ permissions: false (メンバー認証はコントローラーで実行)
    │      │
    │      └─ CommentsController.like(frame)
    │             │
    │             ├─ #checkMember() - メンバー認証確認
    │             │
    │             ├─ CommentsService.likeComment()
    │             │      │
    │             │      ├─ checkEnabled()
    │             │      │      └─ settingsCache.get('comments_enabled')
    │             │      │
    │             │      ├─ Member.findOne() - メンバー取得
    │             │      │
    │             │      ├─ checkCommentAccess() - 権限確認
    │             │      │
    │             │      ├─ CommentLike.findOne() - 重複チェック
    │             │      │
    │             │      └─ CommentLike.add() - いいね追加
    │             │             └─ onCreated() → emitChange('added')
    │             │
    │             ├─ getCommentByID() - キャッシュ無効化用
    │             │
    │             └─ frame.setHeader('X-Cache-Invalidate')
    │
    └─ comments-members.unlike
           │
           └─ CommentsController.unlike(frame)
                  │
                  ├─ #checkMember()
                  │
                  ├─ CommentsService.unlikeComment()
                  │      │
                  │      ├─ checkEnabled()
                  │      │
                  │      └─ CommentLike.destroy({destroyBy})
                  │
                  └─ frame.setHeader('X-Cache-Invalidate')
```

### データフロー図

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

comment_id ──────────────▶ likeComment()
member (context)                 │
                                 ├─ checkEnabled()
                                 │
                                 ├─ Member.findOne()
                                 │
                                 ├─ checkCommentAccess()
                                 │
                                 ├─ CommentLike.findOne()
                                 │      └─ 重複チェック
                                 │
                                 └─ CommentLike.add() ───────▶ comment_likes INSERT
                                        │
                                        └─ emitChange('added') ─▶ comment_like.added event

[いいね数取得フロー]

Comment.findPage() ──────▶ countRelations.likes() ──────▶ count__likes
                                 │
                                 └─ COUNT(comment_likes.id)
                                        WHERE comment_id = comments.id

                          countRelations.liked() ──────▶ count__liked (0 or 1)
                                 │
                                 └─ COUNT(comment_likes.id)
                                        WHERE member_id = context.member.id
                                 │
                                 ▼
                          liked = count__liked > 0 ─────▶ boolean
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| comments-members.js | `ghost/core/core/server/api/endpoints/comments-members.js` | API | Members API エンドポイント |
| comments-controller.js | `ghost/core/core/server/services/comments/comments-controller.js` | コントローラー | リクエスト処理 |
| comments-service.js | `ghost/core/core/server/services/comments/comments-service.js` | サービス | ビジネスロジック |
| comment-like.js | `ghost/core/core/server/models/comment-like.js` | モデル | CommentLikeモデル定義 |
| comment.js | `ghost/core/core/server/models/comment.js` | モデル | カウントクエリ定義 |
| schema.js | `ghost/core/core/server/data/schema/schema.js` | 設定 | DBスキーマ定義 |
