# 通知設計書 14-メディア保存失敗通知

## 概要

本ドキュメントは、Decap CMS のメディアライブラリにおいて、メディアファイルのアップロード・保存処理が失敗した場合に表示されるエラートースト通知の設計を記述する。

### 本通知の処理概要

メディア保存失敗通知は、ユーザーがメディアライブラリを通じて画像やドキュメント等のメディアファイルをアップロード・保存しようとした際に、何らかの理由で処理が失敗した場合にエラー情報をフィードバックするトースト通知である。

**業務上の目的・背景**：コンテンツ管理において、メディアファイル（画像・動画・ドキュメント等）のアップロードは頻繁に行われる操作である。アップロードが失敗した場合、ユーザーがファイルが正常に保存されたと誤認してエントリを公開してしまうことを防ぐ必要がある。本通知により、メディアファイルの保存失敗を即座に認知させ、再アップロードを促す。

**通知の送信タイミング**：`persistMedia` アクション内の catch ブロックで、メディアファイルの永続化処理全体が例外をスローした時点で送信される。アセットストアインテグレーション経由、Git バックエンド経由のいずれの保存方式でも、最外部の catch ブロックで捕捉される。

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

**通知内容の概要**：インライン文字列（i18nキーなし）で `"Failed to persist media: {error}"` というエラーメッセージが表示される。テンプレートリテラルでエラーオブジェクトの文字列表現が直接埋め込まれる。

**期待されるアクション**：ユーザーはエラー内容を確認し、ファイルサイズの確認、対応フォーマットの確認、ネットワーク接続の確認、再度のアップロード試行を行うことが期待される。

## 通知種別

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

## 送信仕様

### 基本情報

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

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

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

## 通知テンプレート

### メール通知の場合

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

### 本文テンプレート

```
Failed to persist media: {error}
```

注意：本通知はi18nキーを使用せず、インライン文字列（テンプレートリテラル）で直接メッセージを構成する。そのため多言語対応されていない。

### 添付ファイル

該当なし

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| error | エラーの詳細情報 | メディア永続化処理中にスローされた Error オブジェクト（テンプレートリテラルで文字列化） | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | メディアライブラリからのファイルアップロード | `persistMedia` thunk内の処理全体が例外をスローした場合 | catch ブロック（行297-306）で発火 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| メディア保存処理が成功した場合 | 成功時は `mediaPersisted` アクションが dispatch され、本エラー通知は表示されない |
| ドラフト編集中のメディア追加 | `editingDraft` が true の場合、`addDraftEntryMediaFile` で処理され、バックエンド永続化は行われない（行283-291）。ただしその前段で例外が発生した場合は本通知が発火する |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[ユーザーがメディアファイルをアップロード] --> B[persistMedia thunk dispatch]
    B --> C{インテグレーションあり?}
    C -->|あり| D[provider.upload]
    C -->|なし| E{privateUpload?}
    E -->|Yes| F[Error throw - Private Upload は Asset Store 専用]
    E -->|No| G[createAssetProxy]
    D --> H[createAssetProxy]
    G --> I[addAsset dispatch]
    H --> I
    I --> J{editingDraft?}
    J -->|Yes| K[addDraftEntryMediaFile dispatch]
    J -->|No| L[backend.persistMedia]
    L --> M{永続化結果}
    M -->|成功| N[mediaPersisted dispatch]
    M -->|失敗| O[catch ブロック]
    F --> O
    O --> P[addNotification dispatch - インライン文字列]
    P --> Q[mediaPersistFailed dispatch]
    Q --> R[トースト表示 8秒後自動消去]
    N --> S[終了]
    K --> S
    R --> S
