開発1 入会申請ステップ定義を言語化する

2026年02月07日

ここからがこのプロジェクトの本体です。
コードより先に、**“絶対にブレない設計物”**を作ります。


入会申請チャット:ステップ定義(確定版)

これは

  • React(既存チャット)
  • Laravel(StepEngine)
  • OpenAI(文章整形)

すべての共通言語になります。


0️⃣ 基本思想(最重要)

  • 1ステップ = 1判断
  • ステップは 順序 を持つが、番号では管理しない
  • 判定は Laravelのみ
  • Reactは 描画係
  • AIは 言い換え係

1️⃣ ステップ一覧(全体像)

intro

email

name

kana

school

grade

phone

participation_type

preferred_date_1

preferred_date_2

motivation

confirmation

completed

※ reject は どのステップからも起こり得る


2️⃣ ステップ定義テーブル(設計の核)

🟢 Step: intro

項目内容
keyintro
質問「これから入会申請の確認を行います。よろしいですか?」
入力yes / no
必須yes
判定no → 終了
email

🟢 Step: email

項目内容
keyemail
質問「メールアドレスを教えてください」
入力text
必須yes
バリデーションemail形式
NG時再質問
name

🟢 Step: name

項目内容
keyname
質問「生徒さんのお名前を教えてください」
入力text
必須yes
kana

🟢 Step: kana

項目内容
keykana
質問「お名前をカタカナで入力してください」
入力text
バリデーションカタカナ
school

🟢 Step: school

項目内容
keyschool
質問「通っている学校名を教えてください」
入力text
grade

🟢 Step: grade

項目内容
keygrade
質問「現在の学年を選んでください」
入力select
選択肢小1〜高3
phone

🟢 Step: phone

項目内容
keyphone
質問「連絡の取れる電話番号を教えてください」
入力text
バリデーション数字 / ハイフン可
participation_type

🟢 Step: participation_type

項目内容
keyparticipation_type
質問「参加形態を教えてください」
入力select
選択肢親子 / 生徒のみ
preferred_date_1

🟢 Step: preferred_date_1

項目内容
keypreferred_date_1
質問「第一希望の日程を教えてください」
入力date
preferred_date_2

🟢 Step: preferred_date_2

項目内容
keypreferred_date_2
質問「第二希望の日程を教えてください(なければ空欄でOK)」
入力date / empty
motivation

🟢 Step: motivation

項目内容
keymotivation
質問「参加のきっかけを教えてください」
入力textarea
必須no
confirmation

🟢 Step: confirmation

項目内容
keyconfirmation
質問「内容を確認し、送信してよろしいですか?」
入力yes / no
判定no → 差し戻し
completed

🟢 Step: completed

項目内容
keycompleted
表示「申請を受け付けました」
処理DB確定

3️⃣ 機械可読な定義(Laravel / React 共通)

{
  "key": "email",
  "type": "text",
  "required": true,
  "validation": ["required", "email"],
  "next": "name"
}

👉 これが StepEngine の燃料


4️⃣ この設計の強さ

  • フォームにも戻せる
  • LINE Bot にも流用できる
  • AIを切っても成立する
  • 法務・運用に説明できる
  • テストが書ける

次にやること(選択)

ここからは 実装に落とします
どこから行くかだけ決めましょう。

次の選択肢

A:このステップ定義を
👉 Laravel 用 StepDefinition.php に落とす

B:既存 React チャットに
👉 このステップ定義を差し込む方法

C:Laravel 側
👉 StepEngine(状態遷移ロジック)を書く

返事は A / B / C のどれかだけでOKです。
ここからは一気に“動くもの”にします 🚀

了解。
では A:Laravel 用 StepDefinition に落とします
ここは そのままコピペして使える設計コードを出します。


Laravel10:入会申請 StepDefinition 設計

目的はこれです👇

  • ステップ定義を1か所に集約
  • Controller / Service / Validator が共通で使える
  • React / OpenAI に引きずられない

① ディレクトリ構成(まず決め打ち)

app/
└─ Domain/
└─ Application/
├─ Step/
│ ├─ StepDefinition.php ← 今回作る
│ └─ StepKeys.php
└─ ApplicationFlow.php

👉 Domain 配下に置くのがポイント
👉 HTTP / UI から切り離す


② StepKeys(キーのenum化)

<?php
// app/Domain/Application/Step/StepKeys.php

namespace App\Domain\Application\Step;

final class StepKeys
{
    public const INTRO               = 'intro';
    public const EMAIL               = 'email';
    public const NAME                = 'name';
    public const KANA                = 'kana';
    public const SCHOOL              = 'school';
    public const GRADE               = 'grade';
    public const PHONE               = 'phone';
    public const PARTICIPATION_TYPE  = 'participation_type';
    public const PREFERRED_DATE_1    = 'preferred_date_1';
    public const PREFERRED_DATE_2    = 'preferred_date_2';
    public const MOTIVATION          = 'motivation';
    public const CONFIRMATION        = 'confirmation';
    public const COMPLETED           = 'completed';
}

