Skip to content

feat: セッションQA機能のフロントエンド実装 - 質問投稿、投票、回答、削除機能 (Vibe Kanban)#578

Open
takaishi wants to merge 20 commits intomainfrom
vk/8ac7-qa
Open

feat: セッションQA機能のフロントエンド実装 - 質問投稿、投票、回答、削除機能 (Vibe Kanban)#578
takaishi wants to merge 20 commits intomainfrom
vk/8ac7-qa

Conversation

@takaishi
Copy link
Contributor

@takaishi takaishi commented Jan 2, 2026

概要

セッション配信画面のチャット機能をQA機能に置き換えるため、フロントエンド側の実装を行いました。イベント参加者がセッションごとに質問を投稿し、スピーカーが回答できる機能を提供します。また、自分の質問を削除できる機能も追加しました。

主な変更内容

新規コンポーネント

SessionQA コンポーネント

  • セッションQA機能のメインコンポーネント
  • 生成されたAPIフック(swagger.ymlから自動生成)を使用
  • ActionCableによるリアルタイム更新
  • 質問のソート機能(投票数順/時間順、デフォルトは時間順)
  • 質問削除機能(3点メニューから)

内部コンポーネント

  • QuestionForm: 質問投稿フォーム(文字数制限なし)
  • QuestionList: 質問一覧表示
  • QuestionItem: 個別の質問表示(3点メニュー付き)
  • VoteButton: 質問への投票ボタン
  • SpeakerAnswerForm: スピーカー向け回答フォーム
  • AnswerItem: 回答の表示

技術実装

Swagger/OpenAPI統合

  • swagger.ymlにAPIエンドポイントを定義
  • yarn rtk-query-codegenでAPIフックを自動生成
  • 生成されたフックを使用して型安全なAPI呼び出しを実現

ActionCable統合

  • QaChannelへの接続(クライアントサイドでのみ動的インポート)
  • リアルタイムでの質問作成、投票、回答作成、質問削除の反映
  • WebSocket接続の管理とエラーハンドリング
  • question_deletedメッセージの処理

質問削除機能

  • Voteアイコンの右に3点メニューを表示(自分の質問の場合のみ)
  • メニューから削除可能
  • 削除確認ダイアログを表示
  • WebSocketで削除をリアルタイム反映

変更されたファイル

  • src/components/Track/Track.tsx: ChatコンポーネントをSessionQAに置き換え
  • src/components/SessionQA/SessionQA.tsx: 生成されたAPIフックを使用、削除機能を追加
  • src/components/SessionQA/internal/QuestionList/QuestionItem/QuestionItem.tsx: 3点メニューを追加
  • src/types/session-qa.ts: 型定義を生成されたAPIからインポート
  • schemas/swagger.yml: SessionQuestion APIエンドポイントを追加

機能詳細

参加者向け機能

  • セッションごとの質問投稿(文字数制限なし)
  • 他の参加者の質問への投票(トグル式)
  • 質問のソート切り替え(投票数順/時間順、デフォルトは時間順)
  • リアルタイムでの質問・回答・投票・削除の反映
  • 自分の質問の削除(3点メニューから)

スピーカー向け機能

  • 自分のセッションへの質問への回答投稿
  • 回答のリアルタイム表示

実装のポイント

  • Swagger/OpenAPI統合: swagger.ymlからyarn rtk-query-codegenでAPIフックを自動生成し、型安全性を確保
  • リアルタイム更新: ActionCableを使用して質問・回答・投票・削除を即座に反映
  • 質問削除: 自分の質問のみ削除可能(フロントエンドとバックエンドの両方でチェック)
  • 3点メニュー: Voteアイコンの右に配置し、UIをすっきりと保つ
  • 並び順の最適化: 質問投稿時の並び順の崩れを防ぐため、WebSocket更新を優先
  • SSR対応: actioncableを動的インポートしてサーバーサイドエラーを回避
  • 型安全性: 生成されたTypeScript型定義により、APIレスポンスの型安全性を確保

技術スタック

  • React, Next.js
  • Redux Toolkit Query (RTK Query)
  • ActionCable (WebSocket)
  • TypeScript
  • styled-components
  • OpenAPI Code Generation (@rtk-query/codegen-openapi)

This PR was written using Vibe Kanban

- SessionQAコンポーネントとその内部コンポーネントを再作成
  - QuestionForm, QuestionList, QuestionItem, VoteButton, SpeakerAnswerForm, AnswerItem
