# 機能設計書 30-レビュー投稿

## 概要

本ドキュメントは、Shopwareストアフロントにおけるレビュー投稿機能の設計を記述したものである。本機能は、ログインした顧客が商品に対してレビュー（評価と感想）を投稿する機能を提供する。

### 本機能の処理概要

レビュー投稿機能は、顧客が商品に対して評価（1〜5点）とタイトル、内容を入力してレビューを投稿し、データベースに保存する機能を提供する。

**業務上の目的・背景**：ECサイトにおいて、商品レビューは購買意思決定の重要な要素である。顧客が実際に商品を使用した感想を共有することで、他の顧客の購買判断を支援し、サイト全体の信頼性向上に寄与する。また、レビューはUGC（ユーザー生成コンテンツ）としてSEO効果も期待できる。

**機能の利用シーン**：
- 購入した商品の感想を共有したい場合
- 他の顧客に商品の良し悪しを伝えたい場合
- 商品に対する評価（星）をつけたい場合
- 既存のレビューを編集・更新したい場合

**主要な処理内容**：
1. レビューデータのバリデーション
2. 顧客情報の自動補完（名前、メール）
3. product_reviewテーブルへの保存（upsert）
4. ReviewFormEventの発火（メール通知用）
5. レビュー一覧画面へのフォワード

**関連システム・外部連携**：
- AbstractProductReviewSaveRoute: レビュー保存API
- EventDispatcher: ReviewFormEventによるメール通知
- SystemConfigService: レビュー機能の有効/無効設定

**権限による制御**：
- ログイン必須（LOGIN_REQUIRED属性）
- XmlHttpRequest（AJAX）として動作

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 3 | 商品詳細ページ | 主画面 | レビュー投稿フォーム（ProductController::saveReview） |

## 機能種別

CRUD操作（Create/Update） / AJAX / フォーム処理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| productId | string | Yes | 商品ID（URLパラメータ） | UUID形式 |
| title | string | Yes | レビュータイトル | 必須、最小5文字 |
| content | string | Yes | レビュー内容 | 必須、最小40文字 |
| points | int | Yes | 評価点数 | 1以上5以下 |
| name | string | No | 投稿者名 | 未指定時は顧客の名を使用 |
| email | string | No | 投稿者メール | 未指定時は顧客のメールを使用 |
| lastName | string | No | 投稿者姓 | 未指定時は顧客の姓を使用 |
| id | string | No | レビューID（更新時） | UUID形式、既存レビューの所有者チェック |
| parentId | string | No | 親レビューID（返信時） | UUID形式 |

### バリデーションルール詳細

| 項目 | ルール | 説明 |
|-----|-------|------|
| name | NotBlank | 空でないこと |
| title | NotBlank, Length(min: 5) | 空でなく、5文字以上 |
| content | NotBlank, Length(min: 40) | 空でなく、40文字以上 |
| points | GreaterThanOrEqual(1), LessThanOrEqual(5) | 1〜5の範囲 |
| id（更新時） | EntityExists(product_review, customerId一致) | 既存レビューで顧客所有 |
| customerId（新規時） | EntityNotExists(product_review, productId+customerId) | 同一商品への重複投稿防止 |

### 入力データソース

- URLルートパラメータ（productId）
- RequestDataBag（POSTデータ）
- SalesChannelContext（顧客情報、販売チャネル情報）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| success | int | 結果ステータス（1: 新規成功, 2: 更新成功, -1: バリデーションエラー） |
| formViolations | ConstraintViolationException | バリデーションエラー詳細（エラー時のみ） |

### 出力先

- HTMLレスポンス（frontend.product.reviewsへのフォワード）
- Twigテンプレート: `storefront/component/review/review.html.twig`

## 処理フロー

### 処理シーケンス

