# 機能設計書 15-エントリグルーピング

## 概要

本ドキュメントは、Decap CMSにおけるエントリグルーピング機能の設計を記述する。コレクション内のエントリをview_groups設定に基づいてグルーピング表示する処理の仕様を網羅する。

### 本機能の処理概要

エントリグルーピング機能は、コレクション設定の`view_groups`で定義されたグルーピング条件に基づき、エントリ一覧をグループ別に分類して表示する機能である。

**業務上の目的・背景**：大量のコンテンツを管理する際、特定のフィールド値（カテゴリ、日付、ステータス等）でエントリを視覚的にグループ化することで、コンテンツの全体像を把握しやすくなる。編集者がコンテンツを効率的にナビゲートするための機能である。

**機能の利用シーン**：コレクション一覧画面でグループコントロールを操作し、view_groupsに定義された条件によるグルーピングを有効/無効にする。一度に有効にできるグルーピングは1つのみ（排他的）。

**主要な処理内容**：
1. `groupByField`: グルーピングリクエストをディスパッチし、全エントリを取得後にグルーピング成功アクションをディスパッチ
2. リデューサーが`GROUP_ENTRIES_REQUEST`でグループのactive状態をトグル（排他制御付き）
3. `selectGroups`セレクターがエントリをグループ化し、GroupOfEntries配列を返す
4. 各グループにはid、label、value、paths（該当エントリのパスSet）が含まれる

**関連システム・外部連携**：ソート機能（No.13）やフィルタ機能（No.14）と併用可能。

**権限による制御**：グルーピング操作自体に権限制御はない。閲覧権限のあるエントリのみがグルーピング対象となる。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 3 | コレクション一覧画面 | 主画面 | グループコントロールによるエントリ一覧のグルーピング表示。groupByFieldアクションを実行 |

## 機能種別

データ表示制御 - エントリ一覧のグルーピング表示

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| collection | Collection (Immutable.Map) | Yes | グルーピング対象のコレクション | - |
| group | ViewGroup | Yes | グループ定義（id, label, field, pattern?） | - |

### ViewGroup型の構造

| プロパティ | 型 | 必須 | 説明 |
|-----------|-----|-----|------|
| id | string | Yes | グループの一意識別子 |
| label | string | Yes | グループの表示ラベル |
| field | string | Yes | グルーピング対象のフィールド名 |
| pattern | string | No | 正規表現パターン（値の一部を抽出する場合） |

### 入力データソース

コレクション設定ファイル（config.yml）の`view_groups`定義。UIのグループコントロールからユーザーが選択する。

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| groups | GroupOfEntries[] | グループ化されたエントリ情報の配列 |

### GroupOfEntries型の構造

| プロパティ | 型 | 説明 |
|-----------|-----|------|
| id | string | グループID（label + value から生成） |
| label | string | グループの表示ラベル |
| value | string / boolean / undefined | グルーピング値 |
| paths | Set<string> | グループに属するエントリのパスのSet |

### 出力先

Redux Store（`entries.group`、`entries.pages`）

## 処理フロー

### 処理シーケンス

```
1. ユーザーがグループコントロールをクリック
   └─ groupByFieldアクションをディスパッチ
2. GROUP_ENTRIES_REQUESTをディスパッチ
   └─ 既存グループを全て削除し、選択グループのactive状態をトグル（排他制御）
3. 既にフェッチ中の場合はグループ状態の更新のみで終了
4. getAllEntriesで全エントリを取得
5. GROUP_ENTRIES_SUCCESSをディスパッチ
   └─ エントリ一覧をRedux状態に保存
6. selectGroupsセレクターがエントリをグループ化
   └─ コンポーネントがグループ別にエントリを表示
```

### フローチャート

