# 画面設計書 33-カテゴリ商品追加画面

## 概要

本ドキュメントは、Medusa Admin Dashboard におけるカテゴリ商品追加画面の設計を記述したものです。

### 本画面の処理概要

特定のカテゴリに商品を追加するための画面です。商品一覧から複数の商品を選択し、一括でカテゴリに紐付けることができます。

**業務上の目的・背景**：ECサイトにおいて商品をカテゴリで分類することは、顧客の商品検索を容易にし、購買体験を向上させるために重要です。新規商品の追加や既存商品の再分類など、商品とカテゴリの紐付けを効率的に行うことで、運用負荷を軽減できます。複数商品の一括操作により、大量の商品を扱う場合でも迅速な対応が可能です。

**画面へのアクセス方法**：カテゴリ詳細画面の商品セクションにある「追加」ボタン押下で、フルスクリーンモーダル形式で表示されます。

**主要な操作・処理内容**：
1. 商品一覧の表示 - ページネーション付きで商品を表示（50件/ページ）
2. 商品の検索・フィルタ - タイトル、作成日、更新日での検索・ソート
3. 商品の選択 - チェックボックスで複数選択（既存紐付け商品は選択不可）
4. 一括追加 - 保存ボタン押下で選択した商品をカテゴリに追加

**画面遷移**：カテゴリ詳細画面から遷移してきます。保存完了後またはキャンセル時にカテゴリ詳細画面に戻ります。

**権限による表示制御**：管理者権限を持つユーザーのみが商品を追加できます。認証されていないユーザーはログイン画面にリダイレクトされます。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 4 | 商品カテゴリ管理 | 主機能 | カテゴリへの商品追加 |
| 1 | 商品CRUD | API連携 | 商品一覧の取得 |

## 画面種別

追加（フルスクリーンモーダル形式）

## URL/ルーティング

`/categories/:id/products`

## 入出力項目

| 項目名 | 項目ID | 型 | 必須 | 最大長 | 入力/出力 | 説明 |
|--------|--------|-----|------|--------|-----------|------|
| カテゴリID | id | string | - | - | 入力(URL) | 商品を追加する対象のカテゴリID |
| 商品ID配列 | product_ids | string[] | 必須 | - | 入力 | 追加する商品のID配列 |
| 検索クエリ | q | string | 任意 | - | 入力 | 商品名での検索 |
| ページ番号 | offset | number | 任意 | - | 入力 | ページネーション用オフセット |

## 表示項目

| 項目名 | 表示形式 | 説明 |
|--------|----------|------|
| 商品一覧テーブル | DataTable | 選択可能な商品一覧 |
| チェックボックス（ヘッダー） | Checkbox | ページ内全選択 |
| チェックボックス（行） | Checkbox | 個別商品選択（既存紐付け商品は無効化） |
| 商品情報カラム | 複数カラム | useProductTableColumnsで定義された標準カラム |
| 検索ボックス | Input | 商品検索用（autofocus） |
| フィルター | Filter | カテゴリ以外のフィルター条件 |
| ソートオプション | Select | タイトル、作成日、更新日 |
| ページネーション | Pagination | 50件/ページ |

## イベント仕様

### 1-商品選択（チェックボックス）

商品のチェックボックスをクリックすると、選択状態が更新されます。

**処理フロー**:
1. RowSelectionStateで選択状態を管理
2. updater関数で選択変更を処理
3. form.setValueでproduct_idsフォーム値を更新
4. 既存紐付け商品（products配列に含まれるID）は選択不可

### 2-保存ボタン押下

選択した商品をカテゴリに追加します。

**処理フロー**:
1. handleSubmitでフォームを検証
2. useUpdateProductCategoryProductsでAPI呼び出し
3. 成功時: トースト通知（categories.products.add.successToast）を表示し、モーダルを閉じる
4. 失敗時: エラーメッセージをトーストで表示

**API呼び出し**:
- エンドポイント: `POST /admin/product-categories/:id/products`
- リクエストボディ:
  ```json
  {
    "add": ["prod_xxx", "prod_yyy"]
  }
  ```

### 3-キャンセルボタン押下

選択を破棄し、モーダルを閉じてカテゴリ詳細画面に戻ります。

### 4-検索

検索ボックスに入力すると、商品タイトルでフィルタリングされます。

### 5-ページネーション

ページを切り替えると、次のページの商品が読み込まれます。

## データベース更新仕様

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| 保存ボタン押下 | product_category_product | INSERT | カテゴリと商品の紐付けを追加 |

### テーブル別更新項目詳細

#### product_category_product（中間テーブル）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | product_category_id | URLパラメータのカテゴリID | カテゴリの識別子 |
| INSERT | product_id | 選択された商品ID | 商品の識別子 |
| INSERT | created_at | 現在日時 | 自動設定 |

## メッセージ仕様

| メッセージID | 種別 | メッセージ内容 | 表示タイミング |
|------------|------|--------------|--------------|
| categories.products.add.successToast | 成功 | {count}件の商品をカテゴリに追加しました | 保存成功時 |
| - | エラー | {error.message} | API呼び出し失敗時 |
| categories.products.add.disabledTooltip | 情報 | この商品は既に追加されています | 既存商品のツールチップ |

## 例外処理

| 例外状況 | 処理内容 |
|---------|---------|
| カテゴリが見つからない | エラーをスローし、エラーバウンダリで処理 |
| API通信エラー | トーストでエラーメッセージを表示 |
| 商品が選択されていない | フォームエラーとして表示（Hint） |
| 認証エラー | ログイン画面にリダイレクト |

