# 通知設計書 13-エントリ非公開化失敗通知

## 概要

本ドキュメントは、Decap CMS の編集ワークフローにおいて、公開済みエントリの非公開化（unpublish）処理が失敗した場合に表示されるエラートースト通知の設計を記述する。

### 本通知の処理概要

エントリ非公開化失敗通知は、公開済みのエントリを編集ワークフローに戻す（非公開化する）操作が何らかの理由で失敗した際に、ユーザーに対してエラー情報を即座にフィードバックするトースト通知である。

**業務上の目的・背景**：公開済みエントリの非公開化は、バックエンドでのエントリ削除（`deleteEntry`）と未公開状態での再永続化（`persistEntry`）という2段階の処理を含む。いずれかのステップで失敗した場合、データの不整合が生じる可能性があるため、ユーザーに即座に失敗を通知し、適切な対応（管理者への連絡、再試行等）を促す必要がある。

**通知の送信タイミング**：`unpublishPublishedEntry` アクション内の `.catch()` ブロックで、`backend.deleteEntry()` または `backend.persistEntry()` のいずれかが例外をスローした時点で送信される。

**通知の受信者**：非公開化操作を実行した現在のログインユーザー（ブラウザ上のセッションユーザー）。クライアントサイドのアプリ内通知であるため、操作を実行した本人のみが受信する。

**通知内容の概要**：i18nキー `ui.toast.onFailToUnpublishEntry` に対応するローカライズされたエラーメッセージが表示される。英語では "Failed to unpublish entry: %{details}"、日本語では "エントリを未公開にするのに失敗しました。%{details}" となる。`%{details}` にはバックエンドから返されたエラーオブジェクトの内容が埋め込まれる。

**期待されるアクション**：ユーザーはエラー内容を確認し、ネットワーク接続の確認、ブラウザのリロード、再度の非公開化操作の試行、または管理者への報告を行うことが期待される。データの不整合が生じている可能性があるため、慎重な対応が求められる。

## 通知種別

アプリ内トースト通知（react-toastify ベース）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期（Redux dispatch） |
| 優先度 | 高 |
| リトライ | なし（通知自体のリトライはなし。操作の再試行はユーザー操作に依存） |

### 送信先決定ロジック

操作を実行した現在のブラウザセッションユーザーに対して、クライアントサイドで直接トースト通知を表示する。Redux ストアの `notifications` ステートに通知が追加され、`Notifications` コンポーネントが react-toastify を通じてUIに描画する。

## 通知テンプレート

### メール通知の場合

該当なし（本通知はアプリ内トースト通知のみ）

### 本文テンプレート

```
Failed to unpublish entry: {details}
```

日本語の場合：
```
エントリを未公開にするのに失敗しました。{details}
```

### 添付ファイル

該当なし

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| details | エラーの詳細情報 | `backend.deleteEntry()` または `backend.persistEntry()` がスローした Error オブジェクト | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | エントリエディタからの非公開化操作 | `backend.deleteEntry()` または `backend.persistEntry()` が例外をスローした場合 | `unpublishPublishedEntry` thunkアクション内の `.catch()` ブロックで発火 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| 非公開化処理が成功した場合 | 成功時は `ui.toast.entryUnpublished` の成功通知が表示され、本エラー通知は表示されない |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[ユーザーがエントリ非公開化を実行] --> B[unpublishPublishedEntry thunk dispatch]
    B --> C[unpublishedEntryPersisting dispatch]
    C --> D[backend.deleteEntry 呼び出し]
    D --> E{削除結果}
    E -->|成功| F[backend.persistEntry 呼び出し]
    E -->|失敗| G[catch ブロック]
    F --> H{永続化結果}
    H -->|成功| I[成功通知 entryUnpublished dispatch]
    H -->|失敗| G
    G --> J[addNotification dispatch - onFailToUnpublishEntry]
    J --> K[unpublishedEntryPersistedFail dispatch]
    K --> L[トースト表示 8秒後自動消去]
    I --> M[終了]
    L --> M
