# 機能設計書 18-カード作成

## 概要

本ドキュメントは、新しいカードを下書きとして作成し、編集画面へ遷移する機能の設計を記載する。

### 本機能の処理概要

本機能は、ボードに新しいカードを作成する機能である。Web画面からの作成では下書き状態で作成され、編集画面に遷移する。API経由での作成では公開状態（published）で直接作成される。

**業務上の目的・背景**：タスク管理において、新しいタスクやアイデアを素早く登録することは重要である。本機能により、ユーザーはワンクリックで新規カードを作成し、すぐに詳細な情報を入力できる。下書き機能により、途中で入力を中断しても内容が保持される。

**機能の利用シーン**：ボード詳細画面の「+」ボタンや「新規カード」ボタンをクリックすると、下書きカードが作成され編集画面に遷移する。API経由（JSON形式）では公開状態のカードが直接作成される。

**主要な処理内容**：
1. HTML形式: 下書きカードを作成（または既存の下書きを再利用）し、編集画面にリダイレクト
2. JSON形式: 公開状態のカードを作成し、201 Createdを返す
3. カード番号（number）の自動採番

**関連システム・外部連携**：下書き編集画面（Cards::DraftsController）と連携。API経由での作成も可能。

**権限による制御**：ユーザーがアクセス可能なボードに対してのみカードを作成可能。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 10 | ボード詳細画面 | 主画面 | 新しいカードを下書きとして作成 |
| 21 | カード下書き画面 | 遷移先画面 | 作成された下書きカードの編集画面 |

## 機能種別

CRUD操作（Create）

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| board_id | UUID | Yes | 対象ボードのID | 存在するボードであること、ユーザーがアクセス可能であること |
| card[title] | String | No (APIのみ) | カードタイトル | 最大255文字 |
| card[description] | String | No (APIのみ) | カード説明 | - |
| card[image] | File | No (APIのみ) | カード画像 | - |
| card[created_at] | DateTime | No (APIのみ) | 作成日時（インポート用） | - |
| card[last_active_at] | DateTime | No (APIのみ) | 最終アクティブ日時（インポート用） | - |

### 入力データソース

- URLパラメータ: board_id
- リクエストボディ: card[title]等（API経由のみ）
- セッション: 認証済みユーザー情報（Current.user）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| card.id | UUID | 作成されたカードのID |
| card.number | BigInt | カード番号（アカウント内で一意） |
| card.status | String | カードステータス（drafted/published） |
| card.board_id | UUID | 所属ボードのID |
| card.creator_id | UUID | 作成者のID |

### 出力先

- データベース: cardsテーブル、accountsテーブル（cards_count更新）
- 画面表示: 下書き編集画面へリダイレクト（HTML形式）
- HTTPヘッダー: 201 Created + Location（API形式）

## 処理フロー

### 処理シーケンス

```
1. リクエスト受信（POST /boards/:board_id/cards）
   └─ set_boardによりboard_idからボードを取得

2. 形式判定
   └─ HTML形式: draft_new_card_inで下書き作成
   └─ JSON形式: cards.create!で公開カード作成

3. HTML形式の場合
   └─ find_or_initialize_byで既存下書きを検索または新規作成
   └─ タイムスタンプを更新
   └─ card_draft_pathへリダイレクト

4. JSON形式の場合
   └─ card_params + creator + status: "published"で作成
   └─ 201 Created + Locationヘッダー

5. カード番号採番（before_create）
   └─ account.increment!(:cards_count).cards_count
```

### フローチャート

```mermaid
flowchart TD
    A[リクエスト受信] --> B[set_board: ボード取得]
    B --> C{リクエスト形式}
    C -->|HTML| D[draft_new_card_in呼び出し]
    C -->|JSON| E[cards.create!呼び出し]
    D --> F{既存下書きあり?}
    F -->|Yes| G[既存下書きを更新]
    F -->|No| H[新規下書き作成]
    G --> I[タイムスタンプ更新]
    H --> I
    I --> J[before_create: 番号採番]
    J --> K[redirect_to card_draft_path]
    E --> L[status: publishedで作成]
    L --> M[before_create: 番号採番]
    M --> N[201 Created + Location]
    K --> O[終了]
    N --> O
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-01 | 下書き再利用 | 同一ユーザー・ボードの既存下書きがあれば再利用 | HTML形式での作成時 |
| BR-02 | 番号自動採番 | カード番号はアカウント内で一意の連番 | カード作成時 |
| BR-03 | API公開作成 | API経由では公開状態（published）で作成 | JSON形式での作成時 |
| BR-04 | タイトル自動設定 | 公開時にタイトルが空なら「Untitled」を設定 | 公開時 |

### 計算ロジック

```ruby
# カード番号採番
def assign_number
  self.number ||= account.increment!(:cards_count).cards_count
end

