# 機能設計書 55-フィルター作成・保存

## 概要

本ドキュメントは、Fizzyアプリケーションにおけるフィルター作成・保存機能の詳細設計を記述する。

### 本機能の処理概要

カード一覧のフィルター条件を作成・保存することで、頻繁に使用する検索条件を再利用可能にする機能である。

**業務上の目的・背景**：プロジェクト管理において、特定の条件でカードを絞り込む操作は頻繁に行われる。例えば「自分がアサインされたカード」「特定のタグが付いたカード」「今週作成されたカード」など、よく使う条件を保存しておくことで、毎回条件を設定し直す手間を省き、作業効率を向上させる。

**機能の利用シーン**：カード一覧画面でフィルター条件を設定した後、その条件を保存して再利用したい場合に使用される。保存したフィルターは次回以降、ワンクリックで同じ条件のカード一覧を表示できる。

**主要な処理内容**：
1. ユーザーがフィルター条件を設定
2. フィルター条件をパラメータとして受け取り
3. パラメータをMD5でダイジェスト化
4. 同一ダイジェストのフィルターが存在すれば再利用、なければ新規作成
5. Turbo Streamでフィルター一覧を更新

**関連システム・外部連携**：特になし

**権限による制御**：ユーザー固有のフィルターのみが作成・表示される。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 18 | カード一覧画面 | 主画面 | カード一覧のフィルター条件を作成・保存 |

## 機能種別

CRUD操作（CREATE）

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| assignment_status | String | No | アサイン状態（unassigned等） | 許可値のみ |
| indexed_by | String | No | インデックス種別（all/closed/not_now/stalled/postponing_soon/golden） | INDEXES定数に含まれること |
| sorted_by | String | No | ソート順（newest/oldest/latest） | SORTED_BY定数に含まれること |
| creation | String | No | 作成日の絞り込み（today/this_week等） | TimeWindowParser対応形式 |
| closure | String | No | クローズ日の絞り込み | TimeWindowParser対応形式 |
| card_ids[] | Array[UUID] | No | 特定カードIDの指定 | 有効なUUID |
| assignee_ids[] | Array[UUID] | No | 担当者IDの指定 | 有効なユーザーID |
| creator_ids[] | Array[UUID] | No | 作成者IDの指定 | 有効なユーザーID |
| closer_ids[] | Array[UUID] | No | クローズ者IDの指定 | 有効なユーザーID |
| board_ids[] | Array[UUID] | No | ボードIDの指定 | 有効なボードID |
| tag_ids[] | Array[UUID] | No | タグIDの指定 | 有効なタグID |
| terms[] | Array[String] | No | 検索語句 | - |

### 入力データソース

画面入力（フィルター設定パネル）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| @filter | Filter | 作成または取得されたフィルターオブジェクト |
| @filters | Array | ユーザーの全フィルター一覧 |

### 出力先

- 画面表示（Turbo Stream）
- データベース（filtersテーブル）

## 処理フロー

### 処理シーケンス

```
1. リクエスト受信
   └─ POST /filters
2. パラメータ正規化
   └─ Filter.normalize_paramsでパラメータを正規化
3. フィルター作成/取得
   └─ Current.user.filters.remember(filter_params)
      3-1. パラメータダイジェスト計算（MD5）
      3-2. 既存フィルター検索（params_digestで照合）
      3-3. 存在すればtouch、なければcreate!
4. レスポンス返却
   └─ Turbo Streamでフィルター一覧を更新
```

### フローチャート

