2026-01-26 質問の逐次表示 plan
質問の逐次表示機能 実装計画
概要
現在、フロントエンドのUIで複数の質問がある場合、すべての質問が一度に表示されている。 ユーザーの要望として、質問を一つずつ順番に表示してほしいという要求がある。
現状分析
問題点
QuestionSection.tsxでquestions.map()を使用して全質問を一度に表示- ユーザーが複数の質問を同時に見ることになり、混乱する可能性
- 現在の実装では、質問ごとに状態管理(
customAnswers,selectedOptions)を行っているが、全質問が同時表示されているため複雑
影響範囲
フロントエンドのみの変更
frontend/src/components/QuestionSection.tsx: 質問表示ロジックの変更frontend/src/types/index.ts: 型定義の確認(変更不要の可能性)
バックエンドは変更不要
- バックエンドは既に複数の質問を配列で送信している
- フロントエンドで表示制御を行う
懸念点
1. currentQuestionIndex の管理場所
重要な前提: page.tsxのhandleAnswer内でsetShowQuestions(false)が実行され、質問UIが一旦非表示になる。バックエンドからの応答で再表示される。つまり、QuestionSectionコンポーネントはアンマウント→再マウントされる。
懸念:
currentQuestionIndexをQuestionSection内部で管理すると、コンポーネントが再マウントされるたびにリセットされてしまう
選択肢:
-
親コンポーネントで管理(推奨):
page.tsxでcurrentQuestionIndexを状態管理- メリット: コンポーネントの再マウントに影響されない
- デメリット: 親コンポーネントの変更が必要
-
親の挙動変更:
handleAnswer内のsetShowQuestions(false)を削除- メリット:
QuestionSection内で完結できる - デメリット: 既存のUX(ローディング中に質問UIを非表示)を変更することになる
- メリット:
決定: 選択肢1(親コンポーネントで管理) を採用
- 既存のUXを維持しつつ、質問の逐次表示を実現できる
page.tsxにcurrentQuestionIndex状態を追加し、propsで渡す
2. 質問の回答方法
懸念:
- バックエンドに質問を一つずつ送るべきか、全て集めてから送るべきか?
選択肢:
-
表示のみ逐次化(推奨): UIでは一問ずつ表示するが、回答はバックエンドに即座に送信
- メリット: シンプル、既存の実装を大きく変更しない
- デメリット: バックエンドとの通信が複数回発生(ただし現在も同じ)
-
全回答収集方式: 全ての質問に回答してから一度に送信
- メリット: バックエンドへの通信が1回で済む
- デメリット: バックエンドAPIの変更が必要、実装が複雑
決定: 選択肢1(表示のみ逐次化) を採用
- バックエンドAPIの変更不要
- ユーザーは一問ずつ集中して回答できる
3. 進捗表示
懸念:
- 「現在 3 問中 1 問目」のような進捗を表示すべきか?
選択肢:
-
進捗表示なし: 現在の質問のみ表示
- メリット: シンプル
- デメリット: あと何問あるか分からない
-
進捗表示あり(推奨): 「質問 1/3」のような表示を追加
- メリット: ユーザーが全体像を把握できる
- デメリット: UI の追加実装が必要(ただし軽微)
決定: 選択肢2(進捗表示あり) を採用
- 実装コストが低い
- ユーザー体験の向上
推奨アプローチ
親コンポーネントで状態管理 + 表示のみ逐次化 + 進捗表示あり
理由:
- 既存の
onAnswerの挙動を変更しない - 既存のUX(ローディング表示)を維持
- ユーザーが一つずつ質問に集中でき、全体の進捗も把握できる
実装計画
フロントエンド変更
1. page.tsx の変更
追加する状態:
currentQuestionIndex: number- 現在表示中の質問のインデックス- 初期値:
0 questionsが変更されたら0にリセット
追加する関数:
handleAnswerWithNext: (answer: string) => void- 現在の
handleAnswerを呼び出す currentQuestionIndexをインクリメント- ただし、最後の質問の場合は既存の挙動(バックエンドに送信)
- 現在の
変更する props:
QuestionSectionにcurrentQuestionIndexを渡すonAnswerの代わりにhandleAnswerWithNextを渡す
2. QuestionSection.tsx の変更
追加する props:
currentQuestionIndex: number- 現在表示すべき質問のインデックス
変更する処理:
- 質問表示:
questions[currentQuestionIndex]のみを表示(mapを削除) - 進捗表示:
currentQuestionIndex + 1/questions.lengthを表示 - 回答送信時:
onAnswerを呼ぶだけ(インクリメントは親が行う)
簡略化する状態:
customAnswers: Record<number, string>→customAnswer: stringselectedOptions: Record<number, string[]>→selectedOptions: string[]- インデックスが不要になるため
削除する処理:
questions.map()によるループidxパラメータの使用
3. 型定義の確認
frontend/src/types/index.tsのQuestion型は変更不要。
実装ステップ
page.tsxにcurrentQuestionIndex状態を追加page.tsxにuseEffectを追加し、questions変更時にcurrentQuestionIndexを0にリセットpage.tsxにhandleAnswerWithNext関数を追加page.tsxからProgressContainerへのpropsにcurrentQuestionIndexを追加ProgressContainer.tsxのprops定義にcurrentQuestionIndexを追加し、QuestionSectionに渡すQuestionSection.tsxのprops定義にcurrentQuestionIndexを追加QuestionSection.tsxで状態を簡略化(Record<number, T>からTへ)QuestionSection.tsxでquestions.map()を削除し、questions[currentQuestionIndex]のみをレンダリングQuestionSection.tsxに進捗表示を追加(例:質問 1/3)- 境界チェック:
currentQuestionIndex >= questions.lengthの場合は何も表示しない
テスト観点
- 単一質問の場合、正常に表示・回答できること
- 複数質問の場合、一つずつ順番に表示されること
- 最後の質問に回答したら、質問セクションが非表示になること
- マルチセレクトの質問が正常に動作すること
- カスタム回答が正常に動作すること
変更ファイル一覧
frontend/src/app/page.tsx:currentQuestionIndex状態の追加handleAnswerWithNext関数の追加useEffectでquestions変更監視ProgressContainerへのprops追加(currentQuestionIndexを渡す)
frontend/src/components/ProgressContainer.tsx:- props定義の変更(
currentQuestionIndex追加) QuestionSectionへのprops追加
- props定義の変更(
frontend/src/components/QuestionSection.tsx:- props定義の変更(
currentQuestionIndex追加) - 質問表示ロジックの変更(
map削除、単一質問表示) - 状態の簡略化(
Record<number, T>からTへ) - 進捗表示の追加
- props定義の変更(
実装後の動作
- バックエンドから複数の質問が送られてくる
page.tsxでquestionsが設定され、currentQuestionIndexが0にリセットされるshowQuestionsがtrueになり、QuestionSectionが表示される- フロントエンドは
questions[0](最初の質問)のみを表示、進捗表示「質問 1/3」を表示 - ユーザーが回答すると:
- 最後の質問でない場合:
currentQuestionIndexをインクリメントし、次の質問を表示(バックエンド通信なし) - 最後の質問の場合:
handleAnswerを呼び、バックエンドに送信→ローディング表示
- 最後の質問でない場合:
- 最後の質問でない場合は、すぐに次の質問(
questions[1]など)が表示される - 最後の質問に回答すると、既存の挙動(ローディング→結果表示)に従う
エッジケースの扱い
1. questions が変更された場合
useEffectでquestionsを監視し、変更があればcurrentQuestionIndexを0にリセット- これにより、バックエンドから新しい質問セットが来た場合に最初から表示される
2. currentQuestionIndex >= questions.lengthの場合
QuestionSectionコンポーネントの先頭で境界チェックを行う- 条件:
!visible || questions.length === 0 || currentQuestionIndex >= questions.length - この場合は
nullを返して何も表示しない
3. ローディング状態の扱い
- 最後の質問でない場合:
handleAnswerWithNextはhandleAnswerを呼ばないため、ローディング表示にならない - 最後の質問の場合:
handleAnswerを呼び、既存のローディング表示動作を実行 - これにより、中間の質問では即座に次の質問が表示され、最後の質問でのみバックエンド通信が発生
4. 最後の質問の判定
currentQuestionIndex === questions.length - 1で判定- この場合のみ
handleAnswerを呼んでバックエンドに送信 - それ以外は
currentQuestionIndexをインクリメントするだけ
備考
- バックエンドの変更は不要
- 最後の質問以外では
handleAnswerを呼ばないため、バックエンド通信が発生しない - 最後の質問に回答した時のみ既存の
handleAnswerの動作(ローディング→バックエンド送信→結果表示)が実行される
実装完了レポート(最終版)
実装サマリー
- 実装日: 2026-01-26
- 変更ファイル数: 5 files
- スコープ: frontend/ ディレクトリのみ(バックエンド変更なし)
主な変更点
- 質問の逐次表示機能を実装
- 進捗表示(「質問 N/M」形式)を追加
- 最後の質問以外ではバックエンド通信なしで次の質問へ遷移
- ドキュメント(screens.md, screen-flow.md)を更新
変更ファイル一覧
| ファイル | 変更内容 |
|---|---|
frontend/src/app/page.tsx | currentQuestionIndex 状態追加、setQuestionsWithReset 関数追加、handleAnswerWithNext 関数追加、ProgressContainer への props 追加 |
frontend/src/components/ProgressContainer.tsx | props 型に currentQuestionIndex 追加、QuestionSection に key と currentQuestionIndex を渡す |
frontend/src/components/QuestionSection.tsx | props 型に currentQuestionIndex 追加、状態を簡略化(Record型から単純型へ)、進捗表示追加、単一質問表示に変更、境界チェック追加 |
frontend/docs/screens.md | 「質問の逐次表示」セクション追加(動作仕様、状態管理)、データフロー図更新 |
frontend/docs/screen-flow.md | 質問回答フローの詳細追加(mermaid図)、全体フロー図更新 |
技術的アプローチと設計判断
1. 状態管理の設計
currentQuestionIndex を親コンポーネント(page.tsx)で管理する設計を採用。
理由: QuestionSection は回答時に setShowQuestions(false) でアンマウントされるため、子コンポーネント内部で状態を持つとリセットされてしまう。
実装コード(page.tsx):
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const setQuestionsWithReset = useCallback((newQuestions: Question[]) => {
setQuestions(newQuestions);
setCurrentQuestionIndex(0);
}, []);
2. コンポーネントのリマウント戦略
ProgressContainer で QuestionSection に key={currentQuestionIndex} を付与。
実装コード(ProgressContainer.tsx):
<QuestionSection
key={currentQuestionIndex}
questions={questions}
visible={showQuestions}
currentQuestionIndex={currentQuestionIndex}
onAnswer={onAnswer}
/>
メリット:
- 質問が切り替わるとコンポーネントがリマウントされ、
customAnswerとselectedOptionsが自動的にリセット - 明示的な状態リセット処理が不要になり、コードがシンプルに
3. 状態の簡略化
変更前(全質問同時表示用):
const [customAnswers, setCustomAnswers] = useState<Record<number, string>>({});
const [selectedOptions, setSelectedOptions] = useState<Record<number, string[]>>({});
変更後(単一質問表示用):
const [customAnswer, setCustomAnswer] = useState("");
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
4. 最後の質問の判定と処理
実装コード(page.tsx):
const handleAnswerWithNext = useCallback(
(answer: string) => {
const isLastQuestion = currentQuestionIndex >= questions.length - 1;
if (isLastQuestion) {
handleAnswer(answer); // バックエンドに送信
} else {
setCurrentQuestionIndex((prev) => prev + 1); // 次の質問を表示
}
},
[currentQuestionIndex, questions.length, handleAnswer]
);
計画からの変更点
実装計画に記載がなかった判断・選択:
-
keyprop によるリマウント戦略: 計画では明示的な状態リセットを想定していたが、React のkeyprop を使用したリマウント戦略を採用(より宣言的でバグが少ない) -
setQuestionsWithReset関数の導入: 計画ではuseEffectによるquestions監視を想定していたが、質問セットと同時にインデックスをリセットする関数を導入(より直接的で確実、副作用の連鎖を回避)
検証結果
| 項目 | 結果 | 備考 |
|---|---|---|
TypeScript 型チェック(npm run type-check) | Pass | エラーなし |
ESLint(npm run lint) | Pass | 警告なし |
ビルド(npm run build) | Pass | 既存の警告のみ(本変更に起因するものはなし) |
実装時の課題
特になし。計画に沿った実装が可能だった。
残存する懸念点
特になし。
ユーザー向け動作変更
変更前の動作
- 複数の質問がある場合、すべてが同時に表示される
- ユーザーは各質問を個別に回答し、それぞれの「Submit」ボタンをクリック
- 各回答ごとにバックエンド通信が発生
変更後の動作
- 複数の質問がある場合、1問ずつ順番に表示
- 「質問 1/3」のような進捗表示が追加
- 最後の質問以外への回答時は、即座に次の質問が表示(バックエンド通信なし、待ち時間なし)
- 最後の質問への回答時のみ、バックエンドに送信してローディング表示
動作確認フロー
1. フロントエンドを起動: cd frontend && npm run dev
2. プロジェクトパスを入力し、複数の質問が発生するコマンドを実行
3. 確認事項:
- 最初の質問のみが表示されること
- 進捗表示「質問 1/N」が表示されること
4. 最初の質問に回答
5. 確認事項:
- 次の質問が表示されること(バックエンド通信なし)
- 進捗表示が「質問 2/N」に更新されること
- 前の質問の入力状態がリセットされていること
6. 最後の質問まで回答を繰り返す
7. 最後の質問に回答
8. 確認事項:
- バックエンドに回答が送信されること
- ローディング表示になること
デプロイ後の確認事項
- 単一質問の場合、正常に表示・回答できること
- 複数質問の場合、一つずつ順番に表示されること
- 進捗表示(質問 N/M)が正しく表示されること
- 最後の質問に回答したら、バックエンドに送信されること
- マルチセレクトの質問が正常に動作すること
- カスタム回答入力が正常に動作すること
- 質問切り替え時に入力状態がリセットされること
- 新しい質問セットを受信した場合、インデックスが0にリセットされること
レビュー結果詳細
コード品質分析
| 観点 | 評価 | 詳細 |
|---|---|---|
| 計画との整合性 | Pass | 全ての要件を満たしている |
| React ベストプラクティス | Pass | useCallback による最適化、適切な状態管理 |
| TypeScript の活用 | Pass | 型安全なプロップス定義 |
| コンポーネント設計 | Pass | key prop による状態リセットは React の推奨パターン |
| コードの可読性 | Pass | 明確な関数名、適切なコメント |
各ファイルのレビュー
page.tsx
良い点:
setQuestionsWithResetで質問とインデックスを同時にリセット(useEffectより直接的)handleAnswerWithNextで最後の質問判定を明確に実装useCallbackによる適切なメモ化
コード例:
const setQuestionsWithReset = useCallback((newQuestions: Question[]) => {
setQuestions(newQuestions);
setCurrentQuestionIndex(0);
}, []);
ProgressContainer.tsx
良い点:
key={currentQuestionIndex}の追加により、質問切り替え時にコンポーネントを強制リマウント- プロップスの追加は最小限で影響範囲を抑制
QuestionSection.tsx
良い点:
- 状態の簡略化(
Record<number, T>からTへ)により複雑度低下 - 境界チェックを先頭で実施(安全な実装)
- 進捗表示「質問 N/M」を日本語で表示
制限事項
-
中間回答の非保存: 最後の質問以外の回答はバックエンドに送信されない
- 設計上の意図的な動作(バックエンド API 変更不要のため)
-
戻るボタンなし: 前の質問に戻る機能は未実装
- 現時点では要件外
テスト推奨事項
ユニットテスト(将来の実装向け)
// QuestionSection.test.tsx
describe("QuestionSection", () => {
it("should display only the current question", () => {
// currentQuestionIndex=0 の場合、questions[0] のみ表示
});
it("should show progress indicator in N/M format", () => {
// 「質問 1/3」形式で表示されること
});
it("should reset state when key changes", () => {
// key prop 変更時に内部状態がリセットされること
});
it("should handle boundary case when index exceeds length", () => {
// currentQuestionIndex >= questions.length で null を返すこと
});
it("should call onAnswer with correct value", () => {
// 選択肢クリック時に onAnswer が正しい値で呼ばれること
});
});
E2E テスト(将来の実装向け)
// e2e/sequential-questions.spec.ts
describe("Sequential Question Display", () => {
it("should display questions one at a time", () => {
// 複数質問が一度に表示されないこと
});
it("should advance to next question after answering", () => {
// 回答後に次の質問に遷移すること
});
it("should submit to backend only on last question", () => {
// 最後の質問でのみ API 呼び出しが発生すること
});
it("should reset input state when question changes", () => {
// 質問切り替え時に入力フィールドがクリアされること
});
});
手動テストマトリクス
| テストケース | 手順 | 期待結果 | 優先度 |
|---|---|---|---|
| 単一質問 | 質問が1つのコマンドを実行 | 「質問 1/1」と表示、回答後にバックエンド送信 | High |
| 複数質問 | 質問が3つのコマンドを実行 | 「質問 1/3」から開始、順次表示、最後でバックエンド送信 | High |
| マルチセレクト | 複数選択可能な質問に回答 | 選択状態が維持され、Submit で次へ | Medium |
| カスタム回答 | テキスト入力で回答 | 入力内容が回答として使用される | Medium |
| 状態リセット | 次の質問に進む | 前の質問の入力状態がクリアされる | High |
| 新しい質問セット | バックエンドから新しい質問を受信 | インデックスが0にリセットされる | Medium |