# 機能設計書 155-記事順序変更

## 概要

本ドキュメントは、Chatwootのヘルプセンターにおける「記事順序変更」機能の設計仕様を定義する。この機能により、管理者はドラッグ＆ドロップやAPIを通じて記事の表示順序を変更できる。

### 本機能の処理概要

**業務上の目的・背景**：
ヘルプセンターにおいて、記事の表示順序は情報のアクセシビリティに直結する。重要な記事を上位に配置したり、関連する記事をグループ化して並べることで、ユーザーが必要な情報をより効率的に見つけられるようになる。この機能は、カテゴリ内での記事の並び順をカスタマイズするために使用される。

**機能の利用シーン**：
- 管理者がカテゴリ内の記事を重要度順に並び替える場面
- 新しい記事を追加した後に、適切な位置に配置する場面
- FAQの順序を質問頻度に応じて調整する場面
- ドラッグ＆ドロップUIで直感的に順序を変更する場面

**主要な処理内容**：
1. 複数記事の位置（position）を一括更新
2. 各記事のIDと新しいpositionのマッピングを受け取る
3. トランザクション内で全記事の位置を更新

**関連システム・外部連携**：
- フロントエンドUI：ドラッグ＆ドロップによる順序変更

**権限による制御**：
- 順序変更：管理者（Administrator）権限が必要

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 95 | 記事編集画面 | 補助画面 | 記事の順序変更 |
| 93 | 記事一覧画面 | 主画面 | 記事一覧でのドラッグ＆ドロップ順序変更 |

## 機能種別

バッチ更新処理 / 位置管理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| portal_id | String | Yes | ポータルのslug | Portal存在確認 |
| positions_hash | Hash | Yes | {article_id => new_position} のハッシュ | 各Article存在確認 |

### 入力データソース

- API：`POST /api/v1/accounts/:account_id/portals/:portal_id/articles/reorder`
- リクエストボディ：`{ positions_hash: { "1": 10, "2": 20, "3": 30 } }`

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| (なし) | - | 成功時はHTTP 200 OKのみ |

### 出力先

- HTTP レスポンス（ステータスコードのみ）

## 処理フロー

### 処理シーケンス

```
1. 順序変更実行
   └─ リクエスト受信 → ポータル取得 → 認可チェック → positions_hash取得 → 各記事のposition更新 → HTTP 200返却
```

### フローチャート

```mermaid
flowchart TD
    A[POST /articles/reorder] --> B[portal取得]
    B --> C{Portalが存在?}
    C -->|No| D[404 Not Found]
    C -->|Yes| E[check_authorization]
    E --> F{管理者権限?}
    F -->|No| G[403 Forbidden]
    F -->|Yes| H[positions_hash取得]
    H --> I[Article.update_positions呼び出し]
    I --> J{各記事が存在?}
    J -->|No| K[ActiveRecord::RecordNotFound]
    J -->|Yes| L[position更新]
    L --> M[HTTP 200 OK]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-155-01 | 一括更新 | 指定された全記事のpositionを一括更新 | reorder実行時 |
| BR-155-02 | 個別更新 | 各記事は個別にfind→updateされる | reorder実行時 |
| BR-155-03 | 存在チェック | 指定IDの記事が存在しない場合はエラー | reorder実行時 |

### 計算ロジック

```ruby
def self.update_positions(positions_hash)
  positions_hash.each do |article_id, new_position|
    article = Article.find(article_id)
    article.update!(position: new_position)
  end
end
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| reorder | articles | UPDATE (複数) | 各記事のpositionを更新 |

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

#### articles

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | position | positions_hash[article_id] | 新しい順序値 |
| UPDATE | updated_at | 現在時刻 | 自動更新 |
| SELECT | id | positions_hash.keys | 更新対象の記事取得 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 404 | NotFound | 指定slugのPortalが存在しない | 正しいslugを指定 |
| 404 | NotFound | 指定IDのArticleが存在しない | 正しいIDを指定 |
| 403 | Forbidden | 管理者権限がない | 管理者権限でアクセス |
| 422 | UnprocessableEntity | positions_hashが不正 | 正しい形式で指定 |

### リトライ仕様

特になし（失敗時は全体がロールバック）

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

- 現在の実装では各記事を個別にupdate!するため、部分的な更新が発生する可能性がある
- 改善案：全体をトランザクションでラップして原子性を保証

## パフォーマンス要件

- 記事数に比例してクエリ数が増加（N+1パターン）
- 大量の記事を一括更新する場合は注意が必要

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

- 管理者権限チェック必須
- 他のポータルの記事IDを指定しても更新可能（要検討）

## 備考

- 現在の実装はN+1クエリパターンのため、大量記事の更新時にパフォーマンス問題が発生する可能性がある
- 改善案：`Article.where(id: ids).update_all`を使用したバルク更新

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | articles_controller.rb | `app/controllers/api/v1/accounts/articles_controller.rb` | reorderアクション |

**主要処理フロー**:
- **42-45行目**: reorder - 順序変更のエントリーポイント

```ruby
def reorder
  Article.update_positions(params[:positions_hash])
  head :ok
end
```

#### Step 2: モデルメソッドを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | article.rb | `app/models/article.rb` | update_positionsクラスメソッド |

**主要処理フロー**:
- **135-141行目**: update_positions - 各記事のposition更新

```ruby
def self.update_positions(positions_hash)
  positions_hash.each do |article_id, new_position|
    article = Article.find(article_id)
    article.update!(position: new_position)
  end
end
```

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

```
Api::V1::Accounts::ArticlesController#reorder
    │
    ├─ before_action :portal
    │      └─ Current.account.portals.find_by!(slug:)
    │
    ├─ before_action :check_authorization
    │
    └─ Article.update_positions(params[:positions_hash])
           │
           └─ positions_hash.each do |article_id, new_position|
                  ├─ Article.find(article_id)
                  └─ article.update!(position: new_position)
```

### データフロー図

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

POST Request        →   ArticlesController#reorder   →   HTTP 200 OK
{ positions_hash:        │
  { "1": 10,            │
    "2": 20,            └─ Article.update_positions
    "3": 30 }                  │
}                             ├─ find(1).update!(position: 10)
                              ├─ find(2).update!(position: 20)
                              └─ find(3).update!(position: 30)
                                       ↓
                              [articles テーブル UPDATE x N]
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| article.rb | `app/models/article.rb` | モデル | update_positionsクラスメソッド |
| articles_controller.rb | `app/controllers/api/v1/accounts/articles_controller.rb` | コントローラー | reorderアクション |
