# バッチ設計書 18-YearInReviewPopulateReportData

## 概要

本ドキュメントは、Wikipedia iOSアプリのYear in Review（年間振り返り）機能のレポートデータを生成するバックグラウンドタスク「YearInReviewPopulateReportData」のバッチ設計書です。

### 本バッチの処理概要

このバッチはユーザーの年間活動データを収集・処理し、Year in Review機能で表示するパーソナライズされたスライドデータを生成します。読んだ記事数、編集数、寄付回数、保存した記事、最もよく読んだ日、閲覧数、カテゴリ、位置情報、トップ記事などの様々なスライドデータをCore Dataに保存し、ユーザーが年間の活動を振り返れるようにします。

**業務上の目的・背景**：Year in Review（YiR）機能は、ユーザーのWikipediaでの1年間の活動を視覚的に振り返る機能です。Spotifyの「Wrapped」のようなパーソナライズされた体験を提供し、ユーザーエンゲージメントの向上とWikipediaへの貢献意識を高めることを目的としています。この機能はユーザーの閲覧履歴、編集活動、保存した記事などのローカルデータと、サーバーからの統計データを組み合わせて、個人化されたレポートを生成します。バックグラウンドタスクとして実装されているため、ユーザーの操作を妨げることなくデータ収集・処理を行えます。

**バッチの実行タイミング**：アプリがバックグラウンドに移行した時、またはYear in Review機能へのアクセス時に非同期で実行されます。

**主要な処理内容**：
1. ユーザーの国コードとYear in Review設定を確認
2. リモート設定（Feature Config）からYiR設定を取得
3. 既存のスライドデータをCore Dataから取得
4. 不足しているスライドのデータコントローラーを生成
5. 各スライドのデータを非同期で収集・評価
6. 評価されたスライドをCore Dataに保存
7. WMFYearInReviewReportとして返却

**前後の処理との関連**：Feature Configのロードが前提となります。生成されたレポートデータはYear in Review UIで表示されます。

**影響範囲**：Core Data（CDYearInReviewReport、CDYearInReviewSlide）、UserDefaults設定に影響します。

## バッチ種別

年間レビュー / データ収集・生成

## 実行スケジュール

| 項目 | 内容 |
|-----|------|
| 実行頻度 | 随時（アプリ使用時、バックグラウンド時） |
| 実行時刻 | 不定 |
| 実行曜日 | 不定 |
| 実行日 | 不定 |
| トリガー | Year in Review機能アクセス / バックグラウンド移行 |

## 実行条件

### 前提条件

| 条件 | 説明 |
|-----|------|
| Year in Review設定有効 | yearInReviewSettingsIsEnabled == true |
| 対象国 | ユーザーの国コードがhideCountryCodesに含まれないこと |
| Feature Config有効 | リモート設定が有効期間内であること（isActive） |
| Core Data利用可能 | WMFCoreDataStoreが利用可能であること |

### 実行可否判定

`shouldPopulateYearInReviewReportData(countryCode:)` メソッドで以下を判定：
- yearInReviewSettingsIsEnabled == true
- countryCodeがnilでない
- Feature ConfigのisActive(for: Date()) == true
- hideCountryCodesにユーザーの国コードが含まれない

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | デフォルト値 | 説明 |
|-------------|-----|-----|-------------|------|
| year | Int | Yes | なし | 対象年（例: 2025） |
| countryCode | String | Yes | なし | ユーザーの国コード |
| primaryAppLanguageProject | WMFProject? | No | nil | 主要言語プロジェクト |
| username | String? | No | nil | ログインユーザー名 |
| userID | Int? | No | nil | ユーザーID |
| globalUserID | Int? | No | nil | グローバルユーザーID |
| savedSlideDataDelegate | SavedArticleSlideDataDelegate | Yes | なし | 保存記事データ取得デリゲート |
| legacyPageViewsDataDelegate | LegacyPageViewsDataDelegate | Yes | なし | ページビューデータ取得デリゲート |

