# ファネル機能 技術仕様書

最終更新: 2026-05-13（ファネル状態遷移設計・ページ種別）

ファネル作成（テンプレ17種）、ページ追加（URL取り込み・UTAGE JSON 変換）、ページエディタ、プレビュー表示、共通レイアウトの通知表示の**現状実装**を整理する。運用手順は `docs/manual.md`。

**表記**: ユーザー向け・本文のプロダクト名は **Monaka**。**PUTAGE** / **Putage** / ブランド文脈の **putage** は使わない。`PUTAGE_*` 環境変数・`putage:*` Artisan・`config/putage.php` 等は既存のコード識別子として維持し、画面文言に載せない（正本: `.cursor/rules/ai-collaborative-development.mdc`）。

**関連ドキュメント**

- `docs/README.md` … `docs/` 内ファイルの説明・索引
- `docs/manual.md` … 利用者向け手順
- `docs/AI Collaborative Development - Design Philosophy.md` … 協働・検証・秘密情報
- `docs/rule.mdc` … AI 向け共通ルール（本書・設計思想の参照義務）

---

## 0. 管理画面の認証（ログイン・新規登録）

- **ルート**（`routes/web.php`）: `GET /` は `/login` へリダイレクト。`GET|POST /login` は `LoginController`（トレイト `AuthenticatesUsers`）。`GET|POST /register` は **`RegisterController`**（`RegistersUsers`）。`/register` を誤って `LoginController` に割り当てると **`showRegistrationForm` / `register` が存在しないため HTTP 500** となる。ルート変更時は両コントローラの役割を混同しないこと。
- **送信用アカウント・シナリオのワークスペース**: 名前付きルート `account.workspace.*`・`scenario.workspace.*`・`scenario.broadcast.*` は **`routes/account-workspace.php`** に定義し、**`auth` ミドルウェアグループ内**で `routes/web.php` から `require base_path('routes/account-workspace.php')` する。ここが欠けるとシナリオ一覧・LINE 設定リンクなどで Blade の `route()` が **`RouteNotFoundException`** になる。
- **送信テスト・DKIM 補助 POST**: `account.mail-test`（`POST /account/{account}/mail-test`・`throttle:10,1`）・`account.line-settings-check`（`POST /account/{account}/line-settings-check`・JSON・`throttle:20,1`）・`account.line-settings.save-messaging` / `account.line-settings.save-login`（PUT・各 `throttle:20,1`）・`account.dkim.dns-check`（`POST /account/dkim/dns-check`・`throttle:30,1`）・`account.dkim.mail-test`（`POST /account/dkim/mail-test`・`throttle:10,1`）は **`routes/web.php`** の `auth` グループ内で定義し、**`Route::resource('account', …)` より前**に置く（`DeliveryAccountController`）。欠けると **各種設定（`account.edit`）** や **送信ドメイン認証** のフォーム生成で **`RouteNotFoundException`** になる。
- **ゲスト向け公開ルート**: 公開登録・解除・LINE open・公開イベント予約・会員ログイン／マイページ・**公開ファネルページ `GET /p/{uid}`**（`public.funnel.page`・`PublicFunnelPageController`・`funnel_pages.uid`・**`throttle:120,1`**。`status=public`、または **`status=scheduled` かつ `publish_start_at` が現在以前**、かつ **`expire_at` が未設定または未来**のみ表示。それ以外は **404**）は **`routes/web-public.php`** に定義し、**`auth` ミドルウェアの外**で `routes/web.php` から `require` する（`VerifyCsrfToken` の `r/*/store` 除外は従来どおり）。欠けると **`RouteNotFoundException`** または **404**（例: `POST /r/…/store`・`/e/{uid}`・`/member/{slug}/login`・**`/p/{uid}` が常に 404**）。
- **管理画面の配信まわり（要ログイン）**: `step.create|edit|update|destroy`・`subscriber.store|bulk-status|destroy`・`event.schedules.*`・`site.bundle.*`・`dashboard.chat.store` は **`routes/web.php`** の `auth` グループ内に定義する。欠けるとシナリオの **ステップ配信・読者一覧の操作・イベント枠・会員サイトのトラック／レッスン編集・ダッシュボードチャット** で **`RouteNotFoundException`** になる。
- **管理ユーザー新規登録**: `GET|POST /register` は **`RegisterController`**（`RegistersUsers`）に接続する。`LoginController` に誤結線すると **`register` メソッド不存在で 500** となる（`docs/funnel-spec.md` §0 冒頭の注意と同じ）。
- **ビュー**: `resources/views/auth/login.blade.php`、`resources/views/auth/register.blade.php`
- **ログイン／登録成功後の遷移先**: `/dashboard`（各コントローラの `$redirectTo`）。
- **白画面（本文空）のトラブル**: HTTP 200 かつ **レスポンス本文が 0 バイト** になることがある。原因の一つは `storage/framework/views` 内の Blade コンパイル結果の破損（**ディスク満杯**の直後など）。サーバで **`php artisan view:clear`**、必要に応じて **`php artisan view:cache`**。ストレージ権限・`artisan` 実行ユーザーは `docs/manual.md` の該当節を参照。
- **PHPUnit と `RefreshDatabase`（重大）**: `Illuminate\Foundation\Testing\RefreshDatabase` は **`migrate:fresh` 相当で接続先 DB のテーブルを全ドロップ**してからマイグレーションし直す。`phpunit.xml` で **`DB_CONNECTION` / `DB_DATABASE` を上書きしない**と **`.env` が指す MySQL（本番・共有）がテスト対象になり、全データが消える**。本番向け `.env` のまま **`php artisan test` を実行しない**（または必ず **SQLite `:memory:` 等の専用テスト DB** に向ける）。**消えた行の復元はバックアップからのリストアのみ**（コードを元に戻してもデータは戻らない）。運用の論理バックアップは **`php artisan putage:db-snapshot`**（接続先 **`DB_DATABASE` スキーマ全体**・`--routines --events`・実行直前に BASE TABLE 件数表示・任意 **`--dated`** または **`PUTAGE_DB_BACKUP_DATED_EACH_RUN`** で日時付きコピー）→ 既定 **`storage/backups/putage-latest.sql.gz`**（`docs/manual.md` 参照）。

### データが一斉に消えたように見えるとき（本番 DB の切り分け）

**アプリのデグレではなく、DB 側のスキーマ欠落・入れ替えが典型**です。コードベース上 **`migrate:fresh` や全 `DROP` を本番ルートから呼ぶ実装は見当たらない**（該当はマイグレーションの `down` や一部 `dropIfExists` のみ）。本番 `putage` で観測された事象は、次の **連鎖で説明がつく**（いわゆる「テーブルをアプリが消した」ではなく **空スキーマ＋マイグレーション途中停止／別 DB 向き**である）。

1. **空（または新規）の MySQL データベース**に `.env` の `DB_DATABASE` が向いた、または **`putage` を DROP → CREATE した**あとに **`php artisan migrate` を実行した**。
2. 歴史的経緯で Git に **初期 `CREATE` が無く `ALTER` のみ**のマイグレーションが先に並んでいたため、Laravel 既定テーブル（`users` 等）のあと、**`funnel_pages` を ALTER する段階で「テーブルが無い」として失敗し、以降のマイグレーションが一切走らない**。
3. 結果として **`migrations` テーブルにも数件しか記録が無い**「中途半端な DB」になり、**ファネル・読者・LINE 用テーブルが存在しない**。画面上は「データもテーブルも消えた」に見える（**行データの消失**と **スキーマ未作成**が重なると最悪に見える）。
4. （別経路）**バックアップから戻さず**に上記状態で運用を続けると、ログイン後も **`funnels` 等が無い SQL エラー**が続く。

**復旧**: 行データの復元は **バックアップ（mysqldump・ホスティングスナップショット・レプリケーション先など）からのリストア**が必要。リポジトリやコード変更だけでは **消えた行を復元できない**。

**防止（運用）**:

- 本番で **`migrate:fresh`・空 DB への初回 `migrate`・`DB_DATABASE` の安易な差し替え**をしない。変更前に **ダンプを取る**。
- マイグレーション失敗後は **`migrations` テーブルと実テーブルの整合**を確認し、「標準テーブルだけある中途半端な DB」で運用を続けない。

**空 DB でスキーマだけ通す（行データは復元しない）**: マイグレーション `2026_03_25_000000_putage_legacy_core_baseline_tables` が、Git に無かった **コア CREATE**（`funnel_groups` / `funnels` / `funnel_pages` / `delivery_account_groups` / `delivery_accounts` / `scenario_groups` / `scenarios` / `subscribers` / `products` / `product_plans` / `orders` / `custom_domains` / `step_messages` / `events` / `event_schedules` / `event_bookings` / `membership_sites` / `bundle_courses` / `courses` / `lessons` / `ab_test_*`）を **`Schema::hasTable` でガード付き**に追加する。これで **`php artisan migrate` が最後まで進み、ダッシュボード等の「テーブル無し 500」は解消しうる**。**過去の読者・LINE 来訪などの行は入らない**（バックアップリストアが別途必要）。併せて、Laravel 10 既定の **`users.id`（bigint）** に合わせるため、`dashboard_chat_messages.user_id` や `delivery_account_custom_placeholders.delivery_account_id` 等、一部既存マイグレーションの FK 型を **bigint 整合**に修正済み（極端に古い int `users` 本番はバックアップ側で型を確認すること）。

---

## 0.x 送信ドメイン認証（DNS）: SPF は include 方式を推奨

### 運用の約束（繰り返し・サポート削減）

- **SSH でサーバに入り `.env` を都度いじる運用にしない。** 既定・強度は **`config/putage.php`** とデプロイで揃え、利用者には **DNS 転記のみ**を案内する。
- 説明の短い正本: **`docs/mail-auth-ops-defaults.md`**（`docs/manual.md`・`docs/mail-auth-for-nonengineers.md`・本節と同趣旨で繰り返し記載）。

### 背景

- SPF は送信元サーバーIP（`ip4:`）をDNSで許可する仕組みだが、一般ユーザーにIPを直接見せずに設定させたいケースがある。
- 送信元メールアドレスにサブドメイン（例: `infomail.example.com`）を使う場合、DNS 上でそのサブドメインが **存在**していない（NXDOMAIN）と、受信側で未認証扱いになりやすい。必要に応じて **A/AAAA** を入れて NXDOMAIN を避ける。

### 方針（monaka）

- 画面で案内する SPF は、可能な限り **`include:` 方式**にする。
  - ユーザー側: `v=spf1 include:<運用側 include ドメイン> -all`
  - 運用側: `<運用側 include ドメイン>` の TXT に `v=spf1 ip4:<送信IP> -all` を登録
- 設定値:
  - `PUTAGE_SPF_INCLUDE_DOMAIN` … 画面に表示・コピーされる `include:` の参照先
  - `PUTAGE_SENDING_IP` … 運用側 TXT（ip4）生成用
- **DMARC**: `MONAKA_DMARC_POLICY`（`none` / `quarantine` / `reject`）と任意 `MONAKA_DMARC_RUA` で画面案内と DNS 照合の基準を一致させる（`App\Support\MailDmarcSpec`）。**環境変数未設定または空のときの製品既定は `quarantine`**（`config/putage.php`）。弱めるときだけ `none` を明示。強いポリシーは他経路の送信が残っていると配信に影響しうる。

### 運用補助コマンド

- `php artisan putage:spf-include-export` で、運用側 DNS に登録する TXT（`ip4:` を含む）を出力する。
- Cloudflare に運用ゾーンがある場合は **`php artisan monaka:spf-include-sync-cloudflare`** で、`_spf.*` の TXT を API で upsert できる（`MONAKA_CLOUDFLARE_API_TOKEN`・ゾーン・`MONAKA_SPF_INCLUDE_DOMAIN`・`MONAKA_SENDING_IP` が必要。DKIM とゾーンが異なるときは `MONAKA_CLOUDFLARE_SPF_ZONE_NAME`）。

## 1. 対象範囲

| 領域 | 内容 |
|------|------|
| ファネル作成 | テンプレ選択、`FunnelPage` 自動生成 |
| ページ追加 | 空ページ / URL取り込み（一般HTML + UTAGE JSON） |
| ページ編集 | セクション・要素 JSON、`editor.blade.php` |
| プレビュー | `preview.blade.php`、`_preview_element.blade.php`、`_preview_sections.blade.php` |
| 共通UI | フラッシュメッセージは `layouts/app.blade.php` に集約 |

---

## 2. 関連ファイル一覧

### バックエンド

| ファイル | 役割 |
|----------|------|
| `app/Services/FunnelTemplateService.php` | テンプレ17種定義、`applyToFunnel` |
| `app/Services/UtagePageImportService.php` | UTAGE `v-bind:data` JSON → `page_content` 変換 |
| `app/Services/FunnelPageImportImageMirrorService.php` | URL 取り込み後の外部画像を `storage` へ保存し URL 差し替え |
| `app/Http/Controllers/FunnelController.php` | ファネル作成・テンプレ適用 |
| `app/Http/Controllers/MediaController.php` | メディアライブラリ・**`apiUpload` / `apiList`**（`POST /api/media/upload`・`GET /api/media`・ページエディタの画像・動画・音声・PDF）。永続化は **`media_files`**（`App\Models\MediaFile`・マイグレーション **`2026_05_12_220000_create_media_files_table`**） |
| `app/Http/Controllers/FunnelPageController.php` | ページ CRUD、`store` の取り込み分岐、**`uploadImage`**（`POST …/page/upload-image`・`funnel.page.upload`・レガシー `page-editor` の画像アップロード） |
| `app/Http/Controllers/PublicFunnelPageController.php` | **`GET /p/{uid}`** ゲスト向け公開ファネルページ（`web-public.php`・`public.funnel.page`） |

### ビュー（主要）

| ファイル | 役割 |
|----------|------|
| `resources/views/layouts/app.blade.php` | `success` / `error` フラッシュ1か所、ファネルサブナビ |
| `resources/views/funnel/create.blade.php` | テンプレ選択 |
| `resources/views/funnel/page/create.blade.php` | ページ追加フォーム（2チェック連動） |
| `resources/views/funnel/page/index.blade.php` | ページ一覧（重複フラッシュなし） |
| `resources/views/funnel/index.blade.php` | ファネル一覧（重複フラッシュなし） |
| `resources/views/funnel/page/editor.blade.php` | エディタ本体・ブロック選択モーダル・D&D |
| `resources/views/funnel/page/preview.blade.php` | プレビュー枠・セクションスタイル |
| `resources/views/funnel/page/_preview_element.blade.php` | 要素別プレビューHTML |
| `resources/views/funnel/page/_preview_sections.blade.php` | セクションループ（プレビューと同等のスタイル計算） |

---

## 3. ファネル作成（テンプレ17種・チャットUI）

- 画面 `funnel/create` は **作り方を2区分**で切替する。**テンプレートから作成**（全17種のカード選択＋右カラムのファネル情報）と **チャットで作成する**（右カラムのファネル情報は非表示。チャット内で目的を入力し「この内容でファネルを作成する」。`creation_mode=chat` 時は `name` 不要・最初のユーザー発話から仮名称を生成、会社は既定、グループなし、`template_key=blank`。会話は `state_transition_notes` に `【チャットで作成】` 付きで保存。ページ自動生成・AI連携は未接続）。
- `FunnelTemplateService::definitions()` が key / name / pages[] を保持。
- `FunnelController@store` で `template_key` を検証し、`applyToFunnel` で `FunnelPage` を `sort_order` 順に作成（チャットモード時は常に `blank`）。
- 無効キー時は `blank` 相当にフォールバックする実装に依存する。

---

## 4. ページ追加フォーム（2チェック連動）

実装: `resources/views/funnel/page/create.blade.php` + `FunnelPageController@store`

- **チェックA** `import_from_url` … ON 時のみ `import_url` 必須・子項目有効。
- **チェックB** `register_destination_urls` … **遷移リンク**を残すか。画像は取り込み後に自サーバへ保存される（§5.3・§5.5）。
- **チェックC** `allow_html_fallback` … OFF（既定）のとき UTAGE ブロック変換に失敗したら **作成しない**。ON のときのみ `html_fallback` で単一 HTML ブロック作成。
- **取り込みを確認** … `POST funnel.page.import-preview`（作成画面のボタン）。画像ミラーはプレビューでは行わない。
- バリデーション: `import_url` は `Rule::requiredIf(import_from_url)`。URL 形式・SSRF は `FunnelPageImportUrlValidator`（§5.0）。

---

## 5. URL 取り込み処理（`FunnelPageUrlImportService` / `FunnelPageController@store`）

### 5.0 取り込み品質（2026-06-02）

| `import_parse_mode` | 意味 |
|---------------------|------|
| `utage_structured` | UTAGE JSON をブロック構造に変換できた |
| `html_fallback` | 変換失敗。ユーザーが **HTMLのみでも作成する** を ON にした場合のみ |
| （null） | URL 取り込み以外で作成 |

- 変換失敗・本文が極端に短い場合は **既定でページ作成を拒否**（`allow_html_fallback` が ON のときのみ `html_fallback`）。
- `import_diagnostics`（JSON）に `detected_format`・`section_count`・`element_count`・`dropped_types`・`unsupported_types`・`warnings` を保存。
- **取り込み確認**: `POST funnel.page.import-preview`（JSON・画像ミラーなし・`throttle:30,1`）。
- **再取り込み**: `POST funnel.page.reimport-url`（`import_source_url` から `page_content` 上書き・`throttle:10,1`）。一覧の ↻ ボタン。
- URL 検証: `FunnelPageImportUrlValidator`（https/http・内部 IP/localhost 拒否・レスポンス最大 5MB）。

### 5.1 HTTP 取得

- `Http::timeout(25)`、ブラウズ風 User-Agent、リダイレクト最大5回。
- 失敗時は **ページを作らず** `back` + `errors`。

### 5.2 安全化（共通前処理）

- `stripDangerousHtmlChunks` … `script`, `noscript`, `iframe`, `object`, `embed` 等を除去。

### 5.3 分岐 A: UTAGE 形式（優先）

1. `UtagePageImportService::tryConvertFromHtml($raw, $registerDest)` を先に実行。
   - `window.__INITIAL_STATE__` 形式は `<script>` 内に JSON を持つため、`script` 除去前の raw HTML を使う。
   - 変換できなかった場合のみ、危険タグ除去後の HTML 断片取り込みへフォールバックする。
2. `v-bind:data='{...}'` または `window.__INITIAL_STATE__={...}` から JSON を括弧バランスで抽出。
3. `elements` ツリー（section / row / col / image / button / text / …）を Monaka の `sections[].elements[]` にマッピング。
   - `row/col` は可能な限り **`columns2/3/4` セクション**として保持し、レイアウト崩れを抑える（UTAGEと同じ状況で再現しやすくする）。
   - `head` は `headline`、`balloon` は `balloon`、`footer` は `footer` へ優先マッピングし、`html` へのフォールバックを減らす。
   - `button` の `img_src` は `image` へ変換せず、**`button`（背景画像ボタン）として保持**する。
   - UTAGE `button.animation=scale` は Monaka `button.animation=scale` に変換し、プレビューでズームイン・ズームアウトを再現する。
   - テキストの `align` は `n.align` → inline `text-align` の順で解決し、未指定時は左寄せを既定とする（中央寄せ固定を回避）。
   - テキスト色は `#hex` に加えて `rgb()/rgba()/hsl()/hsla()/var()` を許容して反映する。
   - **画像 `width`**: UTAGE の `width` は多くの場合 **ピクセル**（例: 410・350）。`attr_width` またはセクション幅／列幅から **Monaka の `%`（最大100%）** に換算する（旧実装の `410%` 直付けは取り込み崩れの原因）。既存ページは `UtagePageImportService::repairImportedImageWidthsInPageContent` で修復可能。
   - **UTAGE `row`（inner ボックス）**: **2列以上**の row、または画像・フォーム等を含む inner 行は、従来どおり **独立した Monaka セクション／2カラム** として出力する。**1列かつ見出し・テキストのみ**の inner 行（ヒーロー上部のピル型帯＋見出し群など）は **親 `section` にフラット化**し、デスク背景（`section_img_src`）と見出しを **1セクション** にまとめる。行のグラデーション・`width`・`border_radius` は先頭の `subhead` 等へ **`bg_type=gradient`・`block_max_width`** として載せる（プレビューは `_preview_element` の `block_max_width` で中央寄せ）。親 `section` の背景だけの帯は、行分割時に最初の Monaka セクションへ継承する。
   - **狭い row を親セクション幅の中に置く**: 親 `section_width`（例: 800）より狭い `row.width`（例: 600）の 2/3/4 カラムは、出力セクションの `section_width` を親幅にし、**`content_max_width`** に row 幅を入れる（プレビュー／公開で内側を中央寄せ）。UTAGE の「800px 帯の中に 600px 2カラム」を再現する。**親も row も `section_width: 100`（全幅）のときは `content_max_width` を付けない**（誤って 960px に狭めるデグレを防止。LP の `design: inner` 白ボックス等）。
   - **inner row の枠線**: UTAGE `row` の `border_width` / `border_color` は独立セクション化時にセクション枠線へ反映する。
   - **`type=form`**: UTAGE の **画像ボタン（`img_src`）** はそのまま **画像ブロック** として表示し、その直下に **登録フォーム** 案内（`form_register`・`import_utage_form_compact`）を置く。シナリオ連携・送信処理は Monaka 側で設定が必要。件数は `import_diagnostics.placeholder_types.form`。
   - **見出しの二重**: UTAGE JSON に同一 `head` が連続する場合がある。取り込み時に **隣接する同一 headline/subhead を1つにまとめる**。
   - **2カラム比率**: UTAGE の **`col_width_1`（％）** が row にある場合は **左列％＋残りを右列**（例: `23` → `23,77`）として `col_ratios` に反映。列幅未指定の row は、各列内の **最大画像幅** から推定する。
   - **ヘッダーロゴ（`format: top`）**: UTAGE の先頭セクション `format=top`・行 `width`（例: 1000）・画像 `width`（ピクセル、例: 150）は、Monaka で **`section.format=top`（画面上部固定）**・**`content_max_width`**（行幅）・画像 **`max-width: 150px` 等の px 固定**（% にすると画面幅に対して巨大化する）として取り込む。
   - **特商法テンプレート型（ラベル｜本文の繰り返し）**: 同一 UTAGE セクション内の **区切り線だけの inner 行**は独立セクションにせず、**直後の 2 カラムセクションの `section_lead_elements`**（全幅の区切り線）へ載せる。見出し＋区切りの inner 行は **白背景・`content_max_width`（例: 800px）** の通常セクションとして出力する。親セクションの灰背景（`#f8f9fa`）と行の白背景（`#ffffff`）が異なるときは、2 カラム出力で **`bg_color`＝親色・`content_card_bg`＝行色**（プレビュー／公開の `section-block-inner` に白カード）とする。`col.padding_side` は **`col_padding`** として列ブロックに反映。複数 `<p>` の本文は `html_content` を保持し、プレビュー CSS で段落余白を補う。**型ごとの突合せ表**: `docs/utage-monaka-block-mapping.md`。
   - **取り込み時のセクション統合は行わない（2026-06-02 方針）**: `card_blocks` へのまとめ込みは **エディタでブロック選択・移動・保存ができなくなる**ため、取り込みでは **行ごとの section / columns2** のまま出力する。灰＋白の見た目は **`bg_color` + `content_card_bg` + `content_max_width`** をセクション単位で付与して再現する。過去に `card_blocks` が残っているページは、エディタ読み込み時 **`expandUtageCompactedSections`** で通常セクションへ展開する（保存で正規化）。プレビューは `card_blocks` があれば `_preview_utage_card_block` で描画可能。
   - **取り込みの限界（要 Monaka 側設定・手直し）**:
     - **`line_btn` / LINE 公式連携**: UTAGE JSON に `line_btn` 型が無いページでは、LINE 用の画像だけが並ぶことがある。**Monaka の LINEブロック＋シナリオ**は自動では付かない。
     - **吹き出し（お客様の声）**: UTAGE は `balloon` ではなく **2カラム row（顔画像＋テキスト）** のことが多い。Monaka は `columns2` として再現するが、吹き出し尾・左右交互レイアウトは **Monaka の吹き出しブロックへ手置き**が必要な場合がある。
     - **ピンク枠の CTA 箱**: `row` の `border_radius` はセクションへ反映するが、枠線色・内側余白の完全一致は保証しない。
     - **100% 見た目一致**: フォント・行間・SP 専用レイアウトはエディタで調整する。
   - **ページ CSS（UTAGE）**: 取得 HTML の `<style>` およびカスタム HTML ブロック内の `<style>` を **`funnel_pages.custom_css`** に取り込む（再取り込み時も上書き）。完全一致の保証はないが、`.cta_section` 等の固定バナー用スタイルの再現を補助する。
   - **ページ背景画像（UTAGE）**: JSON の **`page.background_image_src`** を **`funnel_pages.bg_image_url`**、`page.background_image_style`（例: `cover_fixed`）を **`bg_image_style`**（Monaka は `cover` 等）へ。公開／プレビューでは `body` 固定背景として表示。
   - **セクション背景画像（`section_img_src`）**: UTAGE セクションの装飾画像は、子が `row` 分割で複数セクションになる場合でも **最初に出力する Monaka セクション**の `bg_image` に載せる（`background_image_opacity`・`background_image_style` も継承）。取り込み後は画像ミラーで `/storage/...` に差し替え。
   - **`type=bullet`**: HTML リスト（`text` 内の `<ul><li>`）を Monaka **ボレット** に変換。リッチ HTML は `html_content` に保持し、プレビュー／公開で `bullet-rich-content` として表示する。
   - **セクション背景画像の opacity**: `background_image_opacity` をセクション全体の `opacity` に載せない（子ブロックまで薄くなる不具合を防止）。**背景専用レイヤー**（`section-bg-opacity-layer`）に分離する。実装は `App\Support\FunnelSectionBackgroundStyle` と `preview.blade.php` / `_preview_sections.blade.php` / エディタ `getSectionBackgroundLayerStyle`。
   - **UTAGE ボタンアニメーション**: `animation=shiny` は Monaka の `shine` にマッピング（`scale`・`pulse` も同様）。
