# 機能設計書 32-添付ファイル管理

## 概要

本ドキュメントは、Chatwootにおける会話添付ファイル管理機能の設計仕様を記載する。会話内のメッセージに紐づく添付ファイル（画像、動画、ドキュメント等）のアップロードと一覧表示を行う機能を提供する。

### 本機能の処理概要

**業務上の目的・背景**：カスタマーサポート業務において、顧客とのコミュニケーションでは画像やドキュメントなどのファイル共有が頻繁に必要となる。スクリーンショットによる問題説明、マニュアルやガイドの送付、請求書や契約書のやり取りなど、テキストだけでは伝えられない情報を効率的に交換するために添付ファイル機能が必要となる。

**機能の利用シーン**：
- 顧客がスクリーンショットや画像で問題を報告する場合
- エージェントがPDFマニュアルやガイドを送付する場合
- 請求書、領収書、契約書などのドキュメントをやり取りする場合
- 音声ファイルや動画で説明を補足する場合
- 会話の添付ファイル履歴を確認する場合

**主要な処理内容**：
1. 添付ファイルの直接アップロード（Direct Upload）：ActiveStorageを使用した効率的なファイルアップロード
2. 添付ファイル一覧表示：会話に紐づく全添付ファイルの一覧取得（ページネーション対応）
3. ファイルメタデータ管理：ファイルタイプ、サイズ、拡張子、サムネイルなどの情報管理

**関連システム・外部連携**：
- ActiveStorage：Railsの標準ファイルストレージ機能
- クラウドストレージ：S3等の外部ストレージサービス（設定による）
- 画像処理：サムネイル生成のためのimage_processingライブラリ

**権限による制御**：
- 会話へのアクセス権限（ConversationPolicy#show?）を持つユーザーのみがファイルをアップロード・閲覧可能
- アップロード前にアカウントと会話の存在確認を実施

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 11 | 会話詳細画面 | 主画面 | 添付ファイルのアップロードと表示 |

## 機能種別

CRUD操作（添付ファイルの作成・読取）/ ファイル処理

## 入力仕様

### 入力パラメータ

#### Direct Upload（ファイルアップロード）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| account_id | Integer | Yes | アカウントID（URLパラメータ） | 存在チェック |
| conversation_id | Integer | Yes | 会話のdisplay_id（URLパラメータ） | 存在チェック |
| blob | Object | Yes | ActiveStorageのblob情報 | ActiveStorage標準バリデーション |
| blob.filename | String | Yes | ファイル名 | - |
| blob.byte_size | Integer | Yes | ファイルサイズ（バイト） | 最大40MB（設定可能） |
| blob.checksum | String | Yes | MD5チェックサム | - |
| blob.content_type | String | Yes | MIMEタイプ | 許可されたファイルタイプのみ |

#### Attachments（一覧取得）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| account_id | Integer | Yes | アカウントID | 存在チェック |
| id | Integer | Yes | 会話のdisplay_id | 存在チェック |
| page | Integer | No | ページ番号 | 1以上の整数 |

### 入力データソース

- URLパラメータ: account_id, conversation_id
- リクエストボディ: blob情報（Direct Upload）

## 出力仕様

### 出力データ

#### Direct Upload レスポンス

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | Integer | Blob ID |
| key | String | ストレージキー |
| filename | String | ファイル名 |
| content_type | String | MIMEタイプ |
| byte_size | Integer | ファイルサイズ |
| checksum | String | チェックサム |
| created_at | DateTime | 作成日時 |
| signed_id | String | 署名付きID |
| direct_upload | Object | アップロード先情報 |

#### Attachments 一覧レスポンス

| 項目名 | 型 | 説明 |
|--------|-----|------|
| meta.total_count | Integer | 総添付ファイル数 |
| payload | Array | 添付ファイル配列 |
| payload[].message_id | Integer | メッセージID |
| payload[].thumb_url | String | サムネイルURL |
| payload[].data_url | String | ファイルURL |
| payload[].file_size | Integer | ファイルサイズ |
| payload[].file_type | String | ファイルタイプ |
| payload[].extension | String | 拡張子 |
| payload[].width | Integer | 画像幅（画像の場合） |
| payload[].height | Integer | 画像高さ（画像の場合） |
| payload[].created_at | Integer | 作成日時（Unix時間） |
| payload[].sender | Object | 送信者情報 |

### 出力先

- JSON形式でHTTPレスポンスとして返却

## 処理フロー

### 処理シーケンス