```mermaid
flowchart TD
    A[リクエスト受信] --> B[パラメータ正規化]
    B --> C[パラメータダイジェスト計算]
    C --> D{同一フィルター存在?}
    D -->|Yes| E[既存フィルターをtouch]
    D -->|No| F[新規フィルター作成]
    E --> G[Turbo Stream更新]
    F --> G
    G --> H[終了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-55-01 | 重複防止 | 同一パラメータのフィルターは重複して作成しない | フィルター作成時 |
| BR-55-02 | ダイジェスト照合 | params_digest（MD5）で同一性を判定 | フィルター検索時 |
| BR-55-03 | パラメータ正規化 | 空値除去、デフォルト値除去、ソートを実施 | フィルター作成前 |
| BR-55-04 | ユーザー紐付け | フィルターは作成者（creator）に紐付け | フィルター作成時 |
| BR-55-05 | RecordNotUnique例外処理 | 並行リクエスト時の重複エラーをキャッチして既存フィルターを返す | create!失敗時 |

### 計算ロジック

**パラメータダイジェスト計算**:
```ruby
Digest::MD5.hexdigest(normalize_params(params).to_json)
```

**パラメータ正規化処理**:
1. 空値を除去（compact_blank）
2. デフォルト値を除去（indexed_by: "all", sorted_by: "latest"）
3. 配列値を文字列配列に変換
4. キー名でソート

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| フィルター作成 | filters | INSERT | 新規フィルターレコード作成 |
| フィルター更新 | filters | UPDATE | 既存フィルターのupdated_atを更新 |
| 関連付け | boards_filters, filters_tags等 | INSERT | 中間テーブルへの関連レコード作成 |

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

#### filters

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | 自動生成UUID | 主キー |
| INSERT | account_id | Current.user.account_id | ユーザーのアカウント |
| INSERT | creator_id | Current.user.id | フィルター作成者 |
| INSERT | fields | JSONオブジェクト | フィルター条件を格納 |
| INSERT | params_digest | MD5ハッシュ | パラメータの一意識別子 |
| INSERT | created_at | 現在時刻 | - |
| INSERT | updated_at | 現在時刻 | - |
| UPDATE | updated_at | 現在時刻 | touch時 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | RecordNotUnique | 並行リクエストで重複発生 | 既存フィルターを検索して返却 |

### リトライ仕様

RecordNotUnique例外発生時は自動的に既存レコードを取得する（リトライ不要）

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

create!は単一トランザクションで実行される。関連テーブル（boards_filters等）への挿入も同一トランザクション内で完了。

## パフォーマンス要件

- レスポンス時間: 200ms以内
- インデックス: creator_id + params_digest（ユニーク）

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

- ログイン必須（ApplicationControllerで制御）
- ユーザー固有のフィルターのみ作成・参照可能（creator_idで制限）
- CSRFトークン検証（Railsデフォルト）

## 備考

- フィルターは明示的に削除しない限り保持される
- 同一条件のフィルターは重複して作成されない
- INDEXES定数: %w[all closed not_now stalled postponing_soon golden]
- SORTED_BY定数: %w[newest oldest latest]

---

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

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

### 推奨読解順序

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

まず、フィルターのデータモデルと関連するコンサーンを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | filter.rb | `app/models/filter.rb` | Filterモデル、include、cardsメソッド |
| 1-2 | params.rb | `app/models/filter/params.rb` | パラメータ正規化、ダイジェスト計算 |
| 1-3 | fields.rb | `app/models/filter/fields.rb` | フィールド定義、デフォルト値 |

**読解のコツ**:
- `include Fields, Params, Resources, Summarized`で機能を分割
- `fields`カラムはJSON形式でフィルター条件を格納
- `params_digest`でフィルターの同一性を判定

**主要処理フロー（filter.rb）**:
- **4-5行目**: belongs_to関連（creator, account）
- **7-16行目**: from_params、rememberクラスメソッド
- **19-40行目**: cardsメソッド（フィルター条件に基づくカード取得）

**主要処理フロー（params.rb）**:
- **4-17行目**: PERMITTED_PARAMS定義
- **20-21行目**: find_by_paramsメソッド（ダイジェストで検索）
- **24-25行目**: digest_paramsメソッド（MD5計算）
- **28-36行目**: normalize_paramsメソッド（正規化処理）

**主要処理フロー（fields.rb）**:
- **4-5行目**: INDEXES、SORTED_BY定数
- **32-34行目**: store_accessor定義

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

リクエストを受け取るコントローラーの処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | filters_controller.rb | `app/controllers/filters_controller.rb` | createアクションの処理フロー |

**主要処理フロー**:
- **2行目**: before_actionでset_filtersを実行
- **4-5行目**: createアクション（rememberを呼び出し）
- **14-15行目**: set_filtersメソッド
- **18-19行目**: filter_paramsメソッド（パラメータ正規化）

**読解のコツ**:
- `Filter.normalize_params`でパラメータを正規化してから保存
- `PERMITTED_PARAMS`で許可されたパラメータのみ受け付け

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

```
FiltersController#create
    │
    ├─ [before_action] #set_filters
    │      └─ @filters = Current.user.filters
    │
    └─ Current.user.filters.remember(filter_params)
           │
           ├─ Filter.normalize_params(params)
           │      │
           │      ├─ compact_blank（空値除去）
           │      ├─ reject default_value?（デフォルト値除去）
           │      └─ sort_by（キーソート）
           │
           ├─ Filter.digest_params(attrs)
           │      └─ Digest::MD5.hexdigest(...)
           │
           └─ create!(attrs)
                  │
                  └─ [rescue RecordNotUnique]
                         └─ find_by_params(attrs).tap(&:touch)
```

### データフロー図

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

フィルター条件  ───▶ normalize_params      ───▶ 正規化パラメータ
                       │
                       ▼
                 digest_params           ───▶ params_digest (MD5)
                       │
                       ▼
                 find_by_params
                       │
            ┌──────────┴──────────┐
            ▼                     ▼
       新規作成時            既存の場合
            │                     │
            ▼                     ▼
    Filter.create!           touch実行
    (INSERT)              (UPDATE updated_at)
            │                     │
            └──────────┬──────────┘
                       ▼
                 Turbo Stream       ───▶ ブラウザ
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| filters_controller.rb | `app/controllers/filters_controller.rb` | コントローラー | フィルターのCRUD処理 |
| filter.rb | `app/models/filter.rb` | モデル | フィルターのデータモデル |
| params.rb | `app/models/filter/params.rb` | コンサーン | パラメータ処理 |
| fields.rb | `app/models/filter/fields.rb` | コンサーン | フィールド定義 |
| resources.rb | `app/models/filter/resources.rb` | コンサーン | リソース関連 |
| summarized.rb | `app/models/filter/summarized.rb` | コンサーン | サマリー機能 |
| create.turbo_stream.erb | `app/views/filters/create.turbo_stream.erb` | ビュー | Turbo Streamレスポンス |
| _settings.html.erb | `app/views/filters/_settings.html.erb` | パーシャル | フィルター設定パネル |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義 |
| schema.rb | `db/schema.rb` | スキーマ | filtersテーブル定義 |
