# 機能設計書 26-カードを別ボードに移動

## 概要

本ドキュメントは、Fizzyアプリケーションにおけるカードを別ボードに移動する機能の設計を記載するものです。

### 本機能の処理概要

カードを別ボードに移動する機能は、既存のカードを異なるボードに移動する機能です。カードとその関連データ（イベント履歴、コメント等）をすべて移動先のボードに紐付け直します。

**業務上の目的・背景**：プロジェクト管理において、タスク（カード）の所属を変更する必要が生じることがあります。例えば、チームの再編成や、タスクの分類変更、プロジェクト間でのタスク移管などの場面で、カードを別のボードに移動できることで、柔軟なタスク管理が可能になります。

**機能の利用シーン**：
- プロジェクト間でタスクを移管する場合
- 誤ったボードにカードを作成してしまった場合
- チーム再編成に伴いカードの所属を変更する場合

**主要な処理内容**：
1. 移動先ボードの検証と取得
2. カードのboard_idを更新
3. 関連するイベント履歴のboard_idを更新
4. カラム割り当ての解除（移動元のカラムは移動先に存在しないため）
5. ボード変更イベントの記録
6. 移動先ボードのアクセス権限付与（必要な場合）
7. 非同期での通知削除とデータクリーンアップ

**関連システム・外部連携**：Webhookが設定されている場合、移動イベントが通知されます。

**権限による制御**：カードにアクセス可能かつ移動先ボードにもアクセス可能なユーザーのみ操作可能です。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 23 | カードボード変更画面 | 主画面 | ボード選択UIの表示と移動先選択 |
| 19 | カード詳細画面 | 参照画面 | ボード変更オプションへのリンク |

## 機能種別

CRUD操作（UPDATE） / データ移動

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| card_id | String | Yes | カード番号（number） | 存在するカードであること |
| board_id | UUID | Yes | 移動先のボードID | アクセス可能なボードであること |

### 入力データソース

- URL パラメータからカード番号を取得
- リクエストパラメータから board_id を取得

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| redirect_to | URL | カード詳細画面へのリダイレクト（HTML形式） |
| HTTP Status | Integer | 成功時: 204 No Content（JSON形式） |

### 出力先

- HTML形式：カード詳細画面へリダイレクト
- JSON形式：HTTP 204 No Content

## 処理フロー

### 処理シーケンス

```
1. リクエスト受信（PATCH /cards/:card_id/board）
   └─ カード番号と移動先ボードIDを取得

2. カード取得
   └─ Current.user.accessible_cards からカードを取得

3. ボード取得
   └─ BoardScoped concernにより移動先ボードを取得

4. ボード移動処理実行
   └─ @card.move_to(@board)
       ├─ トランザクション開始
       ├─ カードの board_id を更新
       ├─ 関連イベントの board_id を更新
       └─ トランザクション終了

5. 後処理（handle_board_change）
   └─ トランザクション内
       ├─ カラム割り当てを解除
       ├─ ボード変更イベントを記録
       └─ 担当者へのアクセス権限付与（必要な場合）
   └─ 非同期処理
       ├─ 不要な通知の削除
       └─ データクリーンアップ

6. レスポンス返却
   └─ HTML: カード詳細画面へリダイレクト
   └─ JSON: HTTP 204 No Content
```

### フローチャート

