# 機能設計書 30-外部メディアライブラリ連携

## 概要

本ドキュメントは、Decap CMSの外部メディアライブラリ連携機能について、Cloudinary・Uploadcare等の外部メディアライブラリの初期化・表示・非表示制御の処理を記述した機能設計書である。

### 本機能の処理概要

外部メディアライブラリ（Cloudinary、Uploadcare等）をDecap CMSに統合するための初期化・制御機能である。mediaLibrary.tsモジュールがRedux Store購読を通じて外部ライブラリの初期化を検出し、`registerMediaLibrary`で登録されたライブラリを`once`で1回のみ初期化する。初期化後はcreateMediaLibraryアクションで外部ライブラリのAPIインスタンスをRedux Storeに保存し、openMediaLibrary / closeMediaLibrary内で外部ライブラリのshow/hideを制御する。

**業務上の目的・背景**：Decap CMSの内蔵メディアライブラリ（Git管理）だけでは、大規模な画像・動画管理のニーズに対応できない場合がある。Cloudinary・Uploadcare等の専門的なメディア管理サービスとの統合を可能にすることで、高度なメディア管理・変換・CDN配信等の機能をCMSから利用できるようにする。

**機能の利用シーン**：
- config.ymlで`media_library`にCloudinaryまたはUploadcareを指定した場合の自動初期化
- 外部メディアライブラリのUIを表示してファイルを選択・アップロードする場合
- 外部ライブラリからファイルを選択してエントリに挿入する場合

**主要な処理内容**：
1. Redux Store購読（store.subscribe）で設定変更を監視
2. media_library.name設定の検出時に`initializeMediaLibrary`を呼び出し
3. `getMediaLibrary(name)`でレジストリから外部ライブラリを取得
4. `lib.init({options, handleInsert})`で外部ライブラリを初期化
5. `createMediaLibrary(instance)`でexternalLibraryとしてRedux Storeに保存
6. `openMediaLibrary`時にexternalLibraryの`show()`を呼び出し
7. `closeMediaLibrary`時にexternalLibraryの`hide()`を呼び出し
8. 外部ライブラリからの挿入は`handleInsert`コールバック経由で`insertMedia`を実行

**関連システム・外部連携**：Cloudinary Media Library Widget、Uploadcare Widget。

**権限による制御**：外部メディアライブラリの認証は各サービスの設定に依存。CMSの認証とは独立。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 9 | メディアライブラリ画面 | 補助機能 | Cloudinary・Uploadcare等の外部メディアライブラリとの連携時の表示制御 |

## 機能種別

外部連携 / 初期化処理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes | 外部メディアライブラリ名（"cloudinary", "uploadcare"等） | registerMediaLibraryで登録済みであること |
| options | object | No | メディアライブラリの設定オプション | config.ymlのmedia_libraryセクション |

### 入力データソース

- config.yml: `media_library`セクション（name, options）
- Redux Store: state.config.media_library
- レジストリ: registry.mediaLibraries（registerMediaLibraryで登録済み）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| externalLibrary | MediaLibraryInstance | 外部ライブラリのAPIインスタンス（show, hide, onClearControl, onRemoveControl, enableStandalone） |
| showMediaButton | boolean | スタンドアロンモードでのメディアボタン表示可否 |

### 出力先

- Redux Store: state.mediaLibrary.externalLibrary（外部ライブラリインスタンス）
- Redux Store: state.mediaLibrary.showMediaButton（メディアボタン表示フラグ）

## 処理フロー

### 処理シーケンス

```
1. CMS起動時、store.subscribe()でRedux Store変更を監視開始
   └─ mediaLibrary.tsモジュールの実行時に登録
2. config読み込み成功（CONFIG_SUCCESS）でstate.config.media_library.nameが設定される
3. 購読コールバックでmedia_library.name設定を検出
   └─ state.mediaLibrary.externalLibraryが未設定の場合のみ処理
4. initializeMediaLibrary(name, options)を呼び出し（once関数で1回のみ）
5. getMediaLibrary(name)でレジストリから登録済みライブラリを取得
   a. ライブラリ未登録: configFailed(err)をディスパッチしてエラー表示
   b. ライブラリ登録済み: lib.init({options, handleInsert})で初期化
6. createMediaLibrary(instance)をディスパッチ
   └─ MEDIA_LIBRARY_CREATEアクション
   └─ リデューサーでexternalLibrary, showMediaButtonを設定
7. openMediaLibrary時にexternalLibrary.show({id, value, config, allowMultiple, imagesOnly})を呼び出し
8. closeMediaLibrary時にexternalLibrary.hide()を呼び出し
9. clearMediaControl時にexternalLibrary.onClearControl({id})を呼び出し
10. removeMediaControl時にexternalLibrary.onRemoveControl({id})を呼び出し
```