4. `register_destination_urls` が **OFF** のとき（UTAGE JSON 経路）: 変換時は外部 **画像表示 URL を一時保持**し、**リンク先のみ**除去。保存直前に **`FunnelPageImportImageMirrorService`** が `http(s)` の画像を **自サーバ `storage`（`media/{user_id}/import/`）へ取得・保存**し、`src`／`bg_image`／`avatar_url`／HTML 内の `img` 等を **`/storage/...` に差し替える**（取得失敗時は元 URL のまま・ログ `funnel.import.image_mirror.*`）。従来の **HTML 断片** 経路でも、取り込み成功後は同サービスで `img` をミラーする（`register_destination_urls` OFF 時の `applyDestinationUrlPolicy` による `img` 除去の前に HTML 断片が組み立てられる場合は、ミラー対象の `img` が無いことがある）。
5. 取り込み時に「取り込み元 URL」バナー用 `html` ブロックは自動挿入しない（カスタムHTML最小化）。
6. **テキスト block の先頭**は短い場合 `headline`（h1）に、2番目の短文は `subhead`（h2）に寄せるヒューリスティックあり（長文は `text` のまま）。

### 5.4 分岐 B: 従来（HTML断片）

- `extractBodyInnerHtml` → `applyDestinationUrlPolicy` → 単一 `html` 要素に格納。

### 5.5 永続化

- `page_content` に JSON 文字列で保存。
- URL 取り込み時は **`funnel_pages.import_source_url`**（最大1000文字・nullable）に取り込み元 URL を保存する。ページ一覧（`funnel/page/index`）のページ名直下に **取込元：…** としてリンク・コピー・新規タブ表示できる（`partials/_public_url_inline`・`linkPrefix`）。URL 取り込み以外で作成したページは null。
- **UTAGE セクション幅**: 取り込み時に UTAGE の `section_width`（例: 700）を Monaka セクションの **`section_width`** に保存する。プレビュー／公開では `max-width`＋中央寄せで再現する。
- **ページ背景色（UTAGE）**: UTAGE JSON の **`page.background_color`** を **`funnel_pages.bg_color`** に保存する。未設定（null）のときは **`#ffffff`**（ページ外側の余白。先頭セクションの色をページ全体に載せない）。取り込み時は **`page_width=100`**・**`page_frame=none`** とし、セクションの `section_width`（例: 700px）だけ中央に帯を出す。プレビュー／公開の `.page-wrap` は **透明**、`body` の `bg_color` が左右の余白色になる。

---

## 6. プレビュー用セクションスタイル（デグレ注意）

`preview.blade.php` と `_preview_sections.blade.php` は**同じロジック**を維持すること。編集キャンバス（`editor.blade.php` の `getSectionCanvasStyles`）も同等の背景・余白・枠線を反映する（セクション選択時の右パネル **背景** で `degree` / `bg_color` / `bg_color_end` / `bg_image` 等を編集可能）。

**レイアウトの考え方（セクションと子ブロック）**: **セクションは必須ではない**（画面上の「背景付きエリア」として使うかは任意）。保存形式上はすべて `page_content.sections[]` の **縦の段** に入り、各段の `elements[]` に子ブロックが並ぶ。**背景なしの入れ物**（`wrapperOnly`）は紫バーを出さない。**背景・2/3/4カラム**は左 **レイアウト** から明示追加（`explicit`）。

**ページエディタの3要件（正本・`editor.blade.php`）**:

| # | 要件 | 実装 |
|---|------|------|
| ① | 編集とプレビューの見た目を合わせる | プレビューに無い帯・枠を出さない。`.canvas-insert-slot` は **flow 上 height:0**（余白を足さない） |
| ② | ブロック間・段と段の間に＋で追加 | **段内**: `.section-insert-slot`（先頭・ブロック間・最後の段のみ末尾）。**段と段の間**: `.page-boundary-slot`（`insertBlockBetweenPageSections`＝新しい段を挟む）。**ページ最末尾**: `#main-drop-zone` |
| ③ | ＋が境界で揺れない | ホバーで **高さ・margin を変えない**。＋は **opacity のみ**。`is-insert-hover`＋leave 160ms。当たり判定 28px |

**ブロック追加の配置（経路）**:

| 操作 | 入る場所 |
|------|----------|
| **段と段の間の＋**（`.page-boundary-slot`） | 上の段の直下に **新しい段**（`insertBlockBetweenPageSections`）。ヘッダー段と画像段のあいだなど |
| ページ末尾 **「ここにブロックを追加（下に新しい段）」** | **常に新しい段**（`appendBlockViaMainDropZone`） |
| 左サイドバー | **選択中 or 最後の段の内側**（`appendBlockIntoTargetSection`） |
| 段内の＋（ホバー）・ツールバー・右パネル | **その段の `elements` 内**（`insertIndex`） |
| レイアウト（モーダル） | 段内＋からは **新しい段として挿入**（`insertLayoutAtPagePosition`）。段間＋からは **間に新段** |

**セクションの背景**はセクション選択時の右パネル **背景**。**子ブロック単体の背景・枠線**は文字の帯用。

**編集キャンバスとプレビューの見た目（案A・2026-05-19）**: セクション本体は公開プレビューと同様 **`section-block` + `getSectionCanvasStyles` のインラインのみ**（エディタ用の余白16px・子要素用の白枠・「▼ 子要素」ラベルは廃止）。子ブロックは `.canvas-element` でラップするが **margin はデータどおり・選択枠は `outline`**（常時の padding/枠線帯は出さない）。2/3/4カラムは **`two-col-wrap` / `three-col-wrap` / `four-col-wrap` + `col-block`**（プレビュー `_preview_sections` と同系）。**ブロック間の挿入（案A・プレビュー一致が目的）**: プレビューに無い隙間を出さないため、`.section-insert-slot`（ブロックあり時）は **flow 上 `height:0`**。＋は **絶対配置のオーバーレイ**（`.section-insert-hit` 24px）のみ。DOM は **ブロック間**と**段末**にだけ置き、**先頭スロットは空段のみ**（2段続きで先頭＋末尾が二重にならない）。**空のセクション／空カラム**のみ常時の「+ ブロックを追加」UI。追加経路は従来どおり **セクション上部ツールバー＋**、右パネル **「このセクションにブロックを追加」**、左サイドバー、D&D、メイン末尾ゾーン。

**セクション内ブロック追加（エディタ）**: 上記スロットまたはツールバー／右パネルから要素一覧を開き、ヘッドライン等を追加する。`data-insert-index` / `data-col-index` で挿入位置を指定。読み込み時は `normalizePageSections()` で `elements` を配列に正規化する（未配列だと追加が無反応になる不具合の防止）。

**セクション内ブロックの選択・編集**: キャンバス上のブロック（選択時のみ紫 `outline` の `.canvas-element`）をクリックすると右パネルが **ヘッドライン／テキスト等** に切り替わる。`renderCanvas` のイベントは **`[data-section-id]` ではなく `.canvas-section[data-section-id]`** に付与する（挿入スロットも同属性を持つため、誤ってスロットだけにリスナーが付くとブロッククリックが効かない）。

**セクションの余白は margin のみ（2026-05-19）**: セクション選択時の右パネルは **外側の余白（margin）**（`margin_top` / `margin_bottom` / `margin_left` / `margin_right`、0〜100px・ブロックと同系）。**セクションに padding は出さない**（旧 `padding` / `padding_top` / `padding_side` は読み込み時 `migrateSectionPaddingToMargin` で margin へ移行し、以降は無視）。プレビュー・公開は `_preview_sections` で同様に `margin-*` のみ。ブロック選択時は従来どおり **margin + padding**（テキスト系の上下 padding は背景帯の太さ用）。

- **`margin_top` / `margin_bottom` / `margin_left` / `margin_right`（セクション）**
  - 段全体の外側間隔。`getSectionCanvasStyles` → `blockOuterMarginStyle` と同等。
- **背景**
  - `degree` + `bg_color` + `bg_color_end` … `linear-gradient(deg, …)`。

  **それ以外で `bg_color` が `linear-gradient(...)` や `url(...)` を含む場合**は `background:…` を出力する。
  `background-color: linear-gradient(...)` は無効のため、**白背景＋白文字で見えない**原因になる。

---

## 7. ページエディタ（ブロック追加 UX）

実装: `editor.blade.php`

- **ファネルは読者状態遷移設計を主軸**とする。`funnels` には `funnel_goal`（目的）・`state_transition_notes`（状態遷移設計メモ）・`label_design_notes`（ラベル/停止条件メモ）を持たせ、ファネル設定画面で `LP閲覧 → 登録 → シナリオ → 予約 → 決済 → 会員サイト開放` のような導線を先に記録できる。
- **ページ種別**: `funnel_pages.page_type` は `lp` / `registration` / `line_registration` / `sales` / `payment` / `upsell` / `downsell` / `thanks` / `webinar` / `event_booking` / `member_invitation`。ページ追加・ページ設定・ページ一覧で表示/保存する。これはページの役割メタデータであり、決済・イベント予約・会員開放などの実処理は各ブロック/後続の専用設定で接続する。
- **ページ設定**: `show_header` / `show_footer` / `font_family` を保存する。`font_family` は編集キャンバスと公開プレビューに反映する。`show_footer=false` は公開プレビュー上のフッターブロック非表示に使う（グローバルヘッダーの実体は未接続）。
- **公開ページ（エディタ上部）**: ヘッダーに **プレビュー** の隣で **公開ページ**（`GET /p/{uid}`・新規タブ）を表示する。表示条件は `FunnelPage::isPubliclyAccessible()`（`status=public`、または **`status=scheduled` かつ `publish_start_at` が現在以前**、かつ **`expire_at` が未設定または未来**）。**未保存の編集内容は反映されない**（最後に **保存** した `page_content` が公開側に出る）。**管理プレビュー**（`/funnel/…/preview`）は保存後に未公開でも開けるが、公開ページは上記条件を満たすときのみ。ページ設定保存後はバッジ（公開中／公開予定／非公開）とボタンの表示を JS で同期する。
- **ボタンのアクション種別**: ボタンブロックは `action_mode`・`url`・`target`・任意の `action_note`（設計メモ・公開非表示）を保存する。新規追加時の既定背景色は **`#06c755`**（LINE ボタンと同系の緑・`App\Support\FunnelPageElementDefaults::BUTTON_BG`）。テーマ「緑」も同色。エディタの選択肢は **URL遷移**・**ページ内スクロール** の2種。旧データの LINE登録／シナリオ登録／決済／イベント予約／会員サイト誘導／ポップアップ は読み込み時に `url` へ正規化。新規追加時の既定は **リンク先URL 空**（プレースホルダ「遷移先のURLを入力してください」）・**別タブ / 別ウィンドウ**（`target=_blank`、未設定の旧ボタンも読み込み時に `_blank`）。**ボタン画像**は `bg_image`（アップロードは `button-bg-image`、画面からクリア可）。**アイコン左右**は Bootstrap Icons からグリッド選択。**アニメーション**は `animation`（`none` / `scale` / `pulse` / `shine` / `scale_up` / `push` / `gradient` / `border_fill` / `arrow_slide` / `glass` / `shake` / `neon`）。スタイルは `partials/_button_animation_styles.blade.php` を editor・preview・公開で共有。ホバー系はプレビュー／公開でホバー時に反映。**ページ内スクロール**（`action_mode=scroll`）: `scroll_target_id` に遷移先ブロックの `id` を保存。各ブロックは追加時に `id`（10文字英数字）を付与し、公開 HTML では `id="putage-block-{id}"` のアンカーとなる。ボタンは `href="#putage-block-{scroll_target_id}"`・`target=_self`。エディタではスクロール種別時にリンク先URL／開き方を非表示し、ブロック選択プルダウンを表示。**URL遷移**は `url` と `target` の `<a href>` を反映。シナリオ登録は **`form_register` ブロック**、その他の導線は各専用ブロック／ページ種別で接続する。
- **ブロック挿入**（セクション内）: `.section-insert-slot` クリック／ホバー時の＋、またはツールバー・右パネル → **要素一覧モーダル** → `createDefaultElement(type)` を `insertIndex` 位置へ挿入。空セクションのみ常時表示の追加 UI。
- **要素一覧モーダル**には常に **レイアウト**（セクション／2・3・4カラム）を表示する。セクション内の＋から開いたときは、レイアウトは **当該段の `elements` 内には入れず**、＋の位置（`insertIndex`）に応じて **ページ上の新しい段**として挿入する（先頭＋＝その段の上、末尾＋＝その段の下、中間＋＝段を分割してレイアウト段を挟む）。`insertLayoutAtPagePosition`。
- 取り込み側が `columns3/columns4` を生成する場合があるため、エディタ側でも **2/3/4カラム**を扱えること。`col_ratios` は **パーセント**（合計100）。**新規追加時の既定**: 2カラム `50,50`、**3カラム `35,30,35`**、4カラム `25,25,25,25`。右パネル「レイアウト設定」で列ごとに変更可能。編集キャンバス・公開プレビューは同じ比率で `grid-template-columns` に反映する。
- **ヘッドライン／サブヘッド／テキスト／ボレット**のプロパティパネルでは **表示設定**（表示端末・表示期間）を出さない。代わりに **「設定内容を規定値に設定」** から **既存の同種ブロックにも適用**（同一ページ内・本文・項目テキストは除く）／ **新規の同種ブロックのみ適用**（`page_content.element_style_presets` に保存し、ページ保存で保持）を選べる。その直下に **「設定内容を初期設定に戻す」** があり、**選択中のブロックのみ** 追加時と同じスタイル初期値（色・サイズ・余白・背景・枠線・スマホ文字サイズ等）に戻す（本文・`html_content`・ボレットの `items` は変更しない。ページの「新規のみ」規定値には影響しない）。
- **メイン末尾の追加ゾーン**（`#main-drop-zone`）: ラベル **「ここにブロックを追加（下に新しい段）」**。要素を選ぶと **`appendBlockViaMainDropZone`** で `sections` 末尾に **新しい wrapper 段** を作り、その中に1ブロック置く（上の段の中には入れない）。レイアウト行（セクション／カラム）はモーダルで選べる。
- 左サイドバーの **クリック／ドラッグ** による追加は従来どおり。
- `addElement` 内で `renderCanvas` と `selectElement` が走る。
- **空ページのメインドロップゾーン**（`#main-drop-zone`）は `data-section-id` を持たないため、共通 `setupDropZone()` ではメイン用に分岐する。セクション系を落とした場合はそのままセクション追加、要素系を落とした場合は通常セクションを自動作成して追加する。ここをセクション内ドロップゾーンと同じ処理にすると `addElement(type, undefined, …)` になり、ドラッグ＆ドロップが無反応に見える。
- **全ブロック共通**で、外側余白（margin）/内側余白（padding）はプロパティパネルで **0px〜100px の選択式（px固定）**。
- 余白変更時は `updateElementPreview` で `.canvas-element` と `.el-preview` の style を再適用し、**編集画面上にも即時反映**する。
- リッチテキストのツールバーは、編集中ブロックの上または下の余白側に固定表示し、選択中の文字へ重ねない。ツールバー内のボタン・色・サイズ・フォント操作中は選択範囲を保持し、`selectionchange` で即非表示にしない。
- リッチテキストの色ピッカーは **「文字色」**（`foreColor`）と **「ハイライト」**（`hiliteColor`）のラベル表示で区別する。サイズ指定は `px` に正規化し、既存データの `rem` / `em` は 16px 基準で換算する。
- **登録フォームブロック（`form_register`）× シナリオ連携**（2026-05-19）:
  - 正本はシナリオ側の **登録フォーム・読者項目**（`scenario.workspace.registration-fields` 等）。LP エディタでは **使用するシナリオ**（`scenario_id`）と任意の **登録経路**（`registration_route_token`＝メール用 `?rt=`）のみ `page_content` JSON に保存する。
  - エディタ: プロパティでシナリオ選択。同一シナリオが他ファネルページの `form_register` で既に使われている場合は **`GET funnel.page.scenario-usage-check`** の結果をもとに確認モーダルを表示し、続行を選べば保存可（禁止しない）。
  - 経路一覧: **`GET funnel.scenario.mail-registration-routes`**（`channel_type=email` の登録経路）。
  - 公開・管理プレビュー: `FunnelPageScenarioFormService` がシナリオの公開登録フォーム（メールアカウント設定済・フォーム公開・有効項目あり）を組み立て、`_preview_element` → `_scenario_register_embed` で POST `public.scenario.register.store`（hidden `rt` 任意）。
  - 走査・重複検出: `App\Services\FunnelPageScenarioUsageService`（カラム内ネスト含む）。
- **LINE追加ボタン／LINE QR（`line_btn` / `line_qr`）× シナリオ連携**（2026-05-19）:
  - 手入力の `lin.ee` URL は廃止（旧 `url` のみのデータはシナリオ未設定として警告表示）。`page_content` には **`scenario_id`**（必須）と任意の **`registration_route_token`**（LINE 用 `?rt=`）を保存する。
  - 対象シナリオは **LINEアカウント紐づけ済み**（`line_delivery_account_id` または互換 `account_id` が line/both）。一覧は `FunnelPageLineBlockService::lineScenariosForUser`。
  - エディタ: メール登録フォームと同様にシナリオ選択・他ページ重複時は `funnel.page.scenario-usage-check` の確認モーダル。経路一覧は **`GET funnel.scenario.line-registration-routes`**（`channel_type=line`）。
  - **ボタン**: 公開・プレビューで `GET /line/open/{scenario_uid}`（経路ありは `?rt=`）。`FunnelPageLineBlockService::bundleForElement` → `_preview_element`。
  - **QR**: 経路ありは LINE 登録ページ URL、経路なしは送信用アカウントの **ベーシックID** からの友だち追加 URL（未設定時はプレースホルダ）。画像は QuickChart（`qrImageUrl`）。表示は `partials/_line_qr_embed`（見出し・スキャン手順・「LINEアプリで友だち追加」リンク付き・ブロック全体中央、`el-image` の左寄せ img は使わない）。
  - 走査: `FunnelPageScenarioUsageService`（`line_btn` / `line_qr` を `form_register` と同様に集計）。
  - **同一ページでメール系とLINE系のシナリオが食い違うとき**（2026-05-19）: 登録フォーム／コメント連動（メール）と LINE ブロックで<strong>別シナリオ</strong>を選ぶと確認モーダル（「メール登録と異なる…」「LINE設定と異なる…」）。ブロック追加時は、他方が1件だけなら<strong>同じ `scenario_id` を自動入力</strong>。
- **フッターブロック（`footer`）**（2026-05-19）: 項目ごとに **表示ON/OFF**（`show_privacy_policy` / `show_tokushoho` / `show_company_profile` / `show_copyright`）と URL または Copyright 文言。チェックONのときだけ入力可。公開は `App\Support\FunnelPageFooterConfig` と `partials/_footer_embed`（リンク行＋Copyright・中央配置）。文字サイズ・文字色・Copyright色・区切り線色・背景色・余白を `page_content` に保存。
- **決済フォームブロック（`form_payment`）× 商品プラン**（2026-05-19）:
  - 正本は初期設定の **商品登録**（`setting.products` → 商品・プラン）。LP エディタの `page_content` には **`product_plan_id`** に加え、送信ボタン用の **`button_label`**・**`bg_color`**・**`text_color`**・**`font_size`**・**`align`**・**`font_weight`**・**`border_radius`**・**`padding`**・**`width`** を保存する（通常ボタン／購入ボタンと同系。未設定時は文言 **クレジットカード決済へ進む**・背景 `#0d6efd` 等）。公開時は **`/buy/{planUid}` と同じフォーム**＝`public.partials._product_checkout_form` ＋ `funnel.page.partials._checkout_submit_button`（`App\Support\FunnelCheckoutButtonStyle`）。スタンドアロン購入ページ（`/buy/…`）は従来どおり既定デザイン。
  - **商品未登録**（`products` が0件）のとき: エディタのプロパティとキャンバスに「決済フォームを使用するには **初期設定→商品登録** を実施してください。」を表示。プレビュー／公開でも同趣旨の警告。
  - **プラン選択**: 登録済み商品の全プランをプルダウン表示。埋め込み可能なのは `ProductPlanCheckoutEligibility::isPurchasable` … **active**・**credit**・**telecom**・**one_time**・決済連携 **isConfigured** のみ。未準備プランは選択可だがプレビュー／公開ではフォーム非表示（理由文言）。
  - 公開・管理プレビュー・独自ドメインルート: `FunnelPageProductCheckoutService::bundlesForPublicPage` → `_preview_element` → `_product_checkout_embed`（POST `public.product.checkout.store`）。
  - 走査: `FunnelPageProductCheckoutService::paymentBindingsFromPageContent`。
  - **決済完了後のリダイレクト先**（2026-05-19）: エディタの `form_payment` に **`post_payment_redirect_mode`**（`funnel_page` / `url`）・**`post_payment_funnel_page_id`**（同一ファネル内の**他ページ**・編集中ページは一覧に出さない）・**`post_payment_redirect_url`** を保存。他ページが0件のときはラジオ①を非活性にし「他のページがありません。」を表示。購入 POST 時に hidden で `orders` へ保存し、テレコムへ渡す **`redirect_url`** は `OrderPostPaymentRedirectService` が解決（①＝公開可能な `/p/{pageUid}`、②＝指定 URL、無効時は購入完了標準ページ）。
- **次へ進むブロック（`next_link`）**（2026-05-19）:
  - **`label`**（リンクの文字）・**`next_link_mode`**（`funnel_next`＝ファネル内の**次のページ**／`url`＝**`redirect_url`**）・**`align`**・**`font_size`** を `page_content` に保存。
  - **次のページ**は当該ファネルのページ一覧で **このページの直下**（`sort_order` → `id` の並び）。直下が無いときはエディタの既定モードを **`url`（遷移先を指定）** とし、ラジオ **次のページへ** は非活性＋「ファネル内で次のページを指定してください。」を表示。選択不可時に **次のページへ** を選ぼうとした場合も同文言で **`url` に戻す**。
  - 公開表示: `FunnelPageNextLinkService::resolveForDisplay` → `_next_link`（`funnel_next` 時は次ページが公開可能なら `GET /p/{uid}`）。管理プレビュー（プレビューバーあり）は `funnel.page.preview` へリンク。
- **コメントブロック（`comment_block`）**（2026-05-21、2026-05-19 拡張）:
  - ブロック種別 **`comment_block`**。左パレット・追加モーダルは **インタラクション** グループ。
  - **表示形式**: `layout` = `list` / `balloon` / `card`。**事前コメント**は `page_content` の `comments[]`（名前・本文・アイコンURL・評価・ラベル・表示ON/OFF・投稿日表示文字列）。**訪問者投稿**は `funnel_page_block_comments`（`funnel_page_id` + `block_element_id` + 名前・メール※非公開・本文・**`admin_reply`** / **`admin_replied_at`**）。
  - **入力項目（`form_fields_mode`）**: `name_email_comment`（お名前・メール・コメント）／`name_comment`（お名前・コメントのみ）。正規化は `FunnelPageCommentBlockConfig`。
  - **シナリオ連動**（`name_email_comment` かつ `scenario_link_mode=scenario` かつ `scenario_id`）: 送信時に `FunnelPageCommentScenarioRegistrationService` が当該シナリオへ読者登録（`registration_route` = `public:funnel-comment:{pageUid}:{blockId}`）。重複メールは既存読者を有効化。
  - **送信フォーム**: `show_form`・見出し・補足・送信ボタン色。公開 POST **`POST /p/{uid}/comment/{blockElementId}`**（`public.funnel.page.comment.store`・`throttle:20,1`）。成功時は同一公開 URL のアンカー `#putage-comment-{blockId}` へリダイレクト。
  - **管理者返信**: ファネル **ページ一覧**の **コメント返信**（`funnel.page.comments`）。フォームは **返信者名**・**返信者アイコン**（`media.api.list` / `media.api.upload` で選択。吹き出し・画像ブロックと同系）・**管理者返信本文**を分離。ブロック JSON の `admin_reply_author_name` / `admin_reply_author_avatar_url` が未返信時の初期値。返信画面での変更は当該 `funnel_page_block_comments` 行のみ。公開は `_comment_admin_reply`。
  - 表示: `FunnelPageCommentBlockService` → `funnel.page.partials._comment_block`（事前コメントのあと訪問者コメントを時系列で一覧）。管理プレビューはフォーム送信不可（公開ページで送信）。
  - 旧単体要素 `comment`（1件表示用）はレガシーとして `_preview_element` に残置。新規は **`comment_block`** を使用。
