fn ($q) => $q->latest()->limit(1)]) ->orderByDesc('updated_at') ->paginate(20); return view('dev-tools.flow-tester.index', compact('flows')); } /** * 플로우 생성 폼 */ public function create(): View { return view('dev-tools.flow-tester.create'); } /** * 플로우 저장 */ public function store(Request $request) { $validated = $request->validate([ 'name' => 'required|string|max:100', 'description' => 'nullable|string', 'category' => 'nullable|string|max:50', 'flow_definition' => 'required|json', ]); $validated['flow_definition'] = json_decode($validated['flow_definition'], true); $validated['created_by'] = auth()->id(); $flow = AdminApiFlow::create($validated); return redirect() ->route('dev-tools.flow-tester.edit', $flow->id) ->with('success', '플로우가 생성되었습니다.'); } /** * 플로우 편집 폼 */ public function edit(int $id): View { $flow = AdminApiFlow::findOrFail($id); return view('dev-tools.flow-tester.edit', compact('flow')); } /** * 플로우 수정 */ public function update(Request $request, int $id) { $flow = AdminApiFlow::findOrFail($id); $validated = $request->validate([ 'name' => 'required|string|max:100', 'description' => 'nullable|string', 'category' => 'nullable|string|max:50', 'flow_definition' => 'required|json', 'is_active' => 'boolean', ]); $validated['flow_definition'] = json_decode($validated['flow_definition'], true); $validated['updated_by'] = auth()->id(); $flow->update($validated); return redirect() ->route('dev-tools.flow-tester.edit', $flow->id) ->with('success', '플로우가 수정되었습니다.'); } /** * 플로우 삭제 */ public function destroy(int $id) { $flow = AdminApiFlow::findOrFail($id); $flow->delete(); return redirect() ->route('dev-tools.flow-tester.index') ->with('success', '플로우가 삭제되었습니다.'); } /** * 플로우 복제 */ public function clone(int $id) { $original = AdminApiFlow::findOrFail($id); $clone = $original->replicate(); $clone->name = $original->name.' (복사본)'; $clone->created_by = auth()->id(); $clone->updated_by = null; $clone->save(); return redirect() ->route('dev-tools.flow-tester.edit', $clone->id) ->with('success', '플로우가 복제되었습니다.'); } /** * JSON 유효성 검사 (HTMX) */ public function validateJson(Request $request) { $json = $request->input('flow_definition', ''); try { $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); // 기본 구조 검증 $errors = []; if (! isset($data['steps']) || ! is_array($data['steps'])) { $errors[] = 'steps 배열이 필요합니다.'; } else { foreach ($data['steps'] as $i => $step) { if (empty($step['id'])) { $errors[] = "steps[{$i}]: id가 필요합니다."; } if (empty($step['method'])) { $errors[] = "steps[{$i}]: method가 필요합니다."; } if (empty($step['endpoint'])) { $errors[] = "steps[{$i}]: endpoint가 필요합니다."; } } } if (! empty($errors)) { return response()->json([ 'valid' => false, 'errors' => $errors, ]); } return response()->json([ 'valid' => true, 'stepCount' => count($data['steps'] ?? []), ]); } catch (\JsonException $e) { return response()->json([ 'valid' => false, 'errors' => ['JSON 파싱 오류: '.$e->getMessage()], ]); } } /** * 플로우 실행 */ public function run(int $id) { $flow = AdminApiFlow::findOrFail($id); // 실행 기록 생성 $run = AdminApiFlowRun::create([ 'flow_id' => $flow->id, 'status' => AdminApiFlowRun::STATUS_RUNNING, 'started_at' => now(), 'total_steps' => $flow->step_count, 'executed_by' => auth()->id(), ]); // TODO: 실제 플로우 실행 로직은 Service 클래스로 분리 // 현재는 스캐폴딩만 제공 return response()->json([ 'success' => true, 'run_id' => $run->id, 'message' => '플로우 실행이 시작되었습니다.', ]); } /** * 실행 상태 조회 (Polling) */ public function runStatus(int $runId) { $run = AdminApiFlowRun::findOrFail($runId); return response()->json([ 'status' => $run->status, 'progress' => $run->progress, 'completed_steps' => $run->completed_steps, 'total_steps' => $run->total_steps, 'is_completed' => $run->isCompleted(), 'execution_log' => $run->execution_log, ]); } /** * 실행 이력 목록 */ public function history(int $id): View { $flow = AdminApiFlow::findOrFail($id); $runs = $flow->runs() ->orderByDesc('created_at') ->paginate(20); return view('dev-tools.flow-tester.history', compact('flow', 'runs')); } /** * 실행 상세 보기 */ public function runDetail(int $runId): View { $run = AdminApiFlowRun::with('flow')->findOrFail($runId); return view('dev-tools.flow-tester.run-detail', compact('run')); } }