```mermaid
flowchart TD
    A[移動リクエスト受信] --> B[カード取得]
    B --> C[移動先ボード取得]
    C --> D{アクセス権限あり?}
    D -->|No| E[403 Forbidden]
    D -->|Yes| F[move_to実行]
    F --> G[board_id更新]
    G --> H[イベントboard_id更新]
    H --> I[handle_board_change]
    I --> J[カラム解除]
    J --> K[イベント記録]
    K --> L[アクセス権限付与]
    L --> M[非同期クリーンアップ]
    M --> N{レスポンス形式}
    N -->|HTML| O[カード画面へリダイレクト]
    N -->|JSON| P[204 No Content]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-26-01 | カラム解除 | 移動時にカラム割り当ては解除される | すべての移動操作 |
| BR-26-02 | イベント継承 | カードとコメントのイベント履歴は移動先に引き継がれる | すべての移動操作 |
| BR-26-03 | アクセス権限付与 | 限定アクセスボードの場合、担当者にアクセス権限を付与 | 移動先が限定アクセスの場合 |
| BR-26-04 | 変更記録 | ボード変更イベントに元ボード名と新ボード名を記録 | すべての移動操作 |

### 計算ロジック

特になし

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| カード移動 | cards | UPDATE | board_id, column_id を更新 |
| イベント移動 | events | UPDATE | board_id を更新 |
| アクセス付与 | accesses | INSERT | 担当者へのアクセス権限付与 |
| イベント記録 | events | INSERT | ボード変更イベントを記録 |

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

#### cards

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | board_id | 移動先ボードのID | ボード変更 |
| UPDATE | column_id | null | カラム解除 |

#### events

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | board_id | 移動先ボードのID | カード関連イベント |
| UPDATE | board_id | 移動先ボードのID | コメント関連イベント |
| INSERT | action | "board_changed" | 変更イベント |
| INSERT | particulars | { old_board, new_board } | 変更詳細 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 404 | RecordNotFound | 指定されたカードまたはボードが存在しない | 適切なエラー画面を表示 |
| 403 | Forbidden | 移動先ボードへのアクセス権がない | 403エラーを返却 |

### リトライ仕様

冪等性あり（同じボードへの再移動は状態を変えない）

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

move_to メソッド内でトランザクションを使用し、カードとイベントのboard_id更新を一括で処理します。handle_board_changeコールバック内でも追加のトランザクションでカラム解除とイベント記録を処理します。

## パフォーマンス要件

- 移動処理は2秒以内に完了すること（非同期処理を除く）
- イベント履歴が多い場合でも5秒以内に完了すること

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

- 認証：ログインユーザーのみアクセス可能
- 認可：カードと移動先ボードの両方にアクセス可能なユーザーのみ操作可能
- データ保護：移動後、元ボードのみにアクセス権を持つユーザーには通知が届かなくなる
- CSRF対策：Rails標準のCSRFトークン検証を適用

## 備考

- 移動後のカラム割り当ては手動で再設定する必要がある
- 非同期処理でデータクリーンアップが行われる（remove_inaccessible_notifications_later, clean_inaccessible_data_later）

---

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

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

### 推奨読解順序

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

ボード移動処理の全体像を把握することが重要です。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | card.rb | `app/models/card.rb` | move_toメソッドとhandle_board_changeコールバック |

**読解のコツ**: Card モデルには `move_to` メソッドと `after_update :handle_board_change, if: :saved_change_to_board_id?` コールバックがあります。この2つの連携を理解することが重要です。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | boards_controller.rb | `app/controllers/cards/boards_controller.rb` | updateアクションがボード移動のエントリーポイント |
| 2-2 | board_scoped.rb | `app/controllers/concerns/board_scoped.rb` | ボード取得処理（確認用） |

**主要処理フロー**:
1. **5行目**: `skip_before_action :set_board` - ボード取得をスキップ（editアクション用）
2. **8-10行目**: `edit`アクション - ボード選択画面の表示
3. **12-18行目**: `update`アクション - ボード移動の実行
4. **13行目**: `@card.move_to(@board)` - 移動処理の呼び出し

#### Step 3: ボード移動処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | card.rb | `app/models/card.rb` | move_toメソッドとhandle_board_changeコールバック |

**主要処理フロー**:
- **57-63行目**: `move_to`メソッド
  - **58行目**: トランザクション開始
  - **59行目**: `card.update!(board: new_board)` - ボード更新
  - **60行目**: `card.events.update_all(board_id: new_board.id)` - イベント更新
  - **61行目**: コメント関連イベントの更新
- **74-85行目**: `handle_board_change`メソッド
  - **78行目**: `update! column: nil` - カラム解除
  - **79行目**: `track_board_change_event` - イベント記録
  - **80行目**: `grant_access_to_assignees` - アクセス権限付与
  - **83-84行目**: 非同期クリーンアップ処理

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

```
Cards::BoardsController#update
    │
    ├─ set_card (before_action)
    │      └─ Current.user.accessible_cards.find_by!(number: params[:card_id])
    │
    ├─ BoardScoped#set_board (before_action)
    │      └─ Current.user.boards.find(params[:board_id])
    │
    └─ @card.move_to(@board)
           └─ Card#move_to
                  ├─ transaction
                  │      ├─ update!(board: new_board)
                  │      ├─ events.update_all(board_id: new_board.id)
                  │      └─ Comment.events.update_all(board_id: new_board.id)
                  │
                  └─ after_update :handle_board_change
                         ├─ transaction
                         │      ├─ update!(column: nil)
                         │      ├─ track_board_change_event
                         │      └─ grant_access_to_assignees (条件付き)
                         ├─ remove_inaccessible_notifications_later
                         └─ clean_inaccessible_data_later
```

### データフロー図

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

PATCH /cards/:id/board ──▶ BoardsController#update ──▶ redirect/204
  │                              │
  │ card_id                      ├─ @card.move_to(@board)
  │ board_id                     │    ├─ board_id 更新
  │                              │    └─ events.board_id 更新
  │                              │
  │                              └─ handle_board_change
  │                                   ├─ column_id = null
  │                                   ├─ イベント記録
  │                                   └─ アクセス権限付与
  │
  └─ cards (UPDATE), events (UPDATE/INSERT), accesses (INSERT)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| boards_controller.rb | `app/controllers/cards/boards_controller.rb` | コントローラー | ボード移動リクエストの処理 |
| card.rb | `app/models/card.rb` | モデル | move_toメソッドとコールバック |
| edit.html.erb | `app/views/cards/boards/edit.html.erb` | ビュー | ボード選択UI |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義（79行目） |