- RTK QueryでAPIと通信
- ActionCableでリアルタイム更新
- Track.tsxでSessionQAを使用するように修正
- Apollo Clientのエラーハンドリングを追加(errorPolicy: 'ignore')
  - GraphQL/ネットワークエラーが発生してもアプリを停止しない
  - useViewerCountにもエラーハンドリングを追加
@gitops-for-cloudnativedays gitops-for-cloudnativedays bot added the reviewapps Build ReviewApp environment automatically if this label is granted label Jan 2, 2026
@takaishi takaishi changed the title セッションのQA機能 (vibe-kanban) feat: セッションQA機能のフロントエンド実装 - 質問投稿、投票、回答機能 (Vibe Kanban) Jan 2, 2026
gitops-for-cloudnativedays bot added a commit to cloudnativedaysjp/dreamkast-infra that referenced this pull request Jan 2, 2026
commit: cloudnativedaysjp/dreamkast-ui@d5a298a
action URL: https://github.com/cloudnativedaysjp/dreamkast-ui/actions/runs/20652106776

Co-authored-by: gitops-for-cloudnativedays[bot] <113280573+gitops-for-cloudnativedays[bot]@users.noreply.github.com>
@github-actions
Copy link

github-actions bot commented Jan 2, 2026

…QA and related components

- Simplified import statement for actionCable in SessionQA component
- Streamlined sorting logic for questions by creation date
- Enhanced button formatting in QuestionItem component
- Consolidated props destructuring in SpeakerAnswerForm component
- Improved error logging format in Apollo Client error handling
gitops-for-cloudnativedays bot added a commit to cloudnativedaysjp/dreamkast-infra that referenced this pull request Jan 2, 2026
commit: cloudnativedaysjp/dreamkast-ui@1a85b4d
action URL: https://github.com/cloudnativedaysjp/dreamkast-ui/actions/runs/20652356905

Co-authored-by: gitops-for-cloudnativedays[bot] <113280573+gitops-for-cloudnativedays[bot]@users.noreply.github.com>
@takaishi
Copy link
Contributor Author

takaishi commented Jan 2, 2026

@claude このPRをレビューして

## 修正内容

### バックエンド(Rails)

1. **`SessionQuestion`モデル**: `length: { maximum: 512 }`バリデーションを削除
2. **`SessionQuestionAnswer`モデル**: `length: { maximum: 512 }`バリデーションを削除
3. **ビュー**: すべてのフォームから`maxlength: 512`属性を削除
   - `talks/partial_show/_qa_section.html.erb`
   - `speaker_dashboards/_question_item.html.erb`
   - `speaker_dashboards/create_answer.turbo_stream.erb`
   - `speaker_dashboard/speakers/_talk.html.erb`
4. **プレースホルダーと説明文**: 「最大512文字」の記述を削除

### フロントエンド(React)

1. **`QuestionForm.tsx`**:
   - `maxLength`定数を削除
   - `maxLength`バリデーションを削除
   - プレースホルダーから「最大512文字」を削除
   - 文字数カウント表示を「{bodyLength}/{maxLength}」から「{bodyLength}文字」に変更

2. **`SpeakerAnswerForm.tsx`**:
   - `maxLength`定数を削除
   - `maxLength`バリデーションを削除
   - プレースホルダーから「最大512文字」を削除
   - 文字数カウント表示を「{bodyLength}/{maxLength}」から「{bodyLength}文字」に変更

これで、質問と回答に文字数制限はなくなり、長い質問・回答にも対応できます。データベースの`text`型は十分な容量があるため、問題ありません。
## 実装したテスト

### バックエンド(Rails/RSpec)

1. **Factory(3つ)**
   - `session_question`
   - `session_question_answer`
   - `session_question_vote`

2. **モデルテスト(3つ)**
   - `session_question_spec.rb`: バリデーション、スコープ、メソッド
   - `session_question_answer_spec.rb`: バリデーション、スコープ
   - `session_question_vote_spec.rb`: バリデーション、コールバック、エラーハンドリング

3. **コントローラーテスト(5つ)**
   - `api/v1/session_questions_controller_spec.rb`: index, create, vote
   - `api/v1/session_question_answers_controller_spec.rb`: index, create
   - `talks/create_question_spec.rb`: create_question, show
   - `speaker_dashboards/qa_spec.rb`: show, questions, create_answer, destroy_answer
   - `admin/session_questions_spec.rb`: index, show, toggle_hidden

4. **チャンネルテスト(1つ)**
   - `channels/qa_channel_spec.rb`: subscribedメソッドのセキュリティチェック