```mermaid
flowchart TD
    A[ユーザーがグループ操作] --> B[groupByField dispatch]
    B --> C[GROUP_ENTRIES_REQUEST dispatch]
    C --> D[既存グループ削除 + activeトグル]
    D --> E{isFetching?}
    E -->|Yes| F[グループ状態更新のみで終了]
    E -->|No| G[getAllEntries実行]
    G --> H{成功?}
    H -->|Yes| I[GROUP_ENTRIES_SUCCESS dispatch]
    H -->|No| J[GROUP_ENTRIES_FAILURE dispatch]
    I --> K[selectGroupsでグループ化]
    K --> L[getGroup関数で各エントリのグループ判定]
    L --> M{patternあり?}
    M -->|Yes| N[正規表現マッチで値抽出]
    M -->|No| O[フィールド値をそのまま使用]
    N --> P[グループ別にエントリを分類]
    O --> P
    P --> Q[GroupOfEntries配列を返す]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-15-01 | 排他的グルーピング | 一度に有効にできるグルーピングは1つのみ。新しいグループを選択すると既存は削除される | GROUP_ENTRIES_REQUEST時 |
| BR-15-02 | トグル動作 | 同じグループを再選択するとactiveがfalseになり、グルーピングが解除される | GROUP_ENTRIES_REQUEST時 |
| BR-15-03 | パターン抽出 | patternが指定されている場合、正規表現マッチの最初の結果をグループ値として使用 | selectGroups時 |
| BR-15-04 | undefined値 | フィールド値がundefinedの場合、'missing_value'というIDのグループに分類される | getGroup関数 |
| BR-15-05 | boolean値 | フィールド値がbooleanの場合はそのまま、それ以外はString変換される | getGroup関数 |
| BR-15-06 | グループID生成 | グループIDは`${label}${value}`で生成される | getGroup関数 |

### 計算ロジック

- グループID: `${label}${fieldData}` または `${label}${matched[0]}`（patternあり時）
- パターンマッチ: `new RegExp(pattern)` でコンパイル、`dataAsString.match(regex)` で実行
- missing_value判定: `fieldData === undefined` の場合

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

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

本機能ではデータベース操作は発生しない。Redux Storeへの状態更新のみである。

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

該当なし（クライアントサイド操作のみ）

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| GROUP_ENTRIES_FAILURE | グルーピングエラー | getAllEntriesの取得に失敗した場合 | グループ設定を削除し、isFetching=falseに設定 |
| - | 正規表現エラー | 無効なpatternが指定された場合 | console.warnで警告を出力し、空文字列をグループ値とする |

### リトライ仕様

自動リトライは行わない。ユーザーが再度グルーピング操作を行うことでリトライとなる。

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

Redux状態の更新は同期的に処理される。GROUP_ENTRIES_REQUESTでの排他制御（既存グループの削除）はwithMutations内でアトミックに行われる。

## パフォーマンス要件

- グルーピング実行時は全エントリの取得が必要
- selectGroupsセレクターでlodashのgroupByを使用しており、エントリ数に比例した処理時間
- Immutable.jsのSet操作（paths）が各グループのエントリ参照に使用される

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

- グルーピングパターンは正規表現として評価されるため、ReDoSリスクがあるが、config.ymlで管理者が定義するため実質的なリスクは低い

## 備考

- グルーピングとフィルタリングは独立して動作する。グルーピングはUI上での表示グループ分けであり、エントリの除外は行わない。
- 無効なパターンの場合、console.warnで警告が出力されるが、UIには影響しない

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | entries.ts (reducer) | `packages/decap-cms-core/src/reducers/entries.ts` | GroupOfEntries型（517-524行目のselectGroups戻り値）の構造を確認 |

**読解のコツ**: GroupOfEntriesはid、label、value、pathsの4プロパティを持つ。pathsはImmutable.jsのSetで、グループに属するエントリのファイルパスを保持する。

#### Step 2: アクションクリエータを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | entries.ts (action) | `packages/decap-cms-core/src/actions/entries.ts` | groupByField関数（256-292行目） |

**主要処理フロー**:
1. **257-259行目**: getState()で状態取得、isFetchingチェック
2. **260-265行目**: GROUP_ENTRIES_REQUESTをディスパッチ
3. **267-269行目**: フェッチ中なら早期リターン
4. **272行目**: getAllEntriesで全エントリ取得
5. **273-280行目**: GROUP_ENTRIES_SUCCESSをディスパッチ

#### Step 3: リデューサーの排他制御を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | entries.ts (reducer) | `packages/decap-cms-core/src/reducers/entries.ts` | GROUP_ENTRIES_REQUESTケース（325-337行目） |

**主要処理フロー**:
- **329行目**: 現在のグループ状態を取得（なければfromJS(group)で新規作成）
- **330行目**: `map.deleteIn(['group', collection])` - 既存グループを全て削除（排他制御）
- **331-334行目**: 新しいグループを設定し、activeをトグル

#### Step 4: selectGroupsセレクターを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | entries.ts (reducer) | `packages/decap-cms-core/src/reducers/entries.ts` | selectGroups関数（500-525行目）、getGroup関数（460-498行目） |

**主要処理フロー**:
- **460-498行目**: `getGroup` - エントリのフィールド値からグループ情報を生成。patternがある場合は正規表現マッチ
- **511行目**: lodashのgroupByでエントリをグループ化
- **517-524行目**: GroupOfEntries配列に変換。pathsにSet(entries.map(e => e.get('path')))を設定

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

```
CollectionTopコンポーネント（グループコントロールUI）
    │
    └─ groupByField(collection, group)  [actions/entries.ts:256-292]
           ├─ dispatch(GROUP_ENTRIES_REQUEST)
           │      └─ entries reducer [325-337行目]
           │             ├─ 既存グループ削除
           │             └─ 新グループactive設定
           ├─ getAllEntries(state, collection)  [actions/entries.ts:161-169]
           └─ dispatch(GROUP_ENTRIES_SUCCESS) or dispatch(GROUP_ENTRIES_FAILURE)

セレクター:
selectGroups  [reducers/entries.ts:500-525]
    ├─ selectEntriesGroupField  [379-383行目]
    ├─ getGroup(entry, selectedGroup)  [460-498行目]
    └─ lodash.groupBy(entries, entry => getGroup(entry).id)
```

### データフロー図

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

ViewGroup              ───▶  groupByField               ───▶  Redux entries.group
(field, pattern?)             ├─ GROUP_ENTRIES_REQUEST         ├─ 排他的active設定
                              ├─ getAllEntries                 └─ エントリ一覧更新
                              └─ GROUP_ENTRIES_SUCCESS
                                      │
                                      ▼
                              selectGroups               ───▶  GroupOfEntries[]
                              ├─ getGroup(entry)                ├─ id
                              └─ lodash.groupBy                 ├─ label, value
                                                                └─ paths (Set)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| entries.ts (action) | `packages/decap-cms-core/src/actions/entries.ts` | ソース | groupByFieldアクション |
| entries.ts (reducer) | `packages/decap-cms-core/src/reducers/entries.ts` | ソース | グループ状態管理、selectGroups/getGroupセレクター |
