# 機能設計書 37-カードを既読にする

## 概要

本ドキュメントは、Fizzyアプリケーションにおけるカードの「既読」機能について、その処理内容、入出力仕様、データベース操作を詳細に記述した機能設計書である。

### 本機能の処理概要

**業務上の目的・背景**：カンバンボードで多数のカードを管理する際、自分に関連する通知（メンション、担当割り当て、コメント等）を受け取ることがある。既読機能は、ユーザーがこれらの通知を確認したことを記録し、通知一覧から該当カードの通知を消去する機能である。これにより、未確認の通知と既に対応した通知を明確に区別でき、タスク管理の効率が向上する。

**機能の利用シーン**：カード詳細画面を開いてカードの内容を確認した際、関連する通知を自動または手動で既読にする。また、ボードへのアクセス時間も同時に記録され、ユーザーのアクティビティ追跡に使用される。

**主要な処理内容**：
1. ユーザーがカードに対して既読処理をリクエスト
2. 対象カードのアクセス権を確認
3. 該当ユーザーの該当カードに関連する通知を既読状態に更新
4. ボードへのアクセス時刻を記録

**関連システム・外部連携**：通知システムと連携。Notificationモデルの既読状態を更新する。

**権限による制御**：カードへのアクセス権を持つユーザーが、自分宛ての通知を既読にできる。他のユーザーの通知には影響しない。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 19 | カード詳細画面 | 主画面 | カード詳細表示時に既読処理 |

## 機能種別

CRUD操作（UPDATE）

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| card_id | String | Yes | 対象カードの番号（number） | Current.userがアクセス可能なカードであること |

### 入力データソース

- URLパスパラメータ: card_idはルーティングから取得
- 認証情報: Current.userから現在のログインユーザーを取得

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| turbo_stream | HTML | 通知を削除するTurbo Stream（通知がある場合） |
| HTTP Status | Integer | 成功時: 200 OK |

### 出力先

- 画面表示: Turbo Streamによる通知要素の削除
- データベース: Notificationレコードの既読状態更新、ボードアクセス時刻の記録

## 処理フロー

### 処理シーケンス

```
1. リクエスト受信
   └─ POST /cards/:card_id/reading

2. 認証・認可チェック（ApplicationController）
   └─ Current.userの設定、ログイン状態確認

3. カード取得（CardScoped#set_card）
   └─ Current.user.accessible_cards.find_by!(number: params[:card_id])
   └─ アクセス権がない場合は404エラー

4. ボード設定（CardScoped#set_board）
   └─ @board = @card.board

5. 既読処理（Card::Readable#read_by）
   └─ user.notifications.find_by(card: self)
   └─ notification.read

6. ボードアクセス記録
   └─ @card.board.accessed_by(Current.user)

7. レスポンス生成
   └─ turbo_stream: 通知を削除（存在する場合）
```

### フローチャート

