# 機能設計書 10-商品一覧表示

## 概要

本ドキュメントは、ECモバイルアプリケーションにおける商品一覧表示機能の設計仕様を定義する。カテゴリ別、検索結果、おすすめ商品等の商品一覧をグリッドまたはリスト形式で表示する。

### 本機能の処理概要

**業務上の目的・背景**：ECサイトの中核機能として、ユーザーが商品を閲覧・比較できる一覧画面を提供する。フィルタ、ソート、ページネーション機能により、大量の商品から目的の商品を効率的に見つけられるようにする。

**機能の利用シーン**：
- ホーム画面の商品カルーセル
- カテゴリ画面での商品一覧
- 検索結果画面
- おすすめ商品表示

**主要な処理内容**：
1. GraphQL APIを通じた商品一覧の取得
2. フィルタ条件（カテゴリ、価格帯、属性等）の適用
3. ソート条件の適用
4. ページネーション（無限スクロール）
5. グリッド/リスト表示切替
6. お気に入り、カート追加、比較機能との連携

**関連システム・外部連携**：Bagisto（Laravel製ECプラットフォーム）のGraphQL APIと連携し、allProducts queryを呼び出す。

**権限による制御**：本機能は認証不要で呼び出し可能。ただし、お気に入り状態表示はログインユーザーのみ。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 2 | ホーム画面 | 主画面 | 商品カルーセルの表示 |
| 7 | カテゴリ一覧画面 | 主画面 | カテゴリ別商品一覧 |
| 9 | 商品詳細画面 | 遷移先画面 | 商品タップ時の遷移先 |

## 機能種別

データ取得 / UI描画 / API連携 / ページネーション

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| filters | List<Map<String, dynamic>> | No | フィルタ条件リスト | - |
| filters[].key | String | Yes | フィルタキー（category_id, price, 属性コード等） | - |
| filters[].value | String | Yes | フィルタ値 | - |
| page | int | No | ページ番号（デフォルト1） | 1以上 |
| limit | int | No | 1ページあたりの件数（デフォルト15） | 1以上 |

### 入力データソース

- テーマカスタマイズ設定：product_carouselのfilters
- カテゴリ画面：category_idフィルタ
- 検索結果：name/descriptionフィルタ

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| paginatorInfo.count | int | 現在ページの商品数 |
| paginatorInfo.currentPage | int | 現在のページ番号 |
| paginatorInfo.lastPage | int | 最終ページ番号 |
| paginatorInfo.total | int | 総商品数 |
| data | List<ProductData> | 商品データリスト |
| data[].id | String | 商品ID |
| data[].type | String | 商品タイプ（simple, configurable, bundle等） |
| data[].name | String | 商品名 |
| data[].sku | String | SKU |
| data[].urlKey | String | URLキー |
| data[].isInWishlist | bool | お気に入り登録状態 |
| data[].isInSale | bool | セール中フラグ |
| data[].isSaleable | bool | 購入可能フラグ |
| data[].priceHtml | PriceHtml | 価格情報 |
| data[].images | List<Images> | 商品画像リスト |
| data[].averageRating | double | 平均評価 |

### 出力先

- ホーム画面：GlobalData.allProductsにカルーセルIDをキーとして保存
- カテゴリ画面：CategoryBlocの状態として保持
- 画面UI：グリッド/リストビューで描画

## 処理フロー

### 処理シーケンス

```
1. イベント発行
   └─ ホーム用: FetchAllProductsEvent(filters, id)
   └─ カテゴリ用: FetchSubCategoryEvent(filters, page)

2. BlocがRepositoryを通じてAPI呼び出し
   └─ repository?.getAllProducts(filters: filters, page: page)

3. ApiClientがGraphQL queryを実行
   └─ mutation.allProductsList(filters: filters, page: page, limit: limit)

4. レスポンスをNewProductsModelにパース
   └─ NewProductsModel.fromJson()

5. 状態更新
   └─ ホーム用: FetchAllProductsState.success + GlobalData.allProducts[id]保存
   └─ カテゴリ用: FetchSubCategoryState.success

6. ページネーション処理
   └─ スクロール最下部検出 → page++ → 追加データ取得
```

### フローチャート