- **購入ボタンブロック（`buy_button`）× 商品プラン**（2026-05-19）:
  - **決済フォームと同系**で **`product_plan_id`** とボタン見た目（**`button_label`**・色・サイズ等。旧 **`label`** は読み取り互換）を `page_content` に保存。**LP 上に申込フォームは出さない**（ボタンのみ）。
  - 公開・プレビュー: `FunnelPageProductCheckoutService::bundlesForPublicPage`（`form_payment` と同じ走査）→ `_preview_element` → **`funnel.page.partials._buy_button_checkout_link`**。リンク先は **`GET /buy/{planUid}`**（`public.product.checkout.show`）を **`target="_blank"`** `rel="noopener"` で開く。
  - プラン未選択・商品未登録・決済未準備時の警告表示は `form_payment` と同趣旨。エディタ右パネルは商品プラン選択＋ボタン見た目（リダイレクト設定はなし）。

---

## 8. 要素種別とプレビュー（概要）

- `createDefaultElement` に存在する `type` が主軸。
- **ヘッドライン／サブヘッド／テキスト／ボレット**
  - `bg_type`（none / solid / gradient）、`bg_color`、`bg_color_end`、`bg_degree` で背景を制御。
  - 背景が **単色** または **グラデーション** のとき、プロパティ **「背景帯の太さ（上下）」** で `padding_top` と `padding_bottom` を同一値に設定し、背景付き要素（`h1` / `h2` / 本文 / ボレット枠）の **内側余白＝帯の高さ** を変えられる（0〜64px のプリセット。初回に背景を付けたときは未設定なら **16px** を提案）。左右の余白はブロック外枠（`.el-preview`）の `padding_left` / `padding_right` のまま。エディタ（`editor.blade.php` の `getTextPaddingStyle`・`blockInnerPaddingStyle`）と公開プレビュー（`_preview_element.blade.php` の `textInnerPadding`）で同等反映する。
  - `border_width`、`border_style`、`border_color`、`border_radius` で枠線を制御。
  - `font_size` は `default` または `8px〜100px` の選択式。
  - **ヘッドライン／サブヘッド／テキスト／ボレット**は `font_size_sp`（スマホの文字サイズ）を持つ。既定は **`same`（PCと同じサイズ）**。`8px〜100px` を指定すると、画面幅 **767px 以下**で各ブロックの本文に `font-size` を上書きする（ヘッドライン `.el-headline`、サブヘッド `.el-subhead`、テキスト `.el-text`、ボレット `.el-bullet li`。エディタは `#putage-typography-sp-font-styles`、公開・管理プレビューは `partials/_text_sp_font_styles.blade.php`）。ブロックラッパーは `data-el-id` で要素を特定する。
  - 色入力は `data-prop` テキスト欄と `data-prop-color` ピッカーを相互同期（`#RGB` / `#RRGGBB` / `rgb(r,g,b)` 対応）。
  - **`font_family`（フォント）** … ページ設定の「全体フォント」と同じ選択肢（ゴシック／游ゴシック／メイリオ／明朝／游明朝）。ブロック右パネルでは **「ページと同じ」**（空＝ページ設定を継承）。正本は `FunnelPageTypographyStyle::resolvedFontFamilyCss` / `pageFontFamilyCss`。ページ「標準」（空）の既定スタックは **ヒラギノ／メイリオ系**（`STANDARD_FONT_STACK`）。エディタキャンバス（`.canvas-wrapper`）もプレビュー `body` と同スタックに揃え、2026-05-26 以前の「キャンバスだけ Noto Sans JP」差異を解消。**吹き出し**・**アコーディオン**（質問ヘッダー＋回答本文）も `font_family` を共有する。
  - **`font_weight`（文字の太さ）・`color`・`line_height`・`align`** は `App\Support\FunnelPageTypographyStyle::inlineTextStyle` を正本とし、エディタ（`typographyInlineStyle`）と公開プレビュー（`_preview_element.blade.php`）の **ブロック外枠インライン style** に出力する。**テキスト**ブロックは 2026-05-26 以前にプレビュー側で `font-weight` が欠落していた不具合を修正済み。**ボレット**も同様に `font_weight` / `line_height` を反映。**サブヘッド**は `tag`（h1/h2/h3）をプレビューでも尊重する。キャンパス用 CSS（`.el-headline` 等）の固定太さ・色は廃止し、右パネル設定を優先する。
  - エディタ描画 (`editor.blade.php`) と公開プレビュー (`_preview_element.blade.php`) で同等反映する。
- **ボレット記号**
  - `bullet_marker` で `check / check_bold / arrow / star / dot` を選択可能。
- **動画・音声・PDF** … `_preview_element` で URL 解決（YouTube / Vimeo / 直リンク等）。空 URL はプレースホルダ。
  - **動画メニュー（`video_menu`）** … メディアグループから追加。`episode_count`（1〜10話）・`published_count`（公開話数）・`videos[]`（`label`・`url`）・`active_index`・`accent_color`・`aspect_ratio`（16:9 / 1:1 / 9:16）・再生オプション（`autoplay` / `mute` / `loop` / `controls`）。上部にプレイヤー、下部に話数タブ（未公開話はロック）。`partials/_video_menu_embed.blade.php`・`App\Support\FunnelPageVideoMenuElement`。旧 `video_chapter` / `items[]` のみのデータは読み込み時に正規化。CTA秒数連動・視聴期限・ラベル分岐は後続。
  - 音声の `autoplay` は公開側のみ反映し、編集キャンバスでは常に自動再生しない。
  - PDF はブロック内 **`iframe`（高さは `height` 既定 640px・`.pdf-wrapper`）** で埋め込み表示。Chrome 等は PDF 直リンクの iframe に「開く」だけの UI になるため、**`GET /pdf/embed-viewer`（`pdf.embed-viewer`・ゲスト可・PDF.js）** 経由で **閲覧中リクエストのホスト** 上の `/storage/` 配下 PDF をキャンバス描画する。**`file` クエリは `/storage/...` のパスのみ**（`APP_URL` や別ホストの絶対 URL を渡さない。旧データの絶対 URL は同一ホストの `/storage/` パスに正規化し、他ホストは 403）。`partials/_pdf_embed.blade.php`・`FunnelPublicMediaUrl::embedViewerUrlFromElement()`・エディタ JS の `getPdfEmbedViewerUrl()` も同趣旨。未設定時のみ黄色プレースホルダ。スマホ等で埋め込みが不安定な場合に備え **「別タブで開く」** をツールバーと脚注に併設。PDF.js 失敗時はビューア内に `<object>` フォールバック。
- **メディア選択モーダル（editor）**
  - `element-image / element-video / element-audio / element-pdf` はクリックでモーダル起動。
    - ただし、**既にURLが設定済みのブロックはクリックしてもモーダルを自動起動しない**（未設定時のみ）。
  - モーダル内タブは `image` / `media(動画・音声)` / `pdf`。一覧表示をタブで切替。
  - 音声アップロードは `m4a` / `mp4` を許可。`media_type=audio` 指定時は `video/mp4` でも audio として保存可能。
  - **アップロード API**: `MediaController@apiUpload`（`POST /api/media/upload`・`media.api.upload`・要ログイン・CSRF はフォームの `_token` または `X-CSRF-TOKEN`）。一覧は **`GET /api/media`**（`media.api.list`）。DB は **`media_files`**（マイグレーション **`2026_05_12_220000_create_media_files_table`**）。**未マイグレートの DB では `MediaFile::insert` が失敗し HTTP 500** となり、画面では「アップロードに失敗」に見える。**デプロイ時に運用または CI が `php artisan migrate` を実行**する（協働開発でサーバシェルが使える場合は **エージェントが実行**し、チャットだけに残さず仕様書／実装ログに記録する）。ファイル実体は `storage/app/public/media/{user_id}/`（`public` ディスク）。**`php artisan storage:link` 未作成**だと返却 URL（`/storage/...`）が **404** になり得るため、**初回デプロイまたはストレージ構成変更時に同様に運用／CI／エージェントが実行**する。**画面利用者向けに SSH 手順の正にしない**（`docs/manual.md`・`.cursor/rules/ai-collaborative-development.mdc` の「サーバで実行可能なデプロイ作業」参照）。**JSON の `url` は `request()->getBasePath()` 付きの `/storage/...` 形式**（`APP_URL` とブラウザのホストが一致しない環境でも一覧サムネイル・選択後の表示が壊れないようにする）。
  - **一覧取得の堅牢化**: `GET /api/media` が JSON 配列以外（HTML・401 本文等）を返した場合でも **`filterMediaItemsByTab` が例外で落ちない**よう配列化する。アップロード成功後は **返却 URL に一致するサムネイルを自動選択**し、「選択」ボタンをすぐ押せるようにする。
  - **編集キャンバス上の画像にリンクがある場合**: プレビュー用 `<a>` の既定遷移を **`onclick` で抑止**（編集画面から意図せず別ページへ飛ばない）。プロパティの **画像 URL 入力**は **`"` を含む URL でも HTML が壊れない**よう属性値をエスケープする。
  - **ページ設定保存**: メディア API が返す `/storage/...` の相対 URL を背景画像・OG画像に入れても保存できる（`bg_image_url` / `og_image_url` は URL 形式に限定しない）。ページ設定の保存失敗時は、フロントで最初のバリデーションエラーをトースト表示し、「公開にしたのにステータスが変わらない」状態を見えやすくする。
  - **モーダルの重なり**: メディアモーダルは **`z-index: 12000`**（ブロック種類選択オーバーレイより手前に来るよう上げる）。
  - **回帰調査メモ（2026-05-09）**: `php artisan test` 全 **40** 件パス（この時点のテストスイート）。メディア API の **`url` が `/storage/...` 相対**のとき、**同一オリジンで `public/storage` が配信される**構成では問題なし。**OG 画像（`og_image_url`）やメール HTML など、クローラ／メールクライアントが絶対 URL を要求する用途**では **`https://…` の絶対 URL** を使う（相対のままでは取り込まれない場合あり）。**`resources/views/funnel/page-editor.blade.php`** は **`FunnelPageController` の `edit` が参照しない**（編集画面の正本は **`funnel/page/editor.blade.php`**）。二重管理・取り違えに注意。
- **カウントダウン** … `bg_color`（エリア）、`text_color`（ラベル等）、`timer_bg_color` / `timer_text_color`（数字ブロック）。
- **アコーディオン** … `items[]` の `q`（質問）・`a`（回答 HTML）。**質問（ヘッダー）**: `header_bg_color`・`header_color`・`header_font_size`・`header_font_weight`（既定は背景 `#f8f8f8`・文字 `#222222`・1rem・600）。**回答（本文）**: テキストブロックと同系の `font_size`・`font_weight`・`line_height`・`color`・`align`（既定 `#444444`・1rem・400・1.8・左）。**フォント**はブロックの `font_family`（空ならページ全体フォント）をヘッダー・本文の両方に適用。旧データは読み込み時に `ensureAccordionStyleDefaults` で補完。エディタ・管理プレビュー・公開は `_preview_element` のインライン style で反映。
- **吹き出し（`balloon`）** … `html_content` / `text`・`avatar_url`・`name`・`bg_color`・`text_color`・`font_size`・`font_family`（空ならページ全体フォント）。**アバターサイズ**は `avatar_size_pt`（10〜200・10pt刻み・既定 **50pt**）。旧 `avatar_size`（`50px` 等）は読み込み時に pt へ丸めて補完。**吹き出し位置**は `direction`（`left`＝相手側・頭像左、`right`＝自分側・LINE の自分の吹き出し・頭像右）。本文は `width: fit-content` で不要な折り返しを抑え、長文のみ折り返す。尾（三角形）は `--balloon-bg` で背景色と連動。エディタ・`preview.blade.php`・`_preview_element` で共通。
- **表（`table`）** … 縦横のグリッド。**構成**: `header_rows`（0〜5・上段ヘッダ行数）・`header_cols`（0〜5・左列ヘッダ・0はなし）・`body_rows`（明細行・1〜30）・`body_cols`（明細列・1〜12）。セル本文は `cells` 二次元配列（行×列）。旧 `headers` / `rows` のみのデータは読み込み時に `cells` へ移行。ヘッダセルは行インデックス &lt; `header_rows` または列インデックス &lt; `header_cols` のとき `<th>`、それ以外 `<td>`。**列幅・行高**: `col_widths`（px・列ごと）・`row_heights`（px・行ごと）。未設定時は均等列幅。エディタでは表ブロック選択時に (1) セル**右端**で列幅・**下端**で行高（隣列とのやりくり）、(2) 表外枠の**右端**で全体横幅・**下端**で全体高さをドラッグ（各列・各行を比率で一括スケール、最小列48px・行28px）。**色**: `head_bg_color` / `head_text_color` / `body_bg_color` / `body_text_color`。**枠線・文字**: `border_width` / `border_color` / `border_style`・`font_size`・`font_size_sp`・`line_height`・`cell_pad_x_pc` / `cell_pad_x_sp` / `cell_pad_y`（いずれも `default` 可）。新規既定はヘッダ1行×列3・明細3行×3列（列1〜3）。エディタはキャンバス上でセル直接編集。プレビュー・公開は `App\Support\FunnelPageTableElement` と `partials/_table_element.blade.php`。
  - **回帰メモ（2026-05-19）**: `page_content` に **表ブロック**を含むページで、エディタだけ空キャンバスになることがあった。原因は `migratePageElements()` が **`TABLE_DIM_LIMITS` 等の `const` 定義より前**に実行され、JS が `ReferenceError` で止まり **`renderCanvas()` 未実行**（DB の `page_content` は消えていない。プレビューは PHP 描画のため表示される）。**対応**: 初回 `migratePageElements()` は **初期化ブロック**（`renderCanvas()` の直前）のみで呼ぶ。
  - **エディタのセル入力（2026-05-19）**: 列／行リサイズ用ハンドルを `contenteditable` の `<th>`/`<td>` 内に置いていたため、入力領域が潰れ文字が縦一列に見える不具合があった。**対応**: セルは `<th|td class="table-cell-wrap">` + 内側 `<div class="table-cell-editable" contenteditable>`。ハンドルはラップ要素上に `position:absolute` で配置。保存は内側 `div` から改行を `\n` に正規化して `cells` へ（Enter / `<br>` 対応）。プレビュー・公開は `FunnelPageTableElement::cellDisplayHtml`（`nl2br(e(...))`）。

---

## 9. フラッシュメッセージ

- **成功/エラー表示は `layouts/app.blade.php` のみ**（`session('success')`, `session('error')`）。
- `funnel/page/index.blade.php`、`funnel/index.blade.php` 側の重複アラートは削除済み。

---

## 9.1 管理画面ナビゲーション（共通レイアウト）

