# 機能設計書 83-タグ付け

## 概要

本ドキュメントは、Fat Free CRMにおける「タグ付け」機能の設計仕様を定義する。acts_as_taggable_on gemを活用し、各エンティティに自由にタグを付与できる柔軟な分類機能である。

### 本機能の処理概要

タグ付け機能は、取引先、キャンペーン、リード、連絡先、商談などのエンティティに対して、ユーザー定義のタグを付与する機能である。タグによりエンティティを自由に分類し、検索やフィルタリングに活用できる。

**業務上の目的・背景**：CRMシステムでは、システムで定義されたカテゴリだけでは分類しきれない多様な属性でエンティティを管理したいというニーズがある。例えば「VIP顧客」「注目案件」「要フォロー」といったユーザー独自の観点での分類が必要となる。タグ付け機能により、柔軟で拡張性の高いエンティティ分類が実現できる。

**機能の利用シーン**：
- 営業担当者が取引先を独自の基準（業種、優先度、関係性等）で分類する場合
- マーケティング担当者がキャンペーンをテーマやターゲット層でタグ付けする場合
- 検索時にタグを条件として絞り込む場合
- カスタムフィールドグループをタグに紐付けて動的に表示する場合

**主要な処理内容**：
1. タグの選択/作成：Select2コンポーネントで既存タグの選択または新規タグの作成
2. タグの保存：エンティティ保存時にtag_listパラメータを処理
3. タグによる検索：検索文字列の#プレフィックスまたは高度な検索でのタグ条件
4. タグ関連フィールドグループの表示：タグに紐付くカスタムフィールドグループの動的表示

**関連システム・外部連携**：
- acts_as_taggable_on gem：タグ機能の基盤提供
- Select2：タグ選択UIの提供
- カスタムフィールド機能：タグに紐付くフィールドグループ

**権限による制御**：タグ自体にアクセス権限はなく、全ユーザーが全タグを使用可能。ただし、タグが付与されたエンティティはそのエンティティのアクセス権限に従う。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 9 | 取引先新規作成フォーム | 主画面 | 取引先へのタグ付け |
| 10 | 取引先編集フォーム | 主画面 | 取引先へのタグ付け |
| 13 | キャンペーン新規作成フォーム | 主画面 | キャンペーンへのタグ付け |
| 14 | キャンペーン編集フォーム | 主画面 | キャンペーンへのタグ付け |
| 17 | リード新規作成フォーム | 主画面 | リードへのタグ付け |
| 18 | リード編集フォーム | 主画面 | リードへのタグ付け |
| 22 | 連絡先新規作成フォーム | 主画面 | 連絡先へのタグ付け |
| 23 | 連絡先編集フォーム | 主画面 | 連絡先へのタグ付け |
| 26 | 商談新規作成フォーム | 主画面 | 商談へのタグ付け |
| 27 | 商談編集フォーム | 主画面 | 商談へのタグ付け |
| 42 | タグ管理画面 | 管理画面 | タグの一覧表示・編集・削除 |

## 機能種別

分類・メタデータ管理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| tag_list | Array[String] | No | タグ名の配列 | 空文字列は除外 |
| query | String | No | #プレフィックス付きタグ検索文字列 | #で始まる文字列をタグとして解析 |

### 入力データソース

- 画面入力：Select2マルチセレクトコンポーネント
- URL検索クエリ：#プレフィックス付きキーワード

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| entity.tag_list | Array[String] | エンティティに付与されたタグ名の配列 |
| entity.tags | Array[Tag] | Tagオブジェクトの配列 |

### 出力先

- 画面表示：タグ入力フィールド、エンティティ詳細画面のタグ表示
- データベース：tags、taggingsテーブル

## 処理フロー

### 処理シーケンス

```
1. タグ入力（作成/編集フォーム）
   └─ Select2で既存タグ選択または新規タグ入力
2. フォーム送信
   └─ tag_listパラメータとしてタグ名配列を送信
3. コントローラー処理
   └─ resource_paramsでtag_listを含むパラメータ処理
4. モデル保存
   └─ acts_as_taggable_onがtag_listを処理
5. タグ・タギング保存
   └─ 新規タグはtagsテーブルに作成、関連はtaggingsテーブルに保存
```

