# 機能設計書 14-記事保存

## 概要

本ドキュメントは、Wikipedia iOSアプリにおける「記事保存」機能の設計を記述する。この機能は、ユーザーが興味のあるWikipedia記事をリーディングリストに保存し、オフラインで閲覧できるようにするものである。

### 本機能の処理概要

**業務上の目的・背景**：ユーザーが後で読みたい記事や重要な記事を保存しておくことで、オフライン環境でも記事を閲覧できるようにする。インターネット接続が不安定な環境や、飛行機内などのオフライン環境での利用を想定している。保存された記事はリーディングリストとして管理され、ユーザーはカテゴリ別に整理できる。

**機能の利用シーン**：記事表示画面やExplore画面、検索結果画面などから「保存」ボタン/アイコンをタップすることで記事を保存する。保存された記事はSaved画面の「All articles」またはカスタムリーディングリストに追加される。

**主要な処理内容**：
1. 記事の保存ボタンタップを検知する
2. 記事をデフォルトリーディングリストまたは指定リストに追加する
3. 記事コンテンツをオフライン用にキャッシュする
4. 保存状態をUIに反映する（アイコンの変更など）
5. 保存解除時はリストから削除する

**関連システム・外部連携**：ReadingListsController（WMFReadingListsController）と連携し、Core Data経由でデータを永続化する。ログイン済みユーザーの場合、Wikipedia APIを通じてサーバーと同期する。

**権限による制御**：本機能は認証不要で利用可能。ただし、サーバー同期はログイン済みユーザーのみ。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 1 | Explore画面 | 参照元画面 | 記事カードの保存ボタン |
| 3 | Saved画面 | 結果表示画面 | 保存された記事の一覧表示 |
| 6 | Article画面 | 参照元画面 | 記事表示中の保存ボタン |
| 10 | SearchResults画面 | 参照元画面 | 検索結果からの保存 |

## 機能種別

データ登録 / データ削除 / オフラインキャッシュ

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| article | WMFArticle | Yes | 保存対象の記事オブジェクト | 有効な記事であること |
| readingList | ReadingList? | No | 追加先のリーディングリスト | nilの場合はデフォルトリスト |

### 入力データソース

- 画面操作：保存ボタン/アイコンのタップ
- コンテキストメニュー：長押しメニューからの保存

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| savedDate | Date? | 保存日時（nilの場合は未保存） |
| readingListEntries | Set<ReadingListEntry> | 関連するリーディングリストエントリ |

### 出力先

- Core Data：WMFArticle.savedDate, ReadingListEntry
- UI更新：保存ボタンの状態変更

## 処理フロー

### 処理シーケンス

```
1. ユーザーが保存ボタンをタップ
   └─ toggleSaved() または save(articles:) が呼ばれる
2. 現在の保存状態を確認
   └─ article.savedDate != nil で保存済み判定
3. 保存処理の実行
   └─ ReadingListsController.save(articles:readingList:) を呼び出し
4. Core Dataへの保存
   └─ ReadingListEntry を作成、WMFArticle.savedDate を更新
5. オフラインコンテンツのキャッシュ
   └─ 記事HTML、画像のプリフェッチ
6. UI更新
   └─ 保存ボタンのアイコン/色を更新
7. 同期処理のトリガー（ログイン時）
   └─ ReadingListsSyncOperation をキューに追加
```

### フローチャート

```mermaid
flowchart TD
    A[保存ボタンタップ] --> B{現在の保存状態}
    B -->|未保存| C[記事を保存]
    B -->|保存済み| D[保存解除確認]
    C --> E[ReadingListEntry作成]
    E --> F[WMFArticle.savedDate更新]
    F --> G[オフラインキャッシュ]
    G --> H[UI更新]
    D -->|確認| I[記事を削除]
    D -->|キャンセル| J[処理終了]
    I --> K[ReadingListEntry削除]
    K --> L[WMFArticle.savedDate=nil]
    L --> H
    H --> M{ログイン済み?}
    M -->|Yes| N[同期処理トリガー]
    M -->|No| O[処理終了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-14-01 | デフォルトリスト | リスト未指定時はデフォルトリーディングリストに保存 | readingList == nil |
| BR-14-02 | 重複防止 | 同一記事は同一リストに1回のみ保存可能 | 記事追加時 |
| BR-14-03 | エントリ上限 | 1リストあたりの記事数上限（WMFReadingListsConfigMaxEntriesPerList） | 記事追加時 |
| BR-14-04 | リスト上限 | ユーザーあたりのリスト数上限（WMFReadingListsConfigMaxListsPerUser） | リスト作成時 |
| BR-14-05 | 削除確認 | 保存済み記事の削除時は確認を表示（設定による） | 保存解除時 |

### 計算ロジック

エントリ上限チェック:
```swift
let entryLimit = moc.wmf_readingListsConfigMaxEntriesPerList.intValue
let countOfEntries = Int(readingList?.countOfEntries ?? 0)
if countOfEntries + articles.count > entryLimit {
    throw ReadingListError.entryLimitReached(...)
}
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 記事保存 | WMFArticle | UPDATE | savedDateを現在日時に更新 |
| エントリ作成 | ReadingListEntry | INSERT | リーディングリストエントリを作成 |
| 記事削除 | WMFArticle | UPDATE | savedDateをnilに更新 |
| エントリ削除 | ReadingListEntry | DELETE | リーディングリストエントリを削除 |

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