### フロントエンド(React/Jest)

1. **コンポーネントテスト(3つ)**
   - `SessionQA.spec.tsx`: 基本表示、WebSocket接続
   - `QuestionForm.spec.tsx`: フォーム送信、バリデーション
   - `SpeakerAnswerForm.spec.tsx`: フォーム送信、キャンセル

## テストのカバレッジ

- モデルのバリデーションとスコープ
- APIコントローラーの認証・認可
- 質問の作成・取得・投票
- 回答の作成・削除
- 非表示機能
- ActionCableブロードキャスト
- Turbo Streams
- XSS対策(プロフィール名の表示)

テストを実行して動作を確認してください。必要に応じて追加・修正します。
## 変更内容

### バックエンド(Rails)
1. **管理画面の質問一覧** (`admin/session_questions/index.html.erb`)
   - 「質問者」列を削除
   - テーブルのcolspanを8から7に更新

2. **管理画面の質問詳細** (`admin/session_questions/show.html.erb`)
   - 「質問者:」行を削除
   - 投票者一覧から名前を削除(タイムスタンプのみ表示)

3. **スピーカーダッシュボードの質問一覧** (`speaker_dashboards/_question_item.html.erb`)
   - 「質問者:」行を削除

4. **セッションページのQAセクション** (`talks/partial_show/_qa_section.html.erb`)
   - 「質問者:」行を削除

5. **スピーカーダッシュボードのセッション詳細** (`speaker_dashboard/speakers/_talk.html.erb`)
   - 「質問者:」行を削除

### フロントエンド(React)
6. **QuestionItemコンポーネント** (`SessionQA/internal/QuestionList/QuestionItem/QuestionItem.tsx`)
   - `question.profile.name`の表示を削除

### テスト
7. **ビューのテスト** (`spec/requests/talks/create_question_spec.rb`)
   - プロフィール名の表示を確認するテストを削除

すべてのビューから質問者名(PublicName含む)の表示を削除しました。APIは引き続きプロフィール名を返しますが、UIには表示しません。
## 変更内容

### バックエンド(Rails API)
1. **`Api::V1::SessionQuestionsController#question_json`**
   - `profile` オブジェクト(`id` と `name`)をレスポンスから削除
   - 認証やデータ保存に必要な `@profile` は残しています

### フロントエンド(TypeScript)
2. **`dreamkast-ui/src/types/session-qa.ts`**
   - `SessionQuestion` 型から `profile` フィールドを削除

### テスト
3. **`spec/requests/api/v1/session_questions_controller_spec.rb`**
   - プロフィール名をチェックするテストを削除
   - 作成時のテストで `profile` キーが存在しないことを確認するアサーションに変更

ActionCableのブロードキャストでも `question_json` を使用しているため、自動的に更新されています。APIレスポンスから質問者名は返されません。
変更内容:
- `SessionQA.tsx`の`autoScroll`の初期値を`true`から`false`に変更

これで質問投稿後も現在のスクロール位置が維持されます。
## 修正内容

1. **`handleQuestionSubmit`から`refetchQuestions()`を削除**
   - WebSocketで質問が追加されるため、`refetchQuestions()`は不要
   - これにより、WebSocketの更新と`refetchQuestions()`の結果が競合して一時的にソートが崩れる問題を解消

2. **投票更新時にソートを再適用**
   - `question_voted`メッセージ受信時に、投票数を更新した後、`sortQuestions`を適用してソート順を維持

これにより、時間順でソートしている場合でも、質問投稿時に並び順が崩れなくなります。WebSocketでリアルタイムに質問が追加され、正しいソート順が維持されます。
## 修正内容

1. **`useEffect`で`questionsData`を更新する際のマージ処理を改善**
   - APIから取得した質問をベースにし、WebSocketで追加されたがまだAPIに反映されていない質問を保持
   - ソート後の順序が変わっていない場合は既存の状態を返し、不要な再レンダリングを防止

2. **重複チェックと最適化**
   - APIの質問IDセットを作成し、WebSocketのみの質問を識別
   - ソート後の配列が同じ場合は既存の状態を返し、一瞬の並び順の乱れを防止

これにより、質問投稿時にWebSocketで追加された質問が一時的に失われたり、並び順が崩れたりする問題を解消します。時間順でソートしている場合でも、正しい順序が維持されます。
## 修正内容

1. **`actioncable`のトップレベルインポートを削除**
   - `import * as actionCable from 'actioncable'`を削除