```
1. saveReview（POST /product/{productId}/rating）
   └─ productIdをURLから取得
   └─ ログイン状態を確認（LOGIN_REQUIRED）
   └─ ProductReviewSaveRoute.saveを呼び出し
      └─ レビュー機能の有効性確認（core.listing.showReview）
      └─ 顧客情報の取得（名前、メール）
      └─ 入力データの補完
      └─ バリデーション実行
      └─ product_reviewテーブルへupsert
      └─ ReviewFormEvent発火
   └─ バリデーションエラー時: success=-1でフォワード
   └─ 成功時: success=1（新規）または2（更新）でフォワード
   └─ frontend.product.reviewsへフォワード
```

### フローチャート

```mermaid
flowchart TD
    A[レビュー投稿リクエスト] --> B{ログイン状態?}
    B -->|No| C[ログインページへリダイレクト]
    B -->|Yes| D[ProductReviewSaveRoute.save]
    D --> E{レビュー機能有効?}
    E -->|No| F[ReviewNotActiveException]
    E -->|Yes| G[顧客情報補完]
    G --> H[バリデーション実行]
    H --> I{バリデーション成功?}
    I -->|No| J[ConstraintViolationException]
    J --> K[success=-1でフォワード]
    I -->|Yes| L{更新 or 新規?}
    L -->|新規| M{重複チェック}
    M -->|重複あり| J
    M -->|重複なし| N[product_review INSERT]
    L -->|更新| O{所有者チェック}
    O -->|不一致| J
    O -->|一致| P[product_review UPDATE]
    N --> Q[ReviewFormEvent発火]
    P --> Q
    Q --> R{id指定あり?}
    R -->|Yes| S[success=2でフォワード]
    R -->|No| T[success=1でフォワード]
    S --> U[レビュー一覧表示]
    T --> U
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-30-01 | ログイン必須 | レビュー投稿にはログインが必要 | 常時 |
| BR-30-02 | レビュー機能有効性 | SystemConfigの`core.listing.showReview`が有効であること | 常時 |
| BR-30-03 | 重複投稿防止 | 同一顧客が同一商品に複数のレビューを投稿できない（更新のみ可能） | 新規投稿時 |
| BR-30-04 | 所有者チェック | レビューの更新は投稿者本人のみ可能 | 更新時 |
| BR-30-05 | 初期ステータス | 新規投稿時のステータスはfalse（非公開）| 常時 |
| BR-30-06 | 顧客情報自動補完 | name, lastName, emailが未指定の場合、顧客情報から自動補完 | 常時 |
| BR-30-07 | XmlHttpRequest | AJAX呼び出しとして動作 | 常時 |

### 計算ロジック

特になし

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| レビュー保存 | product_review | UPSERT | レビューデータの新規作成または更新 |

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

#### product_review

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPSERT | id | 指定時は更新、未指定時は自動生成 | 主キー |
| UPSERT | product_id | {productId} | 対象商品ID |
| UPSERT | customer_id | {customerId} | 投稿者顧客ID |
| UPSERT | sales_channel_id | {salesChannelId} | 販売チャネルID |
| UPSERT | language_id | {languageId} | 言語ID |
| UPSERT | external_user | {name} | 表示名 |
| UPSERT | external_email | {email} | 連絡先メール |
| UPSERT | title | {title} | レビュータイトル |
| UPSERT | content | {content} | レビュー内容 |
| UPSERT | points | {points} | 評価点数（1-5） |
| UPSERT | status | false | 公開ステータス（初期は非公開） |

### テーブル構造

#### product_review

| カラム名 | 型 | NULL | 説明 |
|---------|-----|------|------|
| id | BINARY(16) | NO | 主キー |
| product_id | BINARY(16) | NO | 商品ID |
| product_version_id | BINARY(16) | NO | 商品バージョンID |
| customer_id | BINARY(16) | YES | 顧客ID |
| sales_channel_id | BINARY(16) | NO | 販売チャネルID |
| language_id | BINARY(16) | NO | 言語ID |
| external_user | VARCHAR(255) | YES | 外部ユーザー名 |
| external_email | VARCHAR(255) | YES | 外部メールアドレス |
| title | VARCHAR(255) | NO | タイトル |
| content | LONGTEXT | NO | 内容 |
| points | DOUBLE | YES | 評価点数 |
| status | TINYINT(1) | YES | 公開ステータス |
| comment | LONGTEXT | YES | 管理者コメント |
| custom_fields | JSON | YES | カスタムフィールド |
| created_at | DATETIME(3) | NO | 作成日時 |
| updated_at | DATETIME(3) | YES | 更新日時 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| REVIEW_NOT_ACTIVE | 機能無効 | core.listing.showReviewがfalse | StorefrontException::reviewNotActive()をスロー |
| CONSTRAINT_VIOLATION | バリデーションエラー | 入力値が制約に違反 | success=-1でレビューフォームに戻る |
| ENTITY_NOT_EXISTS | レビュー不在 | 更新時に指定IDのレビューが存在しないまたは顧客不一致 | バリデーションエラーとして処理 |
| ENTITY_EXISTS | 重複投稿 | 同一顧客が同一商品に既にレビュー済み | バリデーションエラーとして処理 |

### リトライ仕様

リトライ処理は行わない。バリデーションエラー時はフォーム画面に戻り、ユーザーに修正を促す。

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

DALのupsertメソッドにより、単一トランザクションでレビューデータが保存される。

## パフォーマンス要件

- レスポンス: 500ms以内
- XmlHttpRequestによるページ全体の再読み込み不要
- 重複チェックはインデックスを使用した効率的なクエリ

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

- ログイン必須（LOGIN_REQUIRED属性）
- 所有者チェックによる他者のレビュー編集防止
- EmailIdnConverterによるIDNメールアドレスのエンコード
- XSS対策（Twigの自動エスケープ）
- バリデーションによる不正データの排除

## 備考

- 投稿されたレビューは初期状態で非公開（status=false）、管理者による承認後に公開される
- ReviewFormEventはフロービルダーでメール通知などのアクションをトリガーできる
- v6.8.0.0以降、ReviewNotActiveExeptionの処理方法が変更される
- 同一商品への複数レビューは不可（更新のみ）

---

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

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

### 推奨読解順序

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

レビューのデータ構造を理解。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | ProductReviewEntity.php | `src/Core/Content/Product/Aggregate/ProductReview/ProductReviewEntity.php` | レビューエンティティの構造（productId, customerId, title, content, points, status） |
| 1-2 | ProductReviewDefinition.php | `src/Core/Content/Product/Aggregate/ProductReview/ProductReviewDefinition.php` | テーブル定義、フィールド制約 |

**読解のコツ**: ProductReviewEntityは商品、顧客、販売チャネル、言語とのリレーションを持つ。statusフィールドが公開/非公開を制御する。

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

処理の起点となるコントローラーを確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | ProductController.php | `src/Storefront/Controller/ProductController.php` | saveReviewメソッド |

**主要処理フロー**:
1. **145-194行目**: `saveReview`メソッド - レビュー投稿処理
2. **148-151行目**: Route属性（XmlHttpRequest、LOGIN_REQUIRED）
3. **156-180行目**: ProductReviewSaveRoute.save呼び出しとエラーハンドリング
4. **159-165行目**: バリデーションエラー時のフォワード処理
5. **182-193行目**: 成功時のフォワード処理（success=1 or 2）

#### Step 3: レビュー保存ルートを理解する

レビュー保存の実装を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | ProductReviewSaveRoute.php | `src/Core/Content/Product/SalesChannel/Review/ProductReviewSaveRoute.php` | レビュー保存の実装 |

**主要処理フロー**:
- **64-129行目**: `save`メソッド - レビュー保存処理
- **66-69行目**: レビュー機能の有効性確認
- **71-87行目**: 顧客情報の取得と補完
- **89-91行目**: データ設定とバリデーション呼び出し
- **93-110行目**: レビューデータ構築とupsert
- **112-126行目**: ReviewFormEvent発火

#### Step 4: バリデーションを理解する

バリデーションロジックを確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | ProductReviewSaveRoute.php | `src/Core/Content/Product/SalesChannel/Review/ProductReviewSaveRoute.php` | validateメソッド |

**主要処理フロー**:
- **131-172行目**: `validate`メソッド - バリデーション処理
- **133-139行目**: 基本バリデーション定義（name, title, content, points）
- **141-161行目**: 条件付きバリデーション（新規時は重複チェック、更新時は所有者チェック）

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

```
ProductController
    │
    └─ saveReview (POST /product/{productId}/rating)
           │
           ├─ LOGIN_REQUIRED チェック
           │
           ├─ ProductReviewSaveRoute.save
           │      │
           │      ├─ SystemConfigService.getBool('core.listing.showReview')
           │      │      └─ レビュー機能有効性チェック
           │      │
           │      ├─ 顧客情報補完
           │      │      ├─ customer.getFirstName()
           │      │      ├─ customer.getLastName()
           │      │      └─ customer.getEmail()
           │      │
           │      ├─ validate
           │      │      ├─ NotBlank, Length バリデーション
           │      │      ├─ GreaterThanOrEqual, LessThanOrEqual バリデーション
           │      │      ├─ EntityExists（更新時）
           │      │      └─ EntityNotExists（新規時）
           │      │
           │      ├─ repository.upsert
           │      │      └─ product_review テーブル
           │      │
           │      └─ eventDispatcher.dispatch
           │             └─ ReviewFormEvent
           │
           ├─ ConstraintViolationException キャッチ
           │      └─ success=-1 でフォワード
           │
           └─ forwardToRoute('frontend.product.reviews')
                  └─ success=1（新規）or 2（更新）