③ StepDefinition(中核)

<?php
// app/Domain/Application/Step/StepDefinition.php

namespace App\Domain\Application\Step;

final class StepDefinition
{
    public static function all(): array
    {
        return [

            StepKeys::INTRO => [
                'type'      => 'confirm',
                'question'  => 'これから入会申請の確認を行います。よろしいですか?',
                'required'  => true,
                'next'      => StepKeys::EMAIL,
            ],

            StepKeys::EMAIL => [
                'type'       => 'text',
                'question'   => 'メールアドレスを教えてください',
                'required'   => true,
                'validation' => ['required', 'email'],
                'next'       => StepKeys::NAME,
            ],

            StepKeys::NAME => [
                'type'      => 'text',
                'question'  => '生徒さんのお名前を教えてください',
                'required'  => true,
                'next'      => StepKeys::KANA,
            ],

            StepKeys::KANA => [
                'type'       => 'text',
                'question'   => 'お名前をカタカナで入力してください',
                'required'   => true,
                'validation' => ['required', 'regex:/^[ァ-ヶー ]+$/u'],
                'next'       => StepKeys::SCHOOL,
            ],

            StepKeys::SCHOOL => [
                'type'      => 'text',
                'question'  => '通っている学校名を教えてください',
                'required'  => true,
                'next'      => StepKeys::GRADE,
            ],

            StepKeys::GRADE => [
                'type'     => 'select',
                'question' => '現在の学年を選んでください',
                'required' => true,
                'options'  => [
                    '小1','小2','小3','小4','小5','小6',
                    '中1','中2','中3',
                    '高1','高2','高3',
                ],
                'next' => StepKeys::PHONE,
            ],

            StepKeys::PHONE => [
                'type'       => 'text',
                'question'   => '連絡の取れる電話番号を教えてください',
                'required'   => true,
                'validation' => ['required', 'regex:/^[0-9\-]+$/'],
                'next'       => StepKeys::PARTICIPATION_TYPE,
            ],

            StepKeys::PARTICIPATION_TYPE => [
                'type'     => 'select',
                'question' => '参加形態を教えてください',
                'required' => true,
                'options'  => ['親子', '生徒のみ'],
                'next'     => StepKeys::PREFERRED_DATE_1,
            ],

            StepKeys::PREFERRED_DATE_1 => [
                'type'     => 'date',
                'question' => '第一希望の日程を教えてください',
                'required' => true,
                'next'     => StepKeys::PREFERRED_DATE_2,
            ],

            StepKeys::PREFERRED_DATE_2 => [
                'type'     => 'date',
                'question' => '第二希望の日程を教えてください(なければ空欄でOK)',
                'required' => false,
                'next'     => StepKeys::MOTIVATION,
            ],

            StepKeys::MOTIVATION => [
                'type'     => 'textarea',
                'question' => '参加のきっかけを教えてください',
                'required' => false,
                'next'     => StepKeys::CONFIRMATION,
            ],

            StepKeys::CONFIRMATION => [
                'type'     => 'confirm',
                'question' => '内容を確認し、送信してよろしいですか?',
                'required' => true,
                'next'     => StepKeys::COMPLETED,
            ],

            StepKeys::COMPLETED => [
                'type' => 'complete',
            ],

        ];
    }

    public static function get(string $stepKey): array
    {
        return self::all()[$stepKey] ?? [];
    }
}

④ ここまでで「できていること」

  • ステップ順序が 完全にコードで固定
  • バリデーションルールが 明示的
  • React / OpenAI 非依存
  • テストしやすい
  • 途中差し込み・分岐も可能

⑤ まだ「やっていないこと」(正解)

  • DB保存
  • セッション管理
  • 状態遷移ロジック
  • OpenAI文章生成

👉 全部、次のフェーズ


次の一手(選択)

ここからは 動かす層に入ります。

  • B:既存 React チャットに
     👉 この StepDefinition をどう流すか
  • C:Laravel 側
     👉 StepEngine(状態遷移+validation)実装

返事は B / C のどちらかでOKです。
次は **「動き出す瞬間」**です 🚀

では B:既存 React チャットに、この StepDefinition を“そのまま刺す”方法をやります。

ポイントは一つだけ👇

Reactは StepDefinition を「理解」しない
Reactは StepDefinition を「表示」するだけ


React 側の役割(再確認)

React は チャットUIエンジンであって、

  • 次は何を聞くか?
  • 入力は正しいか?
  • 次のステップは?

👉 一切考えません

それはすべて Laravel が決めて返す


