# 機能設計書 13-ページ編集

## 概要

本ドキュメントは、LEGACY CMSにおける既存固定ページの編集機能について記述する。この機能は管理画面において、既存ページのタイトル、本文、URLスラッグ、セクション情報を編集し、更新を保存する。公開済みページの場合は検索インデックスも自動更新する。

### 本機能の処理概要

**業務上の目的・背景**：ウェブサイトの固定ページ（会社概要、利用規約など）のコンテンツを維持管理するため、管理者が既存ページの内容を編集・更新する必要がある。また、コンテンツ更新時に検索機能との整合性を保つため、検索インデックスの自動更新も行う。

**機能の利用シーン**：管理者がページ一覧から特定のページを選択し編集画面を開く。FCKeditorを使用したリッチテキストエディタでコンテンツを編集し、保存ボタンでAjaxダイアログ経由で更新を実行する。

**主要な処理内容**：
1. ユーザーの権限チェック（ppages + ppageedit権限の確認）
2. editAction：編集画面の表示とページデータの取得
3. saveAction：フォームデータのバリデーションと更新処理
4. detailsAction：ページ詳細情報のAjax取得
5. 公開済みページの場合、Zend_Search_Luceneの検索インデックス更新

**関連システム・外部連携**：
- Zend_Search_Lucene：全文検索インデックスの更新
- FCKeditor：リッチテキストエディタによるコンテンツ編集

**権限による制御**：`ppages`および`ppageedit`リソースへのアクセス権限が必要。保護ページ（page_protected='Y'）の場合、URLとセクションは読み取り専用となる。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 61 | ページ編集画面 | 主画面 | ページ内容の編集フォーム |
| 60 | ページ管理画面 | 呼び出し元画面 | 一覧からの編集リンク |
| 63 | ページ詳細画面 | 参照画面 | Ajax経由での詳細表示 |

## 機能種別

CRUD操作（Update）/ データ更新 / リッチテキスト編集

## 入力仕様

### 入力パラメータ（editAction）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | integer | Yes | 編集対象ページID | 数値のみ |

### 入力パラメータ（saveAction）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | integer | Yes | 更新対象ページID | URLパラメータ |
| title | string | Yes | ページタイトル | NotEmpty |
| content | string | Yes | ページ本文（HTML） | NotEmpty |
| slug | string | Yes | URLスラッグ | NotEmpty, Alnum(スペース許可), 重複チェック |
| section | string | No | セクション名 | 任意 |

### 入力データソース

- URLパラメータ（ページID）
- POSTリクエストボディ（フォームデータ）
- セッション（ログインユーザー情報、ACL情報）

## 出力仕様

### 出力データ（editAction）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| pageArray | array | ページ情報の連想配列 |

### ページ情報の内容

| 項目名 | 型 | 説明 |
|--------|-----|------|
| page_id | integer | ページID |
| page_title | string | ページタイトル |
| page_content | string | ページ本文（HTML） |
| page_slug | string | URLスラッグ |
| page_section | string | セクション名 |
| page_status | string | 公開ステータス |
| page_protected | string | 保護フラグ |
| page_date | datetime | 作成日時 |
| page_edit | datetime | 最終編集日時 |
| page_published | datetime | 公開日時 |
| user_alias | string | 著者エイリアス名 |

### 出力先

- 編集画面ビューテンプレート（`application/modules/admin/views/scripts/pages/edit.phtml`）
- Ajaxダイアログへのレスポンス（saveAction）
- 検索インデックス（Zend_Search_Lucene）

## 処理フロー

### 処理シーケンス（editAction）

```
1. editAction()が呼び出される
   └─ レイアウト設定（admin）

2. 権限チェック
   ├─ ppages権限チェック
   └─ ppageedit権限チェック
      ├─ 権限なし → privileges画面へフォワード
      └─ 権限あり → 次のステップへ

3. パラメータ取得
   └─ idパラメータからページIDを取得

4. ページデータ取得
   └─ Pages::fetchPage($id)
      ├─ データなし → manage画面へリダイレクト
      └─ データあり → ビューへ設定

5. 編集画面表示
   └─ edit.phtml描画
```

### 処理シーケンス（saveAction）

```
1. saveAction()が呼び出される
   └─ レイアウト無効化、ビュー無効化

2. 権限チェック（ppages + ppageedit）

3. パラメータ・バリデーション
   └─ Zend_Filter_Inputによる検証
      ├─ title: NotEmpty
      ├─ content: NotEmpty
      ├─ slug: NotEmpty, Alnum, DB重複チェック（自身除外）
      └─ section: 任意

4. バリデーション結果判定
   ├─ 失敗 → エラーメッセージ表示
   └─ 成功 → 次のステップへ

5. ページ更新処理
   └─ Pages::updatePage()
      ├─ UPDATE文実行
      ├─ ステータスがpublishedの場合
      │    └─ Search::updateEntry() で検索インデックス更新
      └─ 完了

6. 結果表示
   └─ 成功メッセージとCloseボタン表示
```

