# 機能設計書 14-商品ソート

## 概要

本ドキュメントは、Eコマースモバイルアプリケーションにおける商品ソート機能の設計仕様を定義する。本機能は、カテゴリ商品一覧画面において、商品の並び順を変更する機能を提供する。

### 本機能の処理概要

**業務上の目的・背景**：
ECサイトにおいて、商品の並び順を変更する機能は、ユーザーが目的の商品を効率的に見つけるために重要である。価格順、新着順、人気順など、ユーザーの目的に応じた並び替えを提供することで、購買体験を向上させる。

**機能の利用シーン**：
- カテゴリ商品一覧画面で「ソート」ボタンをタップした際
- 価格の安い順/高い順で商品を見たい際
- 新着商品を優先して表示したい際

**主要な処理内容**：
1. カテゴリスラグを基にソートオプション一覧を取得
2. ソートオプションのRadioListTile表示
3. ソート条件の選択と適用
4. ソート条件のクリア
5. 選択したソート条件の永続化（SharedPreferences）

**関連システム・外部連携**：
- Bagisto GraphQL API（ソートオプション取得、商品検索）
- SharedPreferences（選択したソート名の保存）

**権限による制御**：
- 認証不要（ゲストユーザーも利用可能）

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 8 | カテゴリ商品一覧画面 | 親画面 | ソート機能を提供する画面 |
| - | ソートボトムシート | 主画面 | ソート条件選択のモーダル画面 |

## 機能種別

CRUD操作（Read）/ データ連携 / ユーザー入力処理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| categorySlug | String | Yes | カテゴリ識別スラグ | 空文字不可 |
| filters | List<Map<String, dynamic>> | Yes | 現在のフィルター条件 | - |
| page | int? | No | ページ番号 | - |

### 入力データソース

- ユーザー入力（RadioListTileの選択）
- GraphQL API（ソートオプション一覧）
- SharedPreferences（前回選択したソート名）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| filters | List<Map<String, dynamic>> | ソート条件が追加されたフィルター |

### 出力先

- CategoryBloc（FetchSubCategoryEvent発行）
- SharedPreferences（選択したソート名の保存）

## 処理フロー

### 処理シーケンス

```
1. ソートボトムシート表示
   └─ showModalBottomSheetでSortBottomSheet表示
2. ソートオプション取得
   └─ FilterSortFetchEvent発行 → FilterBloc
3. 前回選択値の復元
   └─ appStoragePref.getSortName()
4. ユーザーのソート選択
   └─ RadioListTileのonChanged
5. ソート適用
   ├─ filtersにsort条件追加
   ├─ appStoragePref.setSortName()で保存
   └─ FetchSubCategoryEvent発行
6. ソートクリア
   └─ CLEARボタンでsort条件削除
```

### フローチャート