# 下書き作成（find_or_initialize_by）
def draft_new_card_in(board)
  board.cards.find_or_initialize_by(creator: self, status: "drafted").tap do |card|
    card.update!(created_at: Time.current, updated_at: Time.current, last_active_at: Time.current)
  end
end
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 下書き検索 | cards | SELECT | 既存下書きの有無確認 |
| カード作成 | cards | INSERT | 新規カードレコード作成 |
| 番号採番 | accounts | UPDATE | cards_countをインクリメント |

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

#### cards

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | * | creator_id = Current.user.id, status = 'drafted', board_id = @board.id | 下書き検索 |
| INSERT | id | UUID自動生成 | 主キー |
| INSERT | account_id | board.account_id | デフォルト値 |
| INSERT | board_id | 対象ボードのID | 外部キー |
| INSERT | creator_id | Current.user.id | デフォルト値 |
| INSERT | status | 'drafted' または 'published' | 形式による |
| INSERT | number | account.cards_count + 1 | before_createで設定 |
| INSERT | last_active_at | 現在時刻 | 必須 |

#### accounts

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | cards_count | cards_count + 1 | increment!で更新 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 404 | Not Found | 存在しないboard_idが指定 | RecordNotFound例外 |
| 422 | Unprocessable Entity | バリデーションエラー（API） | エラーメッセージ返却 |
| 401 | Unauthorized | 未認証ユーザー | ログイン画面へリダイレクト |

### リトライ仕様

該当なし（失敗時はエラーを返す）

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

- create!/update!メソッドによる暗黙のトランザクション
- 番号採番（increment!）とカード作成は同一トランザクションではないため、同時実行時に番号が重複する可能性あり（ユニーク制約で防止）

## パフォーマンス要件

- レスポンス時間: 300ms以内
- インデックス: account_id + numberのユニーク制約

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

- 認証: ログイン済みユーザーのみアクセス可能
- 認可: Current.user.boardsでアクセス可能なボードのみ
- パラメータ: Strong Parameters（card_params）で許可パラメータを制限

## 備考

- 下書きカードは他のユーザーには表示されない
- 下書きはボード・ユーザーごとに1つのみ（find_or_initialize_by）
- 公開するまでは編集画面から何度でも内容を更新可能

---

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

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

### 推奨読解順序

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

カードのステータス管理と番号採番を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | card.rb | `app/models/card.rb` | カードモデルの基本構造、before_create :assign_number |
| 1-2 | accessor.rb | `app/models/user/accessor.rb` | draft_new_card_inメソッドの実装 |

**読解のコツ**: find_or_initialize_byは既存レコードがあれば取得、なければ新規オブジェクトを返す。update!で保存される。

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

リクエストを受け付けるコントローラーを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | cards_controller.rb | `app/controllers/cards_controller.rb` | createアクションの実装 |

**主要処理フロー**:
1. **4行目**: before_action :set_boardでボードを取得
2. **13-25行目**: createアクションでHTML/JSON形式を分岐
3. **15-18行目**: HTML形式で下書き作成＆リダイレクト
4. **20-23行目**: JSON形式で公開カード作成

#### Step 3: 番号採番を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | card.rb | `app/models/card.rb` | assign_numberメソッド（91-93行目） |

**主要処理フロー**:
- **91-93行目**: account.increment!(:cards_count).cards_countで採番

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

```
CardsController#create
    │
    ├─ set_board
    │      └─ Current.user.boards.find(params[:board_id])
    │
    └─ respond_to
           │
           ├─ format.html
           │      └─ Current.user.draft_new_card_in(@board)
           │             └─ board.cards.find_or_initialize_by(...)
           │                    └─ card.update!(timestamps)
           │
           └─ format.json
                  └─ @board.cards.create!(...)
                         └─ before_create: assign_number
                                └─ account.increment!(:cards_count)
```

### データフロー図

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

POST /boards/:board_id/cards
         │
         ▼
┌─────────────────┐
│ board取得       │ ───▶ Current.user.boards.find
└─────────────────┘
         │
         ▼
┌─────────────────┐
│ 形式判定        │ ───▶ HTML / JSON
└─────────────────┘
         │
    ┌────┴────┐
    ▼         ▼
┌────────┐ ┌────────┐
│HTML    │ │JSON    │
│draft   │ │create! │
└────────┘ └────────┘
    │         │
    ▼         ▼
┌────────┐ ┌────────┐
│redirect│ │201     │
│draft   │ │Created │
└────────┘ └────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| cards_controller.rb | `app/controllers/cards_controller.rb` | コントローラー | カード作成リクエストの処理 |
| card.rb | `app/models/card.rb` | モデル | カードモデル、番号採番 |
| accessor.rb | `app/models/user/accessor.rb` | concern | draft_new_card_inメソッド |
| drafts_controller.rb | `app/controllers/cards/drafts_controller.rb` | コントローラー | 下書き編集画面 |
| schema.rb | `db/schema.rb` | スキーマ | cardsテーブル定義（202-217行目） |