全体の通信イメージ(確定)

React → Laravel

POST /api/application/chat
{
  "session_id": "uuid",
  "message": "入力値"
}

Laravel → React

{
  "step": "email",
  "question": "メールアドレスを教えてください",
  "type": "text",
  "options": null,
  "status": "waiting"
}

or

{
  "step": "completed",
  "status": "completed"
}

👉 ReactはこのJSONを描画するだけ


① React 側:Stateの最小構成

既存 DiagnosisChat.jsx を前提に書きます。

const [messages, setMessages] = useState([]);
const [currentStep, setCurrentStep] = useState(null);
const [inputType, setInputType] = useState('text');
const [options, setOptions] = useState([]);

② API 通信関数(最重要)

async function sendMessage(message) {
  const res = await fetch('http://localhost:8003/api/application/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      session_id: sessionId,
      message
    })
  });

  return await res.json();
}

③ Laravel レスポンスをそのまま反映

const handleSend = async (userMessage) => {

  // ユーザー発言を追加
  setMessages(prev => [...prev, {
    role: 'user',
    content: userMessage
  }]);

  const data = await sendMessage(userMessage);

  // 質問を表示
  if (data.question) {
    setMessages(prev => [...prev, {
      role: 'system',
      content: data.question
    }]);
  }

  setCurrentStep(data.step);
  setInputType(data.type || 'text');
  setOptions(data.options || []);
};

👉 判定ゼロ
👉 分岐ゼロ


④ 入力UIの切り替え(typeベース)

{inputType === 'text' && (
  <TextInput onSend={handleSend} />
)}

{inputType === 'select' && (
  <SelectInput options={options} onSelect={handleSend} />
)}

{inputType === 'confirm' && (
  <ConfirmButtons onSelect={handleSend} />
)}

{inputType === 'date' && (
  <DateInput onSend={handleSend} />
)}

👉 typeLaravel が決める


⑤ 「戻る」「修正」は考えない(今は)

  • 確認画面でまとめてやる
  • ステップ途中での戻りは 後回し

👉 まずは 一本道を完成させる


⑥ 既存 DiagnosisChat からの変更点まとめ

項目変更
questions_phaseX.js❌ 削除
Phase コンポーネント❌ 不要
質問文❌ Reactに持たない
判定ロジック❌ Reactに書かない
APIレスポンス✅ 主役

⑦ React 側の完成条件

これができていれば 成功👇

  • React は API レスポンスだけ見ている
  • StepDefinition を知らない
  • message / question / type を描画するだけ
  • Laravel を止めると何も進まない(正しい)

次の一手(確定)

次は C:Laravel StepEngine です。

ここでやること:

  • session_id 管理
  • current_step 保持
  • StepDefinition を使った遷移
  • validation
  • JSON レスポンス生成

👉 ここが心臓部

進めてよければ、次はそのまま
C:StepEngine 実装に入ります。

了解。
ここからが本丸です。
**Laravel 側 StepEngine(状態遷移+検証+レスポンス生成)**を、
そのまま使える形で出します。


Laravel10:Application StepEngine 実装

目的はこれ👇

  • セッション単位で進行管理
  • StepDefinition に完全依存
  • React は何も考えなくていい
  • AI なしでも成立

① 全体構成(最終形)

app/
└─ Domain/
   └─ Application/
      ├─ Step/
      │  ├─ StepDefinition.php   ← 既存
      │  ├─ StepKeys.php         ← 既存
      │  └─ StepEngine.php       ← ★ 今回
      └─ ApplicationSession.php  ← セッション状態

② ApplicationSession(状態保持)

DBに入れる前の 最小モデル です。

<?php
// app/Domain/Application/ApplicationSession.php

namespace App\Domain\Application;

class ApplicationSession
{
    public string $sessionId;
    public string $currentStep;
    public array $answers = [];

    public function __construct(string $sessionId)
    {
        $this->sessionId = $sessionId;
        $this->currentStep = 'intro';
    }

    public function setAnswer(string $step, $value): void
    {
        $this->answers[$step] = $value;
    }

    public function getAnswer(string $step)
    {
        return $this->answers[$step] ?? null;
    }

    public function moveTo(string $nextStep): void
    {
        $this->currentStep = $nextStep;
    }
}

※ 最初は DB保存しない
※ Redis / DB はあとで差し替え


③ StepEngine(心臓部)

<?php
// app/Domain/Application/Step/StepEngine.php

namespace App\Domain\Application\Step;

use App\Domain\Application\ApplicationSession;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;

