コメント・アクティビティ 仕様書

スレッドコメント・@メンション・変更履歴の自動記録

ステータス: Draft / 作成日: 2026-05-27 PR #4 — 依存: コア


1. データモデル

1.1 task_comments

pub struct Model {
    pub id: Uuid,
    pub task_id: Uuid,
    pub user_id: Uuid,
    pub body: String,                     // Markdown(@メンション含む)
    pub parent_comment_id: Option<Uuid>,  // スレッド返信(1 段のみ)
    pub created_at: DateTimeUtc,
    pub updated_at: DateTimeUtc,
    pub deleted_at: Option<DateTimeUtc>,  // ソフトデリート
}
カラム 型 制約
id UUID PK
task_id UUID NOT NULL, FK→tasks CASCADE
user_id UUID NOT NULL, FK→users CASCADE
body TEXT NOT NULL
parent_comment_id UUID NULLABLE, FK→task_comments SET NULL
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
deleted_at TIMESTAMPTZ NULLABLE

1.2 task_activities

タスクへのあらゆる変更を自動記録する監査ログ。アプリ層から明示的に書き込む。

カラム 型 制約 説明
id UUID PK  
task_id UUID NOT NULL, FK→tasks CASCADE  
user_id UUID NULLABLE, FK→users SET NULL NULL = システム操作
event_type VARCHAR NOT NULL 下表参照
payload JSONB NOT NULL 変更前後の値
created_at TIMESTAMPTZ NOT NULL DEFAULT now()  

event_type 一覧:

値 ペイロード例
task_created {}
github_issue_imported { "repo_owner": "acme", "repo_name": "backend", "issue_number": 42 }
github_issue_synced { "repo_owner": "acme", "repo_name": "backend", "issue_number": 42 }
status_changed { "from": "Backlog", "to": "In Review" }
priority_changed { "from": "medium", "to": "high" }
assignee_added { "user_id": "uuid", "role": "primary" }
assignee_removed { "user_id": "uuid" }
deadline_changed { "field": "hard_deadline", "from": "...", "to": "..." }
comment_added { "comment_id": "uuid" }
relation_added { "type": "blocks", "target_seq_id": 55 }
relation_removed { "type": "blocks", "target_seq_id": 55 }
label_added { "label_id": "uuid", "name": "bug" }
label_removed { "label_id": "uuid", "name": "bug" }
sprint_changed { "from": "Sprint 2", "to": "Sprint 3" }
archived / unarchived {}
forge_commit_linked { "host": "github", "sha": "a3f92c1…", "message": "fix: … TASK-42", "author_handle": "yupix", "author_name": "Yupix", "html_url": "https://github.com/…/commit/a3f92c1…" }
github_pr_linked { "pr_number": 87, "title": "..." }
github_pr_merged { "pr_number": 87 }

GitHub Issue の一括取り込みと webhook による新規作成は github_issue_imported、 リンク済みタスクへの変更反映は github_issue_synced を記録する。 どちらも user_id = NULL(システム操作)で、タスク・同期リンクと同じトランザクションで保存する。 再取り込み・同じ内容の通知・古い通知・書き戻しの跳ね返りでは履歴を増やさない。 画面には同期元のリポジトリ名と Issue 番号を表示する。導入前の履歴は遡って作成しない。


2. マイグレーション

CREATE TABLE task_comments (
    id UUID PRIMARY KEY,
    task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    body TEXT NOT NULL,
    parent_comment_id UUID REFERENCES task_comments(id) ON DELETE SET NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    deleted_at TIMESTAMPTZ
);