## 備考

- フルスクリーンモーダル形式（RouteFocusModal）で表示
- 1ページあたり50件の商品を表示（PAGE_SIZE = 50）
- プレフィックス"p"でURLパラメータを管理
- 既存紐付け商品は選択不可（enableRowSelectionで制御）
- Zodスキーマでproduct_idsのバリデーション
- useProductTableColumnsで標準の商品カラムを使用
- カテゴリフィルターは除外（useProductTableFilters(["categories"])）

---

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

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

### 推奨読解順序

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

フォームのスキーマと商品データ型を理解することが重要です。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | edit-category-products-form.tsx | `packages/admin/dashboard/src/routes/categories/category-products/components/edit-category-products-form/edit-category-products-form.tsx` | EditCategoryProductsSchema（32-34行目）でproduct_ids配列のバリデーションを確認 |

**読解のコツ**: Zodスキーマでz.array(z.string())として定義されており、商品IDの配列を保持します。

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

ルートコンポーネントから画面がどのように構成されるかを理解します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | category-products.tsx | `packages/admin/dashboard/src/routes/categories/category-products/category-products.tsx` | useParamsでID取得、useProductCategoryでカテゴリと紐付き商品を取得 |

**主要処理フロー**:
1. **7行目**: useParamsでURLからカテゴリIDを取得
2. **9-12行目**: useProductCategoryでカテゴリデータをフェッチ（fields: "products.id"で紐付き商品IDを取得）
3. **14行目**: データ取得完了の判定
4. **23-26行目**: EditCategoryProductsFormにcategoryIdとproductsを渡す

#### Step 3: 商品追加フォームを理解する

商品選択と追加処理の実装詳細を把握します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | edit-category-products-form.tsx | `packages/admin/dashboard/src/routes/categories/category-products/components/edit-category-products-form/edit-category-products-form.tsx` | 選択状態管理、データテーブル、API呼び出しの実装を確認 |

**主要処理フロー**:
- **46-51行目**: 既存商品IDでRowSelectionStateを初期化
- **60-72行目**: updater関数で選択変更時にフォーム値を更新
- **99-101行目**: enableRowSelectionで既存商品を選択不可に設定
- **109-110行目**: useUpdateProductCategoryProductsでミューテーションフック取得
- **112-132行目**: handleSubmitで{ add: product_ids }をAPIに送信

#### Step 4: APIフックを理解する

バックエンドとの通信処理を把握します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | categories.tsx | `packages/admin/dashboard/src/hooks/api/categories.tsx` | useUpdateProductCategoryProducts（155-186行目）の実装を確認 |

**主要処理フロー**:
- **164-165行目**: sdk.admin.productCategory.updateProductsでAPI呼び出し
- **166-180行目**: 成功時にカテゴリと商品のクエリキャッシュを無効化

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

```
CategoryProducts (category-products.tsx)
    │
    ├─ useParams() → カテゴリID取得
    │
    ├─ useProductCategory(id, {fields: "products.id"}) → カテゴリと商品取得
    │      └─ sdk.admin.productCategory.retrieve()
    │
    └─ EditCategoryProductsForm (edit-category-products-form.tsx)
           │
           ├─ useForm() → フォーム状態管理
           │
           ├─ useProducts() → 商品一覧取得
           │      └─ sdk.admin.product.list()
           │
           ├─ useDataTable() → テーブル制御
           │
           ├─ useUpdateProductCategoryProducts(categoryId)
           │      └─ sdk.admin.productCategory.updateProducts()
           │
           └─ _DataTable → 商品一覧表示
                  ├─ Checkbox → 選択コントロール
                  └─ Pagination → ページネーション
```

### データフロー図

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

URLパラメータ(:id) ───▶ useProductCategory ───▶ 既存商品ID取得
                              │
                              ▼
検索/フィルター ─────────▶ useProducts ───▶ 商品一覧表示
                              │
                              ▼
チェックボックス選択 ────▶ RowSelectionState ───▶ product_ids更新
                              │
                              ▼
保存ボタン押下 ─────────▶ useUpdateProductCategoryProducts
                              │
                              ▼
                        SDK API呼び出し ───▶ 商品紐付け追加
                              │
                              ▼
                        クエリキャッシュ無効化 ───▶ UI更新
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| category-products.tsx | `packages/admin/dashboard/src/routes/categories/category-products/category-products.tsx` | ソース | ルートコンポーネント |
| edit-category-products-form.tsx | `packages/admin/dashboard/src/routes/categories/category-products/components/edit-category-products-form/edit-category-products-form.tsx` | ソース | 商品追加フォームコンポーネント |
| index.ts | `packages/admin/dashboard/src/routes/categories/category-products/index.ts` | ソース | エクスポート定義 |
| categories.tsx | `packages/admin/dashboard/src/hooks/api/categories.tsx` | ソース | カテゴリAPI フック |
| products.tsx | `packages/admin/dashboard/src/hooks/api/products.tsx` | ソース | 商品API フック |
| RouteFocusModal | `packages/admin/dashboard/src/components/modals/` | ソース | フルスクリーンモーダル |
| _DataTable | `packages/admin/dashboard/src/components/table/data-table/` | ソース | データテーブルコンポーネント |
| use-product-table-columns.tsx | `packages/admin/dashboard/src/hooks/table/columns/` | ソース | 商品テーブルカラム定義 |
| use-product-table-filters.tsx | `packages/admin/dashboard/src/hooks/table/filters/` | ソース | 商品テーブルフィルター |
