# 機能設計書 154-ポータル記事

## 概要

本ドキュメントは、Chatwootのヘルプセンターにおける「ポータル記事」機能の設計仕様を定義する。記事はナレッジベースの中核となるコンテンツであり、顧客が自己解決するための情報を提供する。

### 本機能の処理概要

**業務上の目的・背景**：
ヘルプセンターにおいて、記事はユーザーが直接参照する最も重要なコンテンツである。FAQや製品マニュアル、トラブルシューティングガイドなど、様々な形式のヘルプコンテンツを作成・管理できる。全文検索、ステータス管理（下書き・公開・アーカイブ）、閲覧数追跡、多言語対応など、プロフェッショナルなナレッジベース構築に必要な機能を提供する。

**機能の利用シーン**：
- サポート担当者がFAQ記事を作成・公開する場面
- 管理者が下書き記事をレビューして公開する場面
- 多言語対応サイトで各言語版の記事を作成する場面
- 記事の検索とフィルタリングを行う場面
- 記事のパフォーマンス（閲覧数）を確認する場面

**主要な処理内容**：
1. 記事の作成：タイトル、コンテンツ、説明、ステータス、カテゴリなどの基本情報を登録
2. 記事の更新：コンテンツの編集、ステータス変更、カテゴリ移動
3. 記事の削除：記事の完全削除
4. 記事一覧・詳細表示：検索、フィルタリング、カウント表示
5. 全文検索：タイトル、説明、コンテンツに対するPgSearch
6. ステータス管理：下書き、公開、アーカイブの状態遷移
7. 閲覧数追跡：記事の閲覧回数をカウント

**関連システム・外部連携**：
- PgSearch：PostgreSQL全文検索
- ActiveStorage：記事内の画像管理（将来的な拡張）
- LLM統合：AI記事生成・要約（LlmFormattable concern）

**権限による制御**：
- 一覧表示・詳細表示：アカウントに所属する全ユーザーがアクセス可能
- 作成・更新・削除：管理者（Administrator）権限が必要

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 93 | 記事一覧画面 | 主画面 | ヘルプ記事の一覧表示 |
| 94 | 記事新規作成画面 | 主画面 | 新しい記事の作成 |
| 95 | 記事編集画面 | 主画面 | 既存記事の編集 |

## 機能種別

CRUD操作 / 全文検索 / ステータス管理 / 統計追跡

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| title | String | Yes | 記事タイトル | presence: true |
| content | Text | Yes | 記事本文 | presence: true |
| description | Text | No | 記事概要 | - |
| slug | String | No | URL識別子 | 自動生成、uniqueness: true |
| locale | String | No | ロケール | default: カテゴリまたはポータルのロケール |
| status | Integer | No | ステータス | enum: draft(0), published(1), archived(2) |
| position | Integer | No | 表示順序 | - |
| category_id | Integer | No | カテゴリID | Category存在確認 |
| author_id | Integer | Yes | 著者ID | presence: true, User存在確認 |
| associated_article_id | Integer | No | 関連元記事ID | Article存在確認 |
| meta.title | String | No | SEOタイトル | - |
| meta.description | String | No | SEO説明 | - |
| meta.tags | Array | No | タグ一覧 | - |

### 入力データソース

- 画面入力：管理画面の記事編集フォーム
- API：`/api/v1/accounts/:account_id/portals/:portal_id/articles` エンドポイント

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | Integer | 記事ID |
| title | String | 記事タイトル |
| content | Text | 記事本文 |
| description | Text | 記事概要 |
| slug | String | URL識別子 |
| locale | String | ロケール |
| status | String | ステータス（draft/published/archived） |
| position | Integer | 表示順序 |
| views | Integer | 閲覧数 |
| account_id | Integer | アカウントID |
| portal_id | Integer | ポータルID |
| category_id | Integer | カテゴリID |
| author_id | Integer | 著者ID |
| author | Object | 著者情報 |
| category | Object | カテゴリ情報 |
| meta | JSON | SEOメタ情報 |
| associated_article_id | Integer | 関連元記事ID |
| created_at | DateTime | 作成日時 |
| updated_at | DateTime | 更新日時 |

### 出力先

- JSON API レスポンス
- JBuilder ビューテンプレート

## 処理フロー

### 処理シーケンス

