# 機能設計書 157-ロゴ管理

## 概要

本ドキュメントは、Chatwootのヘルプセンターにおける「ロゴ管理」機能の設計仕様を定義する。この機能により、管理者はポータルにブランドロゴをアップロード・削除し、企業アイデンティティを反映したヘルプセンターを構築できる。

### 本機能の処理概要

**業務上の目的・背景**：
企業のヘルプセンターでは、ブランドの一貫性を保つことが重要である。ロゴを設定することで、顧客はヘルプセンターが公式なサポートチャネルであることを認識でき、信頼性が向上する。ActiveStorageを活用したファイル管理により、様々な画像形式に対応し、効率的なストレージ管理を実現する。

**機能の利用シーン**：
- 管理者がポータル作成時に企業ロゴをアップロードする場面
- ブランドリニューアルに伴いロゴを更新する場面
- 不要になったロゴを削除する場面

**主要な処理内容**：
1. ロゴのアップロード：ActiveStorage Direct Uploadで署名済みblob IDを取得し、ポータルに添付
2. ロゴの削除：ポータルから添付されているロゴを削除（purge）
3. ロゴ情報の取得：ロゴのURL、ファイル名、コンテンツタイプなどを返却

**関連システム・外部連携**：
- ActiveStorage：ファイルストレージ管理
- ストレージサービス（S3、GCS等）：実際のファイル保存先

**権限による制御**：
- ロゴのアップロード・削除：管理者（Administrator）権限が必要

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 98 | ポータル設定画面 | 主画面 | ポータルロゴの設定 |

## 機能種別

ファイル管理 / CRUD操作（アップロード・削除）

## 入力仕様

### 入力パラメータ

#### アップロード時

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | String | Yes | ポータルのslug | Portal存在確認 |
| blob_id | String | Yes | 署名済みActiveStorage Blob ID | Blob存在確認 |

#### 削除時

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | String | Yes | ポータルのslug | Portal存在確認 |

### 入力データソース

- アップロード：`PATCH /api/v1/accounts/:account_id/portals/:id`（blob_idパラメータ付き）
- 削除：`DELETE /api/v1/accounts/:account_id/portals/:id/logo`

## 出力仕様

### 出力データ

#### ロゴ情報（ポータル詳細内）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | Integer | 添付ファイルID |
| portal_id | Integer | ポータルID |
| file_type | String | コンテンツタイプ（例：image/png） |
| account_id | Integer | アカウントID |
| file_url | String | ファイルのURL |
| blob_id | String | 署名済みBlob ID |
| filename | String | ファイル名 |

#### 削除時

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

### 出力先

- JSON API レスポンス（ポータル詳細内にロゴ情報を含む）
- HTTP レスポンス（削除時はステータスコードのみ）

## 処理フロー

### 処理シーケンス

```
1. ロゴアップロード
   └─ リクエスト受信 → ポータル取得 → 認可チェック → Blob取得 → ロゴ添付 → レスポンス返却

2. ロゴ削除
   └─ リクエスト受信 → ポータル取得 → 認可チェック → ロゴ添付確認 → purge実行 → HTTP 200返却
```

### フローチャート

```mermaid
flowchart TD
    A{アクション判定} -->|update + blob_id| B[fetch_portal]
    A -->|DELETE logo| C[fetch_portal]

    B --> D[check_authorization]
    D --> E[ActiveStorage::Blob.find_signed]
    E --> F{Blobが存在?}
    F -->|No| G[404 Not Found]
    F -->|Yes| H[@portal.logo.attach blob]
    H --> I[レスポンス返却]

    C --> J[check_authorization]
    J --> K{logo.attached?}
    K -->|No| L[何もしない]
    K -->|Yes| M[@portal.logo.purge]
    M --> N[HTTP 200 OK]
    L --> N
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-157-01 | 署名済みBlob | アップロードには事前にDirect Uploadで取得した署名済みblob_idが必要 | ロゴアップロード時 |
| BR-157-02 | 単一ロゴ | ポータルには1つのロゴのみ添付可能（has_one_attached） | ロゴアップロード時 |
| BR-157-03 | 上書き動作 | 新しいロゴをアップロードすると、既存のロゴは自動的に置き換えられる | ロゴアップロード時 |
| BR-157-04 | 削除時のみpurge | logoアクションはDELETEメソッドでのみロゴを削除 | ロゴ削除時 |

### 計算ロジック

```ruby
# ロゴ情報の生成
def file_base_data
  {
    id: logo.id,
    portal_id: id,
    file_type: logo.content_type,
    account_id: account_id,
    file_url: url_for(logo),
    blob_id: logo.blob.signed_id,
    filename: logo.filename.to_s
  }