```
【Direct Upload】
1. リクエスト受信
   └─ account_id, conversation_idの検証

2. 会話・アカウント存在確認
   └─ Current.account.conversations.find_by(display_id: conversation_id)

3. ActiveStorage Direct Upload処理
   └─ 親クラスActiveStorage::DirectUploadsController#createを呼び出し

4. Blob作成・署名付きURLの返却
   └─ クライアントがこのURLに直接ファイルをアップロード

【Attachments一覧】
1. リクエスト受信
   └─ account_id, conversation_idの検証

2. 会話取得・認可チェック
   └─ ConversationPolicy#show?

3. 添付ファイル取得
   └─ @conversation.attachments
   └─ includes(:message)で関連読み込み
   └─ created_at降順でソート
   └─ ページネーション（100件/ページ）

4. レスポンス生成
   └─ JSON形式で返却
```

### フローチャート

```mermaid
flowchart TD
    A[リクエスト受信] --> B{アクション判定}

    B -->|Direct Upload| C[会話・アカウント確認]
    C --> D{存在チェック}
    D -->|失敗| E[処理中断]
    D -->|成功| F[ActiveStorage処理]
    F --> G[署名付きURL返却]

    B -->|Attachments| H[会話取得]
    H --> I{認可チェック}
    I -->|失敗| J[403 Forbidden]
    I -->|成功| K[添付ファイル取得]
    K --> L[ページネーション処理]
    L --> M[JSON生成]
    M --> N[レスポンス返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-32-1 | ファイルサイズ制限 | 最大40MB（MAXIMUM_FILE_UPLOAD_SIZE設定で変更可能） | 全てのアップロード |
| BR-32-2 | ファイルタイプ制限 | 画像・音声・動画・許可されたドキュメント形式のみ（Web Widget経由の場合） | Web Widget経由のアップロード |
| BR-32-3 | ページネーション | 1ページあたり最大100件 | 添付ファイル一覧取得時 |
| BR-32-4 | ソート順 | 作成日時の降順（新しい順） | 添付ファイル一覧取得時 |

### 許可されているファイルタイプ

**メディアファイル**：
- image/* （画像全般）
- video/* （動画全般）
- audio/* （音声全般）

**ドキュメントファイル**：
- text/csv, text/plain, text/rtf
- application/json, application/pdf
- application/zip, application/x-7z-compressed, application/vnd.rar, application/x-tar
- application/msword, application/vnd.ms-excel, application/vnd.ms-powerpoint
- application/vnd.oasis.opendocument.text
- application/vnd.openxmlformats-officedocument.* (Office 2007以降の形式)

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| Direct Upload | active_storage_blobs | INSERT | Blob情報登録 |
| Attachments | attachments | SELECT | 添付ファイル一覧取得 |
| Attachments | messages | SELECT | 関連メッセージ取得（includes） |

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

#### attachments

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | * | conversation.attachments経由 | messages経由で取得 |
| ORDER | created_at | DESC | 新しい順 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 404 | NotFound | 会話が存在しない | 正しいconversation_idを指定 |
| 403 | Forbidden | 会話へのアクセス権限がない | 権限を持つユーザーでアクセス |
| 422 | UnprocessableEntity | ファイルサイズ超過 | 40MB以下のファイルを使用 |
| 422 | UnprocessableEntity | 非対応ファイルタイプ | 許可されたファイルタイプを使用 |

### リトライ仕様

- Direct Uploadは冪等性があり、同じリクエストで再試行可能
- ネットワークエラー時はクライアント側でリトライ処理を実装

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

- Direct Upload: ActiveStorageの標準トランザクション制御に従う
- Attachments一覧: 読み取り専用（トランザクション不要）

## パフォーマンス要件

- 一覧取得: 100件/ページのページネーションで大量データに対応
- includes(:message)による N+1 問題の回避
- サムネイル生成は遅延処理（アクセス時に生成）

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

- ConversationPolicy#show?による会話アクセス権限チェック
- ActiveStorageの署名付きURLによるセキュアなファイルアクセス
- ファイルタイプ検証による悪意のあるファイルのブロック
- ファイルサイズ制限によるDoS攻撃の防止

## 備考

- Direct Uploadは2段階のアップロードプロセス: 1) サーバーから署名付きURLを取得、2) クライアントからストレージに直接アップロード
- サムネイルは画像ファイルのみ生成される（250px幅にリサイズ）
- Instagram経由のメッセージでは外部URLが使用される

---

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

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

### 推奨読解順序

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

まず、添付ファイルのデータモデルを理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | attachment.rb | `app/models/attachment.rb` | 添付ファイルモデル、file_typeのenum、バリデーション |
| 1-2 | conversation.rb | `app/models/conversation.rb` | 会話との関連（has_many :attachments, through: :messages） |

**読解のコツ**: Attachmentモデルはmessageに紐づく。file_typeのenumで画像・音声・動画等を区別。`push_event_data`メソッドがAPI出力の主要データを生成。

**主要処理フロー（attachment.rb）**:
- **27-36行目**: 許可されているファイルタイプの定義
- **42-43行目**: file_type enumの定義
- **45-49行目**: push_event_data - API出力用データ生成
- **62-71行目**: thumb_url - サムネイルURL生成（250px幅）
- **165-181行目**: ファイルバリデーション（サイズ、タイプ）

#### Step 2: Direct Uploadコントローラーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | direct_uploads_controller.rb | `app/controllers/api/v1/accounts/conversations/direct_uploads_controller.rb` | Direct Upload処理の実装 |

**主要処理フロー**:
- **1行目**: ActiveStorage::DirectUploadsControllerを継承
- **6-10行目**: create - 会話とアカウントの存在確認後に親クラスのcreateを呼び出し
- **14-16行目**: conversation - 会話の取得ロジック

#### Step 3: 添付ファイル一覧取得を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | conversations_controller.rb | `app/controllers/api/v1/accounts/conversations_controller.rb` | attachmentsアクションの実装 |
| 3-2 | attachments.json.jbuilder | `app/views/api/v1/accounts/conversations/attachments.json.jbuilder` | JSON出力形式 |

**主要処理フロー（conversations_controller.rb）**:
- **9行目**: ATTACHMENT_RESULTS_PER_PAGE = 100（ページサイズ定数）
- **28-35行目**: attachments - 一覧取得処理
  - includes(:message)でN+1回避
  - order(created_at: :desc)で新しい順
  - pageとperでページネーション

#### Step 4: ビューを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | attachments.json.jbuilder | `app/views/api/v1/accounts/conversations/attachments.json.jbuilder` | 一覧のJSON形式 |

**主要処理フロー**:
- **1-3行目**: meta - 総件数
- **5-16行目**: payload - 各添付ファイルの詳細データ

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

```
【Direct Upload】
Api::V1::Accounts::Conversations::DirectUploadsController
    │
    ├─ EnsureCurrentAccountHelper#current_account
    │
    ├─ conversation
    │      └─ Current.account.conversations.find_by
    │
    └─ create (super)
           └─ ActiveStorage::DirectUploadsController#create
                  └─ ActiveStorage::Blob.create_before_direct_upload!