```
1. 記事作成
   └─ リクエスト受信 → ポータル取得 → 認可チェック → パラメータ検証 → slug自動生成 → locale決定 → position計算 → Article作成 → 関連記事設定 → レスポンス返却

2. 記事更新
   └─ リクエスト受信 → ポータル・記事取得 → 認可チェック → パラメータ検証 → Article更新 → category変更時はposition再計算 → レスポンス返却

3. 記事検索
   └─ リクエスト受信 → ポータル取得 → 検索パラメータ適用 → PgSearch実行 → ページネーション → カウント計算 → レスポンス返却
```

### フローチャート

```mermaid
flowchart TD
    A[リクエスト受信] --> B[ポータル取得]
    B --> C{アクション判定}
    C -->|index| D[検索パラメータ適用]
    C -->|show| E[個別記事取得]
    C -->|create| F[パラメータ検証]
    C -->|update| G[記事取得 + 検証]
    C -->|destroy| H[記事取得]

    D --> I[PgSearch実行]
    I --> J[ステータス・著者フィルタ]
    J --> K[カウント計算]
    K --> L[ページネーション・ソート]
    L --> M[レスポンス返却]

    F --> N[slug自動生成]
    N --> O[locale決定]
    O --> P[position計算]
    P --> Q[Article作成]
    Q --> R[関連記事設定]
    R --> M

    G --> S[Article更新]
    S --> T{category変更?}
    T -->|Yes| U[position再計算]
    T -->|No| M
    U --> M

    H --> V[destroy!実行]
    V --> W[HTTP 200 OK]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-154-01 | slug一意性 | slugはシステム全体で一意 | Article作成時 |
| BR-154-02 | slug自動生成 | slugが未指定の場合、タイムスタンプ+タイトルから自動生成 | Article作成時 |
| BR-154-03 | ステータスデフォルト | 作成時のデフォルトステータスはdraft | Article作成時 |
| BR-154-04 | locale決定順序 | locale = カテゴリのlocale > 指定locale > ポータルのdefault_locale | Article作成・更新時 |
| BR-154-05 | account_id自動設定 | 記事のaccount_idはポータルのaccount_idから自動取得 | Article作成時 |
| BR-154-06 | position自動計算 | 同一カテゴリ内の最大position+10、または10 | Article作成・カテゴリ変更時 |
| BR-154-07 | 閲覧数カウント | 公開記事へのアクセス時にviews+1 | 公開APIアクセス時 |
| BR-154-08 | 関連記事設定 | associated_article_idは最上位の親記事IDに正規化 | 関連記事設定時 |

### 計算ロジック

```ruby
# slug自動生成
slug = "#{Time.now.utc.to_i}-#{title.underscore.parameterize(separator: '-')}"

# position計算
max_position = Article.where(category_id:, account_id:).maximum(:position)
new_position = max_position.present? ? max_position + 10 : 10

# 関連記事ID正規化
root_article_id = article.associated_article_id || article.id
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| create | articles | INSERT | 新規記事の作成 |
| update | articles | UPDATE | 記事情報の更新 |
| destroy | articles | DELETE | 記事の削除 |
| increment_view_count | articles | UPDATE | 閲覧数のインクリメント |

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

#### articles

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | title | リクエストパラメータ | 必須 |
| INSERT | content | リクエストパラメータ | 必須 |
| INSERT | slug | 自動生成または指定値 | 一意 |
| INSERT | status | draft（デフォルト） | enum |
| INSERT | author_id | リクエストパラメータ | 必須 |
| INSERT | account_id | portal.account_id | 自動設定 |
| INSERT | portal_id | params[:portal_id] | 自動設定 |
| INSERT | position | 計算値 | 自動設定 |
| UPDATE | views | views + 1 | 閲覧時（update_column使用） |
| SELECT | - | locale, category_slug, status, author_id, query | 検索条件 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 422 | UnprocessableEntity | title未入力 | タイトルを入力 |
| 422 | UnprocessableEntity | content未入力 | コンテンツを入力 |
| 422 | UnprocessableEntity | slug重複 | 異なるslugを指定 |
| 422 | UnprocessableEntity | author_id未入力 | 著者を指定 |
| 404 | NotFound | 指定IDのArticleが存在しない | 正しいIDを指定 |
| 404 | NotFound | 指定slugのPortalが存在しない | 正しいslugを指定 |
| 403 | Forbidden | 権限不足 | 管理者権限でアクセス |

### リトライ仕様

特になし

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

- create/update処理は暗黙的トランザクション
- increment_view_countはupdate_columnを使用（バリデーションスキップ）

## パフォーマンス要件

- 記事検索：200ms以内（PgSearch使用）
- ページネーションで大量記事に対応
- views更新はupdate_columnでバリデーションスキップによる高速化

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