- **独自ドメイン管理（初期設定）**: `GET /domain`（`setting.domain`）・`POST /domain`（`setting.domain.store`・**成功時は直後に `setting.domain.show` へリダイレクト**）・`GET /domain/{domain}`（`setting.domain.show`）・`POST /domain/{domain}/dns-check`（`setting.domain.dns-check`・**`throttle:30,1`**。`App\Services\SiteCustomDomainDnsVerifyService` が公開 DNS で CNAME（または CNAME フラット化時は接続先と同一の IPv4）を照会し、**成功時のみ** `custom_domains.status=active` と `dns_last_checked_at` / `dns_last_check_ok` / `dns_last_check_result` を更新）・`PUT /domain/{domain}`（`setting.domain.update`・**ドメインのトップに表示するファネルページ** `custom_domains.root_funnel_page_id`・**ファビコン**は **`favicon_url` への手入力ではなく** `multipart` の **`favicon_file`**（extensions: png/jpg/jpeg/gif/webp/ico・最大 512KB）を **`storage/app/public/custom-domain-favicons/{user_id}/{id}.{ext}`** に保存し **`favicon_url` に `asset('storage/…')` 相当の公開 URL** を書く・任意 **`favicon_remove`** で削除）・`POST /domain/{domain}/publish-root-page`（`setting.domain.publish-root-page`・トップに指定済みの自分のファネルページを **`status=public`** にし、`publish_start_at` をクリア）・`DELETE /domain/{domain}`（`setting.domain.destroy`・**アップロード済みファビコンは上記パスならファイルも削除**）。一覧は **`custom_domains` のうち `target_type` が `funnel` または `site` のみ**（**`mail` は送信ドメイン認証** `account.dkim.*` のみで混在させない）。**ルートドメイン不可・サブドメイン必須**（実装は `App\Support\SiteCustomDomainDnsHelp::isSubdomainHost`／ラベル3分割以上の近似。`.co.jp` 等の例外は DNS 業者の画面に合わせて補足）。CNAME の接続先表示は **`config('putage.site_custom_domain_cname_target')`**（`.env` の **`PUTAGE_SITE_CUSTOM_DOMAIN_CNAME_TARGET`**、既定 `tools.monaka-app.com`）。ステータス表示は DB の `status` を **「利用可能」（`active`）／「DNS設定反映待ち」（`pending`）** 等にマッピング。ビューは `setting/domain.blade.php`・`setting/domain-show.blade.php`。**ゲスト向けの `/`（独自ホスト）**: `GET /` は **`PublicCustomDomainRootController@home`** が先に評価され、リクエストの `Host` が `custom_domains.domain` と一致し `status=active` かつ `root_funnel_page_id` が指す `funnel_pages` が同一ユーザーの **公開（`status=public`）** ページであれば、`funnel.page.preview` を **プレビューバーなし**でレンダリング（`publicCustomDomainLanding`）。未 active／ページ未設定／非公開ページはそれぞれ `resources/views/public/custom-domain-*.blade.php` で案内。マイグレーション `2026_05_09_220000_add_root_page_favicon_to_custom_domains`。
- **独自ドメイン一覧 UI と「ファイル一覧」トラブル**: `setting/domain.blade.php` の一覧は **`funnel/page/index.blade.php` と同系**（`card` + `table-responsive` + `table table-hover align-middle` + `thead.table-light`、操作は **`btn-group-sm`**＋歯車・外部リンク・削除のアイコン）。**`status=active` かつ `root_funnel_page_id` が空**のドメインがあるとき、一覧上に **警告アラート**（トップページ必須・切り分け文）を表示する。ゲストが `https://サブドメイン/` を開いて **Monaka の案内 HTML**（`PublicCustomDomainRootController` → `public/custom-domain-*.blade.php`）が返ればリクエストはアプリに届いている。**ディレクトリ一覧だけ**、または **別サイトの HTML** が返る場合は、そのホストの **Apache vhost / ServerAlias / SSL vhost の DocumentRoot が Laravel の `public` になっていない**か、DNS が **別サーバ**を向いている可能性が高い（アプリ外の設定切り分け）。HTTPS で使う独自ドメインは、DNS だけでなく **当該ホスト名の証明書** と `*:443` vhost も必要。DNS 確認成功時のフラッシュは **トップページを選んで保存する**旨を含める（`SettingController@verifySiteDomainDns`）。
- **オペレーター（初期設定）**: テーブル **`operators`**（`2026_05_12_130000_*`）。ログイン共通口は **`operator_identities`**（メール一意・パスワード。`2026_05_19_160000_*`）で、契約ごとの **`operators.operator_identity_id`** に紐づく。**同じメール・パスワードで複数契約（会社）のオペレーターになれる**（2社目以降の追加時は既存パスワード必須）。ログインは契約者と同じ `/login`。複数契約があるときは **`login.operator.choose`** で会社を選択。ログイン中の切り替えは **`POST operator.switch-company`**（画面上部の「契約」プルダウン）。新規追加は `users` 行を作らず identity のみ（旧 `login_user_id` 行はマイグレーションで identity へバックフィル・互換ログイン可）。**`login_user_id` が空かつ identity 未連携の旧行は再追加**。`GET setting.operator` で一覧・追加・**編集**（`PUT setting.operator.update`）。**アクセス権限**は `OperatorPermissionCatalog`（**`module-nav` 同名**）。**チェック OFF の機能は操作不可**（`operator.module`＋ナビ非表示）。オペレーター管理は契約者のみ（`ensureOwnerAccount`）。
- **通知設定（初期設定）**: `GET /notification`（`setting.notification`）・`PUT setting.notification.update`・`POST setting.notification.test`（JSON 可・`SettingNotificationController`）。**`operator_notification_chatwork_accounts`**（`user_id`・任意 `name`・`api_token` encrypted・`room_ids` JSON・最大 **5 アカウント**・各 **5 ルーム ID**）。画面に通知種別の一括トグルはない（各機能で随時設定）。テスト送信は親フォームの `_method=PUT` と分離し **fetch POST**（`test_room_id` に選択枠の入力値を直送・`room_ids[0..4]` は枠位置を保持）。DB の `room_ids` も 5 枠保存（旧詰め配列は読込時に先頭枠へ移行）。各枠に **削除（×）** でクリア。ルーム ID は `rid12345678` 形式でも可で API 送信時は数値 ID に変換。プルダウンは入力済み枠のみ（ラベルに ID 抜粋）。失敗時は HTTP 応答を **APIトークン**／**ルーム ID** 向け文言に変換（`ChatworkApiErrorMessage`）しテスト送信欄下に表示。ルーム起因の失敗は当該枠下に **＜エラー＞** を表示・`room_slot_labels` に保存。成功時は `GET /rooms/{id}` で取得した **ルーム名** を当該枠下と `room_slot_labels`（JSON・5枠）に保存（ルーム ID 変更で当該枠ラベルはクリア）。他画面のルーム選択 UI は未接続（保存データを参照可能）。送信は **`OperatorChatworkNotifyService`**。初期設定ハブに **通知設定** カード。
- **決済連携（初期設定）**: テーブル **`payment_gateway_accounts`** はマイグレーション `2026_05_13_235900_create_payment_gateway_accounts_table`。`GET /payment`（`setting.payment`）で、**テレコム**・**UnivaPay（新システム）**・**UnivaPay（旧システム）**の接続情報をユーザー単位で複数登録する。フォーム項目は **決済代行ごとに分岐**（UTAGE 連携設定に合わせる）。**テレコム**は `merchant_id` に **クライアントIP** のみ必須。保存時に `credentials.webhook_token` を自動発行（`PaymentGatewayAccount::ensureWebhookToken()`）。編集画面の **テレコムクレジットで設定する項目**（`setting/partials/telecom-registration-fields.blade.php`）に、**送信元IP**（`config('putage.sending_ip')`・`.env` の **`MONAKA_SENDING_IP`** または **`PUTAGE_SENDING_IP`**）、**決済データ送信先URL**（`POST|GET /api/telecomcredit/webhook/{token}`・`public.telecomcredit.webhook`）、**退会データ送信先URL**（`…/cancel`・`public.telecomcredit.webhook.cancel`）を読み取り専用＋コピーで表示。**UnivaPay（旧）**は **店舗ID**（`merchant_id`）のみ必須。**UnivaPay（新）**は **店舗ID** に加え、`credentials` に **本番・テスト各々のアプリトークンとシークレット**（`live_app_token` / `live_secret` / `test_app_token` / `test_secret`）を encrypted cast で保存し、画面には再表示しない（編集時は空欄なら既存値維持）。旧形式の `api_key` / `api_secret` は新システムの **本番トークン・シークレット** として読み替え互換。`endpoint_url` および汎用 Webhook シークレット入力欄は使わない。`PaymentGatewayAccount::isConfigured()` はプロバイダ別に上記必須項目の充足を判定する。初期設定ハブの **決済連携** カードは、少なくとも1件の **有効かつ isConfigured** の連携があれば「完了」。同マイグレーションで `product_plans.payment_gateway_account_id` と `orders.payment_gateway_account_id` も追加する。`product.show` のプラン一覧から **専用画面**（モーダルは使わない）で追加・編集する。**有効かつ isConfigured** の決済連携のみ選択可能。
- **商品登録（初期設定）**: `GET /products`（`setting.products`）は初期設定ハブの **商品登録** カードから開く商品一覧（`layouts/setup`・一覧は `card` + `table-hover` + `table-light`）。**商品を追加** → `product.create?setup=1` → 作成後 **プラン管理**（`product.show?setup=1`）から **プランを追加**（`GET product/{product}/plan/create`・`product.plan.create`）または行の編集（`GET product/{product}/plan/{plan}/edit`・`product.plan.edit`）で専用フォーム（`product/plan-form.blade.php`）を開く。追加時は価格・決済タイプ（一括 / 分割 / 継続・無料）・決済方法（クレジット / 銀行振込）・決済連携・**購入後に開放するコース**（`after_purchase_grant_target`）・**購入後に登録するシナリオ**（任意）を設定。編集時は決済タイプ・決済方法は作成時固定（表示のみ）で、金額・ステータス・領収書表示名・決済連携・購入後設定を変更可能。保存は `POST product.plan.store` / `PUT product.plan.update`。保存時はコース単体を `SingleCourseBundleService` で内部 `course_bundles` に解決し `after_purchase_bundle_id` に格納。**テレコム入金確定**（`OrderPaymentFulfillmentService` → `OrderPurchaseFulfillmentService`）で会員作成（`membership_site_members`・ランダムパスワード）＋ `member_bundle_grants`（`source=purchase`）およびシナリオ読者登録（購入者メール・氏名でフォーム項目を埋めて `ScenarioRegistrationService`・既存メールはスキップ）。初回ログイン案内メールは未実装。ハブのバッジは **登録商品件数**（色は公開可能なプランがあるかで判定）（クレジットは `payment_gateway_account_id` 必須）。ラベル・領収書・サンクスページ自動化の一部は後続。左メニューの **商品管理**（`product.index`）は従来どおり通常レイアウト。
- **テレコムクレジット単発決済（公開）**: 対象は **`product_plans`** が `status=active`・`payment_method=credit`・`payment_gateway=telecom`・紐づく `payment_gateway_account` が **有効かつ isConfigured** の **単発（`payment_type=one_time`）** のみ（継続・分割は未接続）。**購入フォーム**: `GET /buy/{planUid}`（`public.product.checkout.show`）・`POST /buy/{planUid}`（`public.product.checkout.store`）で氏名・メール（任意で電話）を受け、`orders` を `status=pending` で作成後、`App\Services\TelecomCredit\TelecomCreditCheckoutService` が **`config('putage.telecom_credit_order_url')`**（既定 `https://secure.telecomcredit.co.jp/inetcredit/secure/order.pl`）へ **`clientip`・`money`・`sendid`（注文 `uid`）・`usrmail`・`username`・`redirect_url`（サンクス）** を付与してリダイレクト。**入金確定**: `PublicTelecomCreditWebhookController` → `TelecomCreditWebhookService::handlePaymentNotification`。URL パスの `{webhookToken}` で連携アカウントを特定。`rel=yes`（または `1`/`ok`/`success`）で成功、`sendid` で注文を引き当て、`OrderPaymentFulfillmentService::markPaid` で `orders.status=paid`・`paid_at` を更新。**失敗時**は `status=failed`。**退会通知**: `…/cancel` で `paid` 注文を `cancelled` に更新（継続課金の本格連携は未実装）。**サンクス**: `GET /buy/thanks/{order}`（`public.product.checkout.thanks`）。**決済後アクション**（会員サイトコースの結合の付与・シナリオ登録・ラベル・領収書・管理者通知）は `OrderPaymentFulfillmentService` がログのみ（`after_purchase_*` 未接続）。**商品管理画面**で公開中テレコムプランには **購入URL**（`/buy/{planUid}`）を表示。UnivaPay・Stripe・銀行振込・無料商品のチェックアウトは未実装。
- **メール送信用ドメイン（初期設定）**: `GET /setup/mail-domain`（`setting.mail-domain`・`SettingController@mailSendingDomain`）。`custom_domains.target_type=mail` の DKIM（CNAME）・DMARC・SPF（TXT）を登録・**設定確認**する。UI は配信の **送信ドメイン認証**（`account.dkim.index`）と同一の partial（`account/partials/dkim-mail-domain-*`）を `layouts/setup` で表示。ドメイン追加・削除・DNS 確認・送信テストの POST は従来どおり `DeliveryAccountController`（`account.dkim.store` 等）で、フォームに **`from_setup=1`** を付与し成功後は **`setting.mail-domain`** へ戻る。初期設定ハブのカード順は **会社情報 → メール送信用ドメイン → メール設定 → LINE設定 → ドメイン**（公開 URL 用ドメインはメール系のあと）。左メニュー `setup-sidebar-nav` も **ドメイン** と **メール設定** の間に同項目を置く。**公開 URL 用の独自ドメイン**（`setting.domain`・`target_type` が `funnel` / `site`）とは別管理。
- **会社情報・法務（複数会社）**: `business_settings` は **1ユーザー複数行**を正とする（`2026_05_13_111000_allow_multiple_business_settings` で `user_id` unique を通常 index に変更し、`is_default` を追加）。`GET /business` で会社情報を追加・編集・削除でき、既定会社を1件選べる。最後の1件は削除不可。削除時、紐づく `funnels.business_setting_id` / `scenarios.business_setting_id` は同ユーザーの残存会社へ付け替える。`funnels` と `scenarios` は任意の `business_setting_id` を持ち、作成・編集時に **使用する会社** を選択する。既存データはマイグレーション時にユーザーの既定会社へ可能な範囲でバックフィルする。領収書・特商法・差出人表示など会社情報を使う将来機能は、この紐づけを参照する。**UI は送信用アカウント一覧と同じパターン**（操作バーの **追加 / グループ管理**、`business_setting_groups` による **グループ別一覧（未分類含む）**、行内のグループ選択・編集・削除）を正とする。`2026_05_13_112500_add_business_setting_groups` で `business_setting_groups` と `business_settings.group_id` / `sort_order` を追加する。グループ削除時は所属会社を未分類へ戻す。
- `layouts/app.blade.php` はメイン列の **最上段に機能ナビ（`module-nav`）**、その直下に **ページタイトル行（`topbar`）** を表示し、両方を `main-header` でまとめて sticky にしている。**上部メニューで選んだ機能**（`setting.*`・`funnel.*`・`scenario.*` 等）では、`topbar` に **機能名** と **ツールチップと同じ説明文**（本文のみ・常時表示）を出す。子画面で `page-title` が機能名と異なるときは、その下に **画面名**（例: 会社情報・法務）を1行追加する。`setting.setup` の本文先頭にあった大見出し・説明・全体進捗バッジの重複ブロックは廃止する（各設定カード右上のバッジは **件数表示**（例: `3件登録済み`・`2件設定済み`）が基本。**決済連携**のみ従来どおり **完了／未設定／要確認**）。主要機能へ横断遷移できる。**`module-nav` の左端に「初期設定」**（`bi-house-gear`・`route('setting.setup')`・`setting.*` でアクティブ）を置き、続けて **ファネル構築・シナリオ作成・会員サイト・イベント・予約・パートナー・契約書** 等を並べる。**ファネル構築 / シナリオ作成 / 会員サイト / イベント・予約 / パートナー / 契約書** には、それぞれの役割を説明する Bootstrap tooltip（`module-nav-tooltip`・HTML 改行あり）を付ける。**シナリオ作成** は `route('scenario.global.index')` への導線で、送信用アカウントを先に選ばずにシナリオを作りメール/LINEアカウントを紐づける入口とする（`scenario.*` / `subscriber.*` / `step.*` でアクティブ）。**メール・LINE配信（送信用アカウント一覧）は `module-nav` に置かない**。送信用アカウント一覧への入口は **左サイドバー「配信」→「メール・LINE配信」** および **`setting.setup` のメール設定／LINE設定カード** など。**初期設定の正本入口**は `GET /setup`（`setting.setup`・`SettingController@setup`）で、カード型のハブから **会社情報・法務**・**メール送信用ドメイン**・**メール設定**・**LINE設定**・**ドメイン**・**オペレーター管理**（ほか決済連携・商品登録）へ遷移する。**初期設定フロー**（`setting.*`・`account.index|create|edit` かつ `setup_channel=mail|line`・`product.*` かつ `setup=1`）の左サイドバーは **`setting/partials/setup-sidebar-nav`** で **初期設定トップ・会社情報・法務・メール送信用ドメイン・メール設定・LINE設定・ドメイン・決済連携・商品登録・オペレーター管理** を並べ、その下に **サポート**（プレースホルダ）のみ。ファネル構築・配信・会員サイト等は出さない。上部 `module-nav` の **初期設定** は上記フロー全体でアクティブ。（会社・法務は `setting.business`、ドメインは `setting.domain`、オペレーターは `setting.operator`。**メール設定**／**LINE設定**は UI 上の区分であり、データは **`delivery_accounts` の複数行**（従来の送信用アカウント）をそのまま利用する。メール設定カードの状態は非アーカイブの `type` が `email` または `both` の行について `from_name`・`from_email` の充足で判定、LINE 設定カードは `line` または `both` について `line_channel_id`・`line_channel_secret` の充足で判定。ハブ先頭の **完了／設定中** バッジは **会社情報・ドメイン・メール設定・LINE設定** の4カードがすべて「完了」のときのみ「完了」）。一覧への導線は **`account.index?setup_channel=mail`** / **`account.index?setup_channel=line`**。子画面は **`layouts/setup.blade.php`** で共通（**左列のサブナビは置かない**。ハブ以外では本文先頭に **`setup-back-bar`**（ボタン風の **「初期設定トップに戻る」** ＋短い案内文）を表示）。メール／LINE の一覧・追加・各種設定も **`setup_channel` 指定時は `layouts.setup` を継承**し、一覧カードは **`setup-form-card`** でハブと同系の見た目に揃える。左サイドバー全体メニューの「設定」も **`setting.setup`** を指す。ドメイン追加は `POST /domain`（`setting.domain.store`）、削除は `DELETE /domain/{domain}`（`setting.domain.destroy`）。オペレーターは `POST /operator`・`PUT /operator/{operator}`・`DELETE /operator/{operator}`。**`business_settings`** はマイグレーション `2026_05_09_120000_create_business_settings_table`（既存環境では `hasTable` でスキップ）で用意する。ファネル入口の表示ラベルは **`module-nav` と左サイドバー（ダッシュボード用フォールバック）で揃えて「ファネル構築」**（リンク先・ルートは従来どおり `funnel.*`）。
- **見た目（共通シェル）**: 左サイドバーは明るい背景＋濃い文字、アクセントは黄色系（アクティブナビ・プライマリボタン等）。メインは淡いグレー背景。`@section('page-subtitle')` がある画面では、**ページタイトル（`page-title`）にホバー／キーボードフォーカス**すると Bootstrap **ツールチップ**（`data-bs-html="true"`）で説明を表示する（見出しは既定「【この画面でできること】」＋本文。見出し差し替えは `@section('page-subtitle-heading', '…')`）。本文は `strip_tags` 後に **全角「。」の直後に `<br>`** を挿入し、ツールチップ用 HTML を組み立てる。`data-bs-title` には **Blade の `{{ }} を使わず** `htmlspecialchars(..., ENT_QUOTES)` のみを出力する（`{{ }}` の `e()` と重なると二重エスケープされ、タグがそのまま見える）（`page-subtitle` 内の HTML 装飾はツールチップでは `strip_tags` で落ちる）。
- 左サイドバーはルート文脈で表示内容を切り替える。**スクロール**は `.sidebar` を `height:100vh`・`.sidebar-scroll` を `flex:1`＋`min-height:0`＋`overflow-y:auto` とし、メニューが長い初期設定・ファネル構築でも下端まで辿れる。**会員サイト文脈**（`site.*`・`$isSiteContext`）では、サイト選択後の左メニュー（**コース / コースの結合 / 受講生管理 / お知らせ管理 / URL管理 / サイト設定 / ページ設定** と子タブ、**サイト一覧**）および **コースワークスペース**（**コース一覧へ戻る / コース設定**）の各リンクに Bootstrap tooltip（`sidebar-nav-tooltip`・`data-bs-placement="right"`・HTML 改行あり）を付ける。文言は `layouts/app.blade.php` の `$siteSidebarTooltips` に集約する。
  - `funnel.*` … **メイン**（ダッシュボード）→ **ファネル構築**（`funnel.index` 等。`funnel` ルートパラメータあり時は **選択中ファネル** 配下）→ **サポート**（`layouts/partials/sidebar-support-nav`・初期設定と同じプレースホルダ）。配信・会員サイト・決済等は出さない。
  - `scenario.global.*` … **シナリオ作成**のみ（メール・LINE配信メニューから切り離す）
  - `account.* / scenario.*（global 以外） / subscriber.* / step.*` … 配信専用メニュー
  - `site.*` … 会員サイト専用メニュー
  - `event.*` … イベント専用メニュー
  - `partner.*` … パートナー専用メニュー
  - `contract.*` … **契約書**専用メニュー（`GET /contract`・`contract.index`）。詳細は **`docs/contract-spec.md`**
  - 上記以外 … 従来の全体メニュー
- 配置統一方針は **タブ → 操作バー → 一覧/フォーム** の順。
- 複数登録系の管理画面は **操作バー（追加・グループ管理）→ グループ別一覧（未分類含む）→ 行内操作** を基本とし、送信用アカウント一覧・ファネル一覧と操作感を揃える。
- `account.mail-test`（POST・`throttle:10,1`）: `account.edit` から、当該送信用アカウントの **送信元（from）** で **テストメール 1 通**を送信。宛先はリクエストの `test_to`。**件名・本文は製品固有名詞を含めない中立表記**（例: 件名「送信テスト（送信経路の確認）」）。**送信経路**: **`use_external_smtp=true` のとき** アカウント保存の外部SMTP、**`false` のとき** Laravel 既定メーラー（`config/mail.php` の `default`・`.env` の `MAIL_*`）。全体オフでプラットフォーム送信も不可なら `PUTAGE_DELIVERY_USE_PLATFORM_MAIL_WHEN_SMTP_BLANK=false` によりエラー。外部SMTPオンでホスト未保存時もエラー。DKIM/DMARC の成否は DNS・送信経路に依存する旨を本文で案内。実装は `App\Services\DeliveryAccountMailService`。
- **`account.line-settings-check`**（POST・JSON・`throttle:20,1`）: `account.edit`（`setup_channel=line` 含む）の **STEP 3「設定を確認」** から呼ぶ。結果は `delivery_accounts.line_settings_last_checked_at` / `line_settings_last_check_result`（JSON・総合判定含む）/ `line_settings_last_check_ok` に保存し、画面再読み込み後も復元する。OK 時は Messaging API から `line_bot_*`・`line_followers_count`（Insight API）も更新。一覧（LINE 系）は **総合 OK** の行にアイコン・`@` 付き basic ID・友だち数（未取得時は Monaka LINE 読者数）を表示。リクエスト JSON で **Messaging API / LINEログイン** の Channel ID・secret（未保存のフォーム値でも可）を受け取り、`App\Services\DeliveryAccountLineSettingsCheckService` が **LINE Platform API**（`oauth/accessToken`・`bot/info`・Webhook endpoint GET）で接続・Webhook URL 一致・**Webhook 配信（`active: true`）**・チャネル ID の取り違えを検証し、**設定完了 / 未設定** を行ごとに返す（URL だけ一致して `active: false` のときは **NG**。`delivery_accounts.line_webhook_last_error` に案内を保存）。**LINEチャット / LINE友だち** は Webhook 未配信時に赤い案内を表示。STEP 1 保存時の `LineMessagingWebhookService` は PUT で `active: true` を試行するが、LINE 公式管理画面の「応答設定 → Webhook 利用する」は手動確認が必要な場合がある。
- **`account.line-settings.save-messaging`**（PUT・`throttle:20,1`）・**`account.line-settings.save-login`**（PUT・`throttle:20,1`）: `account.edit` の **STEP 1 / STEP 2** それぞれ専用フォームから保存。`DeliveryAccountLineSettingsService::saveMessagingStep` / `saveLoginStep` が該当列のみ更新し、STEP 1 保存時は従来どおり `LineMessagingWebhookService::syncWebhookEndpoint` を実行。STEP 1 には任意の **`delivery_accounts.line_messaging_memo`**（Messaging API メモ・最大2000文字）を保存でき、`setup_channel=line` の一覧行にも表示する。親フォームの `account.update` は LINE 項目を含まないため、LINE 系アカウントでも **アカウント名・アーカイブ** のみ更新（LINE 連携は STEP 保存に分離）。
- **`account.edit` の LINE 連携 UI**（`type` が `line` / `both`）: **STEP 1**＝[LINE公式アカウント管理画面](https://manager.line.biz/) の Messaging API（Channel ID / secret・Webhook URL 自動登録の案内）。**STEP 2**＝[LINE Developers コンソール](https://developers.line.biz/console/) で別チャネルとして作成する **LINEログイン**（Channel ID / secret）。**STEP 3**＝上記 API による設定確認ボタン。
- `account.index` には送信用アカウント用の運用操作を実装（画面表記は「送信用アカウント一覧」。メール・LINEを送る箱の一覧）。`archived` クエリで **通常一覧（`delivery_accounts.is_archived=0`）** / **アーカイブ済一覧（`is_archived=1`）** を切替。**ナビの階層**は **横断入口＝左メニュー「配信」や初期設定のメール／LINEカード**（`module-nav` にはメール・LINE配信を置かない）／**中項目＝トップバーの送信用アカウント｜送信ドメイン認証**／**小項目＝本文の「送信用アカウント一覧｜アーカイブ済」タブ**（`delivery-sub-tabs`。左サイドバーのツリーでも同じ切替ができる）。アーカイブは **各種設定（`account.edit`）** のチェックで更新。**初期設定のメール／LINE 導線**ではクエリ **`setup_channel=mail`**（`type` が `email` または `both` のみ）／**`setup_channel=line`**（`line` または `both` のみ）で一覧を絞り込む（`DeliveryAccountController@index` で `mail` / `line` 以外は無視）。絞り込み中は初期設定カードの配下画面として扱い、**ページ見出し（`page-title`）を「メール設定」／「LINE設定」**とする。**トップバーの中項目タブ**（送信用アカウント／シナリオ／送信ドメイン認証）は表示しないが、本文先頭の **小項目タブ**（送信用アカウント一覧／アーカイブ済）は **表示する**。左サイドバーは **初期設定の全項目メニュー**（上記 `setup-sidebar-nav`）を表示し、配信専用の **シナリオ**・**送信ドメイン認証** は出さない。**表示順変更**も非表示（部分一覧だけの並び替え保存を避ける）。
  - グループ管理（追加 / 名称変更 / 削除）※通常一覧のみ
  - 表示順変更（並び順保存）※通常一覧かつ **setup_channel 未指定時のみ**
  - 行単位のグループ割当変更（未分類含む）
  - `type=email|both` のアカウントは `from_name` / `from_email` を必須バリデーション。**外部SMTP**は `delivery_accounts.use_external_smtp`（boolean・既定 false）で明示。**利用しない**ときは SMTP 列はクリアされ、`MAIL_*` 既定経路で送信（`PUTAGE_DELIVERY_USE_PLATFORM_MAIL_WHEN_SMTP_BLANK` が前提）。**利用する**ときは `smtp_host`・`smtp_port` 必須（ユーザー名・パスワード・暗号化は任意。パスワードは空送信時は既存値を維持）。
  - 一覧で **メール設定未完了**（`type` がメール系で from 未入力）および **LINE設定未完了**（`type` が LINE 系で Channel ID または Channel secret 未入力）のバッジを表示して運用漏れを検知。
  - **`account.create`** は **`setup_channel=mail`** のとき配信タイプ **メールのみ固定**（フォームは `type=email` と `after_setup_channel=mail` を送る）、**`setup_channel=line`** のとき **LINEのみ固定**（`type=line`・`after_setup_channel=line`）。**`account.store`** は **`after_setup_channel`** を任意検証（`mail`/`line`）。LINE のみ作成かつ `after_setup_channel=line` のときは成功後 **`account.index?setup_channel=line`** へリダイレクト（初期設定の流れを継続）。メールのみかつ `after_setup_channel=mail` のときは **`account.index?setup_channel=mail`**。それ以外の LINE 系は従来どおり作成直後 **`scenario.index`** へ。
- 左サイドバー（送信用アカウント文脈・ワークスペース外）の **メール・LINE配信** 見出し直下は **ツリー構造**：**送信用アカウント** を親ラベルとし、その下に **送信用アカウント一覧**（`account.index`・通常）と **アーカイブ済**（`?archived=1`）を子リンクとしてインデント表示する。**送信ドメイン認証**（`account.dkim`）はその兄弟（一覧グループと同じ階層の単独リンク）。SMTP／送信者などのアカウント単位の設定とは別。DNS のドメイン認証。親ラベル「送信用アカウント」の左には、本文上部の中項目タブ（送信用アカウント）と同じ人物アイコン（Bootstrap Icons `bi-person`）を付け、同一区分であることを示す。
- メール・LINE配信では **送信用アカウント | 送信ドメイン認証** を **`layouts/app.blade.php` のトップバー（`topbar`）** に置く（中項目。子ビューは必要な場合のみ `@section('delivery-primary-tabs')`）。本文（`content-area`）では **中項目と同じ行を繰り返さない**。`account.index` では **小項目**として **送信用アカウント一覧｜アーカイブ済** を `delivery-sub-tabs` で表示する（`account.dkim` には一覧向けの小項目タブは無い）。ただし **`setup_channel=mail|line` の初期設定導線**では、トップバーの中項目タブ（送信用アカウント／シナリオ／送信ドメイン認証）のみ非表示とし、**`page-title` はメール設定／LINE設定**、本文先頭の **小項目タブ**（一覧／アーカイブ済）は表示する。左サイドバーは **初期設定メニュー**に切り替わり、配信の **シナリオ**・**送信ドメイン認証** は出さない（`$isSetupAccountContext` で配信メニューに入らない）。上部機能ナビの **シナリオ作成**（`scenario.global.index`）はメール・LINE配信から切り離し、左メニューも **シナリオ作成** の1項目だけにする。画面本体はこの中項目タブを出さず、ファネル一覧と同じ通常ページ見出し **シナリオ作成** と本文先頭の **シナリオ一覧｜アーカイブ済み** タブだけを表示する。**追加** ボタンは、非アーカイブのメール系（`email` / `both`）またはLINE系（`line` / `both`）アカウントが1件以上ある場合だけ有効にする。未作成時はボタンを無効化し、メール設定/LINE設定の追加導線を表示する。直POSTでも同条件を満たさない場合は、先にメール設定またはLINE設定でアカウントを作成するようエラー表示する。送信用アカウント配下の `scenario.index` は従来どおりアカウント文脈の配信画面として扱う。`account.index` / `account.dkim` / `account.create` / `account.edit` では **単独の `page-title` を立てず**（補足はツールチップとⓘ）、`scenario.*` 等では必要に応じて **タブ直下に補助見出し**（`page-title`）を表示する。各タブの **Bootstrap ツールチップ**・`page-subtitle` がある画面ではトップバーに **情報アイコン** を併置する。`[data-bs-toggle="tooltip"]` は `layouts/app.blade.php` で一括初期化する。
  - `account.dkim.index` は **送信ドメイン認証（DKIM・DMARC）** 画面として動作する（迷惑メール対策のため DNS にレコードを載せる。実送信設定は各アカウントの「各種設定」）。
  - 認証ドメインの追加 / 削除（永続化は `custom_domains`。Eloquent のタイムスタンプのため `created_at` と `updated_at` の両列が必要。旧スキーマで `updated_at` が欠けると登録時に SQL エラーになるため、マイグレーション `2026_05_01_180000_add_updated_at_to_custom_domains_table` で補う）。もともと `target_type` は `funnel` / `site`（＋必須の `target_id`）向けのため、送信ドメイン認証は `target_type=mail`・`target_id=0`（ユーザー単位・未紐付け）で保存する（enum 拡張は `2026_05_01_190000_extend_custom_domains_target_type_for_mail`）。**一覧は `target_type=mail` のみ**（設定画面の独自ドメイン `site` と混在させない）。DKIM は **ドメイン追加時にシステムがセレクター（`custom_domains.dkim_selector`）を自動発行**し、CNAME のレコード名は `{selector}._domainkey.{ドメイン}`、値は `{selector}.{署名基盤ホスト名}`（`config('putage.dkim_signing_hostname')`・既定 `dkim.putage.maspis.com`、環境変数 `PUTAGE_DKIM_SIGNING_HOSTNAME` で差し替え）。DMARC の TXT は **`App\Support\MailDmarcSpec`**（`.env` の **`MONAKA_DMARC_POLICY`**・任意 **`MONAKA_DMARC_RUA`**）に基づき **`v=DMARC1; p=…`**（任意で **`rua=mailto:…`**）を画面に表示する（製品既定は **`p=quarantine`**（`config/putage.php` / `MailDmarcSpec`）。弱めるときは `none` を明示し利用者 DNS と揃える）。ホストは `_dmarc.{ドメイン}` を表示し、レジストラでは `_dmarc` のみ入力などの注意を脚注で案内。マイグレーション `2026_05_01_200000_add_dkim_selector_to_custom_domains`。
  - ドメインごとの DNS レコード（DKIM: CNAME, DMARC: TXT）表示
  - **テスト送信**（`account.dkim.mail-test`・POST）は **各認証ドメイン行を展開した内側**に置く。送信経路・件名・本文の方針は `account.mail-test` と同様（中立の送信テスト文言）。ドロップダウンはアカウント名だが、**送信元メール（`from_email`）の @ 以降が当該認証ドメインと一致するか、そのサブドメインであるアカウントに限定**して表示する。リクエストに **`mail_domain_id`（`custom_domains.id`・当ユーザーの `target_type=mail`）** を必須とし、サーバ側でも同一条件で整合を検証する（認証ドメインと無関係なアカウントからの送信を防ぐ）。**「送信しました」は SMTP 等が受け付けたことまで**であり、Gmail 等の受信トレイに必ず入る保証はない（迷惑メール・遅延・受信側ポリシー）。**`MAIL_MAILER=log` / `array`** のときは外部に届かないため、**送信前にブロックしてエラー表示**する（`DeliveryAccountController::platformMailerNonDeliveryMessage`）。成功時は **`account.mail_test.sent`**（`context`=`dkim_dmarc` / `delivery_account_edit`）を `Log::info` に残し、宛先ドメインのみ記録する（個人情報はログに残さない方針に沿う）。
  - **DNS 設定確認**（`account.dkim.dns-check`・POST・JSON・`throttle:30,1`）: `mail_domain_id` で当該 `custom_domains`（自ユーザー・`target_type=mail`）。サーバは `App\Services\MailDomainDnsCheckService` で **`dns_get_record` による公開DNS問い合わせ**（アプリサーバのリゾルバ経由。必要時 `DNS_ALL` で TXT/CNAME を再取得）を行い、DKIM（顧客側 CNAME の指し先と署名基盤ホストの TXT・DB の `dkim_public_txt` と整合）、DMARC（`_dmarc.{ドメイン}` が **`v=DMARC1` かつ `p=` が `putage.dmarc_policy` と一致**、**`MONAKA_DMARC_RUA` 設定時は `rua` も一致**）、SPF（画面案内の `v=spf1` と一致、`include` 利用時は include 先の TXT に `ip4:` を含むこと）を JSON で返す。TXT の集約では PHP が同一 RR に `txt` と `entries` の両方を返す場合があるため **`entries` があればそちらのみ採用**し、SPF の「複数レコード」判定が誤るのを防ぐ。各認証ドメインの折りたたみ内「設定確認」で表の **状態** 列に **設定完了 / 未設定** を出し、**未設定時はその行の直下に検出理由の短文**も表示する。**ICMP の ping や実メールの届き判定ではない**。
    - **永続化**: 「設定確認」を押した時刻（`custom_domains.dns_last_checked_at`）と直近の判定結果（`dns_last_check_result`・`dns_last_check_ok`）を DB に保存し、画面に **最終確認** として表示する。あわせて **この認証ドメインを画面に追加した日時**（`custom_domains.created_at`）を **ドメイン登録** として同じ行に表示し、DNS 反映待ちの経過時間の目安に使えるようにする。DNS反映待ち（10分〜最大24時間）を運用上担保するための目安として使う。**画面の「最終確認」「ドメイン登録」表示は `config('app.timezone')`（`.env` の `APP_TIMEZONE`、既定は `Asia/Tokyo`）で整形する**（以前はアプリ既定が UTC の場合があり表示が日本時間とずれる）。**表の「状態」列も再読込後に維持する**: サーバ側で `dns_last_check_result['items']`（`dkim` / `dmarc` / `spf`）を Blade の初回描画に渡し、保存済みなら **設定完了 / 未設定 / —** と詳細を表示する（AJAX のレスポンスのみに依存しない）。
  - 画面上の案内・追加モーダルは **非エンジニア向けの平易語**（ドメイン取得先の DNS 画面へ表を転記する流れ）で記載し、表の列（タイプ・レコード名・値）は DNS 入力用として技術表記のまま表示する。
  - **DNS 業者側の制約（FAQ）**: 一部レジストラでは CNAME の**値（宛先ホスト）**に対して「当該名前の A／AAAA が存在すること」を登録前チェックにすることがあり、「VALUE に入力いただいた値の A／AAAA レコードが登録されていません」と表示される。値 `{セレクター}.{署名基盤ホスト名}` が**署名基盤側の権威 DNS で公開済みでない**・伝播していない・別ホスト名を指している場合に該当する（インフラ確認）。サブドメイン認証（例: `infomail.example.com`）で親ゾーン（`example.com`）のみ編集できる UI のときは、ホスト名に `_domainkey` だけでなく **`infomail` などサブドメインラベルを含める**必要があることがある（画面と表の FQDN を突き合わせる）。
  - **署名基盤ゾーン（monaka 運用者）**: 顧客の CNAME は `{セレクター}.{署名基盤ホスト名}` を指す。**その FQDN に DKIM 用 TXT**（`v=DKIM1; k=rsa; p=...`）が**署名基盤ホストの権威 DNS**に無いと、レジストラの事前検証に失敗し、受信側も DKIM 検証できない。`custom_domains` に `dkim_private_key`（暗号化）・`dkim_public_txt` を保存し、ドメイン追加（`target_type=mail`）時に `App\Services\DkimKeyService` で自動発行。既存行はマイグレーション `2026_05_07_200000_add_dkim_keys_to_custom_domains` でバックフィル。
    - **推奨（案A）**: `MONAKA_DKIM_SIGNING_HOSTNAME`（例: `dkim.tools.monaka-app.com`）の親ゾーン（例: `tools.monaka-app.com`）を **Cloudflare** に置き、お名前.com 等から **NS でサブドメイン委任**する。`.env` で `MONAKA_CLOUDFLARE_DKIM_AUTO_PUBLISH=true` かつ `MONAKA_CLOUDFLARE_API_TOKEN` / `MONAKA_CLOUDFLARE_ZONE_NAME` を設定すると、利用者が認証ドメインを追加するたびに `App\Services\MonakaCloudflareDkimService` が **公開鍵 TXT を Cloudflare に自動 upsert** する（手作業不要）。手順の正本は `docs/monaka-dkim-cloudflare.md`。
    - **手動フォールバック**: API 未使用・障害時は `php artisan monaka:dkim-signing-zone-export`（`--bind` / `--with-a=`）の出力を権威 DNS に手登録する。
    - **送信サーバ（Postfix + OpenDKIM 等）**: アプリの Laravel からは **SMTP で投げるだけ**であり、**実メールに DKIM-Signature を付けるのは MTA 側**が多い。`php artisan putage:opendkim-sync` で KeyTable / SigningTable と鍵ファイルを同期し、milter 再起動まで運用する（未同期だと **From ドメインに署名が付かず**、Gmail が厳しく扱うことがある）。SPF が `ip4:` のみの場合は、Postfix が未登録の IPv6 で Gmail へ直送しないよう `inet_protocols = ipv4` 等で実送信IPと SPF を一致させる（または運用側 SPF に `ip6:` を明示して同期する）。MTA ログで `no signing table match` や Gmail の `not authorized to send email directly` が出ていないことを確認する。
- `scenario.global.index`（`GET /scenario`）は **シナリオを送信用アカウント一覧から独立して作成・一覧する入口**。トップバーの「シナリオ」タブと左メニュー「シナリオ」から開く。`scenario.global.store`（`POST /scenario`）は、シナリオ名と **メール送信用アカウント**（`scenarios.mail_delivery_account_id`）／**LINEアカウント**（`scenarios.line_delivery_account_id`）を任意選択で受け取り、少なくともどちらか1つが必要。互換のため `scenarios.account_id` は主アカウント（メールがあればメール、なければLINE）として保持し、既存の `/account/{account}/scenario/{scenario}/...` ルートは維持する。独立シナリオ一覧でも **グループ管理** を使える。`scenario_groups` は `2026_05_13_113500_add_user_scope_to_scenario_groups` で `user_id` を持ち、`account_id=null` の行を **独立シナリオ一覧用グループ**、`account_id` ありの行を従来どおり **送信用アカウント配下グループ** とする。独立一覧の追加/編集/削除は `scenario.global.group.*`、行割当は `scenario.global.group.assign`。グループ削除時は所属シナリオを未分類へ戻す。
- `scenario.index`（送信用アカウント配下）にも運用操作を実装。
  - シナリオグループ管理（追加 / 名称変更 / 削除）
  - 表示順変更（並び順保存）
  - 行単位のグループ割当変更（未分類含む）
  - `archived` クエリで通常一覧（`is_archived=0`）/ アーカイブ済一覧（`is_archived=1`）を切替。
  - 一覧行には **メール設定済 / メール未設定**、**LINE設定済 / LINE未設定** のステータスバッジを必ず表示する。設定済みの場合は送信用アカウント名と送信元メール、または LINE 表示名 / Basic ID を併記する。`mail_delivery_account_id` / `line_delivery_account_id` のどちらかが入っている新形式の行では、`null` は明示的な「紐づけない」として扱い、`account_id` から勝手に補完しない（両方 null の旧行のみ互換表示）。
  - 一覧行の読者数は **メール** / **LINE** を分けて表示（`withCount` の `subscribers_mail_count`・`subscribers_line_count`）。メールは `Subscriber::scopeCountableForMail`（実メールあり・`@line.local` 仮メール除外）、LINE は `scopeCountableForLine`（`line_user_id` あり）。未紐づけチャネルは **—** 表示で列幅を揃える。同一読者が両方持つ場合は両方の件数に含まれる。行右端は **グループ選択のみ**（読者一覧・ステップ配信のショートカットボタンは置かない。シナリオ名→概要・左メニューが正）。
- **シナリオと送信用アカウントの関係**: 旧実装は `scenarios.account_id` の1本でメール・LINEを兼ねていた。現在は `mail_delivery_account_id` をステップ配信・メール一斉送信・独自置き換え文字の送信元スコープに使い、`line_delivery_account_id` を LINE登録ページ・LINEチャット・LINE友だち・LINE自動応答・LINE経路意図のスコープに使う。未設定の既存行は互換として `account_id` を参照する。マイグレーション `2026_05_12_223000_add_channel_accounts_to_scenarios_table` は既存 `account_id` の `delivery_accounts.type` に応じてメール/LINE列をバックフィルする。
- シナリオ名クリック時の `scenario.workspace.show` はシナリオ選択後の画面として扱い、上部には **送信用アカウント** タブやシナリオ内の横タブを出さず、ページ見出しと左サイドバーの選択中表示は **シナリオグループ名 / シナリオ名**（未分類なら「未分類」）を表示する。戻り先は独立シナリオ一覧（`scenario.global.index`）。見出しは **メール管理**（読者一覧・読者数推移・メール一斉・ステップ・リマインダ・予約/送信済/メールエラー等。LINE一斉送信は含めない）→ **LINE管理**（チャット・LINE一斉送信・友だち・自動応答等）→ **シナリオ設定**。概要（`overview`）は同じ3区分のカード一覧だが、シナリオ作成/設定で **「紐づけない」** を選んだチャネルは概要カード・左メニューに出さない（メール未紐づけならメール管理なし、LINE未紐づけならLINE管理なし）。両方の新カラムが空の旧行だけは `account_id` の種別で互換表示する。本文上のショートカットは **シナリオ一覧へ戻る** のみ（読者一覧・ステップ配信はメール管理カード／左サイドバーから）。`ScenarioWorkspaceController::workspaceNavLinks` のキーは従来どおり `delivery` / `line` / `settings`。
  - **`reader-trends`（読者数推移）** は実装済み（`stub=false`）。GET `scenario.workspace.show` … slug `reader-trends`。クエリ **`from`** / **`to`**（`Y-m-d`・アプリの `config('app.timezone')` の暦日。省略時は直近30日）。集計は **`ScenarioSubscriberTrendService::dailyRows`**（第4引数省略＝全読者）。
  - **`line-reader-trends`（LINE読者数推移）** は実装済み（`stub=false`）。**LINE管理** 列で `line-chat` の直下。`dailyRows(…, 'line')` により **`Subscriber::scopeCountableForLine`**（`line_user_id` あり）のみ集計。メール読者のみの行は含めない。UI は `_line_reader_trends.blade.php`。
  - **`line-registration-routes`（旧 slug）** … `registration-routes?tab=line` へ **301 相当リダイレクト**（同一画面の LINE タブ）。旧 URL ブックマーク用に slug は残す。
  - **`line-chat`（LINEチャット）** は実装済み（`stub=false`）。UTAGE に近い **左リスト／右トーク** の2ペイン。左は `line_webhook_events`（同一送信用アカウント・`source_type=user`・`source_id`＝LINE userId）の**最終イベントが新しい順**。同一 user の **表示名・アイコン** は Messaging API `GET /v2/bot/profile/{userId}` を `line_user_profiles` にキャッシュ（24時間超で再取得、`LineChannelAccessToken` でトークン解決）。GET `q` で表示名・メール・LINE userId を部分一致検索。右ペインは **`ScenarioLineChatService::threadTimeline`** で、(A) Webhook の **フォロー／メッセージ受信**（中央のシステム文・左吹き出し）、(B) **`line_chat_outbound_messages`**（右吹き出し・公式アイコンは `delivery_accounts.line_bot_picture_url` 等）を時系列合成。送信ログの **`source`** は `messaging_api`（POST `scenario.workspace.line-chat.push` → `LineMessagingPushService::pushText` で Messaging API `POST /v2/bot/message/push` 成功後に `sent_at` 付きで insert）と `external_manual`（POST `scenario.workspace.line-chat.external` … 公式・別ツールで送った文面を**手入力で記録**するのみ。LINE 公式管理画面のみの応答は Webhook に来ないため **自動同期はしない**；辻褄合わせは外部記録で行う。送信対象 userId は **`ScenarioLineChatService::isLineUserIdChatVisible`**（当シナリオの読者 `line_user_id` または同一アカウントの Webhook user イベントがあること）で制限。両 POST は `throttle:30,1`、認可は送信用アカウントの `view`。未読・ピン止めは **未接続**。
  - **`line-friends`（LINE友だち）** は実装済み（`stub=false`）。UTAGE 互換イメージの **表示条件（LINE登録名/ラベル）+ 友だち一覧テーブル** を表示。データは `ScenarioLineFriendsService::listRows` が `line_webhook_events`（最新 `event_type` でフォロー/ブロック判定）、`line_user_profiles`（表示名・アイコン）、`subscribers`（`status`/`registration_route`/`labels`/`registered_at`）を合成して作る。テーブル列は **LINE登録名 / ステータス / ラベル / 登録経路 / 登録日 / 行操作**。ステータスは「フォロー中 or ブロック中」＋「配信対象 or 配信除外」を合成表示。行操作は **チャットを開く**、**ラベル編集（チップUI・モーダル）**、**ラベル追加（追加のみフロー）**、**配信対象/除外切替**（POST `scenario.workspace.line-friends.delivery`）。ラベル保存は POST `scenario.workspace.line-friends.labels`（`label_edit_mode`: `replace`＝全置換 / `append`＝既存 `subscribers.labels` へ追記。重複は大小文字無視で除外。最大20件。モーダルでは**自由入力は不可**で **`scenario_labels` に登録したラベル**をクリックしてのみ追加。既存付与のうち未登録名はチップを赤枠表示し、保存時に `ScenarioLabel::filterToRegistered` により**付与から外れる**（サーバでも同フィルタし改ざん防止）。**読者未登録**（Webhook のみ）の LINE userId でも、`ScenarioLineFriendsController::ensureSubscriberForLineUser` が `subscribers` を **`firstOrCreate`（`registration_route=auto:line-friends`・ダミー `email`）** で用意してから更新する。POST 前に **`ScenarioLineChatService::isLineUserIdChatVisible`** で改ざん防止。`ScenarioLineFriendsService::labelSuggestions` は **`scenario_labels` で登録した名前を先頭**にし、続けて読者 `labels` の頻度をマージしてフィルタ用 datalist・「よく使うラベル」に利用。ラベルチップ色は `crc32(label)` ベースで固定割当（6パレット）し、視認性を上げつつ同一ラベルの色を安定化。CSV出力は準備中。
  - **`label-settings`（ラベル設定）** は実装済み（`stub=false`）。シナリオ単位で運用したいラベル名を **`scenario_labels`**（`scenario_id`・`name`・`sort_order`、`scenario_id`+`name` 一意）に登録する。一覧は `ScenarioLabel::orderedWithSubscriberCounts`（読者 `subscribers.labels` を正規化キーで数えた人数を参考表示）。**追加** POST `scenario.workspace.labels.store`（`name`）、**名前変更** PUT `scenario.workspace.labels.update`、**削除** DELETE `scenario.workspace.labels.destroy`（削除後 `sort_order` を詰め直し）、**表示順** POST `scenario.workspace.labels.move`（`dir`=`up`/`down` で隣と `sort_order` スワップ）。いずれも `throttle:30,1`、認可は送信用アカウントの `view`。登録の削除は **`scenario_labels` の行のみ**であり、既に読者に付いている `labels` の文字列は自動では消えない（画面の削除確認文言で明示）。
  - **`line-notifications`（LINE通知設定）** は実装済み（`stub=false`）。`scenario_extended_settings` に **`line_message_notify_destination`** / **`line_upper_alert_notify_destination`** / **`line_account_ban_notify_destination`**（各 `none`/`chatwork`/`slack`/`discord`）と、通知先ごとの接続情報 JSON（`line_*_notify_config`）を保存する。UI は3ブロック（LINEメッセージ通知、LINE上限アラート通知、LINEアカウントBAN通知）。保存は PUT `scenario.workspace.line-notifications.update`、テストは PUT `scenario.workspace.line-notifications.test`。**LINEアカウントBAN通知**は **`LineAccountSuspendService`** により停止（Suspend）検知時に **`LineScenarioExternalNotifyService`** で自動送信（最大約5分ごとの `putage:line-account-suspend-watch`・Messaging API エラー監視・モジュール Webhook `botSuspended`／`POST /api/webhook/line-module`・`MONAKA_LINE_MODULE_CHANNEL_SECRET`）。`delivery_accounts.line_suspend_*`・`line_bot_user_id` に状態を記録。メッセージ通知・上限アラートは保存とテストのみ（自動送信は後続）。正本: `docs/line-account-ban-detection.md`。
  - **`line-auto-reply`（LINE自動応答）** は実装済み（`stub=false`）。`scenario_line_auto_replies` に、管理名称（`management_name`）・応答タイプ（`response_type`: `keyword`/`broadcast`）・一致（`match_type`: `partial`/`exact`）・キーワード（`keywords_text` 複数行）・動作（`action_type`: `send_message`/`run_action`/`send_template`）・メッセージ（`message_body`）・アクション設定（`action_labels_text`/`action_to_status`）・テンプレ本文（`template_body`）を保存する。設定画面は追加フォーム＋一覧編集で、保存先は POST `scenario.workspace.line-auto-reply.store`、更新は PUT `scenario.workspace.line-auto-reply.update`、削除は DELETE `scenario.workspace.line-auto-reply.destroy`。Webhook（`POST /api/webhook/line/{uid}`）の `message.type=text` 受信時は `ScenarioLineAutoReplyService` が評価し、**キーワード応答（`sort_order`,`id` 昇順）優先**・非一致時に**一律応答**を1件だけ実行する。
    - `send_message`: `message_body` を返信（Messaging API `POST /v2/bot/message/reply`）。
    - `run_action`: 該当シナリオ読者を `line_user_id` で確保（未登録なら `auto:line-auto-reply` で自動作成）し、`action_labels_text` をラベル付与、`action_to_status`（`active`/`inactive`）を反映。
    - `send_template`: 現在は暫定として `template_body` をテキスト返信（専用テンプレート管理画面との本接続は後続）。
  - **シナリオ概要**（`overview`）右カラム「シナリオ設定」の一覧は UTAGE に合わせ、**`form-unsubscribe-settings`（登録・解除フォーム設定）は行として出さない**（設定は左メニュー「登録・解除フォーム」展開下のサブリンク、またはフォーム内タブから遷移。`ScenarioWorkspaceController::workspaceNavLinks` の `settings` に含めない）。
  - `scenario-settings` は実装済み。画面は一般的なステップ配信ツールの「シナリオ設定」に近いセクション構成（基本設定・ステップ配信・メール/LINE 統合・メール設定＋置き換え文字表・配信・通知・アーカイブ）。画面文言に外部プロダクト名は出さない。
    - **`scenarios`**: 管理用シナリオ名（`name`）、メモ（`description`）、**シナリオグループ**（`group_id`・未選択は `null`）、アーカイブ（`is_archived`）。`scenario-settings` のグループ選択肢は **独立シナリオ一覧用**（`scenario_groups.user_id`・`account_id=null`）と **当該送信用アカウント配下**（`account_id`）の両方（`ScenarioGroup::queryForScenarioSettings`）。独立一覧では登録済みグループを **シナリオが0件でもセクション見出しとして表示**する。
    - **`scenario_extended_settings`**（`scenario_id` 一意・初回アクセス/保存時に `ScenarioExtendedSetting::ensureForScenario`）: メール登録時の同一個人判定（`duplicate_identity_on_email`: `default` / `device_plus_email` / `email_only`。旧値 `email_normalized` はマイグレーションで `email_only` に正規化）、**デフォルト URL 置換ドメイン**（`default_url_replace_domain`・ホスト名のみ想定。メール内短縮リンクや `%cancel%` 等のシステム置き換えで出力する URL の既定ホストとして将来利用。現状保存のみ）、LINE 友だち追加後フォーム経由のメール統合（`line_friend_form_email_merge`: `none` / `merge_on_match`）、登録当日の配信が締め切り後の扱い（`step_same_day_after_cutoff`: `skip` / `shift_next_day`。旧値 `deliver` は `shift_next_day` に正規化）、メール既定（送信者名・送信元・件名・ヘッダー/フッター HTML）、List-Unsubscribe ヘッダー（`list_unsubscribe_header_mode`）、通知メールアドレス・通知内容、チャット通知先・内容。
    - **メール/LINEアカウント紐づけ**: シナリオ設定で `mail_delivery_account_id` / `line_delivery_account_id` を変更できる。どちらか一方だけのシナリオも可。ただしメール未紐づけ時はステップ配信・メール一斉送信は概要/左メニューに表示せず、LINE未紐づけ時は LINEチャット・LINE登録ページ等を概要/左メニューに表示しない。
    - **未接続の明示**: ステップ配信の締め切り後挙動・メール既定・統合フラグ・同一個人の端末系オプション等は **保存のみ**（実送信・ステップエンジン・URL 置換・LINE 統合ロジックは別途接続予定）。置き換え文字表の読者項目は **登録フォームで「フォーム利用」オンの項目のみ**表示し、フォームに設置していない項目は候補に出さない。システムタグ（`%cancel%` 等）は参照用に表示。登録済みの **独自置き換え**（`delivery_account_custom_placeholders`）があればサイドに一覧リンク付きで併記。
  - 保存先は `scenario.workspace.settings.update`（PUT）。`is_archived=1` は一覧のアーカイブ済タブへ反映。**シナリオ概要**（`overview`）では **「アーカイブに移す」**（確認チェック付きモーダル）→ `POST scenario.workspace.archive` で同様にアーカイブ可能。アーカイブ済みシナリオを開いたときは画面上部に警告バナーと **アーカイブを解除**（`POST scenario.workspace.unarchive`）を表示。
- `placeholders`（独自置き換え文字）は実装済み（`stub=false`）。UTAGE に近い区分（**管理名称**・**`%original-…%` 形式の ID**・**全シナリオ共通の値**・**シナリオ固有の値** の利用しない／利用する＋上書きテキスト）。
  - **`delivery_account_custom_placeholders`**（`delivery_account_id`・`token_key` はアカウント内一意・`management_name`・`common_value`・`sort_order`）。置き換え表記は `%token_key%`（`DeliveryAccountCustomPlaceholder::displayToken()`）。推奨・既定の手動 ID は **`original-` + 接尾辞**（接尾辞は英小文字・数字・`_`/`-`）。自動採番も `original-` 接頭辞。
  - **`scenario_custom_placeholder_values`**（`scenario_id`・`account_placeholder_id` 一意組・`scenario_mode`: `none` / `override`・`scenario_value`）。シナリオ画面からの保存で `updateOrCreate`。
  - **予約語**: `DeliveryAccountCustomPlaceholder::reservedTokenKeys()` と、**当該送信用アカウント配下のいずれかのシナリオ**の読者項目 `field_key`（小文字比較）と重複する `token_key` は不可。
  - **追加** POST `scenario.workspace.placeholders.store` … `management_name` 必須・`common_value` 任意・`add_scenario_mode` / `add_scenario_value`。`set_token_suffix` オンのとき `token_suffix` 必須（正規表現 `^[a-z0-9][a-z0-9_-]*$`、全体キーは `original-{suffix}`）。オフのときは未使用の `original-` + ランダム。最大 **100 件**／送信用アカウント。
  - **一括更新** PUT `scenario.workspace.placeholders.update` … `rows[{id}][management_name]` / `[common_value]` / `[scenario_mode]` / `[scenario_value]`。`rows` のキー集合が **当該アカウントの全 `delivery_account_custom_placeholders.id`** と一致することを検証。
  - **削除** DELETE `scenario.workspace.placeholders.destroy`（`DeliveryAccountCustomPlaceholder`）… **定義ごと削除**（同一アカウントの全シナリオから消える）。`sort_order` 詰め直し。
  - **テンプレ展開**: 未接続（保存・参照・シナリオ設定サイドの一覧のみ）。
  - **移行**: 旧 `scenario_custom_placeholders` が存在した環境では `2026_04_28_160000_refactor_custom_placeholders_to_account_scope` がアカウント行＋シナリオ値へ移行のうえ旧テーブルを削除する。
- `registration-routes`（登録経路）は実装済み（`stub=false`）。**同じ公開登録フォームに対する入口 URL を分け**、読者の `subscribers.registration_route` に判別用の値を付ける（自動の流入解析ではなく、**どの URL の `?rt=` を踏んだか**で識別）。
  - テーブル **`scenario_registration_routes`**（`scenario_id`・**管理名称** `admin_name`・**種類** `channel_type`：`email` / `line`・**URL 用トークン** `public_token`（作成時に自動採番・シナリオ内一意）・`sort_order`）。
  - **メール**経路: 公開登録 `GET/POST /r/{uid}/register` および `POST /r/{uid}/store`・`POST /api/r/{uid}/register` でクエリ／フォームに **`rt={public_token}`** を付けると、登録完了時に `registration_route = public:rt:{public_token}` を保存（未指定時は従来どおり `public:/r/{uid}/register`）。無効な `rt` は **422**（API）／Web は `rt` 項目にバリデーションエラー。確認フローではセッション `register_pending` に `rt` を保持し、確認画面の hidden と確定 POST で引き継ぐ。
  - **LINE**経路: 管理画面で **`/line/open/{uid}?rt={token}`** をコピーできる（種類が LINE のとき）。`GET /line/open/...` で有効な `rt`（LINE 種別の `scenario_registration_routes`）を踏むと、テーブル **`scenario_line_route_visit_intents`** に来訪が積まれる（同一シナリオ・同一トークンの未消費行は置き換え）。その後 **Messaging API の `follow` イベント**（`LineWebhookController` → `ScenarioLineRouteFollowAttributionService`）で、送信用アカウント単位の**未消費意図がちょうど1件**のときに限り消費し、該当シナリオの読者に `registration_route = auto:line-rt:{public_token}` を**新規作成または上書き**する。未消費が複数あるときは誤割当防止のため**スキップ**（`PUTAGE_LINE_ROUTE_VISIT_FIFO_WHEN_AMBIGUOUS=true` で最古1件を強制割当可能・取り違えリスクあり）。**救済**: 同一シナリオに未消費意図が1件だけの状態で **初回テキスト自動応答が読者を新規作成**するとき（`ScenarioLineAutoReplyService::ensureSubscriberForLineUser`）、同シナリオスコープで意図を1件消費して同じく `auto:line-rt:` を付与できる。有効時間は `config('putage.line_route_visit_intent_ttl_minutes')`（**`PUTAGE_LINE_ROUTE_VISIT_INTENT_TTL_MINUTES`**、既定120分）。
  - ルート: POST `scenario.workspace.registration-routes.store`、PUT `…/registration-routes/{scenarioRegistrationRoute}`、DELETE 同、POST `…/move`。コントローラ `ScenarioRegistrationRouteController`。
  - **ファネル LP との連携（2026-05-19）**: ファネルページの **`form_register` ブロック**が当該シナリオかつ **`registration_route_token`＝経路の `public_token`** のとき、登録経路一覧に **ファネル名・ページ名** を表示する（`FunnelPageScenarioUsageService::funnelUsagesByRegistrationRouteTokens`）。**削除**時は確認ダイアログに使用箇所を列挙し、確定後に該当ページの `page_content` から `registration_route_token` を空にしてから経路行を削除する（**経路なし**＝エディタの「— 経路なし（通常登録） —」相当）。既存読者の `subscribers.registration_route` は変更しない。
  - **管理画面 UI（2026-05-19）**: slug **`registration-routes`** 1画面で **Bootstrap タブ**（**メールの登録経路** / **LINEの登録経路**）。種類のコンボボックスは廃止。各タブに **追加フォーム** と **種別ごとの一覧**（メール: 管理名称・入口 URL・**ファネルでの使用**・表示順。LINE: 上記に加え **ページ表示**・**友だち紐づけ**・**読者数**）。`sort_order` は **channel_type ごと**に独立。保存後の戻りは `active_tab`（`email` / `line`）でタブを維持。メール/LINE 両方紐づけのシナリオでは左メニュー **共通 → 登録経路** のみ（LINE 列の別メニュー「流入経路一覧」は廃止し `?tab=line` へ誘導）。
  - 読者一覧の「登録経路」表示: `public:rt:` / `auto:line-rt:` は **`scenario_registration_routes.admin_name`** を参照して表示（削除後はフォールバック文言）。
- `registration-fields`（登録フォーム・読者項目）は実装済み。
  - テーブル `scenario_reader_form_fields`（`scenario_id` は `scenarios.id` と **int unsigned** 整合）。既定12項目は `ScenarioReaderFormDefaults` でシード（初回アクセスまたはシナリオ作成直後に `ensureForScenario`）。
  - 保存: `scenario.workspace.registration-fields.update`（PUT、`field_order` で並び、`fields[{id}]` で各列）。追加: `scenario.workspace.registration-fields.store`（POST）。削除: `scenario.workspace.registration-fields.destroy`（DELETE、**is_preset=0 のみ**）。
  - カスタム項目追加 POST: **`set_field_key`**（チェック、任意・既定オフ）がオンのときのみ **`field_key`** を必須検証（正規表現・シナリオ内ユニーク・既定キーとの衝突不可）。オフのときは **`field_key` を送らない**（入力は `disabled`）想定で、サーバが **`c_` + 英小文字・数字** の未使用キーを自動採番する。画面説明は「キーは任意・外部連携用」旨を明示。
  - 管理画面では **`data_type` をユーザーに選ばせない**。**`input_type` のみ**編集し、保存時に **`data_type` は `input_type` から自動設定**（`date` のみ `date`、それ以外は `string`）。一覧の「入力形式」は日本語説明付きラベル（`ScenarioReaderFormField::inputTypeOptionLabels()`）。
  - 他シナリオ連携（`sync_other_scenarios`）: チェックは `scenario_reader_form_fields` に保存。**同期**は `ScenarioSubscriberCrossSyncService` … **公開登録**（`ScenarioRegistrationService::createFromValidated`）および **読者手動追加**（`SubscriberController@store`）で新規 `Subscriber` 作成直後に実行。同一 `delivery_accounts` 配下の**別シナリオ**で `LOWER(TRIM(email))` が一致する読者行に対し、**当該シナリオに `field_key`・`maps_to` が一致し `sync_other_scenarios=1` の読者項目があるときのみ**、ソース側で連携オンになっている項目の値を反映（**対称条件**: 送り手・受け手の両方で連携オンかつキー／格納先一致）。**空の値は伝播しない**（他シナリオの既存値を消さない）。メールが `@line.local` 形式のプレースホルダのときはスキップ。読者編集 API や一覧からの一括更新は現状対象外（新規作成時のみ）。
- `form-unsubscribe`（登録・解除フォーム）はメニュー土台を実装。
  - 子メニュー: `form-unsubscribe-register` / `form-unsubscribe-cancel` / `form-unsubscribe-settings` / `form-unsubscribe-tags`。
  - **解除フォーム** は公開 `GET/POST /r/{uid}/unsubscribe` 実装済み（`PublicScenarioUnsubscribeController`）。
  - **登録・解除フォーム設定** … テーブル **`scenario_public_form_settings`**（`scenario_id` 一意）。初回は `ScenarioPublicFormSetting::ensureForScenario` で行作成。保存: `scenario.workspace.public-form-settings.update`（PUT）。管理画面はタブで **登録フォーム / サンクスページ / 重複登録ページ / 解除フォーム（アクション枠）** を編集。フォームタグ用の **`embed_tag_stylesheet_url` / `embed_tag_custom_css`** は別ルート `scenario.workspace.embed-tag-style.update`（PUT）で更新。
    - **公開用シナリオ名**・**重複登録の許可**・**登録確認画面の省略**・登録フォーム前後 **HTML**・**head/body 用 JavaScript 挿入**・**フォーム公開ステータス**（`published` / `draft`）・**確認／登録ボタン文言**。
    - **サンクス**: 登録後 **外部 URL リダイレクト** または **HTML**、専用 **JS 挿入**。未設定時は標準完了ページ `GET /r/{uid}/register/thanks`（セッションガード）。
    - **重複**: 「許可しない」かつ既存メールのとき **外部 URL** または **HTML**、標準は `GET /r/{uid}/register/duplicate`（セッションガード）。Web は **同一メッセージで推測しにくい**運用は重複ページ側。
    - **解除フォーム設定**: `unsubscribe_on_cancel_action`（現状 **`none` のみ**、拡張用）。
  - **フォームタグ**（`form-unsubscribe-tags`）… 外部ページ向けにコピーできる HTML 断片を表示。`ScenarioWorkspaceController` が `ScenarioRegistrationService::enabledFields` に基づき **`buildRegisterEmbedSnippet()`** で HTML を組み立て `<textarea readonly>` に渡す（`storage/framework/views` への書き込みを避けるため Blade の `render()` は使わない）。
    - **見栄え**: 断片先頭に **`public/css/scenario-register-embed.css` の内容を `<style id="putage-embed-base-css">` としてインライン埋め込み**（別ドメインでも `APP_URL` に依存せず反映される）。ファイルが読めない場合のみ従来どおり `<link href="{APP_URL}/css/...">` にフォールバックし、`putage.embed_register_base_css_unreadable` をログする。
    - **追加スタイル**: `scenario_public_form_settings` に **`embed_tag_stylesheet_url`**（任意・URL）と **`embed_tag_custom_css`**（任意・`<style>` 内にそのまま出力）を保存でき、フォームタグ画面のフォームから `scenario.workspace.embed-tag-style.update`（PUT）で更新（`ScenarioPublicFormSettingController@updateEmbedTagStyle`）。インライン CSS は管理者信頼前提（他の HTML 挿入と同様）。
    - **UTAGE 互換の形**: `<form method="post" action="/r/{uid}/store">`、クラス名は UTAGE に近い `form-group` / `font-weight-bold` / `badge badge-danger` / `btn btn-block btn-success btn-lg` 等。各入力の `name` は **`field_key`**（既定ではお名前 `name`、メール `mail` など）。
    - **受付**: `POST /r/{uid}/store` は `PublicScenarioRegistrationController@storeWeb` と **同一処理**（`throttle:public-register`）。**CSRF は `VerifyCsrfToken` の `$except` で `r/*/store` のみ除外**し、外部オリジンからの素の `<form>` を可能にする。同一画面内の `POST /r/{uid}/register` は従来どおり **CSRF 必須**（`GET /r/{uid}/register` 上の `@csrf` 付きフォーム用）。
    - **別案**: 断片末尾のコメントで、`<iframe src="/r/{uid}/register">` による埋め込みを案内。
    - フォーム利用項目が 0 件、または `maps_to=email` がフォームに無い場合は警告のみ（断片は出さない）。
- **公開登録（ゲスト）** … `GET /r/{scenario.uid}/register` 形式の登録画面（ゲスト向け本文に **外部製品名の脚注は出さない**・`public/scenario-register.blade.php`）。
  - **HTML**: `GET /r/{scenario.uid}/register`（認証不要）。`register_form_status=draft` のときは **非公開メッセージ**のみ。POST は `throttle:public-register`（IP あたり 30/分）。**`POST /r/{uid}/store`** は埋め込みフォーム用で **`storeWeb` と同じ登録処理**（CSRF 除外は `r/*/store` のみ）。
  - **バリデーションエラー表示**: `ValidationException` 時は `storeWeb` が **`withErrors` + `withInput`** で登録画面へ戻す。各項目は **`@error(field_key)`** でメッセージ表示。画面上部に **一覧アラート**（重複を除く全エラーの要約）も表示。メールの **形式不正・必須・重複（重複登録「許可しない」のとき）** はいずれもメール項目の `field_key` に紐づくメッセージで出る（重複文は `ScenarioRegistrationService` で明示メッセージ化）。
  - **メール検証の強さ（実装済み・限界あり）**: 既定では **`email:rfc,dns`**（`config('putage.registration_email_dns_validation')`・環境変数 **`PUTAGE_REGISTRATION_EMAIL_DNS_VALIDATION`**。`false` のとき **`email` のみ**）。**DNS 有効時**は宛先ドメインに **MX または A** が無い場合は拒否できる（存在しないドメイン・明らかなタイポの一部）。**判定しないもの**: 受信箱の実在、迷惑メール扱い、後からドメインが消えたケース。**公開登録ページ（ゲスト向け）には DNS 等の技術説明文は出さない**（バリデーションのみサーバ側で実施）。**読者一覧**・読者**手動追加**モーダルで、配信フラグと検証の限界を管理向けに明示。読者**手動追加**（`SubscriberController@store`）も同一ルール・同一系のエラーメッセージ。
  - **確認フロー**: `skip_registration_confirm=0` のとき、初回 POST で検証後 **セッション `register_pending`** に入れ `?step=confirm` で確認画面。確定は POST に **`finalize=1`**（`PublicScenarioRegistrationController@finalizeFromConfirmSession`）。確定処理で失敗した場合は **`register_pending` を消す前に** `withInput(検証済み配列)` で入力を復元できるようにする（登録成功時のみ `register_pending` を破棄）。
  - **完了後**: `thanks_redirect_url` があれば **302 外部**、なければ **`GET /r/{uid}/register/thanks`**。レイアウト `layouts/public_register` は **`@stack('public-head-end')` / `public-body-start` / `public-body-end`** でタグ挿入を受け付ける。
  - **受付 API**: `POST /api/r/{scenario.uid}/register`。`draft` 時は **403**。重複不許可で既存メールのときは **422** + `errors`（メール `field_key` に「このメールアドレスは既に登録されています。」等。以前の **409** + `code: duplicate_registration` 専用分岐は廃止し、バリデータの `unique` と統一）。確認フローは **API では省略**（1 POST で登録）。**登録経路**は JSON／フォームボディに **`rt`**（メール用 `public_token`）を任意で付与可。
  - 検証・作成は `ScenarioRegistrationService::validateRegistrationInput` / `createFromValidated`（内部で `validateAndCreate` 相当も維持）。`maps_to=email` の **ユニーク制約**は **`allow_duplicate_registration` が false（重複を許可しない）のときのみ** 付与（`validateRegistrationInput` の第3引数。コントローラでは `! $settings->allow_duplicate_registration` を渡す）。
  - メール列がフォームで利用されていないシナリオは **422**（設定不備）。
  - 入力形式 **radio** / **checkbox** は選択肢（`options_json`）必須。checkbox は API/JSON で **配列**、HTML では `field_key[]` で送信。
- **公開配信解除（ゲスト）** … UTAGE の解除フォームに相当。
  - **HTML**: `GET/POST /r/{scenario.uid}/unsubscribe`（認証不要・`PublicScenarioUnsubscribeController`）。POST は `throttle:public-unsubscribe`（IP あたり 30/分）。
  - **受付 API**: `POST /api/r/{scenario.uid}/unsubscribe`（JSON `email` 必須。成功時 **200** + `message`。422 はバリデーション）。
  - 処理は `ScenarioUnsubscribeService::unsubscribeByEmail` … シナリオ内で **メール（大小文字無視）一致**の読者がいれば **`status=inactive`**。未登録・既に停止でも **画面上は同一の完了メッセージ**（推測防止）。
- **LINE登録ページ（ゲスト）** … UTAGE の `https://utage-system.com/line/open/...` に相当。
  - **HTML**: `GET /line/open/{scenario.uid}`（認証不要・`PublicLineScenarioOpenController@show`）。`throttle:60,1`。クエリ **`?rt={public_token}`** が付き、かつ当該シナリオの登録経路（**種類=LINE**）と一致するとき、送信用アカウントが LINE 対応（`delivery_accounts.type` が `line` / `both`）であれば **`scenario_line_route_visit_intents` に来訪を記録**する。
  - **友だち追加URL**: シナリオの送信用アカウント `delivery_accounts.line_bot_basic_id` から `DeliveryAccount::lineOfficialAddFriendUrl()` で `https://line.me/R/ti/p/@...` を組み立て。未取得時は案内文のみ（QR なし）。
  - **QR画像**: **登録経路付き**（`?rt=` が有効なとき）は QuickChart の対象 URL を **`/line/open/{uid}?rt=…` のフルURL** にし、**先にこのページを開いてから友だち追加**する導線とする（`public/line-scenario-open.blade.php`）。経路なしのときは従来どおり友だち追加URLのみ。いずれも **QuickChart**（`quickchart.io`）利用（第三者サービスに URL が送られる点は制約に記載）。
  - **管理**: ワークスペース slug `line-register-page` / `line-register-page-settings`。設定はテーブル **`scenario_line_register_settings`**（`scenario_id` 一意、`ScenarioLineRegisterSetting::ensureForScenario`）。保存は `scenario.workspace.line-register-settings.update`（PUT、`ScenarioLineRegisterSettingController@update`）。**重複登録**（許可する/しない）・**head/body 先頭・body 末尾の JavaScript**（`script` タグ含む・公開ページではエスケープしない）。`allow_duplicate_registration` は現状フローでは未使用だが UTAGE 相当の保存項目として保持。
  - レイアウト `layouts/public_line_open` は `@stack('public-head-end')` / `public-body-start` / `public-body-end` で挿入を受け付ける。