```mermaid
flowchart TD
    A[商品一覧取得開始] --> B[フィルタ条件設定]
    B --> C[allProducts query実行]
    C --> D[NewProductsModel生成]
    D --> E{呼び出し元}
    E -->|ホーム| F[GlobalData.allProducts保存]
    E -->|カテゴリ| G[FetchSubCategoryState]
    F --> H[カルーセル表示]
    G --> I[グリッド/リスト表示]
    I --> J{スクロール最下部?}
    J -->|Yes| K[追加データ取得]
    K --> C
    J -->|No| L[待機]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | ページネーション | 1ページ15件ずつ取得、スクロールで追加読み込み | カテゴリ画面 |
| BR-002 | お気に入り状態 | ログインユーザーにはisInWishlistを表示 | ログイン時 |
| BR-003 | 価格表示 | 通常価格と特価を区別して表示 | 常時 |
| BR-004 | 在庫状態 | isSaleableがfalseの場合は「売り切れ」表示 | 常時 |
| BR-005 | フィルタ適用 | テーマ設定またはユーザー選択のフィルタを適用 | 常時 |

### 計算ロジック

本機能に計算ロジックは存在しない（価格計算はバックエンド側で実行）。

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

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

モバイルアプリ側ではデータベース操作なし（APIからのデータ取得のみ）。

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

モバイルアプリ側でのローカルストレージ操作なし。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | ネットワークエラー | API通信失敗 | fail状態発行、エラーメッセージ表示 |
| - | 検索結果なし | 条件に一致する商品がない | 「商品が見つかりません」メッセージ表示 |
| - | パースエラー | JSONパース失敗 | 例外をキャッチしてfail状態発行 |

### リトライ仕様

本機能にリトライ処理は実装されていない。

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

モバイルアプリ側ではトランザクション管理は行わない。

## パフォーマンス要件

- API応答時間：3秒以内
- 画像のプリキャッシュによる表示高速化
- ページネーションによる初期読み込み最適化

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

- 価格情報はAPIから取得し、クライアント側で改ざんしない
- お気に入り状態は認証済みユーザーのみに表示

## 備考

- 商品タイプ（simple, configurable, bundle等）によって表示内容が異なる
- グリッド/リスト表示切替はユーザー設定で保持
- 商品画像のプリキャッシュはHomePageViewで実行

---

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

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

### 推奨読解順序

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

レスポンスのデータモデルを確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | filter_product_model.dart | `lib/data_model/categories_data_model/filter_product_model.dart` | NewProductsModel、ProductData、PriceHtml等 |

#### Step 2: ホーム用商品取得を理解する

HomePageBlocでの処理を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | home_page_bloc.dart | `lib/screens/home_page/bloc/home_page_bloc.dart` | FetchAllProductsEvent処理（127-141行目） |

**主要処理フロー**:
- **129行目**: repository?.getAllProducts(filters: event.filters)呼び出し
- **131行目**: GlobalData.productsStream.sink.add(products)
- **133行目**: GlobalData.allProducts[event.id] = products

#### Step 3: カテゴリ用商品取得を理解する

CategoryBlocでの処理を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | categories_bloc.dart | `lib/screens/categories_screen/bloc/categories_bloc.dart` | FetchSubCategoryEvent処理 |
| 3-2 | categories_screen.dart | `lib/screens/categories_screen/categories_screen.dart` | スクロールリスナーとページネーション |

**主要処理フロー**（categories_screen.dart）:
- **66-70行目**: initStateでScrollControllerのリスナー設定
- **73-84行目**: _setItemScrollListenerでスクロール最下部検出、追加データ取得

#### Step 4: リポジトリ層を理解する

API呼び出しの実装を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | home_page_repository.dart | `lib/screens/home_page/bloc/home_page_repository.dart` | getAllProducts()（148-159行目） |
| 4-2 | categories_repository.dart | `lib/screens/categories_screen/bloc/categories_repository.dart` | callCategoriesData()（27-39行目） |

#### Step 5: API通信層を理解する

GraphQL query定義を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | mutation_query.dart | `lib/services/mutation_query.dart` | allProductsList()（441-781行目）- 非常に長いquery |

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

```
HomeScreen / SubCategoryScreen
    │
    ├─ HomePageBloc (ホームカルーセル用)
    │      │
    │      └─ FetchAllProductsEvent
    │             │
    │             └─ HomePageRepository.getAllProducts()
    │
    └─ CategoryBloc (カテゴリ画面用)
           │
           └─ FetchSubCategoryEvent
                  │
                  └─ CategoriesRepository.callCategoriesData()
                         │
                         └─ ApiClient.getAllProducts()
                                │
                                └─ MutationsData.allProductsList()
```

### データフロー図

```
[フィルタ条件]              [API]                    [出力]

filters: [              allProducts query
  {category_id: "1"},         │
  {page: "1"},                ▼
  {limit: "15"}        NewProductsModel
]                             │
        │                     ├──▶ ホームカルーセル (GlobalData.allProducts)
        │                     │
        └─────────────────────├──▶ カテゴリ画面 (グリッド/リスト表示)
                              │
                              └──▶ 検索結果画面
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| filter_product_model.dart | `lib/data_model/categories_data_model/filter_product_model.dart` | ソース | 商品データモデル |
| home_page_bloc.dart | `lib/screens/home_page/bloc/home_page_bloc.dart` | ソース | ホーム用Bloc |
| categories_bloc.dart | `lib/screens/categories_screen/bloc/categories_bloc.dart` | ソース | カテゴリ用Bloc |
| categories_screen.dart | `lib/screens/categories_screen/categories_screen.dart` | ソース | カテゴリ画面UI |
| home_page_repository.dart | `lib/screens/home_page/bloc/home_page_repository.dart` | ソース | ホーム用リポジトリ |
| categories_repository.dart | `lib/screens/categories_screen/bloc/categories_repository.dart` | ソース | カテゴリ用リポジトリ |
| api_client.dart | `lib/services/api_client.dart` | ソース | API通信クライアント |
| mutation_query.dart | `lib/services/mutation_query.dart` | ソース | GraphQLクエリ定義 |
| product_view.dart | `lib/screens/home_page/widget/product_view.dart` | ソース | 商品カードウィジェット |