```

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

### 参照テーブル一覧

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

### 更新テーブル一覧

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

#### Redux Store 更新詳細

| 操作 | 項目 | 更新値 | 備考 |
|-----|------|-------|------|
| INSERT | notifications.notifications[] | `{ id: uuid(), message: 'Failed to persist media: {error}', type: 'error', dismissAfter: 8000 }` | message はインライン文字列（NotificationMessage型ではなくstring型） |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| Private Upload エラー | アセットストアインテグレーション未使用時に `privateUpload: true` を指定 | "The Private Upload option is only available for Asset Store Integration" エラーをスロー |
| バックエンドAPI通信エラー | `backend.persistMedia()` の通信エラー等 | エラー詳細をトースト通知で表示し、`mediaPersistFailed` を dispatch |
| アセットストアアップロードエラー | インテグレーションプロバイダの `upload()` が失敗 | 外部 catch でフォールバック処理後、最終的に永続化失敗で本通知が発火する可能性あり |

### リトライ仕様

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

## 配信設定

### レート制限

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

### 配信時間帯

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

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

- エラーオブジェクトの文字列表現がそのままトースト通知に表示されるため、バックエンドの内部情報が漏洩するリスクがある。
- クライアントサイドのトースト通知であり、操作ユーザー本人のブラウザにのみ表示されるため、リスクは限定的である。
- ファイルアップロード前に同名ファイルの存在チェックが行われ、`window.confirm` でユーザーに確認を求める処理がある（行231-237）。

## 備考

- 本通知はi18nキーを使用せず、テンプレートリテラル ``Failed to persist media: ${error}`` で直接メッセージを構成する（行301）。他の多くの通知がi18nキーを使用しているのに対し、本通知は多言語対応されていない点が特徴的である。
- `dismissAfter: 8000` が設定されており、8秒後に自動消去される。
- `console.error(error)` でコンソールにもエラーが出力される（行298）。

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | notifications.ts (actions) | `packages/decap-cms-core/src/actions/notifications.ts` | `NotificationPayload` の `message` フィールドが `string | NotificationMessage` の union 型であることを理解する。本通知では `string` 型が使用される |
| 1-2 | mediaLibrary.ts | `packages/decap-cms-core/src/actions/mediaLibrary.ts` | `MediaOptions` インターフェース（行437-444）で `privateUpload` や `field` の型を理解する |

**読解のコツ**: 本通知は `addNotification` の `message` に直接文字列を渡す。これは `NotificationMessage` オブジェクト（`key` + `details`）を渡す他の通知とは異なるパターンである。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | mediaLibrary.ts | `packages/decap-cms-core/src/actions/mediaLibrary.ts` | `persistMedia` 関数（行213-309）がメディア保存処理のエントリーポイント。`file` と `opts`（privateUpload, field）を受け取る |

**主要処理フロー**:
1. **行219-220**: バックエンドとインテグレーションの取得
2. **行221**: ファイル名のサニタイズ
3. **行231-237**: 同名ファイルの存在チェックと確認ダイアログ
4. **行239-241**: インテグレーションまたは非ドラフト時に `mediaPersisting` を dispatch
5. **行243-274**: AssetProxy の生成（インテグレーション / 通常）
6. **行276**: `addAsset` を dispatch
7. **行279-296**: メディアファイルの永続化（インテグレーション / ドラフト / バックエンド）
8. **行297-307**: catch ブロック（**本通知の発火箇所**）
9. **行299-305**: `addNotification` を dispatch（インライン文字列）
10. **行306**: `mediaPersistFailed` を dispatch

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | notifications.ts (actions) | `packages/decap-cms-core/src/actions/notifications.ts` | `addNotification` 関数（行18-19）。本通知では `message` が文字列型で渡される |
| 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` | 行35-37: `typeof notification.message == 'string'` の条件分岐により、文字列メッセージはそのまま表示される（i18n翻訳なし） |

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

```
persistMedia (mediaLibrary.ts:213)
    |
    +-- sanitizeSlug (lib/urlHelper.ts)
    |
    +-- [同名ファイル存在時] deleteMedia (mediaLibrary.ts:311)
    |
    +-- mediaPersisting (mediaLibrary.ts:458)
    |
    +-- createAssetProxy (valueObjects/AssetProxy.ts)
    |
    +-- addAsset (media.ts)
    |
    +-- [インテグレーション] getBlobSHA → createMediaFileFromAsset
    |   [ドラフト] getBlobSHA → createMediaFileFromAsset → addDraftEntryMediaFile
    |   [バックエンド] backend.persistMedia
    |
    +-- [成功時] mediaPersisted (mediaLibrary.ts:462)
    |
    +-- [失敗時] catch ブロック (mediaLibrary.ts:297)
            |
            +-- console.error(error)
            +-- addNotification (notifications.ts:18) -- インライン文字列
            |       |
            |       +-- NOTIFICATION_SEND → reducer
            |               |
            |               +-- Notifications.tsx useEffect → toast()
            |
            +-- mediaPersistFailed (mediaLibrary.ts:470)
```

### データフロー図

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

ユーザーファイル選択 ────> persistMedia thunk ──────────────> Redux dispatch
  (File, opts)                   |                                    |
                         ファイル名サニタイズ                          |
                                 |                                    |
                         AssetProxy 生成                       NOTIFICATION_SEND
                                 |                                    |
                         backend.persistMedia /               reducer: notifications
                         provider.upload                              |
                                 |                            Notifications コンポーネント
                         [例外スロー]                                 |
                                 |                            react-toastify toast()
                         addNotification({                           |
                           message: `Failed to               トースト表示（8秒後自動消去）
                             persist media: ${error}`,
                           type: 'error',
                           dismissAfter: 8000
                         })
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| mediaLibrary.ts | `packages/decap-cms-core/src/actions/mediaLibrary.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コンポーネント |
| urlHelper.ts | `packages/decap-cms-core/src/lib/urlHelper.ts` | ソース | `sanitizeSlug` によるファイル名サニタイズ |
| AssetProxy.ts | `packages/decap-cms-core/src/valueObjects/AssetProxy.ts` | ソース | アセットプロキシの生成 |