```

## データベース参照・更新仕様

### 参照テーブル一覧

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

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| Redux Store: notifications | INSERT | 通知オブジェクトを notifications 配列に追加 |
| Redux Store: editorialWorkflow | UPDATE | UNPUBLISHED_ENTRY_PERSIST_FAILURE により永続化リクエスト状態をリセット |

#### Redux Store 更新詳細

| 操作 | 項目 | 更新値 | 備考 |
|-----|------|-------|------|
| INSERT | notifications.notifications[] | `{ id: uuid(), message: { key: 'ui.toast.onFailToUnpublishEntry', details: error }, type: 'error', dismissAfter: 8000 }` | uuid v4 で一意ID生成 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| エントリ削除失敗 | `backend.deleteEntry()` の通信エラー等 | エラー詳細をトースト通知で表示し、`unpublishedEntryPersistedFail` を dispatch |
| 再永続化失敗 | `backend.persistEntry()` の通信エラー等 | 同上。削除済みでpersistが失敗した場合はデータ不整合の可能性あり |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 0回（自動リトライなし） |
| リトライ間隔 | 該当なし |
| リトライ対象エラー | 該当なし（ユーザーによる手動再試行のみ） |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | なし |
| 1日あたり上限 | なし |

### 配信時間帯

制限なし（ユーザー操作に応じて随時発生）

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

- エラーメッセージにバックエンドの内部情報が含まれる可能性がある。ただし、クライアントサイドのトースト通知であり、操作ユーザー本人のブラウザにのみ表示されるため、情報漏洩リスクは限定的である。
- 非公開化処理が中間段階（削除後・再永続化前）で失敗した場合、エントリデータの不整合が生じる可能性がある点に注意が必要である。

## 備考

- 非公開化失敗時は `unpublishedEntryPersistedFail` が dispatch される（行564）。これは `UNPUBLISHED_ENTRY_PERSIST_FAILURE` アクションを生成する。
- 本通知の i18nキーは `ui.toast.onFailToUnpublishEntry` であり、多言語対応済みである。
- `dismissAfter: 8000` が設定されており、8秒後に自動消去される。

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | notifications.ts (actions) | `packages/decap-cms-core/src/actions/notifications.ts` | `NotificationPayload` インターフェース（行8-12）で通知データの型を理解する |
| 1-2 | notifications.ts (reducers) | `packages/decap-cms-core/src/reducers/notifications.ts` | `Notification` 型（行17-22）でストア内の通知状態構造を理解する |

**読解のコツ**: エラー通知では `message.details` にErrorオブジェクトが渡され、`%{details}` テンプレート変数に展開される。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | editorialWorkflow.ts | `packages/decap-cms-core/src/actions/editorialWorkflow.ts` | `unpublishPublishedEntry` 関数（行525-567）が非公開化処理のエントリーポイント。`.catch()` ブロック（行556-565）でエラー通知が発火する |

**主要処理フロー**:
1. **行529**: `selectEntry` で公開済みエントリを取得
2. **行531**: `unpublishedEntryPersisting` を dispatch
3. **行533**: `backend.deleteEntry()` 呼び出し
4. **行535-541**: `backend.persistEntry()` 呼び出し
5. **行556-565**: `.catch()` ブロック（**本通知の発火箇所**）
6. **行557-562**: `addNotification` を dispatch（エラー通知）
7. **行564**: `unpublishedEntryPersistedFail` を dispatch

#### Step 3: 通知ディスパッチの仕組みを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | notifications.ts (actions) | `packages/decap-cms-core/src/actions/notifications.ts` | `addNotification` 関数（行18-19） |
| 3-2 | notifications.ts (reducers) | `packages/decap-cms-core/src/reducers/notifications.ts` | `NOTIFICATION_SEND` ケース（行40-47） |

#### Step 4: UIレンダリングを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | Notifications.tsx | `packages/decap-cms-core/src/components/UI/Notifications.tsx` | 行37で `t()` 関数がi18nキーを翻訳し、`%{details}` を展開する |

#### Step 5: i18nメッセージを確認する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | index.js (en) | `packages/decap-cms-locales/src/en/index.js` | 行279: `onFailToUnpublishEntry: 'Failed to unpublish entry: %{details}'` |
| 5-2 | index.js (ja) | `packages/decap-cms-locales/src/ja/index.js` | 行266: `onFailToUnpublishEntry: 'エントリを未公開にするのに失敗しました。%{details}'` |

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

```
unpublishPublishedEntry (editorialWorkflow.ts:525)
    |
    +-- unpublishedEntryPersisting (editorialWorkflow.ts:130)
    |
    +-- backend.deleteEntry (バックエンドAPI: エントリ削除)
    |       |
    |       +-- backend.persistEntry (バックエンドAPI: 再永続化)
    |               |
    |               +-- [失敗時] .catch() ブロック (editorialWorkflow.ts:556)
    |                       |
    |                       +-- addNotification (notifications.ts:18)
    |                       |       |
    |                       |       +-- NOTIFICATION_SEND → reducer (reducers/notifications.ts:40)
    |                       |               |
    |                       |               +-- Notifications.tsx useEffect
    |                       |                       |
    |                       |                       +-- toast() (react-toastify)
    |                       |
    |                       +-- unpublishedEntryPersistedFail (editorialWorkflow.ts:150)
```

### データフロー図

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

ユーザー非公開化操作 ────> unpublishPublishedEntry thunk ────> Redux dispatch
  (collection, slug)             |                                    |
                         backend.deleteEntry                          |
                                 |                                    |
                         backend.persistEntry                  NOTIFICATION_SEND
                                 |                                    |
                         [例外スロー]                          reducer: notifications
                                 |                                    |
                         addNotification({                    Notifications コンポーネント
                           message: {                                |
                             key: 'ui.toast.                  react-toastify toast()
                               onFailToUnpublishEntry',              |
                             details: error                   トースト表示（8秒後自動消去）
                           },
                           type: 'error',
                           dismissAfter: 8000
                         })
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| editorialWorkflow.ts | `packages/decap-cms-core/src/actions/editorialWorkflow.ts` | ソース | 非公開化処理のthunkアクション。失敗時に通知を dispatch する |
| notifications.ts (actions) | `packages/decap-cms-core/src/actions/notifications.ts` | ソース | `addNotification` アクションクリエーター |
| notifications.ts (reducers) | `packages/decap-cms-core/src/reducers/notifications.ts` | ソース | 通知ステートの管理リデューサー |
| Notifications.tsx | `packages/decap-cms-core/src/components/UI/Notifications.tsx` | ソース | トースト通知のUIコンポーネント |
| index.js (en) | `packages/decap-cms-locales/src/en/index.js` | i18n | 英語翻訳定義 |
| index.js (ja) | `packages/decap-cms-locales/src/ja/index.js` | i18n | 日本語翻訳定義 |