### フローチャート

```mermaid
flowchart TD
    A[タグ入力/選択] --> B{新規タグ?}
    B -->|Yes| C[タグ名入力]
    B -->|No| D[既存タグ選択]
    C --> E[フォーム送信]
    D --> E
    E --> F[tag_listパラメータ処理]
    F --> G[acts_as_taggable_on処理]
    G --> H{タグ存在チェック}
    H -->|存在しない| I[tagsテーブルに新規作成]
    H -->|存在する| J[既存タグを使用]
    I --> K[taggingsテーブルに関連作成]
    J --> K
    K --> L[保存完了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-83-01 | タグの自動作成 | 存在しないタグ名が指定された場合は自動的に新規作成 | フォーム送信時 |
| BR-83-02 | タグの共有 | タグは全ユーザー間で共有される | 常時 |
| BR-83-03 | タグ検索構文 | 検索文字列の#プレフィックスをタグ名として解析 | 検索実行時 |
| BR-83-04 | カウンターキャッシュ | タグの使用回数はtaggings_countで管理 | タギング追加/削除時 |
| BR-83-05 | フィールドグループ連動 | タグに紐付くカスタムフィールドグループを動的に表示 | タグ選択/解除時 |

### 計算ロジック

- **タグ検索の解析**: `#tag_name`形式の文字列からタグ名を抽出
- **複数タグ**: カンマ区切りで結合して検索条件に使用

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| タグ付与 | tags | INSERT/SELECT | 新規タグ作成または既存タグ取得 |
| タグ付与 | taggings | INSERT | エンティティとタグの関連作成 |
| タグ削除 | taggings | DELETE | エンティティとタグの関連削除 |
| タグ検索 | taggings, tags | SELECT | タグ条件での検索 |

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

#### tags

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | name | タグ名 | 新規タグ作成時 |
| SELECT | id, name | name = 指定タグ名 | 既存タグ検索時 |
| UPDATE | taggings_count | +1 or -1 | カウンターキャッシュ更新 |

#### taggings

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | tag_id, taggable_id, taggable_type, context | タグID、エンティティID、エンティティタイプ、'tags' | タグ付与時 |
| DELETE | - | tag_id, taggable_id, taggable_type | タグ削除時 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | 無効なタグ名 | 空文字や不正文字のタグ名 | 無視して処理続行 |

### リトライ仕様

タグ付け処理にリトライ機能はない。

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

エンティティの保存と同一トランザクション内でタグ付けが処理される。エンティティ保存が失敗した場合、タグ付けもロールバックされる。

## パフォーマンス要件

- taggingsテーブルのインデックスによる効率的な検索
- カウンターキャッシュによるタグ使用回数の高速取得

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

- **認証**：ログインユーザーのみタグ操作可能
- **認可**：タグ自体には権限なし、エンティティのアクセス権限に従う
- **XSS対策**：タグ名のエスケープ処理

## 備考

- acts_as_taggable_on gemの機能を最大限活用
- context='tags'として登録（他のcontextは使用していない）
- カスタムフィールドグループとの連携により、タグに応じた追加フィールドを動的表示可能

---

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

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

### 推奨読解順序

#### Step 1: モデルのタグ設定を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | account.rb | `app/models/entities/account.rb` | acts_as_taggable_onの設定 |

**主要処理フロー**:
- **67行目**: `acts_as_taggable_on :tags`でタグ機能を有効化
- この1行で、tag_list、tags、tagged_with等のメソッドが使用可能になる

**読解のコツ**: acts_as_taggable_on gemのDSLを理解することが重要。`:tags`はcontextを指定しており、複数のcontextを持つことも可能。

#### Step 2: Taggingモデルを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | tagging.rb | `app/models/polymorphic/tagging.rb` | ActsAsTaggableOn::Taggingの拡張 |

**主要処理フロー**:
- **8-9行目**: ActsAsTaggableOn::Taggingを継承し、hookポイントを設定