- 管理者権限チェック必須
- XSS防止：コンテンツのサニタイズ
- SQLインジェクション防止：PgSearchによるパラメータ化クエリ

## 備考

- `Article.include_mod_with('Concerns::Article')`により拡張機能を追加可能
- LlmFormattable concernによりAI統合機能を提供
- 閲覧数追跡はトラッキングピクセル（.png）経由でも可能

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | article.rb | `app/models/article.rb` | スキーマ定義、バリデーション、関連モデル、検索機能 |

**読解のコツ**:
- **1-33行目**: Schema Informationコメントでテーブル構造を把握
- **35行目**: `include PgSearch::Model` - 全文検索機能
- **36行目**: `include LlmFormattable` - AI統合
- **38-52行目**: 関連モデル定義（associated_articles, category, portal, author）
- **54-61行目**: バリデーション
- **63-65行目**: `before_create :add_position_to_article` - position自動計算
- **67行目**: `enum status: { draft: 0, published: 1, archived: 2 }`
- **69-77行目**: スコープ定義（search_by_*）
- **83-97行目**: pg_search_scope設定 - 重み付き全文検索

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | articles_controller.rb | `app/controllers/api/v1/accounts/articles_controller.rb` | CRUD操作 |

**主要処理フロー**:
1. **7-19行目**: index - 記事検索、カウント計算、ソート
2. **24-29行目**: create - デフォルトステータス設定、記事作成、関連記事設定
3. **32-35行目**: update - 記事更新
4. **37-40行目**: destroy - 記事削除
5. **42-45行目**: reorder - 記事順序変更
6. **49-60行目**: set_article_count - 各種カウント計算

#### Step 3: 公開API側を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | articles_controller.rb | `app/controllers/public/api/v1/portals/articles_controller.rb` | 公開記事API |

**主要処理フロー**:
- **8-18行目**: index - 公開記事一覧（published only）
- **20-22行目**: show - 個別記事表示
- **24-37行目**: tracking_pixel - 閲覧数追跡用1x1ピクセル

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

```
Api::V1::Accounts::ArticlesController
    │
    ├─ before_action :portal
    │      └─ Current.account.portals.find_by!(slug:)
    │
    ├─ before_action :fetch_article
    │      └─ @portal.articles.find(params[:id])
    │
    ├─ index
    │      ├─ set_article_count
    │      │      ├─ @articles.count
    │      │      ├─ @articles.search_by_author
    │      │      └─ @articles.published/draft/archived.count
    │      │
    │      ├─ Article.search(list_params)
    │      │      ├─ search_by_category_slug
    │      │      ├─ search_by_locale
    │      │      ├─ search_by_author
    │      │      ├─ search_by_status
    │      │      └─ text_search (PgSearch)
    │      │
    │      └─ order_by_position / order_by_updated_at
    │
    ├─ create
    │      ├─ @portal.articles.create!(article_params)
    │      │      ├─ ensure_account_id
    │      │      ├─ ensure_article_slug
    │      │      ├─ ensure_locale_in_article
    │      │      └─ add_position_to_article
    │      │
    │      └─ @article.associate_root_article
    │
    └─ destroy
           └─ @article.destroy!
```

### データフロー図

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

API Request         →   ArticlesController       →   JSON Response
(params)                    │
                           ├─ portal取得
                           │     (slug → Portal)
                           │
                           ├─ Article Model
                           │     ├─ バリデーション
                           │     ├─ slug自動生成
                           │     ├─ locale決定
                           │     └─ position計算
                           │
                           └─ PgSearch
                                 (全文検索)
                                       ↓
                              [articles テーブル]

Public API          →   tracking_pixel           →   1x1 PNG
(article_slug)              │
                           └─ increment_view_count
                                       ↓
                              [articles.views UPDATE]
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| article.rb | `app/models/article.rb` | モデル | 記事のデータモデル定義 |
| articles_controller.rb | `app/controllers/api/v1/accounts/articles_controller.rb` | コントローラー | 管理API CRUD操作 |
| articles_controller.rb | `app/controllers/public/api/v1/portals/articles_controller.rb` | コントローラー | 公開API |
| category.rb | `app/models/category.rb` | モデル | 親カテゴリ |
| portal.rb | `app/models/portal.rb` | モデル | 親ポータル |
| user.rb | `app/models/user.rb` | モデル | 著者情報 |
| chatwoot_markdown_renderer.rb | `app/services/chatwoot_markdown_renderer.rb` | サービス | マークダウンレンダリング |
