# 機能設計書 72-個人スニペット

## 概要

本ドキュメントは、GitLabの個人スニペット機能について、その処理概要、入出力仕様、処理フロー、データベース操作仕様を定義する。

### 本機能の処理概要

個人スニペット機能は、ユーザーがプロジェクトに関係なく個人的なコードスニペット（コード断片）を作成・管理・共有するための機能である。

**業務上の目的・背景**：開発者は日常的に様々なコードスニペット、設定ファイル、スクリプトなどを保存・共有する必要がある。プロジェクトに紐づかない汎用的なスニペットや、個人的なメモとして保管したいコードがある場合、個人スニペット機能を利用することで、ユーザーの個人空間にスニペットを保存できる。また、公開設定により他のユーザーと共有することも可能である。

**機能の利用シーン**：
- 個人的なコードメモやテンプレートを保存する場合
- プロジェクトを跨いで再利用可能なユーティリティコードを保管する場合
- 公開スニペットとして他のユーザーにコード例を共有する場合
- 秘密のスニペット（URLを知っている人のみアクセス可能）を作成する場合

**主要な処理内容**：
1. 個人スニペットの作成（タイトル、説明、コード内容、ファイル名、可視性レベルの設定）
2. スニペットの編集（既存スニペットの内容更新）
3. スニペットの削除
4. スニペット一覧の表示（ダッシュボード、エクスプローラ）
5. スニペットへのコメント付与
6. スニペットへのアワード絵文字付与
7. Markdownプレビュー機能

**関連システム・外部連携**：
- Gitリポジトリ（スニペットの内容はGitリポジトリとして管理される）
- Akismetスパム検知（スパムチェック機能）
- Markdownレンダリング
- 組織（Organization）との紐付け

**権限による制御**：
- スニペット作成: ログインユーザーであれば作成可能
- スニペットの編集: スニペット作成者のみ
- スニペットの削除: スニペット作成者のみ
- スニペットの閲覧: スニペットの可視性レベルに依存（Private: 作成者のみ、Internal: ログインユーザー、Public: 全員）

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 17 | スニペット一覧 | 主画面 | ユーザースニペットの一覧表示 |
| 21 | スニペット探索 | 参照画面 | 公開スニペットの探索 |
| 279 | スニペット詳細 | 結果表示画面 | 個人スニペット詳細 |
| 280 | スニペット新規作成 | 入力画面 | 個人スニペット作成 |
| 281 | スニペット編集 | 入力画面 | 個人スニペット編集 |

## 機能種別

CRUD操作 / テキスト処理 / バージョン管理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| title | String | Yes | スニペットのタイトル | 最大255文字 |
| description | String | No | スニペットの説明 | 最大1MB |
| file_name | String | No | ファイル名（シンタックスハイライト用） | 最大255文字 |
| content | String | Yes | スニペットの内容（コード） | 必須、サイズ制限あり（設定値による） |
| visibility_level | Integer | No | 可視性レベル（0:Private, 10:Internal, 20:Public） | 0, 10, 20のいずれか |
| organization_id | Integer | No | 所属組織ID | 有効な組織ID |

### 入力データソース

- 画面入力（新規作成・編集フォーム）
- URLパラメータ（ページ番号）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | Integer | スニペットID |
| title | String | タイトル |
| description | String | 説明 |
| content | String | スニペット内容 |
| file_name | String | ファイル名 |
| visibility_level | Integer | 可視性レベル |
| author | User | 作成者情報 |
| created_at | DateTime | 作成日時 |
| updated_at | DateTime | 更新日時 |
| web_url | String | スニペットのWebURL |
| secret | Boolean | 秘密のスニペットかどうか |

### 出力先

- 画面表示（HTML）
- JSON API レスポンス
- 埋め込み用スニペット（embeddable）

## 処理フロー

### 処理シーケンス

```
1. 認証確認（閲覧時は任意）
   └─ ユーザーがログイン済みか確認
2. スニペット操作権限確認
   └─ 作成/編集/削除の権限をチェック
3. バリデーション実行
   └─ 入力パラメータの検証
4. スパムチェック（作成/編集時）
   └─ Akismetによるスパム判定
5. スニペット保存
   └─ DBへのレコード保存
6. リポジトリ操作
   └─ Gitリポジトリの作成/コミット
7. 一時ファイル移動（アップロードファイル）
   └─ ユーザーからスニペットへ関連付け変更
8. レスポンス返却
   └─ 成功/失敗の結果を返す
```

### フローチャート