#### WMFArticle

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | savedDate | 現在日時 or nil | 保存/削除時 |
| UPDATE | isDownloaded | true/false | オフラインキャッシュ状態 |

#### ReadingListEntry

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | articleKey | 記事のキー | - |
| INSERT | list | 対象リーディングリスト | - |
| INSERT | createdDate | 作成日時 | - |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| entryLimitReached | ビジネスエラー | リスト内記事数が上限超過 | エラーメッセージ表示 |
| unableToAddEntry | 処理エラー | 保存処理の失敗 | エラーメッセージ表示 |
| listLimitReached | ビジネスエラー | リスト数が上限超過 | エラーメッセージ表示 |

### リトライ仕様

自動リトライは行わない。ユーザーが再度操作することで再試行可能。

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

Core Dataコンテキストの保存は一連の操作完了後に一括で行う。

## パフォーマンス要件

- 保存操作は即座にUIに反映
- オフラインキャッシュはバックグラウンドで非同期実行

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

- 保存された記事はローカルデバイスに暗号化なしで保存
- サーバー同期はHTTPS経由で暗号化

## 備考

ReadingListsFunnel.sharedを通じて保存/削除イベントのログ記録が行われる。

---

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

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

### 推奨読解順序

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

保存記事のデータ構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | WMFArticle | `WMF Framework/` | 記事を表すCore Dataエンティティ、savedDate属性が保存状態を表す |
| 1-2 | ReadingList | `WMF Framework/` | リーディングリストを表すCore Dataエンティティ |
| 1-3 | ReadingListEntry | `WMF Framework/` | リーディングリストエントリを表すCore Dataエンティティ |

**読解のコツ**: WMFArticle.savedDateがnilでなければ保存済み。ReadingListEntryが記事とリストの関連を表す。

#### Step 2: コントローラーを理解する

ReadingListsControllerが保存処理の中心。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | ReadingListsController.swift | `Wikipedia/Code/ReadingListsController.swift` | リーディングリストの操作を管理 |

**主要処理フロー**:
1. **159-177行目**: createReadingList - リーディングリスト作成
2. **179-198行目**: throwLimitErrorIfNecessary - 上限チェック
3. 保存/削除メソッド（save, unsave等）

#### Step 3: 保存画面を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | SavedArticlesCollectionViewController.swift | `Wikipedia/Code/SavedArticlesCollectionViewController.swift` | 保存記事一覧表示 |

**主要処理フロー**:
- **50行目付近**: readingListsController使用
- スワイプアクションでの削除処理
- 複数選択での一括操作

#### Step 4: 保存ボタンの実装を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | SaveButton.swift | `Wikipedia/Code/` | 保存ボタンUI |

**主要処理フロー**:
- 保存状態によるアイコン切り替え
- タップアクションの処理

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

```
ArticleViewController / ExploreViewController
    │
    ├─ 保存ボタンタップ
    │      └─ toggleSaved() / save(articles:)
    │
    └─ ReadingListsController
           ├─ save(articles:readingList:)
           │      ├─ ReadingListEntry作成
           │      └─ WMFArticle.savedDate更新
           │
           ├─ unsave(articles:)
           │      ├─ ReadingListEntry削除
           │      └─ WMFArticle.savedDate=nil
           │
           └─ sync()
                  └─ ReadingListsSyncOperation
```

### データフロー図

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

保存ボタンタップ ───▶ ReadingListsController ───▶ UI更新
                          │
                          ├─▶ Core Data
                          │      ├─ WMFArticle更新
                          │      └─ ReadingListEntry作成/削除
                          │
                          └─▶ オフラインキャッシュ
                                 └─ 記事コンテンツ保存
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| ReadingListsController.swift | `Wikipedia/Code/` | ソース | リーディングリスト操作管理 |
| SavedArticlesCollectionViewController.swift | `Wikipedia/Code/` | ソース | 保存記事一覧表示 |
| WMFArticle+Extensions | `WMF Framework/` | ソース | 記事エンティティ拡張 |
| ReadingList+CoreDataClass | `WMF Framework/` | ソース | リーディングリストエンティティ |
| ReadingListEntry+CoreDataClass | `WMF Framework/` | ソース | エントリエンティティ |
| ReadingListsFunnel.swift | `Wikipedia/Code/` | ソース | イベントログ |
