# 機能設計書 33-カードをピン留め

## 概要

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

### 本機能の処理概要

**業務上の目的・背景**：カンバンボードで作業中のユーザーが、自分にとって重要なカード、頻繁に参照するカード、または後で作業を再開したいカードを素早くアクセスできるようにする必要がある。ピン留め機能は、ユーザー個人のショートカットリストを作成し、どのボードを開いていても常にピン留めカードにアクセスできるようにする。これは全ユーザーに影響するゴールデン設定とは異なり、完全に個人的なブックマーク機能である。

**機能の利用シーン**：複数のボードやプロジェクトを横断して作業する際、自分が担当するカードや注目しているカードをピン留めしておくことで、画面右上のピントレイから即座にアクセスできる。会議中にすぐ参照したいカード、レビュー待ちのカード、進捗を追跡したいカードなどをピン留めする。ショートカットキー（Shift+P）でも操作可能。

**主要な処理内容**：
1. ユーザーがカードに対してピン留めをリクエスト
2. 対象カードのアクセス権を確認
3. Pinレコードを作成してピン留め状態を永続化
4. ピントレイにカードを追加（Turbo Stream broadcast）
5. ピン留めボタンの表示を更新

**関連システム・外部連携**：Turbo Streamsによるリアルタイムな画面更新。同一ユーザーの複数セッション間でピントレイが同期される（Action Cable経由）。

**権限による制御**：カードへのアクセス権を持つすべてのユーザーが各自のピン留めを設定可能。ピン留めはユーザーごとに独立しており、他のユーザーには影響しない。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 19 | カード詳細画面 | 主画面 | ピン留めボタンのクリック、Shift+Pショートカット |
| 26 | カードピン画面 | 主画面 | ピン状態の表示 |
| 49 | ピン一覧画面 | 結果表示画面 | ピン留めしたカードの一覧表示 |

## 機能種別

CRUD操作（CREATE）

## 入力仕様

### 入力パラメータ

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

### 入力データソース

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

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| turbo_stream | HTML | ピン留めボタンの差し替え用HTML（Turbo Stream形式） |
| HTTP Status | Integer | 成功時: 200 OK（turbo_stream）/ 204 No Content（JSON） |
| broadcast | Turbo Stream | ピントレイへのカード追加（Action Cable経由） |

### 出力先

- 画面表示: Turbo Streamによるピン留めボタンの部分置換
- ピントレイ: broadcast_prepend_toによるリアルタイム追加
- JSON API: no_content（ボディなし）

## 処理フロー

### 処理シーケンス

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

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::Pinnable#pin_by）
   └─ pins.find_or_create_by!(user: Current.user)
   └─ 既存の場合はそのレコードを返却

6. ピントレイへのブロードキャスト
   └─ broadcast_prepend_to [ Current.user, :pins_tray ]

7. レスポンス生成
   └─ turbo_stream: ピン留めボタンを差し替え
   └─ json: 204 No Content
```

### フローチャート

```mermaid
flowchart TD
    A[POST /cards/:card_id/pin] --> B[認証チェック]
    B --> C{ログイン済み?}
    C -->|No| D[401 Unauthorized]
    C -->|Yes| E[カード取得]
    E --> F{アクセス権あり?}
    F -->|No| G[404 Not Found]
    F -->|Yes| H[pin_by実行]
    H --> I{既にピン留め済み?}
    I -->|Yes| J[既存レコードを返却]
    I -->|No| K[Pinレコード作成]
    J --> L[ピントレイにbroadcast]
    K --> L
    L --> M[レスポンス生成]
    M --> N{リクエスト形式}
    N -->|turbo_stream| O[ピン留めボタン差し替え]
    N -->|json| P[204 No Content]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-33-01 | 重複防止 | 同一ユーザー・同一カードの組み合わせは一意 | UNIQUE制約（card_id, user_id） |
| BR-33-02 | ユーザー独立 | ピン留めはユーザーごとに独立して管理 | user_idでフィルタリング |
| BR-33-03 | アクセス権制御 | ユーザーがアクセス可能なカードのみピン留め可能 | accessible_cardsスコープで制御 |
| BR-33-04 | 表示順序 | ピン留めは作成日時の降順で表示 | scope :ordered |

### 計算ロジック

特になし。

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| ピン留め | pins | INSERT | ピン留め状態を記録 |

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

#### pins

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | UUIDを自動生成 | 主キー |
| INSERT | account_id | user.account_id | ユーザー経由で取得 |
| INSERT | card_id | 対象カードのID | 外部キー |
| INSERT | user_id | Current.user.id | ピン留めしたユーザー |
| INSERT | created_at | 現在時刻 | 自動設定、表示順序に使用 |
| INSERT | updated_at | 現在時刻 | 自動設定 |