```mermaid
flowchart TD
    A[開始] --> B{アクション種別}
    B -->|一覧/詳細| C{認証必要?}
    C -->|No| D[スニペット取得]
    C -->|Yes| E{認証済み?}
    E -->|No| F[ログイン画面へ]
    E -->|Yes| D
    B -->|作成/編集/削除| E
    D --> G{可視性チェック}
    G -->|アクセス可| H[表示/レイアウト決定]
    G -->|アクセス不可| I[403エラー]
    H --> J[レンダリング]
    J --> K[終了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-72-01 | 組織への紐付け | 個人スニペットは組織（Organization）に紐付けられる | スニペット作成時 |
| BR-72-02 | レイアウト切り替え | 作成者以外が閲覧する場合はexploreレイアウトを使用 | スニペット詳細表示時 |
| BR-72-03 | ダッシュボードリダイレクト | indexアクセス時、ログイン済みならdashboard、未ログインならexploreへリダイレクト | index表示時 |
| BR-72-04 | 埋め込み可能条件 | 公開スニペットのみ埋め込み可能 | embeddable?チェック時 |

### 計算ロジック

なし

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| スニペット作成 | snippets | INSERT | 新規スニペットレコード作成 |
| スニペット作成 | snippet_repositories | INSERT | スニペット用リポジトリ情報作成 |
| スニペット作成 | snippet_statistics | INSERT | スニペット統計情報作成 |
| スニペット編集 | snippets | UPDATE | スニペット情報更新 |
| スニペット削除 | snippets | DELETE | スニペットレコード削除 |
| 一覧取得 | snippets | SELECT | スニペット一覧取得 |

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

#### snippets

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | title | ユーザー入力値 | 必須 |
| INSERT | description | ユーザー入力値 | 任意 |
| INSERT | content | ユーザー入力値 | 必須 |
| INSERT | file_name | ユーザー入力値 | 任意 |
| INSERT | author_id | current_user.id | 作成者 |
| INSERT | project_id | NULL | 個人スニペットはプロジェクトなし |
| INSERT | organization_id | 組織ID | 所属組織 |
| INSERT | visibility_level | ユーザー入力値またはデフォルト | 可視性 |
| INSERT | type | 'PersonalSnippet' | STI識別子 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 401 | 認証エラー | 未ログイン状態での作成・編集アクセス | ログイン画面へリダイレクト |
| 403 | 権限エラー | 他ユーザーのスニペット編集試行 | エラーメッセージ表示 |
| 404 | NotFound | 存在しないスニペットへのアクセス | 404ページ表示 |
| 422 | バリデーションエラー | 入力値の検証失敗 | エラー内容を表示して再入力促進 |

### リトライ仕様

リポジトリ操作失敗時は、スニペットレコードを削除してロールバックする。

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

- スニペット作成時、DBレコード保存とリポジトリ作成は同一トランザクションで管理
- リポジトリ作成失敗時はDBレコードもロールバック

## パフォーマンス要件

- 一覧取得: ページネーション適用
- 個別取得: 1秒以内のレスポンス

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

- 可視性レベルに基づくアクセス制御
- スパム検知（Akismet連携）
- XSS対策（コンテンツのサニタイズ）
- CSRF対策
- 秘密のスニペット機能（URLを知っている人のみアクセス可能）

## 備考

- 個人スニペットはプロジェクトに紐づかないため、project_idはNULL
- 組織（Organization）への紐付けが必須
- PersonalSnippetクラスとしてSTI（Single Table Inheritance）で実装

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | snippet.rb | `app/models/snippet.rb` | Snippetモデルの属性、バリデーション、関連定義を確認 |
| 1-2 | personal_snippet.rb | `app/models/personal_snippet.rb` | 個人スニペット固有の実装を確認 |

**読解のコツ**: PersonalSnippetはSnippetのサブクラス（STI）であり、project_idがNULLのスニペットを表す。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | snippets_controller.rb | `app/controllers/snippets_controller.rb` | 個人スニペット用コントローラーの構造を確認 |

**主要処理フロー**:
1. **15行目**: `skip_before_action :authenticate_user!` で一覧・詳細表示は認証不要
2. **19-21行目**: `index`アクションでダッシュボードまたはエクスプローラへリダイレクト
3. **23-25行目**: `new`アクションで新規PersonalSnippetオブジェクト作成
4. **36-42行目**: `determine_layout`で作成者かどうかでレイアウトを切り替え

#### Step 3: サービス層を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | create_service.rb | `app/services/snippets/create_service.rb` | スニペット作成のビジネスロジック |
| 3-2 | base_service.rb | `app/services/snippets/base_service.rb` | 共通処理の確認 |

**主要処理フロー（create_service.rb）**:
- **53-62行目**: `build_from_params`でプロジェクト有無によりモデルを切り替え
- **59-62行目**: プロジェクトなしの場合はPersonalSnippetを生成
- **119-125行目**: `move_temporary_files`で一時ファイルをスニペットに関連付け

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

```
SnippetsController
    │
    ├─ index
    │      └─ redirect_to dashboard_snippets_path または explore_snippets_path
    │
    ├─ new
    │      └─ PersonalSnippet.new
    │
    └─ (SnippetsActions concern経由)
           ├─ show
           │      └─ Snippet.find
           │
           ├─ create
           │      └─ Snippets::CreateService
           │             ├─ PersonalSnippet.new
           │             ├─ snippet.save
           │             ├─ create_repository
           │             └─ move_temporary_files
           │
           └─ update
                  └─ Snippets::UpdateService
```

### データフロー図

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

フォーム入力 ───▶ Controller ───▶ HTML/JSON
    │                │
    │                ▼
    │          CreateService
    │                │
    │                ├─▶ PersonalSnippet ───▶ snippetsテーブル
    │                │                          (project_id=NULL)
    │                │
    │                └─▶ Repository ───▶ Gitリポジトリ
    │
    └── params[:personal_snippet]
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| snippets_controller.rb | `app/controllers/snippets_controller.rb` | コントローラー | 個人スニペットのルーティング処理 |
| snippets_actions.rb | `app/controllers/concerns/snippets_actions.rb` | コンサーン | スニペット共通アクション |
| snippet.rb | `app/models/snippet.rb` | モデル | スニペットのデータ構造定義 |
| personal_snippet.rb | `app/models/personal_snippet.rb` | モデル | 個人スニペット固有実装 |
| create_service.rb | `app/services/snippets/create_service.rb` | サービス | スニペット作成処理 |
| update_service.rb | `app/services/snippets/update_service.rb` | サービス | スニペット更新処理 |
| destroy_service.rb | `app/services/snippets/destroy_service.rb` | サービス | スニペット削除処理 |
| application_controller.rb | `app/controllers/snippets/application_controller.rb` | コントローラー | スニペット共通ベースコントローラー |
