# 機能設計書 16-カラム位置変更

## 概要

本ドキュメントは、カラムの表示順序を左右に移動する機能の設計を記載する。

### 本機能の処理概要

本機能は、ボード内のカラム（列）の表示順序を左または右に移動する機能である。隣接するカラムとposition値を入れ替えることで、カラムの並び順を変更する。

**業務上の目的・背景**：カンバンボードでは、ワークフローの順序に合わせてカラムを配置することが重要である。例えば、「To Do」「In Progress」「Done」の順序を変更したい場合や、新しく追加したカラムを適切な位置に移動したい場合に使用される。

**機能の利用シーン**：ボード詳細画面やボード設定画面において、カラムの左右移動ボタンをクリックしてカラムの位置を変更する。移動はTurbo Streamsによりリアルタイムに画面に反映される。

**主要な処理内容**：
1. 移動対象カラムと移動方向（左/右）を特定
2. 隣接カラムを取得
3. 両カラムのposition値を入れ替え（swap）
4. Turbo StreamsでUIを更新

**関連システム・外部連携**：Turbo Streamsを使用してリアルタイムUI更新を行う。2つのコントローラー（LeftPositionsController、RightPositionsController）で左右移動を分離。

**権限による制御**：ユーザーがアクセス可能なカラムのみ移動可能。ColumnScopedによりカラムへのアクセス権を検証。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 10 | ボード詳細画面 | 主画面 | カラムの左右移動ボタン操作 |
| 13 | ボードカラム詳細画面 | 参照画面 | カラム詳細からの位置変更 |

## 機能種別

データ操作処理 / 位置入れ替え

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| column_id | UUID | Yes | 対象カラムのID | 存在するカラムであること、ユーザーがアクセス可能であること |

### 入力データソース

- URLパラメータ: column_id
- ルーティング: /columns/:column_id/left_position（左移動）、/columns/:column_id/right_position（右移動）
- セッション: 認証済みユーザー情報（Current.user）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| column.position | Integer | 更新後のカラムposition |
| adjacent_column.position | Integer | 入れ替え相手のカラムposition |

### 出力先

- データベース: columnsテーブル（2レコードのposition更新）
- 画面表示: Turbo StreamsによるUI更新

## 処理フロー

### 処理シーケンス

```
1. リクエスト受信（POST /columns/:column_id/left_position or right_position）
   └─ ColumnScopedによりcolumn_idからカラムを取得

2. 隣接カラム取得
   └─ left_position: @column.left_column
   └─ right_position: @column.right_column

3. 位置入れ替え
   └─ @column.move_left or move_right
   └─ swap_position_with メソッドで両者のpositionを交換

4. レスポンス生成
   └─ Turbo Streams: 両カラムのUIを更新
```

### フローチャート

```mermaid
flowchart TD
    A[リクエスト受信] --> B{移動方向}
    B -->|left| C[LeftPositionsController]
    B -->|right| D[RightPositionsController]
    C --> E[ColumnScoped: カラム取得]
    D --> E
    E --> F{移動方向}
    F -->|left| G[left_column取得]
    F -->|right| H[right_column取得]
    G --> I[move_left呼び出し]
    H --> J[move_right呼び出し]
    I --> K{隣接カラム存在?}
    J --> K
    K -->|No| L[何もしない]
    K -->|Yes| M[swap_position_with]
    M --> N[transaction開始]
    N --> O[self.position更新]
    O --> P[other.position更新]
    P --> Q[commit]
    Q --> R[Turbo Streams応答]
    L --> R
    R --> S[終了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-01 | 端での移動制限 | 左端のカラムはこれ以上左に移動できない（left_columnがnil） | 左移動時 |
| BR-02 | 端での移動制限 | 右端のカラムはこれ以上右に移動できない（right_columnがnil） | 右移動時 |
| BR-03 | position値交換 | 隣接カラムと自分のposition値を入れ替える | 移動成功時 |
| BR-04 | トランザクション保証 | 2つのposition更新は同一トランザクション内で実行 | 移動時 |

### 計算ロジック

```ruby
# 左隣のカラム取得
def left_column
  board.columns.where("position < ?", position).sorted.last
end

# 右隣のカラム取得
def right_column
  board.columns.where("position > ?", position).sorted.first
end

# position入れ替え
def swap_position_with(other_column)
  return if other_column.nil?
  transaction do
    old_position = self.position
    self.update_column(:position, other_column.position)
    other_column.update_column(:position, old_position)
  end
end
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 左隣取得 | columns | SELECT | position < 自分のカラムを降順で1件取得 |
| 右隣取得 | columns | SELECT | position > 自分のカラムを昇順で1件取得 |
| 位置更新 | columns | UPDATE | 2レコードのpositionを更新 |

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