end
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| upload | active_storage_attachments | INSERT/UPDATE | 添付ファイル情報 |
| upload | active_storage_blobs | SELECT | Blob情報の取得 |
| delete | active_storage_attachments | DELETE | 添付情報の削除 |
| delete | active_storage_blobs | DELETE | Blob情報の削除（非同期） |

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

#### active_storage_attachments

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | record_type | 'Portal' | 関連モデル |
| INSERT | record_id | @portal.id | ポータルID |
| INSERT | name | 'logo' | 添付名 |
| INSERT | blob_id | blob.id | Blob参照 |
| DELETE | - | record_type='Portal', name='logo' | 削除条件 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 404 | NotFound | 指定slugのPortalが存在しない | 正しいslugを指定 |
| 404 | NotFound | 指定blob_idのBlobが存在しない | 有効なblob_idを指定 |
| 403 | Forbidden | 管理者権限がない | 管理者権限でアクセス |
| 422 | UnprocessableEntity | ファイル形式が不正 | 対応形式の画像を使用 |

### リトライ仕様

特になし

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

- ロゴ添付は暗黙的トランザクション
- purgeによる削除はActiveStorageの非同期ジョブで実行

## パフォーマンス要件

- ロゴアップロード：100ms以内（添付のみ、実際のアップロードはDirect Upload）
- ロゴ削除：50ms以内

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

- 管理者権限チェック必須
- blob_idは署名済みで改ざん防止
- ファイル形式のバリデーション（画像のみ許可が推奨）

## 備考

- Direct Uploadを使用することで、サーバーを経由せずに直接ストレージにアップロード可能
- ActiveStorageのpurgeは非同期でBlobとファイルの両方を削除

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | portal.rb | `app/models/portal.rb` | has_one_attached :logo の定義 |

**読解のコツ**:
- **34行目**: `has_one_attached :logo` - ActiveStorageによるロゴ添付定義
- **49-59行目**: `file_base_data` - ロゴ情報を生成するメソッド

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

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

**主要処理フロー**:
- **44-47行目**: logo - ロゴ削除アクション

```ruby
def logo
  @portal.logo.purge if @portal.logo.attached?
  head :ok
end
```

- **63-67行目**: process_attached_logo - ロゴ添付処理

```ruby
def process_attached_logo
  blob_id = params[:blob_id]
  blob = ActiveStorage::Blob.find_signed(blob_id)
  @portal.logo.attach(blob)
end
```

#### Step 3: 認可ポリシーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | portal_policy.rb | `app/policies/portal_policy.rb` | logo?メソッド |

**主要処理フロー**:
- **26-28行目**: logo? - 管理者のみ許可

```ruby
def logo?
  @account_user.administrator?
end
```

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

```
Api::V1::Accounts::PortalsController
    │
    ├─ update (blob_id付き)
    │      │
    │      ├─ before_action :fetch_portal
    │      │      └─ Current.account.portals.find_by(slug:)
    │      │
    │      ├─ before_action :check_authorization
    │      │      └─ PortalPolicy#update?
    │      │
    │      └─ process_attached_logo
    │             ├─ ActiveStorage::Blob.find_signed(blob_id)
    │             └─ @portal.logo.attach(blob)
    │
    └─ logo (DELETE)
           │
           ├─ before_action :fetch_portal
           │
           ├─ before_action :check_authorization
           │      └─ PortalPolicy#logo?
           │
           └─ @portal.logo.purge
```

### データフロー図

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

PATCH Request       →   update + process_attached_logo  →   JSON Response
(blob_id)                  │                                (logo info)
                          ├─ Blob.find_signed
                          │
                          └─ logo.attach(blob)
                                       ↓
                              [active_storage_attachments INSERT]

DELETE Request      →   logo                           →   HTTP 200 OK
                          │
                          └─ logo.purge
                                       ↓
                              [active_storage_attachments DELETE]
                              [active_storage_blobs DELETE (async)]
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| portal.rb | `app/models/portal.rb` | モデル | has_one_attached :logo定義、file_base_data |
| portals_controller.rb | `app/controllers/api/v1/accounts/portals_controller.rb` | コントローラー | logoアクション、process_attached_logo |
| portal_policy.rb | `app/policies/portal_policy.rb` | ポリシー | logo?認可 |
| storage.yml | `config/storage.yml` | 設定 | ストレージ設定（S3、GCS等） |