2. **`useEffect`内で動的インポートに変更**
   - `typeof window === 'undefined'`でサーバーサイドをチェック
   - `import('actioncable')`でクライアントサイドでのみ動的インポート
   - クリーンアップ関数を正しく設定

これにより、Next.jsのサーバーサイドレンダリング時に`window is not defined`エラーが発生しなくなります。`actioncable`はクライアントサイドでのみ実行されます。
## 修正内容

1. **`createSessionQuestion`の`invalidatesTags`を削除**
   - 質問投稿時にAPI再取得が発生しないように変更
   - WebSocket更新の���で処理

2. **`useEffect`の更新ロジックを簡素化**
   - 初回ロード時のみ`questionsData`を使用
   - その後はWebSocket更新のみで処理
   - ソート変更時は既存の質問を再ソート

3. **WebSocket更新を優先**
   - `handleWebSocketMessage`で質問を追加し、即座にソート
   - `useEffect`との競合を回避

これにより、質問投稿時にWebSocketで追加された質問が即座に正しい位置に表示され、一時的な並び順の崩れを防止します。時間順でソートしている場合でも、新しい質問が最上部に正しく表示されます。
…esc`(新しい順)です。

## 修正内容

1. **ソート関数の可読性を向上**
   - `created_at`のパースを明示的に変数に分離
   - コメントを追加して意図を明確化

2. **時間順のソートロジック**
   - 新しい順(降順): `timeB - timeA`
   - バックエンドの`order_by_time`(`created_at: :desc`)と一致

現在の実装では、時間順を選択すると新しい質問が上に表示されます。もし「古い順」を期待している場合は、ソートロジックを変更できます。どちらにしますか?
## 変更内容

- `sortBy`の初期値を`'votes'`から`'time'`に変更
- デフォルトで時間順(新しい順)で表示されます

これで、QA画面を開いた際に、デフォルトで時間順(新しい質問が上)で表示されます。
## 修正内容

1. **`sortBy`をrefで管理**
   - `sortByRef`を追加し、`sortBy`の変更時にrefも更新
   - `setQuestions`のコールバック内で最新の`sortBy`を参照可能に

2. **`handleWebSocketMessage`の修正**
   - `sortByRef.current`を使用して最新の`sortBy`を参照
   - 依存配列から`sortBy`を削除し、`sortQuestions`のみに

これにより、投票数順を選択している時に質問を投稿しても、正しく投票数順でソートされます。WebSocketで追加された質問も、現在選択されているソート順(投票数順または時間順)に従って表示されます。
## 変更内容

- ソートボタンの順序を変更
  - 左: 時間順
  - 右: 投票数順

これで、時間順が左、投票数順が右に表示されます。
## 変更内容

1. **swagger.ymlにSessionQuestionのAPIエンドポイントを追加**
   - GET `/api/v1/talks/{talkId}/session_questions` - 質問一覧取得
   - POST `/api/v1/talks/{talkId}/session_questions` - 質問作成
   - POST `/api/v1/talks/{talkId}/session_questions/{id}/vote` - 投票
   - GET `/api/v1/talks/{talkId}/session_questions/{sessionQuestionId}/session_question_answers` - 回答一覧取得
   - POST `/api/v1/talks/{talkId}/session_questions/{sessionQuestionId}/session_question_answers` - 回答作成

2. **スキーマ定義を追加**
   - `SessionQuestion`, `SessionQuestionAnswer`, `SessionQuestionsResponse`, `SessionQuestionCreateRequest`, `SessionQuestionVoteResponse`, `SessionQuestionAnswersResponse`, `SessionQuestionAnswerCreateRequest`, `SpeakerInfo`

3. **コード生成を実行**
   - `yarn rtk-query-codegen`を実行してAPIフックを生成

4. **SessionQAコンポーネントを更新**
   - 手動で定義したエンドポイントを削除
   - 生成されたAPIフック(`useGetApiV1TalksByTalkIdSessionQuestionsQuery`, `usePostApiV1TalksByTalkIdSessionQuestionsMutation`など)を使用
   - 型定義を生成された型に置き換え
   - `enhanceEndpoints`で`invalidatesTags`を削除し、WebSocket更新を優先

これで、DreamkastとDreamkast-UI間のAPIはswagger.ymlと`yarn rtk-query-codegen`で管理されるようになりました。
…`を使っていたため、`id`に変更しました。

これで投票が正常に動作するはずです。
## 実装内容

