# 機能設計書 12-ローカルバックアップ

## 概要

本ドキュメントは、Decap CMSにおけるローカルバックアップ機能の設計を記述する。編集中のドラフトエントリをlocalForageに保存・復元・削除する処理の仕様を網羅する。

### 本機能の処理概要

ローカルバックアップ機能は、エントリ編集中のドラフトデータをブラウザのローカルストレージ（localForage）に自動保存し、予期しない画面遷移やブラウザクラッシュからのデータ復元を可能にする機能である。

**業務上の目的・背景**：CMSでのコンテンツ編集は長時間に及ぶことがあり、ブラウザのクラッシュやネットワーク障害、誤った画面遷移によって編集データが失われるリスクがある。ローカルバックアップにより、未保存のデータを自動的に保護し、データ損失リスクを最小化する。

**機能の利用シーン**：エントリ編集画面および新規エントリ作成画面で利用される。編集中に2秒のデバウンス間隔で自動保存され、エントリを再度開いた際にバックアップの存在が検出されると、復元ダイアログが表示される。

**主要な処理内容**：
1. `persistLocalBackup`: 編集中のドラフトエントリをlocalForageに保存（バックエンドの`persistLocalDraftBackup`メソッド経由）
2. `retrieveLocalBackup`: localForageから保存済みドラフトを取得し、メディアファイルのAssetProxyを復元、Redux状態に`localBackup`として格納
3. `loadLocalBackup`: 復元されたローカルバックアップをドラフトとして適用（`DRAFT_CREATE_FROM_LOCAL_BACKUP`アクション）
4. `deleteLocalBackup`: localForageから保存済みドラフトを削除（正常保存後やユーザーが復元を拒否した場合）

**関連システム・外部連携**：localForageライブラリ（IndexedDB/WebSQL/localStorageのラッパー）を使用。バックエンド実装クラスの`persistLocalDraftBackup`/`getLocalDraftBackup`/`deleteLocalDraftBackup`メソッドを経由する。

**権限による制御**：ローカルストレージへの操作はクライアントサイドで完結するため、権限による制御は不要。ただし、復元されたデータの保存時にはバックエンドの認証が必要。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 6 | エントリ編集画面 | 主画面 | 編集中ドラフトのlocalForageへの自動保存（2秒デバウンス）・復元・削除処理 |
| 7 | エントリ新規作成画面 | 主画面 | 新規エントリ編集中のドラフトのlocalForageへの自動保存・復元処理 |

## 機能種別

データ保護 / ローカルキャッシュ管理 - クライアントサイドのデータ永続化

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| entry | EntryMap (Immutable.Map) | Yes（保存時） | 保存対象のドラフトエントリ | - |
| collection | Collection (Immutable.Map) | Yes | 対象のコレクション定義 | - |
| slug | string | Yes（取得・削除時） | エントリのスラッグ | - |

### 入力データソース

Redux状態の`entryDraft.entry`（現在編集中のドラフト）。エディタコンポーネントが2秒のデバウンス間隔でpersistLocalBackupをディスパッチする。

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| localBackup | Map({ entry: EntryMap }) | 復元されたバックアップエントリ（entryDraftに格納） |
| entry | EntryValue | localForageから復元されたエントリデータ |
| mediaFiles | AssetProxy[] | 復元されたメディアファイルのプロキシオブジェクト |

### 出力先

- localForage（IndexedDB/WebSQL/localStorage）：保存先
- Redux Store（`entryDraft.localBackup`）：復元先

## 処理フロー

### 処理シーケンス

```
■ 保存処理
1. エディタコンポーネントが編集を検知（2秒デバウンス後）
   └─ persistLocalBackupアクションをディスパッチ
2. persistLocalBackupがバックエンドのpersistLocalDraftBackupを呼び出し
   └─ localForageにドラフトデータを保存

■ 復元処理
1. エントリ編集画面がマウントされる
   └─ retrieveLocalBackupアクションをディスパッチ
2. バックエンドのgetLocalDraftBackupからデータを取得
   └─ エントリが存在する場合
       ├─ メディアファイルのAssetProxyを復元
       ├─ addAssetsでアセットを登録
       └─ localBackupRetrievedでRedux状態を更新
3. ユーザーに復元ダイアログを表示
   └─ 復元を選択: loadLocalBackupアクションをディスパッチ
   └─ 拒否を選択: deleteLocalBackupアクションをディスパッチ

■ 削除処理
1. エントリの正常保存後、またはユーザーが復元を拒否
   └─ deleteLocalBackupアクションをディスパッチ
2. バックエンドのdeleteLocalDraftBackupを呼び出し
   └─ localForageからデータを削除
```