```mermaid
flowchart TD
    A[カテゴリ商品一覧画面] --> B[SORTボタンタップ]
    B --> C[SortBottomSheet表示]
    C --> D[FilterSortFetchEvent発行]
    D --> E[ソートオプション取得]
    E --> F[前回選択値を復元]
    F --> G[RadioListTile表示]
    G --> H{ユーザー操作}
    H -->|ソート選択| I[filtersにsort追加]
    H -->|CLEARタップ| J[sort条件削除]
    I --> K[appStoragePref.setSortName]
    K --> L[FetchSubCategoryEvent発行]
    L --> M[ボトムシート終了]
    J --> N[appStoragePref.setSortName空文字]
    N --> O[FetchSubCategoryEvent発行]
    O --> P[ボトムシート終了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-01 | 単一選択 | ソート条件は1つのみ選択可能 | 常時 |
| BR-02 | 選択永続化 | 選択したソート条件はアプリ再起動後も保持 | ソート選択時 |
| BR-03 | クリア時page削除 | ソート変更時にpageフィルターも削除 | ソート選択時 |

### 計算ロジック

- ソートフィルター形式：`{"key": "\"sort\"", "value": "\"sortValue\""}`

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| ソートオプション取得 | attributes | SELECT | sortOrdersフィールドを取得 |
| 商品検索 | products | SELECT | ソート条件で商品を検索 |

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

#### attributes（GraphQL経由）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | sortOrders | category_slug = 指定値 | GraphQL: getFilterAttributes |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | API失敗 | ソートオプション取得エラー | エラーログ出力、空リストを使用 |

### リトライ仕様

特になし（親画面のPull-to-Refreshで対応）

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

読み取り専用のため、トランザクション制御は不要。

## パフォーマンス要件

- ソートオプション取得: 1秒以内（フィルター属性と同時取得）
- ソート適用後の商品取得: 2秒以内

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

- ソート値のサニタイズはバックエンドで実施

## 備考

- ソート画面はModalBottomSheetとして表示
- SortOrderモデルにはtitle（表示名）とvalue（API値）が含まれる

---

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

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

### 推奨読解順序

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

ソート機能で扱うデータモデルを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | filter_product_model.dart | `lib/data_model/categories_data_model/filter_product_model.dart` | SortOrderクラスの構造（116-139行目） |

**読解のコツ**: SortOrderはkey, value, title, order, positionフィールドを持ち、titleが表示名、valueがAPI送信値。

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

ソート画面の起点を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | sub_categories_view.dart | `lib/screens/categories_screen/widget/sub_categories_view.dart` | SORTボタンのタップハンドラ（242-257行目） |
| 2-2 | sort_screen.dart | `lib/screens/filter_screen/view/sort_screen.dart` | SortBottomSheetクラス |

**主要処理フロー（sort_screen.dart）**:
1. **37-42行目**: initState()でgetSortValue()呼び出し、FilterSortFetchEvent発行
2. **44-46行目**: getSortValue()で前回選択値をSharedPreferencesから取得
3. **58-67行目**: getSort()でBLoCの状態に応じてUI構築
4. **69-163行目**: sortList()でRadioListTile一覧を生成
5. **82-103行目**: CLEARボタンでソート条件クリア、appStoragePref.setSortName空文字
6. **115-154行目**: RadioListTile選択時のonChangedでソート適用

#### Step 3: BLoC層を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | filter_event.dart | `lib/screens/filter_screen/bloc/filter_event.dart` | FilterSortFetchEventの定義 |
| 3-2 | filter_bloc.dart | `lib/screens/filter_screen/bloc/filter_bloc.dart` | FilterSortFetchEventの処理 |

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

```
SubCategoriesView (widget/sub_categories_view.dart)
    │
    ├─ GestureDetector (SORTボタン, 242行目)
    │      │
    │      └─ showModalBottomSheet → SortBottomSheet
    │
    └─ CategoryBloc
           │
           └─ FetchSubCategoryEvent (ソート適用時)

SortBottomSheet (sort_screen.dart)
    │
    ├─ FilterBloc
    │      │
    │      └─ FilterSortFetchEvent
    │             │
    │             └─ FilterRepositoryImp.getFilterProducts()
    │                    │
    │                    └─ ApiClient.getFilterAttributes()
    │
    ├─ appStoragePref
    │      ├─ getSortName() (前回選択値取得)
    │      └─ setSortName() (選択値保存)
    │
    └─ CategoryBloc
           │
           ├─ OnClickSubCategoriesLoaderEvent
           │
           └─ FetchSubCategoryEvent
                  │
                  └─ CategoriesRepo.callCategoriesData()
```

### データフロー図

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

categorySlug ───────────▶ FilterSortFetchEvent
                              │
                              ▼
                         FilterRepositoryImp.getFilterProducts()
                              │
                              ▼
                         ApiClient.getFilterAttributes()
                              │
                              ▼
                         GraphQL API
                              │
                              ▼
                         GetFilterAttribute.sortOrders
                              │
                              ▼
                         List<SortOrder> ──────────▶ RadioListTile表示

ユーザー選択 ───────────▶ RadioListTile.onChanged
                              │
                              ▼
                         filters配列に"sort"追加
                              │
                              ▼
                         appStoragePref.setSortName()
                              │
                              ▼
                         FetchSubCategoryEvent
                              │
                              ▼
                         ソート適用済み商品取得
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| sort_screen.dart | `lib/screens/filter_screen/view/sort_screen.dart` | ソース | ソートボトムシートUI |
| filter_bloc.dart | `lib/screens/filter_screen/bloc/filter_bloc.dart` | ソース | フィルター/ソート状態管理BLoC |
| filter_event.dart | `lib/screens/filter_screen/bloc/filter_event.dart` | ソース | フィルター/ソートイベント定義 |
| filter_state.dart | `lib/screens/filter_screen/bloc/filter_state.dart` | ソース | フィルター/ソート状態定義 |
| filter_repository.dart | `lib/screens/filter_screen/bloc/filter_repository.dart` | ソース | フィルター/ソートAPIリポジトリ |
| filter_product_model.dart | `lib/data_model/categories_data_model/filter_product_model.dart` | ソース | SortOrderデータモデル |
| sub_categories_view.dart | `lib/screens/categories_screen/widget/sub_categories_view.dart` | ソース | ソート呼び出し元 |
| app_storage_pref.dart | `lib/utils/app_storage_pref.dart` | ソース | ソート名の永続化 |