【Attachments一覧】
Api::V1::Accounts::ConversationsController#attachments
    │
    ├─ conversation
    │      └─ Current.account.conversations.find_by!
    │      └─ authorize @conversation, :show?
    │
    ├─ @conversation.attachments
    │      └─ includes(:message)
    │      └─ order(created_at: :desc)
    │      └─ page().per()
    │
    └─ attachments.json.jbuilder
           └─ attachment.push_event_data
```

### データフロー図

```
【Direct Upload】
[入力]                    [処理]                         [出力]

Blob情報        ───▶ DirectUploadsController  ───▶ 署名付きURL
(filename,             │                              (direct_upload.url)
 byte_size,            ▼
 content_type)    ActiveStorage::Blob
                       │
                       ▼
                active_storage_blobs
                    (DB Table)

【Attachments一覧】
[入力]                    [処理]                         [出力]

conversation_id ───▶ ConversationsController  ───▶ JSON Response
page                       │                          (meta + payload)
                           ▼
                    Attachment.push_event_data
                           │
                           ▼
                    attachments / messages
                        (DB Table)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| direct_uploads_controller.rb | `app/controllers/api/v1/accounts/conversations/direct_uploads_controller.rb` | コントローラー | Direct Uploadエンドポイント |
| conversations_controller.rb | `app/controllers/api/v1/accounts/conversations_controller.rb` | コントローラー | attachmentsアクション（28-35行目） |
| attachment.rb | `app/models/attachment.rb` | モデル | 添付ファイルデータモデル |
| conversation.rb | `app/models/conversation.rb` | モデル | 会話モデル（attachments関連） |
| message.rb | `app/models/message.rb` | モデル | メッセージモデル（添付ファイル親） |
| attachments.json.jbuilder | `app/views/api/v1/accounts/conversations/attachments.json.jbuilder` | ビュー | 一覧JSON生成 |
| conversation_policy.rb | `app/policies/conversation_policy.rb` | ポリシー | アクセス権限制御 |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義（141行目） |