### 入力データソース

| データソース | 形式 | 説明 |
|-------------|------|------|
| Core Data | CDYearInReviewReport | 既存のレポートデータ |
| Feature Config | JSON | リモート設定（isActive、hideCountryCodes等） |
| UserDefaults | Key-Value | ユーザー設定（yearInReviewSettingsIsEnabled等） |
| ローカルデータ | 各種 | 保存記事、閲覧履歴等 |
| MediaWiki API | JSON | 編集数、閲覧統計等 |

## 出力仕様

### 出力データ

| 出力先 | 形式 | 説明 |
|-------|------|------|
| Core Data (CDYearInReviewReport) | Managed Object | レポートエンティティ |
| Core Data (CDYearInReviewSlide) | Managed Object | スライドエンティティ |
| 戻り値 | WMFYearInReviewReport? | 生成されたレポート構造体 |

### 出力ファイル仕様

ファイル出力はありません。Core Dataに永続化されます。

## 処理フロー

### 処理シーケンス

```
1. 実行条件チェック
   └─ shouldPopulateYearInReviewReportData で判定
2. バックグラウンドタスク開始
   └─ beginBackgroundTask で延長時間を確保
3. 既存スライドID取得
   └─ Core Dataから対象年のレポートを取得
   └─ 既存スライドIDのSetを作成
4. スライドデータコントローラー生成
   └─ YearInReviewSlideDataControllerFactory を使用
   └─ 不足スライドのコントローラーを生成
5. 各スライドのデータ収集
   └─ populateSlideData を非同期で実行
   └─ 成功時: isEvaluated = true
   └─ 失敗時: isEvaluated = false
6. Core Dataへの保存
   └─ fetchOrCreateでレポート取得/生成
   └─ 評価済みスライドをCDSlideに変換
   └─ shouldFreezeフラグに基づき既存スライドを保持/削除
7. バックグラウンドタスク終了
   └─ endBackgroundTask
8. レポート返却
   └─ WMFYearInReviewReport構造体を返す
```

### フローチャート

```mermaid
flowchart TD
    A[バッチ開始] --> B{shouldPopulateYearInReviewReportData?}
    B -->|No| C[nil返却]
    B -->|Yes| D[beginBackgroundTask]
    D --> E[既存スライドID取得]
    E --> F[SlideDataControllerFactory生成]
    F --> G[不足スライドのコントローラー作成]
    G --> H{各スライドを処理}
    H --> I[populateSlideData実行]
    I --> J{成功?}
    J -->|Yes| K[isEvaluated = true]
    J -->|No| L[isEvaluated = false]
    K --> M{次のスライド}
    L --> M
    M -->|あり| H
    M -->|なし| N[Core Dataにレポート保存]
    N --> O[評価済みスライドをCDSlideに変換]
    O --> P[endBackgroundTask]
    P --> Q[WMFYearInReviewReport返却]
```

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

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

| 処理 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| レポート取得/生成 | CDYearInReviewReport | SELECT/INSERT | 対象年のレポートを取得または生成 |
| スライド取得 | CDYearInReviewSlide | SELECT | 既存スライドIDの取得 |
| スライド削除 | CDYearInReviewSlide | DELETE | shouldFreeze=falseのスライドを削除 |
| スライド追加 | CDYearInReviewSlide | INSERT | 新規評価済みスライドを追加 |
| レポート更新 | CDYearInReviewReport | UPDATE | スライドセットを更新 |

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

#### CDYearInReviewReport

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT/INSERT | year | 対象年（Int32） | fetchOrCreate使用 |
| UPDATE | slides | 評価済みスライドのSet | NSSet型 |