### フローチャート

```mermaid
flowchart TD
    A[store.subscribe開始] --> B{state.config.media_library.name?}
    B -->|No| C[監視継続]
    B -->|Yes| D{externalLibrary未設定?}
    D -->|No| C
    D -->|Yes| E[initializeMediaLibrary once]
    E --> F[getMediaLibrary name]
    F --> G{ライブラリ登録済み?}
    G -->|No| H[configFailed エラー]
    G -->|Yes| I[lib.init options, handleInsert]
    I --> J[createMediaLibrary instance]
    J --> K[MEDIA_LIBRARY_CREATE dispatch]
    K --> L[externalLibrary + showMediaButton設定]
    L --> M[openMediaLibrary時 -> show]
    L --> N[closeMediaLibrary時 -> hide]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-30-01 | 1回のみ初期化 | `once`関数により、initializeMediaLibraryは最初の1回のみ実行される | 常時 |
| BR-30-02 | 未登録ライブラリのエラー | registerMediaLibraryで事前登録されていないライブラリ名が指定された場合、configFailedエラーを発生 | getMediaLibraryがundefinedを返す場合 |
| BR-30-03 | handleInsertコールバック | 外部ライブラリからのファイル選択はhandleInsert関数経由でinsertMediaアクションをディスパッチ | 外部ライブラリの挿入操作時 |
| BR-30-04 | APIメソッドのデフォルト値 | show, hide, onClearControl, onRemoveControl, enableStandaloneの各メソッドが未定義の場合、空関数がデフォルト値として使用される | createMediaLibrary内 |
| BR-30-05 | showMediaButton制御 | enableStandalone()の戻り値でヘッダーのメディアボタン表示が制御される | MEDIA_LIBRARY_CREATE時 |
| BR-30-06 | 外部ライブラリ存在時の内蔵UI非表示 | externalLibraryが設定されている場合、内蔵メディアライブラリのモーダルUIは表示されず、外部ライブラリのUIが表示される | openMediaLibrary時 |

### 計算ロジック

特になし。

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 外部ライブラリ登録 | Redux Store: state.mediaLibrary | UPDATE | externalLibrary, showMediaButtonの設定 |

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

#### Redux Store: state.mediaLibrary

| 操作 | 項目（キー） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | externalLibrary | MediaLibraryInstance | MEDIA_LIBRARY_CREATE時 |
| UPDATE | showMediaButton | boolean | enableStandalone()の戻り値 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | ライブラリ未登録エラー | config.ymlのmedia_library.nameに対応するライブラリが未登録 | configFailed(err)をディスパッチ、エラー画面表示 |
| - | 初期化失敗 | lib.init()の実行中にエラー | onceにより再試行不可。ページリロードが必要 |

### リトライ仕様

`once`関数により初期化は1回のみ。失敗時の自動リトライは実装されていない。

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

該当なし。

## パフォーマンス要件

- 外部ライブラリの初期化は非同期で実行されるため、CMSの起動を遅延させない
- store.subscribeのコールバックはonce関数とexternalLibrary存在チェックで不要な再実行を防止

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

- 外部メディアライブラリのAPIキーはconfig.ymlのmedia_library.optionsで設定される
- 外部サービスとの通信はHTTPS経由で行われる（各ライブラリの実装に依存）

## 備考

- registerMediaLibrary関数はlib/registry.jsに定義され、同名のライブラリの重複登録をエラーとする
- Cloudinaryライブラリは`packages/decap-cms-media-library-cloudinary/`で実装
- Uploadcareライブラリは`packages/decap-cms-media-library-uploadcare/`で実装
- デフォルトエクスポート（decap-cms-default-exports）でCloudinaryとUploadcareが自動登録される

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | mediaLibrary.ts (reducer) | `packages/decap-cms-core/src/reducers/mediaLibrary.ts` | **60-64行目**: MEDIA_LIBRARY_CREATEリデューサー。externalLibraryとshowMediaButtonの設定 |
| 1-2 | mediaLibrary.ts (actions) | `packages/decap-cms-core/src/actions/mediaLibrary.ts` | **50-59行目**: createMediaLibrary関数。APIメソッドのデフォルト値設定 |

**読解のコツ**: MediaLibraryInstanceインターフェースはshow, hide, onClearControl, onRemoveControl, enableStandaloneの5つのメソッドを持つ。

#### Step 2: 初期化フローを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | mediaLibrary.ts | `packages/decap-cms-core/src/mediaLibrary.ts` | **29-40行目**: initializeMediaLibrary関数（once）。getMediaLibraryでレジストリ検索、lib.initで初期化、createMediaLibraryでRedux Store登録 |
| 2-2 | mediaLibrary.ts | `packages/decap-cms-core/src/mediaLibrary.ts` | **42-51行目**: store.subscribe()による設定検出。media_library.nameの存在とexternalLibrary未設定を条件に初期化開始 |

**主要処理フロー**:
1. **42-43行目**: store.subscribe()でRedux Store変更を購読
2. **45行目**: state.config.media_library?.name をチェック
3. **46行目**: !state.mediaLibrary.get('externalLibrary')で未初期化を確認
4. **48行目**: initializeMediaLibrary(name, config)を呼び出し
5. **30行目**: getMediaLibrary(name)でレジストリ検索
6. **31-35行目**: 未登録時のエラー処理
7. **37行目**: lib.init({options, handleInsert})で初期化
8. **38行目**: createMediaLibrary(instance)でRedux Storeに登録

#### Step 3: レジストリの登録を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | registry.js | `packages/decap-cms-core/src/lib/registry.js` | **221-230行目**: registerMediaLibrary関数（重複チェック付き）とgetMediaLibrary関数 |

#### Step 4: 外部ライブラリとの連携を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | mediaLibrary.ts (actions) | `packages/decap-cms-core/src/actions/mediaLibrary.ts` | **81-101行目**: openMediaLibrary内のexternalLibrary.show()呼び出し（94-98行目） |
| 4-2 | mediaLibrary.ts (actions) | `packages/decap-cms-core/src/actions/mediaLibrary.ts` | **103-112行目**: closeMediaLibrary内のexternalLibrary.hide()呼び出し（108行目） |
| 4-3 | mediaLibrary.ts (actions) | `packages/decap-cms-core/src/actions/mediaLibrary.ts` | **61-79行目**: clearMediaControl、removeMediaControlでの外部ライブラリAPI呼び出し |

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

```
store.subscribe()                              [mediaLibrary.ts:42]
    |
    +-- initializeMediaLibrary(name, options)   [mediaLibrary.ts:29] (once)
        |
        +-- getMediaLibrary(name)               [lib/registry.js:228]
        |
        +-- lib.init({options, handleInsert})    [各外部ライブラリ]
        |       |
        |       +-- handleInsert(url)            [mediaLibrary.ts:23]
        |               +-- insertMedia(url)     [actions/mediaLibrary.ts:114]
        |
        +-- createMediaLibrary(instance)         [actions/mediaLibrary.ts:50]
            |
            +-- MEDIA_LIBRARY_CREATE             [reducers/mediaLibrary.ts:60]
                +-- externalLibrary 設定
                +-- showMediaButton 設定