## エラー処理

### エラーケース一覧

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

### リトライ仕様

リトライ処理は不要。find_or_create_by!により冪等性が保証されているため、重複リクエストは既存レコードを返却する。

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

Pin.find_or_create_by!は単一のINSERTまたはSELECT操作であり、明示的なトランザクション制御は行わない。ActiveRecordのデフォルトトランザクション動作に依存する。

## パフォーマンス要件

- レスポンス時間: 200ms以内
- 同時アクセス: card_id, user_idに対するUNIQUE制約により整合性を保証

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

- 認証: ApplicationControllerのbefore_actionでログイン状態を確認
- 認可: accessible_cardsスコープにより、ユーザーがアクセス権を持つカードのみに操作を限定
- CSRF対策: Railsの標準CSRF保護機構を使用
- プライバシー: ピン留めは個人情報であり、他のユーザーからは参照不可

## 備考

ピン留めは個人的なブックマーク機能であり、カードの状態やアクセス権には影響しない。カードが削除されるとピン留めも削除される（dependent: :destroy）。

---

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

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

### 推奨読解順序

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

ピン留めは、Pinモデルでカードとユーザーの多対多関連として実装されている。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | pin.rb | `app/models/pin.rb` | Pinモデルの構造。belongs_to :card, :userでの関連付け |
| 1-2 | pinnable.rb | `app/models/card/pinnable.rb` | Cardモデルにincludeされるconcern。pin_byメソッドの定義 |

**読解のコツ**: `Pin`はカードとユーザーの中間テーブル的な役割を持つ。`find_or_create_by!`で冪等性を確保している点に注目。

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

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

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

**主要処理フロー**:
1. **9行目**: `@card.pin_by Current.user`でピン留めを実行、@pinに代入
2. **11行目**: `broadcast_add_pin_to_tray`でピントレイにbroadcast
3. **13-16行目**: レスポンス形式に応じてturbo_streamまたはJSONを返却

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

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

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

**主要処理フロー**:
- **5行目**: `has_many :pins`でカードとの関連を定義
- **7行目**: `after_update_commit :broadcast_pin_updates`でカード更新時にピン表示も更新
- **10-12行目**: `pinned_by?`でピン留め状態の判定
- **18-20行目**: `pin_by`メソッドでfind_or_create_by!を使用

#### Step 4: ブロードキャストを理解する

ピントレイへのリアルタイム反映はAction Cable経由で行われる。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | pins_controller.rb | `app/controllers/cards/pins_controller.rb` | broadcast_add_pin_to_tray（31-33行目） |

**主要処理フロー**:
- **32行目**: `broadcast_prepend_to [ Current.user, :pins_tray ]`でユーザー固有のチャンネルにbroadcast

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

```
Cards::PinsController#create
    │
    ├─ CardScoped#set_card
    │      └─ Current.user.accessible_cards.find_by!(number:)
    │
    ├─ CardScoped#set_board
    │      └─ @card.board
    │
    ├─ Card#pin_by (Card::Pinnable)
    │      └─ pins.find_or_create_by!(user:)
    │             └─ Pin.create! または Pin.find
    │
    ├─ broadcast_add_pin_to_tray
    │      └─ @pin.broadcast_prepend_to [ Current.user, :pins_tray ]
    │             └─ Action Cable broadcast
    │
    └─ render_pin_button_replacement
           └─ turbo_stream.replace
```

### データフロー図

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

card_id      ───▶ CardScoped#set_card    ───▶ @card
(URLパラメータ)       │
                     ▼
               Card::Pinnable#pin_by
                     │
                     ├───────────────────────▶ pinsテーブル
                     │                         (INSERT/SELECT)
                     ▼
               broadcast_add_pin_to_tray  ───▶ Action Cable
                     │                         (ピントレイ更新)
                     ▼
               render_pin_button_replacement ─▶ Turbo Stream HTML
                                               (ボタン更新)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| pins_controller.rb | `app/controllers/cards/pins_controller.rb` | コントローラ | HTTPリクエストの処理 |
| card_scoped.rb | `app/controllers/concerns/card_scoped.rb` | コントローラconcern | カード取得の共通処理 |
| pinnable.rb | `app/models/card/pinnable.rb` | モデルconcern | ピン留めのビジネスロジック |
| pin.rb | `app/models/pin.rb` | モデル | ピン留め状態を表すActiveRecordモデル |
| card.rb | `app/models/card.rb` | モデル | カードの基底モデル |
| _pin_button.html.erb | `app/views/cards/pins/_pin_button.html.erb` | ビュー | ピン留めボタンのパーシャル |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義（85行目） |
| schema.rb | `db/schema.rb` | スキーマ | pinsテーブル定義（406-416行目） |