### フローチャート

```mermaid
flowchart TD
    A[エントリ編集開始] --> B[retrieveLocalBackup dispatch]
    B --> C{localForageにバックアップあり?}
    C -->|Yes| D[メディアファイルAssetProxy復元]
    D --> E[addAssetsでアセット登録]
    E --> F[localBackupRetrieved dispatch]
    F --> G[復元ダイアログ表示]
    G --> H{ユーザー選択}
    H -->|復元| I[loadLocalBackup dispatch]
    I --> J[DRAFT_CREATE_FROM_LOCAL_BACKUP]
    J --> K[バックアップからドラフト作成]
    H -->|拒否| L[deleteLocalBackup dispatch]
    C -->|No| M[通常のドラフト作成]
    L --> M
    K --> N[編集続行]
    M --> N
    N --> O{フィールド変更?}
    O -->|Yes（2秒後）| P[persistLocalBackup dispatch]
    P --> Q[localForageに保存]
    Q --> N
    O -->|No| N
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-12-01 | 自動保存間隔 | ドラフトの変更は2秒のデバウンス後にlocalForageに保存される | エディタ画面で編集中 |
| BR-12-02 | バックアップ復元判定 | エントリを開いた際にバックアップが存在すれば復元ダイアログを表示 | エントリ編集画面表示時 |
| BR-12-03 | newRecordフラグ | バックアップから復元時、pathの有無でnewRecordフラグが決定される | loadLocalBackup時 |
| BR-12-04 | hasChangedフラグ | バックアップからの復元後はhasChanged=trueに設定される | loadLocalBackup時 |
| BR-12-05 | バックアップ削除タイミング | エントリの正常保存後にバックアップが削除される | persistEntry成功後 |
| BR-12-06 | メディアファイル復元 | バックアップ内のメディアファイルはAssetProxyとして復元される | retrieveLocalBackup時 |

### 計算ロジック

- `newRecord`の判定: `!backupEntry.get('path')` - パスが存在しない場合はtrue（新規エントリ）
- メディアファイルの復元: `file.file || file.url`がある場合はcreateAssetProxy、ない場合はgetAssetで取得

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

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

| 操作 | 対象ストレージ | 操作種別 | 概要 |
|-----|-------------|---------|------|
| persistLocalBackup | localForage | INSERT/UPDATE | ドラフトデータの保存 |
| getLocalDraftBackup | localForage | SELECT | 保存済みドラフトの取得 |
| deleteLocalDraftBackup | localForage | DELETE | 保存済みドラフトの削除 |

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

#### localForage（ブラウザローカルストレージ）

| 操作 | キー | 値 | 備考 |
|-----|-----|-----|------|
| INSERT/UPDATE | collection名 + slug から生成されるキー | ドラフトエントリのJSON | バックエンド実装に依存 |
| SELECT | collection名 + slug | EntryValueオブジェクト | mediaFilesを含む |
| DELETE | collection名 + slug | - | 正常保存後に実行 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | ストレージエラー | localForageの容量超過または権限不足 | サイレントに失敗。ユーザーへの通知なし |
| - | データ復元エラー | バックアップデータの破損 | バックアップなしとして扱い、通常のドラフト作成に進む |
| - | メディアファイル復元エラー | アセットプロキシの作成に失敗 | Promise.allで処理されるため、一つの失敗で全体が失敗する可能性がある |

### リトライ仕様

自動保存は2秒間隔のデバウンスで繰り返されるため、一時的な失敗は次回の自動保存で上書きされる。復元処理にリトライはない。

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

localForageへの書き込みは非同期だが、Promiseベースで逐次処理される。複数の同時書き込みが発生した場合はlocalForage内部でシリアライズされる。

## パフォーマンス要件

- 自動保存は2秒のデバウンス間隔で実行されるため、連続的な入力によるパフォーマンス低下を防止
- localForageの読み書きは非同期で行われ、UIスレッドをブロックしない
- メディアファイルの実体はバックアップに含まれる場合があり、大容量ファイルがある場合はストレージ容量に注意

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

- ローカルストレージに保存されるデータは暗号化されていない
- 機密性の高いコンテンツを含むエントリは、ローカルストレージに平文で保存される
- 共有端末での使用時は、ブラウザのプライベートモードを推奨

## 備考

- localForageはIndexedDB > WebSQL > localStorageの優先順位でストレージを選択する
- バックエンド実装クラス（Backend）が実際のlocalForage操作を行うため、保存キーの構造はバックエンド実装に依存する

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | Entry.ts | `packages/decap-cms-core/src/valueObjects/Entry.ts` | EntryValueインターフェース（23-42行目）の構造。特にmediaFilesプロパティがバックアップの復元に重要 |
| 1-2 | entryDraft.js | `packages/decap-cms-core/src/reducers/entryDraft.js` | initialState（37-43行目）とlocalBackupの状態構造を確認 |

**読解のコツ**: entryDraftリデューサーの`localBackup`はMap({ entry: EntryMap })形式で格納される。通常のドラフト状態とは別の位置に保持される点に注目。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | entries.ts | `packages/decap-cms-core/src/actions/entries.ts` | 4つのバックアップ関連関数を理解する |

**主要処理フロー**:
1. **463-470行目**: `persistLocalBackup` - バックエンドのpersistLocalDraftBackupを呼び出し
2. **483-516行目**: `retrieveLocalBackup` - localForageからバックアップを取得し、メディアファイルのAssetProxyを復元
3. **449-453行目**: `loadLocalBackup` - DRAFT_CREATE_FROM_LOCAL_BACKUPアクションをディスパッチ
4. **518-524行目**: `deleteLocalBackup` - localForageからバックアップを削除

#### Step 3: リデューサーの復元処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | entryDraft.js | `packages/decap-cms-core/src/reducers/entryDraft.js` | DRAFT_LOCAL_BACKUP_RETRIEVEDケース（92-98行目）とDRAFT_CREATE_FROM_LOCAL_BACKUPケース（67-79行目） |

**主要処理フロー**:
- **92-98行目**: `DRAFT_LOCAL_BACKUP_RETRIEVED` - entry: fromJS(entry)のMapをlocalBackupキーに格納
- **67-79行目**: `DRAFT_CREATE_FROM_LOCAL_BACKUP` - localBackupからentryを取得、newRecordフラグをパスの有無で判定、hasChanged=trueに設定

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

```
EditorComponent（UIコンポーネント）
    │
    ├─ persistLocalBackup(entry, collection)  [actions/entries.ts:463-470]
    │      └─ backend.persistLocalDraftBackup(entry, collection)
    │             └─ localForage.setItem(key, entryData)
    │
    ├─ retrieveLocalBackup(collection, slug)  [actions/entries.ts:483-516]
    │      ├─ backend.getLocalDraftBackup(collection, slug)
    │      │      └─ localForage.getItem(key)
    │      ├─ createAssetProxy / getAsset（メディアファイル復元）
    │      ├─ addAssets(assetProxies)
    │      └─ localBackupRetrieved(entry)  → DRAFT_LOCAL_BACKUP_RETRIEVED
    │
    ├─ loadLocalBackup()  [actions/entries.ts:449-453]
    │      └─ DRAFT_CREATE_FROM_LOCAL_BACKUP dispatch
    │             └─ entryDraftReducer  [reducers/entryDraft.js:67-79]
    │
    └─ deleteLocalBackup(collection, slug)  [actions/entries.ts:518-524]
           └─ backend.deleteLocalDraftBackup(collection, slug)
                  └─ localForage.removeItem(key)
