# 機能設計書 34-カードのピン留め解除

## 概要

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

### 本機能の処理概要

**業務上の目的・背景**：ピン留めしたカードの作業が完了した場合や、他のカードを優先すべき状況になった場合に、ピン留めを解除する必要がある。ピントレイの整理整頓を行い、現在注目すべきカードのみをピン留め状態に保つことで、効率的なタスク管理を実現する。ピン留め数が増えすぎるとピントレイの有用性が低下するため、不要になったピン留めを積極的に解除することが推奨される。

**機能の利用シーン**：ピン留めしていたタスクが完了した場合、または関心が薄れたカードをピントレイから除外したい場合に使用する。カード詳細画面のピン留めアイコンをクリックするか、ピン一覧画面から解除できる。ショートカットキー（Shift+P）でもトグル操作が可能。

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

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

**権限による制御**：自分がピン留めしたカードのみ解除可能。他のユーザーのピン留めには影響しない。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 19 | カード詳細画面 | 主画面 | ピン留めボタン（解除状態）のクリック、Shift+Pショートカット |
| 26 | カードピン画面 | 主画面 | ピン解除操作 |
| 49 | ピン一覧画面 | 主画面 | ピン留め一覧からの解除 |

## 機能種別

CRUD操作（DELETE）

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| 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_remove_toによるリアルタイム除去
- JSON API: no_content（ボディなし）

## 処理フロー

### 処理シーケンス

```
1. リクエスト受信
   └─ DELETE /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#unpin_by）
   └─ pins.find_by(user: Current.user)
   └─ tap { it.destroy }で削除

6. ピントレイからの除去ブロードキャスト
   └─ broadcast_remove_to [ Current.user, :pins_tray ]

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

### フローチャート

```mermaid
flowchart TD
    A[DELETE /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[unpin_by実行]
    H --> I[Pinレコード検索]
    I --> J{ピン留め存在?}
    J -->|No| K[nilを返却]
    J -->|Yes| L[Pinレコード削除]
    L --> M[ピントレイからbroadcast_remove]
    K --> N[レスポンス生成]
    M --> N
    N --> O{リクエスト形式}
    O -->|turbo_stream| P[ピン留めボタン差し替え]
    O -->|json| Q[204 No Content]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-34-01 | 自己所有制限 | 自分のピン留めのみ解除可能 | user: Current.userで検索 |
| BR-34-02 | 安全削除 | ピン留め未設定のカードに対する解除は無視 | find_byがnilを返す場合 |
| BR-34-03 | 即時反映 | 解除後、ピントレイから即座に除去 | broadcast_remove_to |

### 計算ロジック

特になし。

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| ピン留め解除 | pins | DELETE | ピン留め状態を削除 |

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

#### pins

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | - | card_id = 対象カード, user_id = Current.user | 削除対象の検索 |
| DELETE | - | 上記で見つかったレコード | レコード全体を削除 |

## エラー処理

### エラーケース一覧

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

### リトライ仕様

リトライ処理は不要。ピン留めが存在しない場合でもエラーにならないため、冪等性が保証される。

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

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

## パフォーマンス要件

- レスポンス時間: 200ms以内
- card_id, user_idに対する複合インデックスにより高速な検索を保証

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

- 認証: ApplicationControllerのbefore_actionでログイン状態を確認
- 認可: user条件により、自分のピン留めのみを操作可能
- CSRF対策: Railsの標準CSRF保護機構を使用
- プライバシー: 他のユーザーのピン留めは操作不可

## 備考

ピン留め解除はピン留め設定の逆操作であり、カードのコンテンツや状態には影響しない。解除後も再度ピン留めすることが可能。

---

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

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

### 推奨読解順序

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

ピン留め解除は、Pinレコードの削除によって実現される。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | pin.rb | `app/models/pin.rb` | Pinモデルの構造 |
| 1-2 | pinnable.rb | `app/models/card/pinnable.rb` | unpin_byメソッドの定義（22-24行目） |

**読解のコツ**: `unpin_by`メソッドは`tap`メソッドを使用してPinを返却しつつdestroyを呼び出す。これによりbroadcast用のPinオブジェクトを取得できる。

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

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

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

**主要処理フロー**:
1. **20行目**: `@card.unpin_by Current.user`でピン留め解除を実行、@pinに代入
2. **22行目**: `broadcast_remove_pin_from_tray`でピントレイからbroadcast除去
3. **24-27行目**: レスポンス形式に応じてturbo_streamまたはJSONを返却

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

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

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

**主要処理フロー**:
- **22-24行目**: `unpin_by`メソッドでfind_byで検索し、tapでdestroyを呼び出し

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

ピントレイからの除去はAction Cable経由で行われる。

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

**主要処理フロー**:
- **36行目**: `broadcast_remove_to [ Current.user, :pins_tray ]`でユーザー固有のチャンネルから除去

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

```
Cards::PinsController#destroy
    │
    ├─ CardScoped#set_card
    │      └─ Current.user.accessible_cards.find_by!(number:)
    │
    ├─ CardScoped#set_board
    │      └─ @card.board
    │
    ├─ Card#unpin_by (Card::Pinnable)
    │      ├─ pins.find_by(user:)
    │      └─ tap { it.destroy }
    │             └─ Pin#destroy
    │
    ├─ broadcast_remove_pin_from_tray
    │      └─ @pin.broadcast_remove_to [ Current.user, :pins_tray ]
    │             └─ Action Cable broadcast
    │
    └─ render_pin_button_replacement
           └─ turbo_stream.replace
```

### データフロー図

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

card_id      ───▶ CardScoped#set_card    ───▶ @card
(URLパラメータ)       │
                     ▼
               Card::Pinnable#unpin_by
                     │
                     ├───────────────────────▶ pinsテーブル
                     │                         (DELETE)
                     ▼
               broadcast_remove_pin_from_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行目） |