### フローチャート

```mermaid
flowchart TD
    A[開始: editAction] --> B{ppages+ppageedit権限あり?}
    B -->|No| C[privileges画面へフォワード]
    B -->|Yes| D[idパラメータ取得]
    D --> E[Pages::fetchPage]
    E --> F{データあり?}
    F -->|No| G[manage画面へリダイレクト]
    F -->|Yes| H[edit.phtml描画]

    I[開始: saveAction] --> J{権限チェック}
    J -->|No| K[privileges画面へフォワード]
    J -->|Yes| L[バリデーション実行]
    L --> M{バリデーション成功?}
    M -->|No| N[エラーメッセージ表示]
    M -->|Yes| O[Pages::updatePage]
    O --> P{page_status=published?}
    P -->|Yes| Q[Search::updateEntry]
    P -->|No| R[成功メッセージ表示]
    Q --> R
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-01 | タイムスタンプ更新 | 保存時に最終編集日時を更新 | 常時 |
| BR-02 | URLスラッグ一意性 | スラッグは他ページと重複不可 | 常時（自身は除外） |
| BR-03 | 保護ページ制限 | 保護ページはURL/セクション変更不可 | page_protected='Y' |
| BR-04 | 検索インデックス連動 | 公開済みページは検索インデックスも更新 | page_status='published' |
| BR-05 | HTMLコンテンツ | コンテンツはHTMLエンティティをデコードして保存 | 常時 |

### 計算ロジック

検索インデックスのURL生成：`/page/{slug}/`

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| ページ取得 | pages | SELECT | 編集対象ページ取得 |
| ページ取得 | users | SELECT | 著者情報結合取得 |
| ページ更新 | pages | UPDATE | ページ内容の更新 |

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

#### pagesテーブル（SELECT）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | 全カラム | page_id = {id} | usersテーブルとJOIN |

#### pagesテーブル（UPDATE）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | page_title | 入力されたタイトル | |
| UPDATE | page_content | html_entity_decode(入力された本文) | HTMLデコード |
| UPDATE | page_slug | 入力されたスラッグ | |
| UPDATE | page_section | 入力されたセクション | nullable |
| UPDATE | page_edit | NOW() | 自動設定 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | 権限エラー | ppages/ppageedit権限なし | privileges画面へフォワード |
| - | データなし | 指定IDのページが存在しない | manage画面へリダイレクト |
| IS_EMPTY | バリデーションエラー | タイトルが空 | "Title is required"メッセージ |
| IS_EMPTY | バリデーションエラー | コンテンツが空 | "Content is required"メッセージ |
| IS_EMPTY | バリデーションエラー | URLが空 | "URL is required"メッセージ |
| NOT_ALNUM | バリデーションエラー | URLに不正文字 | "Invalid URL"メッセージ |
| ERROR_RECORD_FOUND | バリデーションエラー | URLが重複 | "URL in use"メッセージ |
| - | ID未指定 | idパラメータなし | "Page Not Specified!"メッセージ |

### リトライ仕様

特になし（ユーザーがフォームを修正して再送信）

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

単一UPDATE操作のため、明示的なトランザクション管理は行っていない。ただし、検索インデックス更新が失敗してもDB更新はロールバックしない。

## パフォーマンス要件

- 単一レコードのSELECT/UPDATE操作
- 検索インデックス更新はファイルI/Oを伴うため、やや時間がかかる可能性あり

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

- ACLによる権限チェック（ppages + ppageeditリソース）
- Zend_Filter_Inputによる入力バリデーション
- Zend_Validate_Db_NoRecordExistsによるスラッグ重複チェック（SQLインジェクション対策込み）
- 保護ページのURL/セクション変更はUI側でreadonly属性により制限

## 備考

- FCKeditor（WYSIWYG HTMLエディタ）を使用
- Dojo Toolkitのタブコンテナでページ/タグを切り替え
- タグ管理機能との連携（tagspane.phtml partial）

---

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

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

### 推奨読解順序

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

ページデータの構造と更新対象カラムを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | database.sql | `database.sql` | pagesテーブル定義（206-220行目）、page_slugのUNIQUE制約 |
| 1-2 | Pages.php | `application/models/Pages.php` | fetchPage()、updatePage()メソッド |

**読解のコツ**: `page_slug`にUNIQUE制約があり、バリデーションでZend_Validate_Db_NoRecordExistsを使用して重複チェックしている。

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

editActionとsaveActionの2つのアクションを確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | PagesController.php | `application/modules/admin/controllers/PagesController.php` | editAction（54-76行目）、saveAction（126-211行目） |

**主要処理フロー（editAction）**:
1. **56行目**: ppages + ppageedit権限の複合チェック
2. **58行目**: idパラメータ取得
3. **63-64行目**: Pages::fetchPage()でページデータ取得
4. **66-68行目**: データなしの場合リダイレクト

**主要処理フロー（saveAction）**:
1. **128行目**: 権限チェック
2. **130-131行目**: レイアウト/ビュー無効化
3. **141-160行目**: バリデータ定義（title, content, slug, section）
4. **156行目**: Zend_Validate_Db_NoRecordExistsで重複チェック（自身除外）
5. **166-172行目**: Pages::updatePage()呼び出し
6. **174-179行目**: 成功メッセージ出力

#### Step 3: モデル層の処理を理解する

Pagesモデルの更新処理と検索インデックス連携を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | Pages.php | `application/models/Pages.php` | fetchPage()（146-160行目）、updatePage()（207-237行目） |

**主要処理フロー（updatePage）**:
- **209-215行目**: 更新データ配列の構築
- **211行目**: html_entity_decode()でHTMLエンティティをデコード
- **217行目**: UPDATE文実行
- **219行目**: fetchPage()で更新後データを再取得
- **221-236行目**: 公開済みの場合、Search::updateEntry()で検索インデックス更新

#### Step 4: ビューテンプレートを理解する

編集画面のUI実装を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | edit.phtml | `application/modules/admin/views/scripts/pages/edit.phtml` | 編集フォームのテンプレート |
| 4-2 | details.phtml | `application/modules/admin/views/scripts/pages/details.phtml` | 詳細情報Ajax取得用 |

**主要処理フロー（edit.phtml）**:
- **35行目**: FCKeditor PHPクラスのinclude
- **39行目**: Ajax経由でdetails.phtml読み込み
- **41-119行目**: タブコンテナ構成
- **44-117行目**: ページ編集フォーム
- **67-74行目**: FCKeditorの初期化とコンテンツ設定
- **82-94行目**: URLスラッグ入力（保護ページは読み取り専用）
- **121行目**: タグ管理partialのinclude

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

```
一覧画面 [Editリンククリック]
    │
    └─ PagesController::editAction()
           │
           ├─ ACL::isAllowed() [権限チェック x2]
           │
           └─ Pages::fetchPage($id)
                  │
                  └─ edit.phtml表示
                         ├─ FCKeditor初期化
                         ├─ detailsAction() [Ajax]
                         └─ タグパーシャル