class StepEngine
{
    public function handle(
        ApplicationSession $session,
        $input
    ): array {
        $stepKey = $session->currentStep;
        $definition = StepDefinition::get($stepKey);

        // 完了済み
        if ($stepKey === StepKeys::COMPLETED) {
            return [
                'status' => 'completed',
                'step'   => StepKeys::COMPLETED,
            ];
        }

        // バリデーション
        if (!empty($definition['validation'])) {
            $this->validate($definition['validation'], $input);
        }

        // 値保存
        $session->setAnswer($stepKey, $input);

        // 次のステップへ
        $nextStep = $definition['next'] ?? StepKeys::COMPLETED;
        $session->moveTo($nextStep);

        $nextDef = StepDefinition::get($nextStep);

        return [
            'status'   => 'waiting',
            'step'     => $nextStep,
            'type'     => $nextDef['type'] ?? null,
            'question' => $nextDef['question'] ?? null,
            'options'  => $nextDef['options'] ?? null,
        ];
    }

    protected function validate(array $rules, $value): void
    {
        $validator = Validator::make(
            ['value' => $value],
            ['value' => $rules]
        );

        if ($validator->fails()) {
            throw new ValidationException($validator);
        }
    }
}

👉 ロジックは完全に StepDefinition 駆動


④ Controller(API入口)

<?php
// app/Http/Controllers/ApplicationChatController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Domain\Application\ApplicationSession;
use App\Domain\Application\Step\StepEngine;
use Illuminate\Validation\ValidationException;

class ApplicationChatController extends Controller
{
    protected static array $sessions = [];

    public function __invoke(Request $request)
    {
        $sessionId = $request->input('session_id');
        $message   = $request->input('message');

        if (!$sessionId) {
            return response()->json(['error' => 'session_id required'], 400);
        }

        if (!isset(self::$sessions[$sessionId])) {
            self::$sessions[$sessionId] = new ApplicationSession($sessionId);
        }

        $session = self::$sessions[$sessionId];
        $engine  = new StepEngine();

        try {
            return response()->json(
                $engine->handle($session, $message)
            );
        } catch (ValidationException $e) {
            return response()->json([
                'status'   => 'error',
                'step'     => $session->currentStep,
                'message'  => '入力内容を確認してください',
                'errors'   => $e->errors(),
            ], 422);
        }
    }
}

⑤ Route 定義

// routes/api.php

use App\Http\Controllers\ApplicationChatController;

Route::post('/application/chat', ApplicationChatController::class);

⑥ ここまでで「動くもの」

  • React → /api/application/chat
  • Laravel が:
    • 今の step を判断
    • validation
    • 次の質問を返す
  • React は描画だけ

👉 完全に設計どおり


⑦ 今は“あえて”やっていないこと(正解)

  • DB保存
  • セッション永続化
  • 差し戻し
  • OpenAI
  • 確認画面生成

👉 一本道を完成させるのが最優先


次の段階(選択)

ここからは 仕上げフェーズです。

次の選択肢

D:セッションを DB / Redis に永続化
E:確認ステップ(summary)生成
F:OpenAI を質問文整形として組み込み

返事は D / E / F のどれかでOKです。
ここまで来たら、ほぼ完成です 🚀

最新のお知らせ

thumb
2026年2月26日
なぜDDDは「オニオン」や「クリーン」とセットで語られるのか?現場で役立つ5つの本質的教訓

1. はじめに:私たちはなぜ「DDD難民」になってしまうのか...

thumb
2026年2月26日
UNIXとC言語の誕生

1969年に、デニスリッチーはケン・トンプソンと共に、ベル研究...

No Image
2026年2月25日
2026 AI企業のこれからを予測してみよう

2026年、AI企業は「技術の凄さ」を競う段階から、「社会のイン...

thumb
2026年2月24日
DeepSeek、Moonshot AI、MiniMaxの3社が偽アカウント2.4万超を作って、Claude1600万回以上不正使用

いや、本当にえぐいニュースですよね。巨額の資金と時間を...

thumb
2026年2月24日
プログラミング・パラダイムシフト

プログラム、プログラミングという世界は10年に1度くらいでパラ...

thumb
2026年2月23日
ヨハン・セバスチャン・バッハ の世界

ヨハン・セバスチャン・バッハ の世界 SUNOでクラシカル...

thumb
2026年2月17日
【SaaS全滅】時価総額160兆円が消失したSaaSapocalypseの全貌と市場構造の激変

SaaSapocalypse サース・アポカプリス さーす・あぽかぷりす...

No Image
2026年2月13日
ダーツについて調べてまとめてみました

久しぶりに知っている営業から電話があったので、キーワードを...

thumb
2026年2月12日
AIエージェントの「USB-C」:Model Context Protocol(MCP)が変える未来

AI活用の新標準:MCP解説 1. イントロダクション:...

thumb
2026年2月11日
2026年版:ローカルコンテナの中に、自分だけのAI環境を構築したい

ローカルコンテナ(Docker)を使って自分だけのAI環境を構...