```mermaid
flowchart TD
    A[POST /cards/:card_id/reading] --> B[認証チェック]
    B --> C{ログイン済み?}
    C -->|No| D[401 Unauthorized]
    C -->|Yes| E[カード取得]
    E --> F{アクセス権あり?}
    F -->|No| G[404 Not Found]
    F -->|Yes| H[read_by実行]
    H --> I{通知存在?}
    I -->|No| J[@notification = nil]
    I -->|Yes| K[通知を既読に更新]
    J --> L[ボードアクセス記録]
    K --> L
    L --> M[レスポンス生成]
    M --> N{通知あり?}
    N -->|Yes| O[turbo_stream.remove通知]
    N -->|No| P[空レスポンス]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-37-01 | 自己通知限定 | 自分宛ての通知のみ既読にできる | user.notificationsで検索 |
| BR-37-02 | 冪等性 | 通知がない場合でも正常終了 | find_byがnilを返す場合 |
| BR-37-03 | アクセス記録 | 既読処理と同時にボードアクセス時刻を記録 | record_board_access |
| BR-37-04 | カード単位 | カードに関連するすべての通知が既読対象 | card: selfで検索 |

### 計算ロジック

特になし。

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 既読更新 | notifications | UPDATE | read_at, statusの更新 |
| アクセス記録 | board_accesses | INSERT/UPDATE | ボードアクセス時刻の記録 |

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

#### notifications

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | - | user_id = Current.user, card_id = 対象カード | 既読対象の検索 |
| UPDATE | read_at | 現在時刻 | 既読処理 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 401 | Unauthorized | 未ログイン状態でアクセス | ログイン画面へリダイレクト |
| 404 | Not Found | 存在しないカード番号、またはアクセス権なし | エラーページ表示 |

### リトライ仕様

リトライ処理は不要。通知がない場合でも正常終了するため、冪等性が保証される。

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

Notification.readは単一のUPDATE操作であり、明示的なトランザクション制御は行わない。ボードアクセス記録は独立した処理として実行される。

## パフォーマンス要件

- レスポンス時間: 200ms以内
- 通知検索はuser_id, card_idの複合条件で高速に実行

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

- 認証: ApplicationControllerのbefore_actionでログイン状態を確認
- 認可: user.notificationsにより、自分の通知のみを操作可能
- CSRF対策: Railsの標準CSRF保護機構を使用

## 備考

既読機能はカード詳細画面を開いた際に自動的に呼び出されることが多い。また、destroyアクションで未読に戻すことも可能。

---

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

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

### 推奨読解順序

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

既読機能は、Notificationモデルのread_atフィールドを更新することで実現される。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | readable.rb | `app/models/card/readable.rb` | Cardモデルにincludeされるconcern。read_by/unread_byメソッドの定義 |

**読解のコツ**: `read_by`メソッドはユーザーの通知を検索し、見つかった場合にreadメソッドを呼び出す。`&.tap(&:read)`で安全呼び出しとtapを組み合わせている。

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

コントローラのcreateアクションがエントリーポイントとなる。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | readings_controller.rb | `app/controllers/cards/readings_controller.rb` | createアクションの実装（4-7行目） |
| 2-2 | card_scoped.rb | `app/controllers/concerns/card_scoped.rb` | カード取得とボード設定のconcern |

**主要処理フロー**:
1. **5行目**: `@card.read_by(Current.user)`で既読処理を実行、@notificationに代入
2. **6行目**: `record_board_access`でボードアクセス時刻を記録

#### Step 3: ドメインロジックを理解する

Cardモデルに混入されたReadableモジュールでビジネスロジックが定義されている。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | readable.rb | `app/models/card/readable.rb` | read_byメソッドの実装（4-6行目） |

**主要処理フロー**:
- **4-6行目**: `read_by`メソッドでユーザーの通知を検索し、tap(&:read)で既読にする

#### Step 4: ボードアクセス記録を理解する

既読処理と同時にボードアクセス時刻が記録される。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | readings_controller.rb | `app/controllers/cards/readings_controller.rb` | record_board_access（14-17行目） |

**主要処理フロー**:
- **16行目**: `@card.board.accessed_by(Current.user)`でボードアクセス時刻を記録

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

```
Cards::ReadingsController#create
    │
    ├─ CardScoped#set_card
    │      └─ Current.user.accessible_cards.find_by!(number:)
    │
    ├─ CardScoped#set_board
    │      └─ @card.board
    │
    ├─ Card#read_by (Card::Readable)
    │      ├─ user.notifications.find_by(card: self)
    │      └─ notification&.tap(&:read)
    │             └─ Notification#read
    │
    ├─ record_board_access
    │      └─ @card.board.accessed_by(Current.user)
    │
    └─ turbo_stream (create.turbo_stream.erb)
           └─ turbo_stream.remove @notification
```

### データフロー図

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

card_id      ───▶ CardScoped#set_card    ───▶ @card
(URLパラメータ)       │
                     ▼
               Card::Readable#read_by
                     │
                     ├───────────────────────▶ notificationsテーブル
                     │                         (UPDATE read_at)
                     ▼
               record_board_access      ───▶ board_accessesテーブル
                     │                         (INSERT/UPDATE)
                     ▼
               turbo_stream render     ───▶ Turbo Stream HTML
                                            (通知要素削除)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| readings_controller.rb | `app/controllers/cards/readings_controller.rb` | コントローラ | HTTPリクエストの処理 |
| card_scoped.rb | `app/controllers/concerns/card_scoped.rb` | コントローラconcern | カード取得の共通処理 |
| readable.rb | `app/models/card/readable.rb` | モデルconcern | 既読処理のビジネスロジック |
| card.rb | `app/models/card.rb` | モデル | カードの基底モデル |
| create.turbo_stream.erb | `app/views/cards/readings/create.turbo_stream.erb` | ビュー | 既読時のTurbo Streamレスポンス |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義（87, 90行目） |