- `subscriber.index` は `q`（名前/メール/電話/LINE userId）・`status`（active/inactive）・`route`（`public_form`=公開登録フォーム由来 `registration_route` が `public:%`、`line`=`auto:line%`、`manual`=`manual:putage`、`other`=それ以外）で絞り込み可能。一覧は **登録経路**列でラベル表示（内部値は `registration_route`。一覧は `SubscriberController`）。
- `step.index` は **タイプ別タブ・タイプ選択なし**（このプロダクトではステップ配信＝メール前提）。**`type=email`** は通常どおり一覧・編集。**メール以外**が DB に残っている場合は **レガシー行**として **削除のみ**（`DELETE step.destroy`）を表示し、`step.edit` は案内付きで一覧へ戻す。一覧は **作成順や `sort_order` ではなく、登録起点の時間軸順**（登録直後 → 登録から〇時間〇分後 → 〇日後＋時刻）で表示する。一覧のタイミング文言は **`schedule_mode`** に応じて表示（登録直後／登録から〇時間〇分後／〇日後＋時刻）。**`GET step.create`** の専用ページで、配信条件・配信メール・プレビュー・送信タイミング種別・既存読者・送信後アクションを POST。**`step.store`（追加）成功後は `step.edit` へリダイレクト**し、URL 置換・テスト送信 UI など続きを編集する。
- **ステップ編集**（`step.edit` / `PUT step.update`）はメールステップ専用フォーム。**削除**は `DELETE step.destroy`（編集画面上部のボタン）→ 成功時は `step.index` へ。DB は **`step_messages`** にマイグレーション `2026_05_09_140000_extend_step_messages_utage_style` と `2026_05_13_134500_add_step_delivery_engine_configs` で拡張した列を使用。
  - **メールタイプ**: **配信条件**（全員／条件指定。条件指定時は `condition_config.required_labels` / `excluded_labels` のラベル条件を送信エンジンが判定）、**配信メール**（送信者名・送信元メール・件名・A/Bテストフラグ・テキスト/HTML・本文・読者項目／システム／独自の置き換えチップ挿入）、**プレビュー一行テキスト**、**送信のタイミング**（登録直後／登録〇日後の〇時／登録〇時間〇分後）、**既存読者への送信ポリシー**（`schedule_all_readers` は通常送信、`skip_existing_readers` はステップ作成前登録者をスキップ、`reserve_skip_late` は作成時点で予定時刻超過の既存読者をスキップ）、**送信後アクション**（`action_config` にラベル付与/除外、現在シナリオ停止、同一ユーザー所有の別シナリオ登録を保存し、送信成功後に実行）、**リンククリック時アクション**（現状 `none` のみ）、**URL置換ドメイン**（`default`=シナリオ既定想定／`scenario`=`scenario_extended_settings.default_url_replace_domain` 利用想定）、**URL見え方**（現状「元のURL」のみ）、**テスト送信**（UIのみ・無効）、**ステータス**（稼働中/停止）。
  - **送信元が空欄のとき**: \`step.create\` / \`step.edit\` で、実際に参照される既定を画面に明示する。優先は **シナリオ設定**のデフォルト送信者（`scenario_extended_settings.default_mail_from_name` / `default_mail_from_email`）、未設定なら **送信用アカウント**の送信元（`delivery_accounts.from_name` / `from_email`）。いずれも空なら「未設定」と表示（Blade 共有パーツ `resources/views/account/scenario/partials/step_mail_from_defaults.blade.php`）。
  - **本文の置き換えチップ**: \`partials/step_mail_placeholder_chips.blade.php\` で、**読者項目は登録フォームで「フォーム利用」オンの項目だけ**を「項目名」＋トークンで並べる。フォームに設置していない項目は、送信時に値が空になりやすいため候補に出さない。**システムは説明ラベル＋トークン**を並べる。読者項目と同じ \`field_key\` のシステムトークンは二重表示しない。
  - **ステップメール実送信（バッチ）**: \`PUTAGE_STEP_MAIL_DISPATCH_ENABLED=true\` のとき Laravel のスケジューラが **毎分** \`putage:step-mail-dispatch\` を登録（サーバの cron に \`php artisan schedule:run\` が必要）。無効時はコマンド・スケジュールとも動かない。\`php artisan putage:step-mail-dispatch --force\` で設定を無視して手動試験可。**処理**: \`StepMailDispatchService\` が **status=active** かつメールありの読者を走査し、シナリオのアーカイブ・送信用アカウント種別・SMTP／プラットフォーム既定経路を確認。ステップは **type=email・稼働中**を、読者ごとの **実送信予定時刻が早い順** に並べ、**未送信の次の1通**だけ対象にする（作成順・`sort_order` 順ではない）。**送信予定時刻**は \`registered_at\` と \`schedule_mode\`（登録直後／〇日後の時刻／〇時間〇分後）から算出。未来日時の次ステップは \`scenario_mail_deliveries.status=reserved\` として予約中に同期する。予定到来後に既存読者ポリシーとラベル条件を評価し、対象外なら \`step_message_deliveries.status=skipped_existing_reader\` または \`skipped_condition\` を記録して、予約中レコードがあれば \`cancelled\` に更新する。送信成功時は \`status=sent\` を記録し、同じ読者ロック内で送信後アクション（ラベル付与/除外、現在シナリオ停止、別シナリオ登録）を実行する。アクション失敗時もメール再送を避けるため送信済み記録は残し、\`sent_action_failed\` と \`error_message\` に記録する。送信失敗時は \`status=failed\` を記録し、\`scenario_mail_delivery_errors\` にメール配信エラーとして保存する。**マージ**は \`StepMailMergeService\`（読者項目・システム・独自置き換え）。**未実装・簡略化**: 条件の対象者プレビュー、本文内 URL 短縮・プレビュー一行の実送信、画面のテスト送信ボタンは従来どおり未接続または保存のみ。
- **予約中 / 送信済**（`scheduled` / `sent`）は実装済み（`stub=false`）。共通テーブル **`scenario_mail_deliveries`** に `scenario_id`・`delivery_account_id`・`subscriber_id`・`source_type`（`step` / `broadcast` / `event_reminder_day_before` / `event_reminder_hour_before`）・対象ID・`email`・`subject`・`send_at`・`queue_created_at`・`sent_at`・`status`（`reserved` / `sent` / `cancelled` / `failed`）・`delivery_channel=mail`・`mailer`・`message_id`・`error_message` を保持する。`MailDeliveryStateService` が予約/送信済/失敗/取消への更新を担当する。`scheduled` 表示時は当該シナリオのステップ配信とシナリオ連携済みイベントリマインドの未来予約を同期してから、`reserved` を送信予定日時順に最大300件表示する。`sent` は `sent` を送信日時の新しい順に最大300件表示する。**送信済は SMTP 送信処理成功の意味であり、受信箱到達の保証ではない**。メール一斉送信は読者ごとに送信直前に `reserved`、成功時に `sent`、失敗時に `failed` を記録する。
- **リマインダ配信**（`reminder-delivery`）は **メールのみ実装済み**（`stub=false`）。テーブル **`scenario_reminders`**（`scenario_id`・`event_id`・`title`・`channel=mail`・`status`・`timing_type`=`before`/`after`・`timing_days`/`timing_hours`/`timing_minutes`・任意 `fixed_time`・`exclude_cancelled`・`condition_required_labels` / `condition_excluded_labels`・`subject`・`body_text`）。対象イベントは **同一ユーザーかつ `events.scenario_id` が当該シナリオ**のものだけ選択可。画面は `resources/views/account/scenario/_reminder_delivery.blade.php`、保存は `ScenarioReminderController`（`scenario.workspace.reminders.*`）。送信エンジンは `ScenarioReminderDeliveryService::processDue` で、`putage:event-reminders` の先頭で実行する。判定は「現在時刻 >= イベント開始から算出した配信予定時刻」「予約が pending/confirmed」「同一シナリオの読者が active」「必須/除外ラベル条件一致」「未送信/未失敗/未取消」。送信はシナリオのメール送信用アカウント（シナリオ既定送信者 → アカウント送信者）を使い、`{{name}}`・`{{event_name}}`・`{{event_date}}`・`{{event_time}}`・`{{zoom_url}}`（同名 `%...%` も可）を本文・件名に置換する。`scenario_mail_deliveries.source_type=event_reminder` と `scenario_reminder_id` / `event_booking_id` で予約中・送信済・失敗・取消を管理する。既存の `events.reminder_enabled` 固定リマインド（前日/約1時間前）は、当該イベントに有効な `scenario_reminders` が無い場合のみ互換実行する。**範囲外**: LINE リマインダ、LINE未登録診断、Googleカレンダー/Zoom API 自動連携、複数回再試行（現状は一度 `failed` になった行は自動再試行しない）、事後フォロー用シナリオ自動生成。
- **メール配信エラー**（`email-errors`）は実装済み（`stub=false`）。テーブル **`scenario_mail_delivery_errors`** に、ステップ配信・メール一斉送信で **Monaka の送信処理中に検知した SMTP 接続／認証／送信拒否等の例外**を保存する。保存項目は `scenario_id`・`delivery_account_id`・`subscriber_id`・`source_type`（`step` / `broadcast`）・`step_message_id`・`scenario_broadcast_id`・`email`・`error_message`・`detected_at`。`MailDeliveryErrorRecorder` が記録を担当し、画面は `resources/views/account/scenario/_mail_delivery_errors.blade.php` で新しい順200件を表示する。**範囲外**: 受信サーバーから後で返るバウンスメールや外部MTA webhook の取り込みは、受信用メールボックス／Webhook 署名検証／MTA別 payload 対応が必要なため後続拡張。
- **メール一斉送信（シナリオ）**: ルート \`GET scenario.broadcast.index\`（一覧）・\`GET scenario.broadcast.create\`・\`POST scenario.broadcast.store\`（\`throttle:10,1\`）・\`GET scenario.broadcast.show\`。コントローラ \`ScenarioBroadcastController\`。テーブル \`scenario_broadcasts\`（\`scenario_id\`・\`name\`・\`audience\`（現状 \`all_active\` のみ）・\`mail_format\`・\`subject\`・\`body_html\` / \`body_text\`・\`status\`：\`pending\` / \`processing\` / \`completed\` / \`failed\`・**\`scheduled_at\`**・**\`queued_at\`**・\`dispatch_cursor\`・\`total_targets\`・\`sent_count\`・\`failed_count\`・\`last_error\`・\`completed_at\`）。**対象読者**: **active** かつ **メール非空**（ステップの送信対象と同趣旨）。**送信元**: シナリオ \`scenario_extended_settings\` の既定送信者 → 送信用アカウント \`from_name\` / \`from_email\`（ステップの「空欄時既定」と同じ優先。一斉送信フォームにステップ用の送信者上書き欄は無い）。**本文・件名のマージ**は \`StepMailMergeService\`。**配信日時指定**: 作成画面の \`scheduled_at\`（`datetime-local`、アプリタイムゾーン）を空欄にすると即時キュー投入、未来日時を入れると Laravel キューの **delay** で `jobs.available_at` をその時刻へ設定する。画面の状態は `pending` かつ `scheduled_at` が未来なら **予約中** と表示する。**送信実行**: \`store\` 成功後に \`ProcessScenarioBroadcastChunkJob\` をキュー投入し、未来日時なら delay する。ジョブは予定時刻より早く実行された場合も `scheduled_at` まで再 delay し、\`Cache::lock\` で同一 broadcast の並行処理を抑止し、\`ScenarioBroadcastSendService::processNextChunk\` をループ（チャンクサイズは \`config('putage.scenario_broadcast_chunk_size')\`、環境変数 \`PUTAGE_SCENARIO_BROADCAST_CHUNK_SIZE\` で上書き可・既定 25・上限 100）。**前提**: \`QUEUE_CONNECTION\` が \`database\` 等のとき **\`jobs\` テーブル**（マイグレーション \`2026_05_11_100001_create_jobs_table\`・既存時はスキップ）と **\`php artisan queue:work\`**（または Supervisor 等）が無いと送信が進まない。SMTP 未設定かつプラットフォーム既定メールも無効な場合はジョブ内で \`failed\` に更新。ワークスペース slug \`bulk-email\` は専用画面への案内（\`stub=false\`）。Blade: \`broadcast_index\` / \`broadcast_create\` / \`broadcast_show\`・\`partials/step_mail_placeholder_chips\` の \`mode=bc\`（挿入ボタン class \`bc-insert-token\`）。

---

## 10. 制約・既知の限界

### 10.1 イベント予約（公開）・会員サイト（現状実装）

- **公開イベント（ゲスト）** … `GET /e/{event.uid}`（`events.is_published` かつ `status=active` のみ。ルートパラメータ名 `publishedEvent`・`RouteServiceProvider` で `uid` 解決）。`PublicPublishedEventController`。イベント作成・編集（`event.create` / `event.edit`・共有パーシャル `event/partials/_settings_fields`）では **`admin_name`**（任意）・**参加費**（`participation_fee_type`=`free`|`paid`、有料時は `participation_fee_amount`・任意の **`limited_price_enabled`** + **`limited_prices`** JSON 配列［`starts_at` / `ends_at` / `amount`］）・**重複申込**（`allow_duplicate_booking`、false=禁止）・**リマインダ配信**（`reminder_enabled`、する/しない）・**連携配信アカウント**（`delivery_account_id`・ログインユーザの `delivery_accounts` のうち **送信元メールと LINE チャネルIDが両方設定済み** のみ選択可）・**リマインダ送信者名/メール**（`reminder_from_name` / `reminder_from_email`、アカウント選択で自動入力可）を保存する。加えて `name` / `type` / `description` / **`location`** / `capacity` / `zoom_link` / **`apply_start_at`** / **`apply_end_at`** / `is_published` 等。公開ページには説明・開催場所・**参加費表示**（`EventSettingsNormalizer::participationFeeLabel`・期間限定価格は現在時刻で判定）・申込受付期間を表示し、受付中の予約枠は **カレンダー（既定・週表示）/ 一覧** タブ（`EventScheduleCalendarPresenter`・FullCalendar）。カレンダー初期月は **開催中（いま枠が進行中）なら当月**、**すべて未来の枠なら最も早い枠の月**（例: 6/6 開催で今日が5月なら6月）。予約可・残席ありの枠クリックで `public.event.book` へ。`apply_start_at` より前は「申込受付開始前」、`apply_end_at` より後は「申込受付は終了しました」として予約枠を出さない。枠は **`event_schedules`**（`status=open`・開始が現在の1時間前以降を一覧）。イベント作成だけでは予約枠は作らず、管理詳細の「日程・枠の追加」で手動追加または一括生成する。予約は `GET /e/{uid}/book?schedule={id}` → `POST /e/{uid}/book`（`throttle:public-event-book`）→ `GET /e/{uid}/thanks/{booking}`。バリデーションはメール **`email:rfc,dns`**（公開登録と同趣旨）。**重複申込禁止**時は同一イベント×同一メールの既存 `event_bookings`（`pending`/`confirmed`）があると申込不可（`EventBookingRecorder`）。**確認メール** `EventBookingConfirmationMail`（`EventOutboundMailService`・連携アカウントの SMTP / 送信元上書き）。**有料参加費の決済ゲート**（申込時の課金）は未実装（表示・設定のみ）。
- **シナリオ連携（任意）** … `events.scenario_id` が、ログインユーザ所有の送信用アカウント配下の **`scenarios.id`** と一致するときのみ有効。予約確定で `subscribers` を **`registration_route = event:{uid}`** で作成／更新し、**`subscriber_label_on_book`** を `labels` 配列に追加（既存ラベルは維持）。
- **申込フォーム項目** … テーブル **`event_booking_form_fields`**（`event_id`・`field_key`・`label`・`maps_to`=`name`|`email`|`phone`|`custom`・`form_enabled`・`is_required`・`input_type`・`options_json`・`is_preset`）。初回表示で **`EventBookingFormDefaults::ensureForEvent`** がお名前・メール・電話の既定行を投入。既存の **`events.booking_field_definitions`**（JSON）のみのイベントは、初回 `ensure` 時にカスタム行へ取り込む（互換）。管理 UI はシナリオの **登録フォーム・読者項目** と同系（`event/workspace/booking-fields`・`PUT|POST|DELETE event/{event}/workspace/booking-fields`・`EventBookingFormFieldController`）。公開予約フォーム（`public/event/book`）は **`EventBookingFormService`** で有効項目のみ描画・検証。お名前・メールは常に表示（メールは必須固定）。**流入トークン（任意）** … クエリまたは hidden の **`rt`** → `event_bookings.registration_route_token` に保存（媒体自動判定はせず、URL パラメータ記録のみ）。
- **イベント管理ワークスペース（管理）** … イベントを選択すると左サイドバーに **日程設定 / 申込者 / リマインダ配信 / 申込フォーム・申込者項目 / 申込フォーム / 担当者設定 / イベント設定**（会員サイトと同様の構成）。ルートは `GET /event/{event}/workspace/{slug}`（`event.workspace.show`・`slug` 省略時は `schedules`）。`event.show` は `schedules` へリダイレクト。**公開URLの表示**はファネル **ページ一覧**（`funnel/page/index`）と共通の `partials/_public_url_inline`（リンククリックで公開ページ・横にコピー）を、ワークスペース上部ツールバー（`event/partials/_workspace_toolbar`）と **イベント一覧**（`event/index`）でも用いる。操作列の **公開ページ** は `btn-outline-success` の `bi-box-arrow-up-right` アイコン。**イベント設定**（`settings`）は参加費・重複申込・リマインダ・連携配信アカウント（`PUT event.workspace.settings.update`）。**詳細設定**（`event.edit`）は説明・場所・枠の長さ・シナリオ連携など。**申込フォーム**（`booking-form`）はサンクス・公開ON。**担当者設定**（`staff`）は `event_staff_members` をイベント単位で複数登録。一覧は `event.workspace` の `staff`、追加・編集は `event.staff.create|edit`（`EventStaffMemberController`）。**Googleカレンダー連携はなし**。保存項目は **基本**（担当者名・管理名称）・**オンライン会議**（`online_meeting_service`=`none|zoom|google_meet`・表示用アカウント名・会議URL。Zoom API 自動連携は未実装）・**メール通知**（カンマ区切り通知先・申込/キャンセル時=`default|notify|none`）・**チャット通知**（`chat_notify_destination`=`none|chatwork|slack|discord`・暗号化 `chat_notify_config`・申込/キャンセル時モード）・**担当者固有の日程**（曜日別時間帯 JSON・特定日 JSON。スロット自動生成への反映は後続）。申込確定時に全担当者へ `EventStaffBookingNotifyService`（メール＋Chatwork/Slack/Discord）を試行。シナリオ連携済みの **リマインダ配信** 画面からは `scenario.workspace` の `reminder-delivery` へ誘導。
- **日程管理（管理）** … `event.workspace` の **日程設定** を UTAGE に近い **設定カード＋予約枠一覧** に整理（`PUT event.workspace.schedules.update`・`EventScheduleSettingsController`・`EventScheduleSettingsNormalizer`）。**基本設定**（所要時間・開催前後の確保時間・定員）・**担当者**（1名固定 / 提示して選択・`schedule_staff_member_id`）・**オンライン会議利用**フラグ・**曜日別時間帯**（複数枠・追加/削除）・**特定日**（任意）・**祝日**（開催する/しない・`JapanesePublicHoliday` 簡易判定）・**表示期間**（自動＝いつから＋何日分 / 期間指定）を `events.schedule_settings`（JSON）と `slot_duration_minutes` / `buffer_before_minutes` / `buffer_after_minutes` に保存。枠の間隔は **所要時間＋開催後の確保時間** で自動決定（別UIなし。`slot_interval_minutes` は保存時に算出）。**保存して予約枠を生成** で `EventScheduleSlotGenerator::generateFromSavedSettings`（申込受付期間 `apply_*` と交差・重複スキップ）。手動1件追加・日付範囲の簡易一括生成は折りたたみ内に残す（`POST event/{event}/schedules`・`schedules/generate`）。枠の **定員** は入力空で **`event_schedules.capacity` null（無制限）**。削除は `DELETE` 行内または **`POST …/schedules/bulk-destroy`**。一覧・カレンダーは最大1000件。**予約枠一覧**は **カレンダー**（既定）と **表** をタブ切替（`EventScheduleCalendarPresenter`・FullCalendar）。色分け: 予約可（緑）・残席あり（橙）・満席（赤）・受付終了（灰）。枠クリックで詳細・削除。表タブのみ一括削除。
- **リマインドメール** … `events.reminder_enabled` が true かつ **`PUTAGE_EVENT_REMINDERS_ENABLED=true`**（`config/putage.php`）のとき、スケジューラが **`putage:event-reminders`** を **5分毎** 実行。旧固定リマインド（前日/約1時間前・`event_bookings.reminder_*_sent_at`）は **`EventOutboundMailService`** 経由で `reminder_from_*` または連携アカウントの送信元・SMTP を使用。現在はシナリオワークスペースの **リマインダ配信**（`scenario_reminders`）を優先し、同一イベントに有効なリマインダが無い場合だけ旧固定リマインドを互換実行する。**Google カレンダー連携・Zoom API 自動生成・決済ゲート・LINEリマインダ**は未実装。
- **会員サイト サイト設定（設計正本）** … 基本設定・決済連携・コース整理の要件と **Monaka 実装対応表** は **`docs/membership-site-settings-design.md`**。現行 **サイト設定**（`GET site/{site}/edit`）はサイト名・説明・ロゴ・ヘッダー・テーマ・公開・スラッグに加え **コースカテゴリ**（`membership_sites.course_categories` JSON・最大50件・`App\Support\MembershipSiteCourseCategories`）。**URL管理**・**ページ設定**（4区分）・**受講生管理**・**コースの結合**は別画面。決済連携と購入後開放は **初期設定 → 決済連携**・**商品プラン**（`after_purchase_bundle_id`）。会員マイページの **「コースの結合で絞り込み」**（`course_page_settings.show_category_filter`）は **付与済みコースの結合名** による絞り込み。教材カテゴリの絞り込みは **コースの結合内のコース一覧**（`member/course-bundle`・`?course_category=`）で、ページ設定の「コースの結合で絞り込み」が ON のときカテゴリが1件以上あれば表示する。
- **会員サイト（ゲスト）** … `GET /member/{slug}/login`（`slug` は `membership_sites.slug`・英数字・ハイフン・アンダースコア。公開側は slug のみで解決するため、今後作成・更新する slug は **全体一意**で検証する）。認証は **`site_member`** ガード（`config/auth.php` の guard/provider、モデル **`SiteMember`** / テーブル **`membership_site_members`**）。`membership_sites.status=active` のサイトのみログイン可能。ログイン後 `GET /member/{slug}/`（ダッシュボード）・`GET /member/{slug}/pack/{courseBundle}`（付与済み **コースの結合** `course_bundles.id` のコース一覧）・`GET /member/{slug}/bundle/{bundle}`（教材 **コース** `bundle_courses.id`）・`GET /member/{slug}/bundle/{bundle}/track/{track}/lesson/{lesson}`（`courses` / `lessons` の id）。**ミドルウェア** `site.member` で **URL の slug と会員の `site_id` が一致**することを検証（他サイトの slug ではログアウト）。**ログアウト**は `POST /member/{slug}/logout`（**セッション全体は invalidate しない**／管理ユーザ `web` ガードとの共存のため）。**コース画像** … `bundle_courses.thumbnail_url` を **管理のコース一覧**（`GET site/{site}`・左メニュー「コース」）と **会員のコースの結合内コース一覧**（`member/course-bundle`）の両方で、カード先頭に **全幅・16:9**（`min-height` 180px／md 以上 200px・`max-height` 280px・`object-fit: cover`）で表示。共通パーシャル `partials/_bundle_course_thumbnail`（`variant=card-top`）。未設定時は同じ高さ域のプレースホルダ（「画像なし」＋アイコン）。`alt` はコース名。会員側グリッドは `col-12 col-md-6`（モバイル1列・md 以上2列）。マイページ（`member/dashboard`）は **コースの結合**のみ表示のためコース画像は出さない（`course_page_settings.display_mode` の grid/list もコースの結合単位）。
- **会員権限（コースの結合）** … テーブル **`member_bundle_grants`**（`site_member_id` + **`course_bundle_id`** 一意・`source`=`manual` 等）。付与単位は **コースの結合**。会員マイページに表示される教材コースは、付与済みコースの結合に含まれる **`bundle_courses.status=published`** のみ。**レッスン閲覧**は **コース基本設定の開放日・締切**（`course_settings.automation`）を満たしたうえで、コース単位の開始日（付与日起算＋`release_start_*`）の **0 時** に **`lessons.drip_days`** を加算した日以降、かつ **`lessons.is_published`**・`published_at`・内部コンテナ **`courses.status=published`**。`learning_style=sequential_completion` のときは **前レッスンのドリップ解除前は次をロック**（視聴完了判定は未実装）。コンテンツ本文は管理画面で HTML 入力（会員画面は `{!! !!}` 表示。信頼できる管理者のみ前提）。
- **会員サイト ゲストプレビュー（URL管理）** … `GET site/{site}/url`（`site.url`）で **ログインURL** と **プレビューURL（1本）** を表示（`MemberSitePreviewService::previewEntryUrl`）。入口は `GET /member/{slug}/preview/start`（`member.preview.start`・`signed` のみ）。成功時にセッション `member_site_preview` を開始し `member.preview.dashboard` へリダイレクト。以降 `GET /member/{slug}/preview/...` は `member.site.preview` でセッション検証（画面ごとの署名URLは不要）。マイページ・コース・レッスン・お知らせ等へ通常リンクで遷移可。プレビュー中は **`bundle_courses.status=published` のコースをすべて**マイページに表示（付与・オファー設定は見ない。実会員は `member_bundle_grants` 付与分のみ）。**公開済みレッスン**はドリップ/順番ロックをバイパス。URL管理・プレビュー帯に「全コース表示」と明記。画面上部にプレビュー帯。POST（受講完了・コメント・テスト等）は不可。有効期限は `config/putage.php` の `member_site_preview_url_ttl_days`（既定90・`PUTAGE_MEMBER_SITE_PREVIEW_URL_TTL_DAYS`）。下書きサイトでも可。
- **会員・コンテンツの管理画面** … `routes/web.php` で **`site.url`**（`GET site/{site}/url`・`MembershipSiteController@url`・ログイン案内 URL とプレビュー URL 表示）および **`site.members.*`**（`MembershipSiteMemberController`・`GET/POST site/{site}/members`・`DELETE site/{site}/members/{member}`・`PUT …/password`・`POST/DELETE …/grants`）を `Route::resource('site', …)` より **前**に定義する。`GET site/{site}/members` で会員追加・パスワード更新・コースの結合の付与・付与削除。**用語（会員サイト）** … **コース**＝教材そのもの（DB: `bundle_courses`）。配下に **グループ**（DB: `courses`）と **レッスン**（`lessons`）。**コースの結合**＝複数コースをまとめた開放パック（DB: `course_bundles` ＋ `course_bundle_items`・**任意**）。受講生付与の保存は `member_bundle_grants.course_bundle_id` だが、管理画面では **`grant_target`** で **コース単体**（`course:{bundle_courses.id}`）または **コースの結合**（`bundle:{course_bundles.id}`）を選べる。コース単体のときは `SingleCourseBundleService` が `legacy_source_bundle_course_id` 付きの内部コースの結合を自動作成し、**コースの結合一覧には出さない**（`visibleInAdmin`）。商品の購入後開放（`after_purchase_bundle_id`）は引き続きコースの結合 ID。コースを作っただけでは会員に見えない。受講生への付与（または購入後開放）まで閲覧不可。

**会員サイト一覧・コース一覧のコメント表示** … `GET site`（`site.index`）の各サイトカードに、当該サイト配下の **未回答コメント**（`admin_reply` 空）件数をバッジ表示（0件は非表示）。`GET site/{site}`（`site.show`・コース一覧）では上部サマリーに **未回答コメント（サイト合計）**、各コースカードに **コメントON/OFF**（`course_settings.comments.mode`）・**投稿数**・**未回答**（赤バッジ・`site.course.workspace.comments` へリンク）。集計は `MembershipSiteLessonCommentService::unrepliedCountsBySiteIds` / `statsForCourseIds`。

**コースワークスペース（管理）** … **コース**（`bundle_courses`）の **コース設定** から `overview`（初期設定ハブ同型のカード一覧）を開く。`slug`: `overview` / `groups` / `lessons` / `students` / `progress` / `comments` / `settings`（カード一覧・作業の推奨順は **グループ → レッスン**）。旧 `workspace/reorder` は廃止（`/reorder` は `lessons` へリダイレクト）。**受講状況**（`workspace/progress`・`CourseMemberProgressAdminService`）… 当該コースを含むコースの結合に付与された受講生をページング一覧。列: メール・名前・進捗バー・完了（`member_lesson_completions`／公開レッスン数・会員画面と同じ）・最終完了日時・受講可否（期間外・対象外）。サマリー: 登録受講生数・公開レッスン・表示ページの平均完了率。順番受講コース時は案内文を表示。**グループ**＝レッスンのまとまり（DB: `courses`・画面表記は **グループ**／**グループ管理**。旧称のトラック・レッスングループは使わない）。**コース（`bundle_courses`）新規作成時**に `BundleCourse::ensureDefaultLessonGroup()` で **「メイングループ」**（`status=published`・`sort_order=0`）を1件自動作成する。`site.bundle.groups.*` で追加 CRUD（グループのサムネイルは `thumbnail_file`／メディア選択・`CourseGroupThumbnailService`・`membership-site-course-group-thumbnails/{user_id}/`。URL手入力は廃止）。**グループの表示順**は `workspace/groups` で ↑↓ 操作後 `PUT site.course.workspace.groups.order`。**レッスンの表示順**は **レッスン設定** → 各レッスン編集の **表示順**（`sort_order`）。**レッスン**は `GET …/groups/{group}/lessons/create|edit`（`site.bundle.lessons.*`）。一覧は `workspace/lessons`（グループ別）。`tracks` ワークスペース URL は `groups` へリダイレクト。

#### 管理画面フォームのレイアウト（正）

会員サイトの **設定系フォーム**（コース基本設定・サイト設定など）は次を正とする。

- **外枠** … 管理画面のグレー背景上に、左揃えの白 **`card`** を縦に積む（フォーム全体の中央寄せはしない）。
- **区分見出し** … 各ブロックは **`card-header`**（例: 基本設定・自動化）＋ **`card-body`**。
- **1項目の並び（必須）** … **`form-label` を上**、**入力コントロールを直下**に縦積み（ラベルとフィールドの **横並び 2 列**は使わない）。
- **禁止** … Bootstrap の **`col-lg-3` / `col-md-3` ＋ `col-lg-9` / `col-md-9`** でラベル列と入力列を分け、ラベルを **`text-end`** にするレイアウト（ラベルと入力の間に広い余白が空き、読みづらくなる）。
- **補足** … 各コントロール直下に **`form-text`**（`hint`）。必須はラベル横に `*`。
- **入力幅** … `card-body` 内では原則 **フル幅**（`form-control` / `form-select`）。可読性のため **入力だけ** に `max-width` を付けることは可（例: 長文テキスト・日時）。**ラベル列と入力列の間にガターを作る目的**での max-width は不可。
- **保存** … フォーム下部の左揃え・緑の **保存** ボタン（`site/edit` と同系）。
- **正本実装** … 行パーシャル `resources/views/site/course/partials/_settings_form_row.blade.php`（`@component` で `label` / `required` / `badge` / `hint` / スロット）＋画面 `resources/views/site/course/workspace/settings.blade.php`。
- **参照パターン** … `resources/views/site/edit.blade.php`（サイト設定の card ＋縦積みラベル）。
- **新規画面** … 会員サイトに設定フォームを追加するときは、上記パーシャルまたは **同一マークアップ**（`mb-3`・上ラベル・下フィールド・`form-text`）に揃える。一覧テーブル UI は §9.1 の `card` + `table-responsive` 系（本節とは別）。

**コース基本設定（専用フォーム）** … `workspace/settings`（`PUT site.course.workspace.settings.update`）。**同一サイト内の他コースから `course_settings` をコピー** … コース追加モーダル（`POST site.course.store`・任意 `copy_settings_from_course_id`）・設定画面上部（`POST site.course.workspace.settings.copy-from`・`BundleCourseSettingsCopyService`）。コピー対象は `course_settings` JSON のみ（コース名・サムネイル・レッスンは含まない）。カテゴリ ID・受講対象ラベルはコピー先サイトで再検証。上記 **管理画面フォームのレイアウト（正）** に従う区分フォーム（基本／**レッスン基本設定**／未購入者オファー／自動化の各 `card`）。**基本** … コース名（`name`）・管理名称・種類（`course_type`・表示のみの分類）・コース画像（`thumbnail_file` / メディア）・**コース一覧のボタン**（`course_settings.basic.button_text_mode`＝`default`（表示「受講する」）／`custom`＋`button_text_custom` 最大50文字。旧 `button_text` キーは読み取り互換）・進捗率表示・動画オプション反映（`show_video_options_hint`）・**カテゴリ**（`course_settings.basic.course_category_id`＝サイト設定の `course_categories[].id`・未分類は空）・ステータス（`published` / `draft`）・一覧表示順（`sort_order`）・説明（`description`）。**未購入者オファー** … `course_settings.offers.offer_mode`＝`disabled`｜`always`（常時表示オファー）｜`limited`（期間限定・`limited_start_at`／`limited_end_at`）。**常時と期間限定は同時選択不可**（ラジオ）。`detail_button_label`・`detail_url`。会員マイページは教材コース単位のカード一覧（未付与かつオファー有効＝「未購入」＋詳細ボタン）。旧 `always_on`／`limited_time` は読み取り互換。**レッスン基本設定** … `course_settings.lessons`：`default_status`（新規レッスン追加時の公開状態初期値・`published`/`draft`/`private`）・`completion_button_label`（会員レッスン画面の受講完了ボタン・本文直下に常時表示・生徒の受講完了を登録。既定「受講完了にする」。次レッスン解放は `automation.learning_style=sequential_completion` のときのみ）・`completion_after_action`（ラジオ・既定 `next_lesson`＝完了後に次の解放済みレッスンへ。最終レッスンまたはすでに受講済みの場合はコース内レッスン一覧 `member.bundle.show` へ。`stay`＝同レッスン画面のまま「受講済みです」表示）。本文下の任意ボタンは各レッスンで個別設定（コース共通の既定なし）。**コメント**（同一 `card` 内）… `course_settings.comments.mode`＝`disabled`｜`enabled`（全レッスン）｜`per_lesson`（`lesson_settings.comments_enabled`）。`default_admin_reply_name` は運営返信の初期表示名。`require_approval` は承認まで非公開。投稿は `membership_site_lesson_comments`（`MembershipSiteLessonCommentService`）。会員は解放済みレッスンのみ `POST member.lesson.comment.store`（`throttle:20,1`）。管理は `workspace/comments`・`PUT site.course.comments.reply`・`DELETE site.course.comments.destroy`。ファネル `comment_block`（`funnel_page_block_comments`）とは別系統。**受講停止時動作**は UI＋JSON 保存のみ（会員ロジック未接続）。**受講対象者**（画面の説明文と一致）… `none`＝付与済み受講生は常に閲覧可。`after_days`＝付与から `audience_days_after_grant` 日経過後に開放。`labels`＝付与済みかつ `audience_label_ids` のラベルが1つ以上付いた受講生のみ閲覧可。ラジオで排他選択。会員サイト **ラベル設定**（`site.labels.*`・`membership_site_labels`）と **受講生管理** のラベル編集（`membership_site_members.labels` JSON）で付与。判定は `BundleCourseAccessService::memberMatchesAudience`（マイページは対象外時ボタン無効・「あと〇日で受講可能」等）。**自動化** … `bundle_courses.course_settings`（JSON・マイグレーション `2026_05_28_100000_add_course_settings_to_bundle_courses`）の `automation`：`release_start_*`（付与日起算のコース全体開放日）・`release_end_*`（受講締切）・`learning_style`（`all_unlocked`＝ドリップ・公開日時のみ／`sequential_completion`＝**前レッスンを受講完了にした人だけ**次を解放。画面文言「受講完了にすると次のレッスンが受講可能」）。`video_completion_percent`（1〜100・既定90）… HTML5動画はこの視聴率で自動完了。YouTube／Vimeoはレッスン画面の受講完了ボタン（`BundleCourseSettings::resolveCompletionButtonLabel`）。完了記録は `member_lesson_completions`（`MemberLessonCompletionService`）。正規化は `App\Support\BundleCourseSettings`（保存時にサイトのカテゴリ ID を検証）、会員のボタン文言は `BundleCourseSettings::resolveButtonTextLabel`（`member/bundle`・`member/course-bundle`）。**コース画像**は `thumbnail_url` を管理コース一覧・会員のコースの結合内一覧のカード先頭に表示（`partials/_bundle_course_thumbnail`）。会員閲覧制御は `App\Services\MembershipSite\BundleCourseAccessService`。進捗バーは **受講完了登録済みレッスン数／公開レッスン数**（`member_lesson_completions`・受講スタイルに関係なく同じ）。視聴可能になっただけでは進捗に含めない。

**レッスン設定（個別・UTAGE 相当の第1段）** … `lessons` に `slug`・`browser_title`・`summary`・`status`（`published` / `draft` / `private`・保存時 `is_published` と同期）・`lesson_type`（`normal` 既定｜`test`）・`test_settings`（JSON: `passing_score` 0〜100・`randomize_questions`（設問順シャッフル・既定オフ）・`questions[]` 各 `prompt`・`choices[]`・`correct_index` 0始まり）・`parent_lesson_id`・`published_at`・`thumbnail_url`・`lesson_settings`（JSON: 動画自動再生・HTML5 シーク/倍速・添付表示・`comments_enabled`）を保存。マイグレーション `2026_05_29_100000_add_lesson_type_and_test_settings_to_lessons`。基本（ページ名・タイトル・スラッグ自動/手動・公開状態・表示順・親レッスン・説明）・コンテンツ（TinyMCE 本文・動画・ボタン・添付URL・リンク）・公開制御（ドリップ日数・公開日時）。会員 URL は従来どおり `/member/{slug}/bundle/{bundle}/track/{track}/lesson/{lesson}`（数値 ID）。スラッグは管理・将来用。閲覧は **コースの結合の付与**＋`status=published`＋`published_at` 到来＋ドリップ。**レッスン編集 UI** … スラッグはページ名から自動生成（任意で手動変更）。アイキャッチ・資料はアップロード/メディア（`LessonThumbnailService`・`LessonAttachmentService`）。本文下ボタンはコース一覧ボタン・受講完了ボタンとは別（文言・URLは各レッスンで個別設定。公開状態の新規初期値は `lessons.default_status`）。公開タイミングはレッスンのドリップ/公開日時と、コース基本設定の自動化を画面内で区別表示。`lesson_settings` に動画オプション・`comments_enabled`。レッスン画面の共通CSSは **ページ設定 → レッスンページ**（レッスン個別のヘッダー非表示・専用CSSの管理項目は廃止。旧 `hide_member_header` 等は読み取り互換のみ）。**会員表示** … `lessons.thumbnail_url` があるとき **レッスン閲覧**（`member/lesson`・タイトル直下・16:9 全幅）と **コース内レッスン一覧**（`member/bundle`・`variant=list-thumb`）に表示（`partials/_bundle_course_thumbnail`）。未設定時は一覧・詳細とも画像ブロックなし（プレースホルダは出さない）。**テストレッスン（MVP）** … 管理のレッスン追加/編集で種別 **通常｜テスト** を選択。テストは択一のみ・設問ビルダーで保存。会員は `member/lesson` でラジオ回答→`POST member.lesson.test-submit`（`LessonTestScoringService`）。**合格点以上**で `member_lesson_completions`（`completion_source=test_pass`）。不合格は再挑戦可（完了登録なし）。手動の受講完了ボタンはテストでは不可。順番受講（`sequential_completion`）も **合格＝完了** と同じく次レッスン解放。**未実装** … 複数選択・記述・設問シャッフル・受験回数上限・管理側の受験結果一覧。

**未実装** … スラッグ URL ルート（会員閲覧は数値 ID のまま）。視聴率による次レッスン解放・順番受講は **実装済み**（`sequential_completion`・`member_lesson_completions`）。

**教材コース（`bundle_courses`）の削除** … `DELETE site.course.destroy` の前に `BundleCourseDeletionService::purgeRelatedAccess` を実行。**単体開放**用 `course_bundles`（`legacy_source_bundle_course_id`）および**当該コースのみ**を含むコースの結合は削除し、`member_bundle_grants` も解除（FK cascade）。商品プランの `after_purchase_bundle_id` は null。既にコースだけ消して付与が残った環境は `php artisan putage:prune-orphan-course-bundles`（`--site=` 任意）。会員マイページは**公開中の教材が1件以上あるコースの結合の付与**のみ表示。

**コースの結合** … `GET site/{site}/bundle-settings` で **`course_bundles`** 一覧（含むコース数・付与済み受講生数・商品プラン連携）。`POST site.bundles.store` → `GET site.bundles.edit` で **既存教材コース（`bundle_courses`）をチェック**して `PUT site.bundles.update`。**コースの結合画面からレッスン編集（コース設定）は開かない**（教材編集は **コース一覧 → コース設定**）。マイグレーション `2026_05_26_120000_create_course_bundles_and_remap_grants` で、旧 `member_bundle_grants.bundle_course_id` 付与は **単体開放用 `course_bundles` へ自動移行**（名称に「（単体開放）」が付く場合あり）。`2026_05_12_233000_extend_membership_site_builder_fields` で、サイトの **ロゴ/ヘッダー/ヘッダー右上リンク/テーマカラー**、教材コース（`bundle_courses`）の **サムネイル**（`thumbnail_file` アップロードまたはメディア選択・png/jpg/gif/webp・5MB以下・`membership-site-course-thumbnails/{user_id}/`）/ **公開状態**、トラックの **公開状態**、レッスンの **動画タイプ・動画URL/埋め込み・ボタン・添付ファイル・関連リンク**を保存できる。ロゴは `logo_file` で **png / jpg / gif / webp（5MB以下）** を `storage/app/public/membership-site-logos/{user_id}/` にアップロードし、`logo_url` へ公開URLを保存する。PHP側上限は `public/.user.ini` で `upload_max_filesize=8M`・`post_max_size=12M` を既定にする。PHPが先に弾いた場合も `validation.uploaded` を出さず、画面には日本語で選び直しを案内する。差し替え時はこの管理下の旧ロゴのみ削除する。`menu_label` / `menu_url` は会員画面上部右側に出す任意リンクで、**表示名とURLの両方がある場合のみ**表示する。`2026_05_13_003500_add_membership_site_navigation_pages` で **ページ設定**（ログイン画面・マイページ・レッスン下部の案内文）と **お知らせ管理**（`membership_site_announcements`、公開中かつ公開日時到来分を会員マイページに表示）を追加。サイドバーは選択中サイトがあるとき **コース / コースの結合 / 受講生管理 / お知らせ管理 / URL管理 / サイト設定 / ページ設定** を表示し、全体設定に **サイト一覧** を置く。**削除**は `GET site/{site}/delete` の専用確認画面を挟み、**「削除すると元に戻すことはできません。削除します。」チェックボックス必須**で `DELETE site/{site}` を実行する。削除時は管理下ロゴを削除し、サイト配下の会員・バンドル・トラック・レッスン・お知らせは外部キーで削除される。`product_plans.after_purchase_bundle_id` は **`course_bundles.id`** を指す（削除対象サイト配下のコースの結合を参照する商品プランはサイト削除時に **null** へ戻す）。商品プランは **`after_purchase_bundle_id`** を画面で選択できるが、**Stripe / UnivaPay / 銀行振込の購入完了 Webhook から会員作成・自動付与する処理は未接続**（接続時は既存 `member_bundle_grants.source=purchase` を使う想定）。

**ページ設定（4区分）** … `GET site/{site}/page-settings?tab=course|lesson|login|fixed`（`PUT site.page-settings.update`・`tab` クエリで保存対象を分岐）。**コースページ** … `membership_sites.course_page_settings`（JSON）と `dashboard_page_message`（HTML）。ヘッダー配色・ロゴ・ヘッダーメニュー（URL／お知らせ一覧／**固定ページ**・最大5）・トップ説明文・お知らせ/カテゴリ/検索・フッター（色・メニュー最大10）・プレビュー `POST site.page-settings.preview-course`。コースページのヘッダーロゴ・ログインページのロゴ/背景画像は、**既存画像があるときもファイル欄を表示**し、**新しいファイルを選んで保存するだけで差し替え**（旧ファイルはサーバ側で削除）。画像を消すだけのときは削除チェックを使う（サイト設定のロゴと同趣旨）。**レッスンページ** … サイト共通の `lesson_footer_message`（リッチテキスト）・`lesson_page_settings`（JSON: `show_prev_next`・`custom_css` 等）。**個別レッスン**の本文・動画・ドリップ・公開状態等は **コース設定 → レッスン設定**（専用フォーム・`lessons`）。サイト共通の下部案内・前後リンクは本画面の **レッスンページ** タブ（`lesson_footer_message` / `lesson_page_settings`）。閲覧可能条件は **コースの結合付与**（`member_bundle_grants`）＋`status`・`published_at`・ドリップ（コース/レッスンを作っただけでは会員に見えない）。**ログインページ** … `login_page_message`（HTML）・`login_page_settings`（JSON: ページタイトル・ロゴ/背景画像・独自 CSS/JS）。管理画面にフォーム/遷移・共通ヘッダー/フッターの項目は出さない（会員側はフォーム常時表示・ログイン後 `member.dashboard`・ヘッダー/フッター非表示）。会員 URL は `/member/{slug}/login`。**固定ページ** … テーブル `membership_site_fixed_pages`（`title`・`slug`・`body`・`status`・`show_in_nav`・`access_type`・`sort_order` 等）。`slug` は **タイトルから自動生成**（`MembershipSiteFixedPageSlug`・日本語のみのときは `page` 系）。管理画面で「URLを自分で指定する」をオンにしたときだけ手入力。SEO/OGP/独自 CSS/JS の管理項目は **出さない**（DB列は残置・保存時は既定値）。`show_in_nav=true` かつ公開中のページは会員レイアウト **左メニュー**（`member/partials/_sidebar_nav`・`MemberLayoutComposer`）に表示（閲覧権限でフィルタ）。管理は **ページ設定 → 固定ページ** と `site.fixed-pages.*`。公開 URL は `/member/{slug}/page/{pageSlug}`。**未実装（設計上の拡張）** … レッスン URL のスラッグルート・レッスン SEO・新規登録導線・パスワード再設定。コメント・視聴完了連動・ラベル条件公開は **コース/レッスン設定側で実装済み**（本節「コース基本設定」「レッスン設定」参照）。

- 登録・解除フォーム設定の **HTML／JavaScript 挿入** は管理画面では **テキストエリア入力**（UTAGE のような WYSIWYG エディタは未接続）。挿入内容は **信頼できる管理者のみ** が編集する前提（公開ページではエスケープしない箇所あり）。
- LINE登録ページの **QR生成** は外部 **QuickChart** 依存（オフライン・ブロック時は画像が出ない）。**登録経路（LINE）**は URL パラメータを Webhook に直接渡せないため、**来訪記録＋`follow` 突合せ**で `registration_route` を付ける（上記 `registration-routes` / `scenario_line_route_visit_intents`）。**同時に複数の未処理流入**があると自動では紐づけない設計（`.env` で FIFO 強制可・誤紐づけリスクあり）。**既存友だちが URL 非経由のまま**の場合は従来どおり経路なし／別経路の読者行のままになり得る（運用で URL を踏ませる）。
- 取得先の Bot 対策・認証・完全 SPA には非対応の場合あり。
- UTAGE 変換はヒューリスティック。複雑なレイアウトは編集で補正が必要なことがある。
- UTAGE 動画の `m3u8` 等は環境によってプレビュー再生できないことがある。
- **イベント予約**: 流入元の自動判定はなく **`rt` 等の URL パラメータ記録**のみ。Google カレンダー双方向同期・決済後のみ予約開放・イベント日時基準のステップ配信エンジンは **未実装**（リマインダ配信はメールのみ実装）。
- **会員サイト**: 会員登録のセルフサービス（申込フォーム）・購入後のログイン情報メール自動送信・Stripe/UnivaPay/銀行振込の購入完了からの自動付与・動画ホスティング・学習進捗の詳細分析・コミュニティ機能は **未実装または最小限**。動画は YouTube / Vimeo / 直リンク / 埋め込み HTML の外部配信を前提に表示する。レッスン HTML と埋め込みHTMLは **XSS 対策の自動サニタイズなし**（管理者信頼前提）。

---

## 11. 受け入れ確認チェックリスト（回帰用）

### 管理ログイン（認証）

- [ ] `GET /login` でログイン画面が表示され、フォームが **空でない**（白画面でない）
- [ ] `GET /register` で新規登録画面が表示される（**500 にならない**）
- [ ] 正しい認証情報で `POST /login` 後に `/dashboard` へ遷移する

### ファネル

- [ ] テンプレ17件が作成画面に表示される
- [ ] 選択テンプレどおりページ数が生成される

### ページ追加・取り込み

- [ ] 取り込み OFF で URL 未入力でも作成できる
- [ ] 取り込み ON で URL 未入力はバリデーションエラー
- [ ] HTTP 失敗時、ページレコードが増えない
- [ ] UTAGE 変換失敗時、**HTMLのみでも作成する** OFF ならページが作られない
- [ ] **取り込みを確認** で `parse_mode`・セクション/ブロック数が返る
- [ ] 再取り込み（↻）で `import_parse_mode` が更新される
- [ ] UTAGE 公開 URL でブロックが極端に空にならない（代表URLで確認）
- [ ] チェックB OFF で外部リンク・画像が抑止される（分岐Bおよびサービス内ポリシー）

### プレビュー

- [ ] セクションのグラデーション背景が「見えない」にならない
- [ ] 白文字テキストが背景付きで識別できる
- [ ] 編集画面で変更後に「プレビュー」を押すと、保存処理の成功後に最新内容で開く（キャッシュで古い内容を表示しない）
- [ ] プレビュー上部バーの「最終保存時刻」が編集保存後に更新され、最新データ表示の判別に使える
- [ ] セクション背景画像の不透明度は `1` を 100% として扱い、意図せず全体が薄くならない

### エディタ

- [ ] 「+ ブロックを追加」でモーダルが開き、テキスト以外も選べる
- [ ] Undo / Redo ボタンで編集履歴を戻す / やり直すができる（`Ctrl+Z` / `Ctrl+Shift+Z` / `Ctrl+Y`）
- [ ] ヘッドライン/サブヘッド/テキストは、HTML文字列入力ではなくキャンバス直接編集を基本操作にする
- [ ] セクション内追加でレイアウト行が出ない
- [ ] 2/3/4カラムのプロパティで列幅比率（%）を変更できる
- [ ] セクション内の各ブロック間（前後）に「+ ブロックを追加」が表示され、挿入位置を指定して追加できる
- [ ] リッチツールバーの「フォント / サイズ / 字間」選択が編集キャンバス上で反映される
- [ ] ヘッドライン／サブヘッド／テキスト／ボレットの文字サイズが「デフォルト + 8px〜100px」選択で反映される
- [ ] ヘッドライン／サブヘッド／テキスト／ボレットの「スマホの文字サイズ」が既定「PCと同じサイズ」で、8px〜100px 指定時は SP 幅（767px 以下）でエディタ・プレビュー・公開ページに反映される
- [ ] 色設定でカラーピッカーとテキスト値が相互連動する
- [ ] 画像／動画／音声／PDF ブロックのクリックでメディア選択モーダルが開く
- [ ] 画像／動画／音声／PDF ブロックで、**未設定時のみ**クリックでモーダルが開く（設定済みは開かない）
- [ ] メディアモーダルで画像・動画/音声・PDFのタブ切替表示ができる
- [ ] 音声で m4a / mp4 を選択でき、音声ブロックに適用できる
- [ ] 音声のループ/コントロール設定が編集画面と公開ページの `<audio>` 出力に反映される
- [ ] 音声の自動再生設定は公開ページでのみ反映され、編集キャンバスでは自動再生しない
- [ ] PDF選択後、編集画面とプレビューでブロック内に埋め込み表示される（`iframe`）
- [ ] margin / padding が **0px〜100px の選択式**で変更できる（全ブロック共通）
- [ ] margin / padding の変更が**編集キャンバスに即時反映**される
- [ ] ヘッドライン／サブヘッド／テキスト／ボレットで背景・枠線が設定できる
- [ ] ヘッドライン等で背景が単色／グラデーションのとき、「背景帯の太さ（上下）」で帯の高さがエディタ・プレビュー・公開ページに反映される
- [ ] ボレットの記号が5種類から選べ、編集画面とプレビューで一致する
- [ ] 保存・再読み込みで JSON が保持される

### 通知

- [ ] 成功メッセージが二重に出ない（一覧系画面）

### 共通レイアウト

- [ ] 上部の機能ナビ（`module-nav`）が各画面で表示される（**ファネル構築の横に「シナリオ作成」** を置く。**メール・LINE配信は上部 `module-nav` に置かない**。送信用アカウント一覧は左メニュー「配信」または初期設定のメール／LINEカードから開く）
- [ ] 配信 / 会員サイト / イベント / パートナーで、左サイドバーが機能別メニューに切り替わる
- [ ] ファネル画面では従来の全体メニュー導線を維持する
- [ ] 送信用アカウント一覧でグループ管理（追加・名称変更・削除）が動作する
- [ ] 送信用アカウント一覧で表示順変更が保存され、再読み込み後も順序が保持される
- [ ] 送信用アカウント行のグループ選択変更が保存され、グループ見出しに反映される
- [ ] メール配信対応アカウント（email/both）で送信者名・送信元メールアドレス未設定時に保存できない
- [ ] 送信用アカウント一覧でメール設定未完了の警告が表示される
- [ ] 配信メニューの DKIM・DMARC認証設定画面が開き、認証ドメインの追加/削除ができる
- [ ] DKIM・DMARC認証設定画面でドメインごとの DNS 設定値（CNAME/TXT）が表示される
- [ ] シナリオ一覧でシナリオグループ管理（追加・名称変更・削除）が動作する
- [ ] シナリオ一覧で表示順変更が保存され、再読み込み後も順序が保持される
- [ ] シナリオ行のグループ選択変更が保存され、グループ見出しに反映される
- [ ] シナリオ作成画面（`scenario.global.index`）は通常見出し **シナリオ作成** を表示し、メール・LINE配信の中項目タブを出さない
- [ ] シナリオ一覧の「アーカイブ済み」タブで `is_archived=1` のデータが表示される
- [ ] シナリオ設定（`scenario-settings`）でシナリオ名・メモ・グループ・アーカイブおよび `scenario_extended_settings` の各項目を保存できる（空のグループは未分類として `group_id=null`）
- [ ] 独自置き換え文字（`placeholders`）で管理名称・共通値・シナリオ固有の利用/上書きを保存でき、定義の削除でアカウント配下から除去される（予約語／読者キー衝突は拒否）
- [ ] 登録フォーム・読者項目で既定項目が表示され、保存・並び替え・フォーム利用/必須の切替ができる
- [ ] 登録フォーム・読者項目でカスタム項目を追加・削除できる（既定項目は削除不可）
- [ ] `/r/{uid}/register` で公開フォームが開き、POST で読者が作成される
- [ ] `POST /api/r/{uid}/register` で JSON 登録ができる（重複メールは 422）
- [ ] 独自ドメイン詳細で、トップに指定したファネルページが未公開の場合に警告が出て、「このトップページを公開する」で当該 `funnel_pages.status` が `public` になり、独自ドメイン `/` が 200 で表示される
- [ ] 読者一覧で検索/ステータス/**登録経路**絞り込みが動作し、ページネーション遷移でも条件が維持される
- [ ] 読者一覧で手動追加（メール必須・同一シナリオ内メール重複不可）・複数選択の一括有効/停止・削除が動作する
- [ ] 読者一覧・ステップ配信で、URL の送信用アカウントと異なる `scenario_id` を指定すると 404 になる
- [ ] **メール一斉送信**（`scenario.broadcast.*`）で一覧・作成・詳細が開き、`POST` 後にキューワーカー起動時のみ送信カウントが進む（ワーカー停止時は `processing` のまま等）
- [ ] メール以外の古い `step_messages` がある場合、一覧にレガシー行が出て削除できる／`step.edit` 直叩きはメール以外なら一覧へ案内される
- [ ] **イベント**: `event.workspace`（日程設定）で枠追加・一括生成・削除ができ、イベント設定・申込者画面が開ける。`is_published` 時に `GET /e/{uid}` が表示される
- [ ] **イベント予約**: 公開フォームから POST でき、サンクス表示・（メール設定があれば）確認メールが送られる
- [ ] **会員サイト**: `site/{id}` でコースのサムネイル・公開状態を保存でき、`workspace/lessons` でグループ別レッスンを保存できる。`workspace/groups` でグループ管理。登録受講生・受講状況・コース基本設定へ遷移できる
- [ ] **会員サイト**: `site/{id}/members` で会員追加・コースの結合の付与/削除ができ、`/member/{slug}/login` でログイン後に公開済みの付与コースのレッスンがドリップ条件に従い表示される
- [ ] **会員サイト**: 選択中サイトの左メニューに **コース / コースの結合 / 受講生管理 / お知らせ管理 / URL管理 / サイト設定 / ページ設定** が表示され、各画面へ遷移できる
- [ ] **会員サイト**: お知らせ管理で公開中のお知らせを保存すると、会員マイページに表示される。ページ設定は **コースページ / レッスンページ / ログインページ / 固定ページ** の4区分で編集でき、コース一覧・レッスン下部・ログイン画面・固定ページ URL に反映される
- [ ] **会員サイト**: コースページ設定で、ヘッダー配色・ロゴ/メニュー表示・トップ説明文・お知らせ・カテゴリ/検索・背景色・文字サイズ・表示形式（カード/リスト）を保存すると、`member.dashboard` に反映される
- [ ] **会員サイト**: サイト一覧またはサイト設定から削除確認画面へ進め、チェックボックス未選択では削除できず、選択時のみ削除される。商品プランのコースの結合紐づけは削除時に未設定へ戻る
- [ ] **商品連携**: `product/{id}/plan/create`・`plan/{id}/edit` で「購入後に開放するコース」を保存できる（テレコム入金確定後の自動付与は `OrderPurchaseFulfillmentService`）
- [ ] **会社情報**: `GET /business` で複数会社を追加・編集でき、既定会社を選べる。ファネル作成/編集・シナリオ追加で「使用する会社」を選べ、保存後に一覧へ会社名が表示される

---

## 12. 仕様変更時の運用

- 開発開始前の確認は、単に本書を開くことでは完了しない。コード編集前に **(1) 読んだ正本と章、(2) 変更前に守る既存挙動、(3) 今回の受け入れ条件、(4) デグレしやすい箇所、(5) 検証計画** を短く残す。これが書けない場合は、仕様確認またはコード調査が不足している。
- 本書・マニュアル・Cursor Rule に確認観点が無く、同種のデグレが再発しうると分かった場合は、実装と同じ差分で確認観点を追記する。追記先は原則 **本書の該当節と §11 の受け入れ確認チェックリスト**、利用手順に影響する場合は `docs/manual.md`。
- 挙動を変えたら **本書 `docs/funnel-spec.md` を更新**し、§11 のチェックの追加・修正を行う（§0 の認証・キャッシュ注記に影響する場合は当該節も更新）。
- 利用手順に触れる場合は **`docs/manual.md`** も更新する。
- ドキュメント全体の索引・各ファイルの役割を変えたら **`docs/README.md`**（およびルート `SPEC.md`）を更新する。
- サーバ反映前は `git status` / `git diff` で修正対象を比較し、意図しない差分がないことを確認してから反映する。
- 開発・反映は次の必須フローで行う。
  1) 修正する
  2) 修正前バージョンと比較して修正箇所をチェックする
  3) アップロードする
  4) 確認する（問題があれば再度修正）
  5) 報告する
