ここからがこのプロジェクトの本体です。
コードより先に、**“絶対にブレない設計物”**を作ります。
入会申請チャット:ステップ定義(確定版)
これは
- 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
| 項目 | 内容 |
|---|---|
| key | intro |
| 質問 | 「これから入会申請の確認を行います。よろしいですか?」 |
| 入力 | yes / no |
| 必須 | yes |
| 判定 | no → 終了 |
| 次 |
🟢 Step: email
| 項目 | 内容 |
|---|---|
| key | email |
| 質問 | 「メールアドレスを教えてください」 |
| 入力 | text |
| 必須 | yes |
| バリデーション | email形式 |
| NG時 | 再質問 |
| 次 | name |
🟢 Step: name
| 項目 | 内容 |
|---|---|
| key | name |
| 質問 | 「生徒さんのお名前を教えてください」 |
| 入力 | text |
| 必須 | yes |
| 次 | kana |
🟢 Step: kana
| 項目 | 内容 |
|---|---|
| key | kana |
| 質問 | 「お名前をカタカナで入力してください」 |
| 入力 | text |
| バリデーション | カタカナ |
| 次 | school |
🟢 Step: school
| 項目 | 内容 |
|---|---|
| key | school |
| 質問 | 「通っている学校名を教えてください」 |
| 入力 | text |
| 次 | grade |
🟢 Step: grade
| 項目 | 内容 |
|---|---|
| key | grade |
| 質問 | 「現在の学年を選んでください」 |
| 入力 | select |
| 選択肢 | 小1〜高3 |
| 次 | phone |
🟢 Step: phone
| 項目 | 内容 |
|---|---|
| key | phone |
| 質問 | 「連絡の取れる電話番号を教えてください」 |
| 入力 | text |
| バリデーション | 数字 / ハイフン可 |
| 次 | participation_type |
🟢 Step: participation_type
| 項目 | 内容 |
|---|---|
| key | participation_type |
| 質問 | 「参加形態を教えてください」 |
| 入力 | select |
| 選択肢 | 親子 / 生徒のみ |
| 次 | preferred_date_1 |
🟢 Step: preferred_date_1
| 項目 | 内容 |
|---|---|
| key | preferred_date_1 |
| 質問 | 「第一希望の日程を教えてください」 |
| 入力 | date |
| 次 | preferred_date_2 |
🟢 Step: preferred_date_2
| 項目 | 内容 |
|---|---|
| key | preferred_date_2 |
| 質問 | 「第二希望の日程を教えてください(なければ空欄でOK)」 |
| 入力 | date / empty |
| 次 | motivation |
🟢 Step: motivation
| 項目 | 内容 |
|---|---|
| key | motivation |
| 質問 | 「参加のきっかけを教えてください」 |
| 入力 | textarea |
| 必須 | no |
| 次 | confirmation |
🟢 Step: confirmation
| 項目 | 内容 |
|---|---|
| key | confirmation |
| 質問 | 「内容を確認し、送信してよろしいですか?」 |
| 入力 | yes / no |
| 判定 | no → 差し戻し |
| 次 | completed |
🟢 Step: completed
| 項目 | 内容 |
|---|---|
| key | completed |
| 表示 | 「申請を受け付けました」 |
| 処理 | 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} />
)}
👉 type は Laravel が決める
⑤ 「戻る」「修正」は考えない(今は)
- 確認画面でまとめてやる
- ステップ途中での戻りは 後回し
👉 まずは 一本道を完成させる
⑥ 既存 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です。
ここまで来たら、ほぼ完成です 🚀