#### columns

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | * | position < @column.position, ORDER BY position DESC LIMIT 1 | 左隣取得 |
| SELECT | * | position > @column.position, ORDER BY position ASC LIMIT 1 | 右隣取得 |
| UPDATE | position | 相手のposition値 | update_columnで直接更新 |
| UPDATE | position | 自分の元position値 | 相手カラムに設定 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 404 | Not Found | 存在しないcolumn_idが指定 | RecordNotFound例外 |
| 401 | Unauthorized | 未認証ユーザー | ログイン画面へリダイレクト |
| - | 移動不可 | 端のカラムをさらに端方向へ移動 | 何もせず正常終了 |

### リトライ仕様

該当なし（失敗時はエラーを返すか、何もしない）

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

- swap_position_withメソッド内で明示的にtransactionブロックを使用
- 2つのupdate_columnが同一トランザクション内で実行
- ロールバック時は両方のposition更新がキャンセルされる

## パフォーマンス要件

- レスポンス時間: 100ms以内
- インデックス: board_id + positionの複合インデックスにより高速検索

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

- 認証: ログイン済みユーザーのみアクセス可能
- 認可: ColumnScopedによりCurrent.user.accessible_columnsでアクセス可能なカラムのみ

## 備考

- update_columnを使用しているため、コールバック（after_save等）はスキップされる
- 端のカラムを端方向に移動しようとしても、何もせずに正常終了する（エラーにならない）

---

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

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

### 推奨読解順序

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

カラムのposition管理と隣接カラム取得のロジックを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | positioned.rb | `app/models/column/positioned.rb` | left_column/right_column/swap_position_withの実装 |

**読解のコツ**: sortedスコープはposition昇順。left_columnは「自分より小さいpositionの中で最大」、right_columnは「自分より大きいpositionの中で最小」を取得する。

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

2つのコントローラーを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | left_positions_controller.rb | `app/controllers/columns/left_positions_controller.rb` | 左移動のcreateアクション |
| 2-2 | right_positions_controller.rb | `app/controllers/columns/right_positions_controller.rb` | 右移動のcreateアクション |
| 2-3 | column_scoped.rb | `app/controllers/concerns/column_scoped.rb` | カラム取得処理 |

**主要処理フロー（left_positions_controller.rb）**:
1. **5行目**: @left_column = @column.left_columnで左隣を取得
2. **6行目**: @column.move_leftで位置を入れ替え

**主要処理フロー（right_positions_controller.rb）**:
1. **5行目**: @right_column = @column.right_columnで右隣を取得
2. **6行目**: @column.move_rightで位置を入れ替え

#### Step 3: 位置入れ替えロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | positioned.rb | `app/models/column/positioned.rb` | swap_position_withメソッド（44-52行目） |

**主要処理フロー**:
- **18-20行目**: left_columnで左隣カラムを取得（position < 自分）
- **22-24行目**: right_columnで右隣カラムを取得（position > 自分）
- **44-52行目**: swap_position_withでトランザクション内でposition交換

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

```
Columns::LeftPositionsController#create
    │
    ├─ ColumnScoped (concern)
    │      └─ set_column (Current.user.accessible_columns.find)
    │
    ├─ @column.left_column
    │      └─ board.columns.where("position < ?", position).sorted.last
    │
    └─ @column.move_left
           └─ swap_position_with(left_column)
                  └─ transaction
                         ├─ self.update_column(:position, other.position)
                         └─ other.update_column(:position, old_position)

Columns::RightPositionsController#create
    │
    ├─ ColumnScoped (concern)
    │      └─ set_column
    │
    ├─ @column.right_column
    │      └─ board.columns.where("position > ?", position).sorted.first
    │
    └─ @column.move_right
           └─ swap_position_with(right_column)
```

### データフロー図

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

POST /columns/:column_id/left_position
         │
         ▼
┌─────────────────┐
│ column取得      │ ───▶ accessible_columns.find
└─────────────────┘
         │
         ▼
┌─────────────────┐
│ left_column取得 │ ───▶ position < self の最大
└─────────────────┘
         │
         ▼
┌─────────────────┐
│ swap_position   │ ───▶ transaction内でUPDATE x 2
└─────────────────┘
         │
         ▼
┌─────────────────┐
│ Turbo Streams  │ ───▶ 両カラムのUI更新
└─────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| left_positions_controller.rb | `app/controllers/columns/left_positions_controller.rb` | コントローラー | 左移動リクエストの処理 |
| right_positions_controller.rb | `app/controllers/columns/right_positions_controller.rb` | コントローラー | 右移動リクエストの処理 |
| column_scoped.rb | `app/controllers/concerns/column_scoped.rb` | concern | カラム取得 |
| positioned.rb | `app/models/column/positioned.rb` | concern | position管理ロジック |
| column.rb | `app/models/column.rb` | モデル | カラムモデル |
| schema.rb | `db/schema.rb` | スキーマ | columnsテーブル定義（239-249行目） |