CREATE TABLE task_activities (
    id UUID PRIMARY KEY,
    task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
    user_id UUID REFERENCES users(id) ON DELETE SET NULL,
    event_type VARCHAR NOT NULL,
    payload JSONB NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_comments_task ON task_comments(task_id, created_at)
    WHERE deleted_at IS NULL;
CREATE INDEX idx_activities_task ON task_activities(task_id, created_at DESC);

3. @メンション処理

コメント本文中の @ユーザー名 をサーバー側でパースし、通知を生成する。

パース対象: @username 形式(アルファベット・数字・_・- を含む)
1. body から正規表現で @mention を抽出
2. username でユーザーを検索
3. 見つかった場合 → notifications テーブルに type=mentioned で挿入
   (通知システムは PR #5 で実装。本 PR では mention 抽出のみ行い、
    通知テーブルが存在しない場合は INSERT をスキップする)

4. API

コメント

メソッド パス 説明
GET /tasks/{id}/comments コメント一覧(スレッド構造、新着順)
POST /tasks/{id}/comments 投稿
PUT /tasks/{id}/comments/{cid} 編集(投稿者本人のみ)
DELETE /tasks/{id}/comments/{cid} ソフトデリート(投稿者 or テナントオーナー)

POST リクエスト:

{
  "body": "この件は @tanaka さんに確認してください。",
  "parent_comment_id": null
}

GET レスポンス(スレッド構造):

{
  "comments": [
    {
      "id": "uuid",
      "user": { "id": "uuid", "name": "田中" },
      "body": "設計は完了しました。",
      "replies": [
        {
          "id": "uuid",
          "user": { "id": "uuid", "name": "鈴木" },
          "body": "レビュー依頼します。",
          "created_at": "2026-05-27T12:00:00Z"
        }
      ],
      "created_at": "2026-05-27T11:00:00Z",
      "updated_at": "2026-05-27T11:00:00Z",
      "is_deleted": false
    }
  ]
}

削除済みコメントは "body": null, "is_deleted": true で返す(返信があれば親は残す)。

アクティビティ

メソッド パス 説明
GET /tasks/{id}/activities 変更履歴一覧(新着順)

レスポンス:

{
  "activities": [
    {
      "id": "uuid",
      "event_type": "status_changed",
      "user": { "id": "uuid", "name": "田中" },
      "payload": { "from": "Backlog", "to": "In Review" },
      "created_at": "2026-05-27T11:00:00Z"
    }
  ]
}

5. フロントエンド(Phase B)

タスク詳細のコメントセクション(実装済み)

タスク詳細ページに、コメントだけの独立セクションとして実装している。

──── コメント ────────────────────────────────────
  🧑 田中  11:10
  設計は完了しました。
    └ 🧑 鈴木  11:15
      レビュー依頼します。
      [返信フォーム]
  [新規投稿フォーム]
  • 一覧はクライアント取得(vue-query)。取得失敗はセクション内だけで倒し、 再試行導線を出す。投稿フォームは取得失敗時も残す(GET と POST は独立)

  • 投稿・返信(1 段スレッド)・編集・削除。削除済みコメントはプレースホルダ表示で、 編集・削除・返信の導線を出さない

  • 編集ボタンは投稿者本人のコメントにのみ表示(backend は本人以外を 403 にする)。 削除ボタンは全コメントに表示し、可否は backend 判定に委ねる (テナントオーナーにも許可されており frontend からは判定できないため)

  • 削除済みでないコメントのうち updated_at != created_at のものに「(編集済み)」を 表示する(backend は作成時に両カラムへ同一時刻を入れる。削除済みは プレースホルダ表示のため対象外)

  • 本文は素テキスト表示(改行保持・エスケープ)

コンポーネント

コンポーネント ファイル
TaskComments(一覧+投稿・返信フォーム) components/tasks/TaskComments.vue
TaskCommentItem(1 コメントの表示・編集・削除) components/tasks/TaskCommentItem.vue

残件

  • アクティビティとの時系列混在表示(TaskTimeline): 当初案のコメントと アクティビティを混在させるタイムラインは未実装。GET /tasks/{id}/activities の消費側も未接続

  • コメント本文の Markdown(KFM)描画: 本文はデータとしては Markdown (@メンション含む)を想定するが、表示は素テキストのまま。KFM レンダラの コメント本文への接続は docs/frontend/kfm-phase1-implementation.md でも 未接続の残件とされている