```

### データフロー図

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

■ 保存フロー
ドラフトエントリ    ───▶  persistLocalBackup      ───▶  localForage
(entryDraft.entry)        └─ backend.persistLocalDraftBackup

■ 復元フロー
localForage         ───▶  retrieveLocalBackup     ───▶  entryDraft.localBackup
                          ├─ getLocalDraftBackup          └─ entry
                          └─ createAssetProxy             └─ mediaFiles

■ 適用フロー
entryDraft.localBackup ───▶ loadLocalBackup       ───▶  entryDraft.entry
                             └─ DRAFT_CREATE_FROM_LOCAL_BACKUP
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| entries.ts | `packages/decap-cms-core/src/actions/entries.ts` | ソース | バックアップ関連アクションクリエータ |
| entryDraft.js | `packages/decap-cms-core/src/reducers/entryDraft.js` | ソース | ドラフト状態リデューサー（バックアップ関連ケース） |
| Entry.ts | `packages/decap-cms-core/src/valueObjects/Entry.ts` | ソース | EntryValue型定義 |
| AssetProxy.ts | `packages/decap-cms-core/src/valueObjects/AssetProxy.ts` | ソース | メディアファイルプロキシオブジェクト |
| media.ts | `packages/decap-cms-core/src/actions/media.ts` | ソース | addAssets、getAssetアクション |