#### CDYearInReviewSlide

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id | スライドID（String） | Set<String>として取得 |
| INSERT | year | 対象年（Int32） | - |
| INSERT | id | スライドID（String） | WMFYearInReviewPersonalizedSlideID.rawValue |
| INSERT | data | スライドデータ（Data?） | JSON等のバイナリデータ |
| DELETE | - | shouldFreeze=falseのスライド | 再評価が必要なスライド |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | CustomError.missingExperimentsDataController | experimentsDataControllerがnil | 正常ケース（実験非対象） |
| - | WMFDataControllerError.coreDataStoreUnavailable | Core Dataストアが利用不可 | アプリ再起動 |
| - | ネットワークエラー | API呼び出し失敗 | 次回実行時に再試行 |
| - | スライドデータエラー | 個別スライドのpopulateSlideData失敗 | isEvaluated=falseで継続 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | なし（次回バッチ実行時に再試行） |
| リトライ間隔 | - |
| リトライ対象エラー | 個別スライドのエラーは継続、全体エラーは次回 |

### 障害時対応

バックグラウンドタスクの時間制限によりタスクが中断された場合、次回実行時に不足スライドのみ再処理されます。shouldFreeze=trueのスライドは既存データが保持されます。

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

| 項目 | 内容 |
|-----|------|
| トランザクション範囲 | Core Data backgroundContext内 |
| コミットタイミング | saveIfNeeded呼び出し時 |
| ロールバック条件 | Core Data保存エラー時 |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定処理件数 | 9スライド（readCount, editCount, donateCount, saveCount, mostReadDate, viewCount, mostReadCategories, location, topArticles） |
| 目標処理時間 | バックグラウンドタスク時間内（約30秒） |
| メモリ使用量上限 | 特に制限なし（バックグラウンドContext使用） |

## 排他制御

UIBackgroundTaskIdentifierで二重起動を防止します（dataPopulationBackgroundTaskID != .invalid の場合はスキップ）。

## ログ出力

| ログ種別 | 出力タイミング | 出力内容 |
|---------|--------------|---------|
| なし（内部処理） | - | - |

## 監視・アラート

| 監視項目 | 閾値 | アラート先 |
|---------|-----|----------|
| なし | - | - |

## 備考

- タスク識別子: `year-in-review-populate-report-data`（WMFBackgroundTasksNameKey.yearInReviewPopulateReportData）
- 対象年: WMFYearInReviewDataController.targetYear（2025）
- アプリ共有リンク: https://apps.apple.com/app/apple-store/id324715238?pt=208305&ct=yir_2025_share&mt=8

### スライドID一覧

| スライドID | データコントローラー | 説明 |
|-----------|---------------------|------|
| readCount | YearInReviewReadCountSlideDataController | 読んだ記事数 |
| editCount | YearInReviewEditCountSlideDataController | 編集数 |
| donateCount | YearInReviewDonateCountSlideDataController | 寄付回数 |
| saveCount | YearInReviewSaveCountSlideDataController | 保存した記事数 |
| mostReadDate | YearInReviewMostReadDateSlideDataController | 最もよく読んだ日 |
| viewCount | YearInReviewViewCountSlideDataController | 閲覧数 |
| mostReadCategories | YearInReviewMostReadCategoriesSlideDataController | よく読んだカテゴリ |
| location | YearInReviewLocationSlideDataController | 位置情報関連 |
| topArticles | YearInReviewTopReadArticleSlideDataController | トップ記事 |

### 関連するユーザー設定（UserDefaults）

| キー | 説明 |
|-----|------|
| yearInReviewSettingsIsEnabled | Year in Review機能の有効/無効 |
| seenYearInReviewFeatureAnnouncement | 機能告知の表示済みフラグ |
| seenYearInReviewIntroSlide | イントロスライドの表示済みフラグ |
| tappedYIR | Year in Reviewをタップしたフラグ |
| yearInReviewSurveyPresented | アンケート表示済みフラグ |
| qualifiesForIcon2025 | 2025アイコン資格フラグ |