```

### データフロー図

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

POST /product/{productId}/rating ──────────────────────────────────► レビュー一覧HTML
(title, content, points)        │
                                ▼
                     ┌─────────────────────┐
                     │ ProductController   │
                     │ ::saveReview        │
                     └─────────┬───────────┘
                               │
                               ▼
                     ┌─────────────────────┐
                     │ ProductReview       │
                     │ SaveRoute.save      │
                     └─────────┬───────────┘
                               │
                               ▼
                     ┌─────────────────────┐
                     │ SystemConfigService │◄──── core.listing.showReview
                     │ (有効性チェック)    │
                     └─────────┬───────────┘
                               │
                               ▼
                     ┌─────────────────────┐
                     │ DataValidator       │
                     │ (バリデーション)    │
                     └─────────┬───────────┘
                               │
                               ▼
                     ┌─────────────────────┐
                     │ EntityRepository    │
                     │ (upsert)            │─────► product_review
                     └─────────┬───────────┘
                               │
                               ▼
                     ┌─────────────────────┐
                     │ EventDispatcher     │
                     │ (ReviewFormEvent)   │─────► メール通知等
                     └─────────┬───────────┘
                               │
                               ▼
                     ┌─────────────────────┐
                     │ forwardToRoute      │
                     │ (frontend.product.  │─────────────────────────► レビュー一覧
                     │  reviews)           │                           (success=1/2/-1)
                     └─────────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| ProductController.php | `src/Storefront/Controller/ProductController.php` | ソース | コントローラー（エントリーポイント） |
| AbstractProductReviewSaveRoute.php | `src/Core/Content/Product/SalesChannel/Review/AbstractProductReviewSaveRoute.php` | ソース | レビュー保存API抽象クラス |
| ProductReviewSaveRoute.php | `src/Core/Content/Product/SalesChannel/Review/ProductReviewSaveRoute.php` | ソース | レビュー保存API実装 |
| ProductReviewEntity.php | `src/Core/Content/Product/Aggregate/ProductReview/ProductReviewEntity.php` | ソース | レビューエンティティ |
| ProductReviewDefinition.php | `src/Core/Content/Product/Aggregate/ProductReview/ProductReviewDefinition.php` | ソース | レビューテーブル定義 |
| ReviewFormEvent.php | `src/Core/Content/Product/SalesChannel/Review/Event/ReviewFormEvent.php` | ソース | レビュー投稿イベント |
| review.html.twig | `src/Storefront/Resources/views/storefront/component/review/review.html.twig` | テンプレート | レビュー表示テンプレート |