openMediaLibrary()
    +-- externalLibrary.show({id, value, config, allowMultiple, imagesOnly})

closeMediaLibrary()
    +-- externalLibrary.hide()
```

### データフロー図

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

config.yml            store.subscribe検出              Redux Store
(media_library)           |                               (externalLibrary)
        |                 v
        +---------> initializeMediaLibrary               外部ライブラリUI
                         |                               (show/hide)
レジストリ              getMediaLibrary
(登録済みライブラリ)        |                               handleInsert
        |                 v                               (insertMedia)
        +---------> lib.init() -> createMediaLibrary

ユーザー操作          openMediaLibrary                  外部UI表示
(メディアボタン)          |
        +---------> externalLibrary.show()
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| mediaLibrary.ts | `packages/decap-cms-core/src/mediaLibrary.ts` | ソース | 外部メディアライブラリの初期化・Store購読 |
| registry.js | `packages/decap-cms-core/src/lib/registry.js` | ソース | registerMediaLibrary / getMediaLibrary |
| mediaLibrary.ts | `packages/decap-cms-core/src/actions/mediaLibrary.ts` | ソース | createMediaLibrary / openMediaLibrary / closeMediaLibrary |
| mediaLibrary.ts | `packages/decap-cms-core/src/reducers/mediaLibrary.ts` | ソース | MEDIA_LIBRARY_CREATEリデューサー |
| index.js | `packages/decap-cms-media-library-cloudinary/src/index.js` | ソース | Cloudinaryメディアライブラリ実装 |
| index.js | `packages/decap-cms-media-library-uploadcare/src/index.js` | ソース | Uploadcareメディアライブラリ実装 |