edit.phtml [Saveボタンクリック]
    │
    └─ postDialog('/admin/pages/save/id/{id}/')
           │
           └─ PagesController::saveAction()
                  │
                  ├─ ACL::isAllowed() [権限チェック]
                  │
                  ├─ Zend_Filter_Input [バリデーション]
                  │
                  └─ Pages::updatePage()
                         │
                         ├─ Zend_Db::update() [DB更新]
                         │
                         └─ [公開済みの場合]
                                │
                                └─ Search::updateEntry() [検索インデックス更新]
```

### データフロー図

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

ページID (GET)    ───▶ editAction              ───▶ edit.phtml
                         └─ Pages::fetchPage()        ├─ フォーム表示
                                                      └─ FCKeditor

                                    │
                                    ▼

フォームデータ     ───▶ saveAction              ───▶
(title,content,         ├─ バリデーション              ├─ [成功] 成功メッセージ
 slug,section)          └─ Pages::updatePage()        └─ [失敗] エラーメッセージ
                                    │
                                    ▼
                          pagesテーブル UPDATE
                                    │
                                    ▼ [公開済みの場合]
                          Search::updateEntry()
                          (Luceneインデックス更新)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| PagesController.php | `application/modules/admin/controllers/PagesController.php` | コントローラー | editAction, saveAction, detailsAction |
| Pages.php | `application/models/Pages.php` | モデル | fetchPage(), updatePage() |
| Search.php | `application/models/Search.php` | モデル | updateEntry()検索インデックス更新 |
| edit.phtml | `application/modules/admin/views/scripts/pages/edit.phtml` | ビュー | 編集画面テンプレート |
| details.phtml | `application/modules/admin/views/scripts/pages/details.phtml` | ビュー | 詳細情報Ajax取得用 |
| tagspane.phtml | `application/modules/admin/views/scripts/_partials/tagspane.phtml` | ビュー | タグ管理パーシャル |
| fckeditor.php | `public/_scripts/fckeditor/fckeditor.php` | ライブラリ | FCKeditor PHPクラス |
| database.sql | `database.sql` | スキーマ | pagesテーブル定義 |