#### Step 3: ビューのタグ入力フォームを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | _tags.html.haml | `app/views/shared/_tags.html.haml` | タグ入力UIの実装 |

**主要処理フロー**:
- **6行目**: `f.select :tag_list`でSelect2を使用したタグ入力フィールド生成
- `data: {tags: true}`で新規タグ入力を許可
- `data: {url: ...}`でフィールドグループ取得用URLを設定
- `multiple: true`で複数タグ選択を許可

#### Step 4: JavaScriptのタグ処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | crm_tags.js.coffee | `app/assets/javascripts/crm_tags.js.coffee` | タグ選択時の動的処理 |

**主要処理フロー**:
- **10行目**: `select2:select`イベントでタグ選択検知
- **11-17行目**: タグに紐付くフィールドグループを動的にロード
- **19行目**: `select2:unselect`イベントでタグ解除検知
- **20行目**: タグに紐付くフィールドグループを削除

#### Step 5: タグ検索処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | entities_controller.rb | `app/controllers/entities_controller.rb` | タグ検索の実装 |

**主要処理フロー**:
- **155行目**: `scope.tagged_with(tags, on: :tags)`でタグ検索を適用
- **195-211行目**: `parse_query_and_tags`で#プレフィックス付き検索文字列をパース

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

```
ブラウザ（タグ選択/入力）
    │
    ├─ Select2 Component
    │      │
    │      ├─ select2:select イベント
    │      │      └─ crm_tags.js.coffee
    │      │             └─ Ajaxでfield_groupを取得
    │      │
    │      └─ select2:unselect イベント
    │             └─ crm_tags.js.coffee
    │                    └─ フィールドグループ要素を削除
    │
    └─ フォーム送信
           │
           └─ EntityController (create/update)
                  │
                  ├─ resource_params
                  │      └─ tag_listを含むパラメータ
                  │
                  └─ entity.update(resource_params)
                         │
                         └─ acts_as_taggable_on処理
                                │
                                ├─ Tag.find_or_create_by(name: tag_name)
                                │
                                └─ Tagging.create(tag: tag, taggable: entity)
```

### データフロー図

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

Select2 UI          ──────▶  tag_list params              ──────▶  Array[String]
                                │
                                ▼
                          EntityController
                                │
                                ▼
                          entity.tag_list = params[:tag_list]
                                │
                                ▼
                          acts_as_taggable_on
                                │
                    ┌───────────┴───────────┐
                    ▼                       ▼
               tags テーブル          taggings テーブル
               (新規タグ作成)         (関連作成/削除)


[検索]                          [処理]                          [出力]

"#tag1 keyword"     ──────▶  parse_query_and_tags        ──────▶  ["keyword", "tag1"]
                                │
                                ▼
                          tagged_with(tags, on: :tags)
                                │
                                ▼
                          検索結果                        ──────▶  Filtered Entities
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| tagging.rb | `app/models/polymorphic/tagging.rb` | ソース | Taggingモデル（acts_as_taggable_on拡張） |
| account.rb | `app/models/entities/account.rb` | ソース | acts_as_taggable_on設定（67行目） |
| campaign.rb | `app/models/entities/campaign.rb` | ソース | acts_as_taggable_on設定（55行目） |
| contact.rb | `app/models/entities/contact.rb` | ソース | acts_as_taggable_on設定（89行目） |
| lead.rb | `app/models/entities/lead.rb` | ソース | acts_as_taggable_on設定（67行目） |
| opportunity.rb | `app/models/entities/opportunity.rb` | ソース | acts_as_taggable_on設定（75行目） |
| entities_controller.rb | `app/controllers/entities_controller.rb` | ソース | タグ検索処理（155行目、195-211行目） |
| _tags.html.haml | `app/views/shared/_tags.html.haml` | テンプレート | タグ入力フォーム |
| crm_tags.js.coffee | `app/assets/javascripts/crm_tags.js.coffee` | JavaScript | タグ選択時の動的処理 |
| 20141230205454_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb | `db/migrate/` | マイグレーション | カウンターキャッシュ追加 |