### バックエンド(Rails)
1. **`Api::V1::SessionQuestionsController#destroy`を追加**
   - 自分の質問のみ削除可能(`profile_id`でチェック)
   - 削除時にActionCableでブロードキャスト

2. **`TalksController#destroy_question`を追加**
   - Rails側のビューから削除可能

3. **ルートを追加**
   - API: `DELETE /api/v1/talks/:talk_id/session_questions/:id`
   - Rails: `DELETE /:event/talks/:id/destroy_question`

4. **APIレスポンスに`profile_id`を追加**
   - フロントエンドで自分の質問かどうかを判定可能に

### フロントエンド(React)
5. **削除機能を追加**
   - `QuestionItem`に削除ボタンを追加(自分の質問のみ表示)
   - WebSocketで`question_deleted`メッセージを受信して質問を削除

### Rails側のビュー
6. **削除ボタンを追加**
   - `talks/partial_show/_qa_section.html.erb`に削除ボタンを追加(自分の質問のみ表示)

### Swagger/OpenAPI
7. **swagger.ymlに削除エンドポイントを追加**
   - コード生成を実行してAPIフックを生成

これで、UIとDKの両方から、自分が投稿した質問を削除できます。
## 変更内容

1. **3点メニューボタンを追加**
   - Voteアイコンの右に「⋯」ボタンを配置(自分の質問の場合のみ表示)

2. **ドロップダウンメニューを実装**
   - メニューボタンをクリックするとメニューが開く
   - メニューから「削除」を選択可能
   - メニュー外をクリックすると閉じる

3. **スタイルを追加**
   - `MenuButton`: 3点メニューボタン
   - `MenuOverlay`: メニュー外クリック検出用のオーバーレイ
   - `Menu`: ドロップダウンメニューコンテナ
   - `MenuItem`: メニュー項目(削除)

これで、Voteアイコンの右に3点メニューが表示され、そこから削除できます。
gitops-for-cloudnativedays bot added a commit to cloudnativedaysjp/dreamkast-infra that referenced this pull request Jan 3, 2026
commit: cloudnativedaysjp/dreamkast-ui@c69d9c7
action URL: https://github.com/cloudnativedaysjp/dreamkast-ui/actions/runs/20670897324

Co-authored-by: gitops-for-cloudnativedays[bot] <113280573+gitops-for-cloudnativedays[bot]@users.noreply.github.com>
@takaishi takaishi changed the title feat: セッションQA機能のフロントエンド実装 - 質問投稿、投票、回答機能 (Vibe Kanban) feat: セッションQA機能のフロントエンド実装 - 質問投稿、投票、回答、削除機能 (Vibe Kanban) Jan 3, 2026
## 修正内容

1. **未使用のインポートを削除**
   - `SessionQuestionAnswer`を削除(使用されていない)
   - `dayjs`と`setupDayjs`を削除(使用されていない)

2. **関数の定義順序を修正**
   - `sortQuestions`を`useEffect`の前に移動
   - `handleWebSocketMessage`を`useEffect`の前に移動

3. **型エラーを修正**
   - `profile.speakerId`の参照を修正(Profile型に`speakerId`が含まれていないため、暫定的に`false`を返すように変更)

4. **未使用の変数を修正**
   - `event`パラメータをオプショナルに変更
   - `setAutoScroll`を削除(`autoScroll`のみ使用)

5. **テストのモックデータを更新**
   - `profile`オブジェクトを`profile_id`に変更(最新のAPIレスポンス形式に合わせる)

すべてのテストが成功しています。
gitops-for-cloudnativedays bot added a commit to cloudnativedaysjp/dreamkast-infra that referenced this pull request Jan 3, 2026
commit: cloudnativedaysjp/dreamkast-ui@0bb2ead
action URL: https://github.com/cloudnativedaysjp/dreamkast-ui/actions/runs/20670974992

Co-authored-by: gitops-for-cloudnativedays[bot] <113280573+gitops-for-cloudnativedays[bot]@users.noreply.github.com>
@takaishi takaishi self-assigned this Jan 4, 2026
@takaishi takaishi requested a review from a team January 4, 2026 10:54
@takaishi takaishi marked this pull request as ready for review January 4, 2026 10:54
@github-actions github-actions bot removed the reviewapps Build ReviewApp environment automatically if this label is granted label Jan 8, 2026
@jacopen
Copy link
Contributor

jacopen commented Jan 11, 2026

@claude このPRをレビューして

1 similar comment
@jacopen
Copy link
Contributor

jacopen commented Jan 11, 2026

@claude このPRをレビューして

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants