fn ($q) => $q->latest()->limit(1)]) ->orderByDesc('created_at') ->paginate(20); // 세션에 저장된 토큰 (API Explorer와 공유) $savedToken = session('api_explorer_token'); $selectedUserId = session('api_explorer_user_id'); $selectedUser = $selectedUserId ? \App\Models\User::find($selectedUserId) : null; return view('dev-tools.flow-tester.index', compact('flows', 'savedToken', 'selectedUser')); } /** * 플로우 생성 폼 */ 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(); // AJAX 요청인 경우 JSON 응답 반환 if (request()->ajax() || request()->wantsJson()) { return response()->json([ 'success' => true, 'message' => '플로우가 삭제되었습니다.', ]); } 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 유효성 검사 및 메타 정보 추출 */ 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, ]); } // meta 정보 추출 (최상위 또는 meta 객체에서) $meta = $data['meta'] ?? []; $tags = $meta['tags'] ?? []; // 카테고리 추론: 최상위 category > tags[0] > endpoint에서 추출 (login 제외) $category = $data['category'] ?? $tags[0] ?? null; if (! $category && ! empty($data['steps'])) { // login, auth, refresh, logout 등 인증 관련 엔드포인트는 건너뛰고 추출 $authEndpoints = ['/login', '/logout', '/refresh', '/auth']; foreach ($data['steps'] as $step) { $endpoint = $step['endpoint'] ?? ''; // 인증 엔드포인트가 아닌 첫 번째 스텝에서 카테고리 추출 if (! in_array($endpoint, $authEndpoints) && preg_match('#^/([^/]+)#', $endpoint, $matches)) { $category = $matches[1]; break; } } } // 이름 추론: 최상위 name > meta.name > description 첫 부분 $name = $data['name'] ?? $meta['name'] ?? null; if (! $name) { $description = $data['description'] ?? $meta['description'] ?? null; if ($description) { // 설명의 첫 번째 줄 또는 50자까지 $name = mb_substr(strtok($description, "\n"), 0, 50); } } // 설명 추론: 최상위 description > meta.description $description = $data['description'] ?? $meta['description'] ?? null; return response()->json([ 'valid' => true, 'stepCount' => count($data['steps'] ?? []), 'extracted' => [ 'name' => $name, 'description' => $description, 'category' => $category, 'author' => $meta['author'] ?? null, 'tags' => $tags, 'version' => $data['version'] ?? '1.0', ], ]); } 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(), ]); try { // FlowExecutor로 실제 실행 $executor = new FlowExecutor; $result = $executor->execute($flow->flow_definition); // 실행 결과 저장 $run->update([ 'status' => $result['status'], 'completed_at' => now(), 'duration_ms' => $result['duration'], 'completed_steps' => $result['completedSteps'], 'failed_step' => $result['failedStep'], 'execution_log' => $result['executionLog'], 'error_message' => $result['errorMessage'], 'api_logs' => $result['apiLogs'] ?? [], ]); return response()->json([ 'success' => $result['status'] === 'SUCCESS', 'run_id' => $run->id, 'status' => $result['status'], 'message' => $this->getResultMessage($result), 'result' => $result, ]); } catch (\Exception $e) { // 예외 발생 시 실패 처리 $run->update([ 'status' => AdminApiFlowRun::STATUS_FAILED, 'completed_at' => now(), 'error_message' => $e->getMessage(), ]); return response()->json([ 'success' => false, 'run_id' => $run->id, 'status' => 'FAILED', 'message' => '실행 오류: '.$e->getMessage(), ], 500); } } /** * 실행 결과 메시지 생성 */ private function getResultMessage(array $result): string { return match ($result['status']) { 'SUCCESS' => "플로우 실행 완료! ({$result['completedSteps']}/{$result['totalSteps']} 스텝 성공)", 'FAILED' => '플로우 실행 실패: '.($result['errorMessage'] ?? '알 수 없는 오류'), 'PARTIAL' => "부분 성공: {$result['completedSteps']}/{$result['totalSteps']} 스텝 완료", default => "실행 완료 (상태: {$result['status']})", }; } /** * 실행 상태 조회 (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')); } /* |-------------------------------------------------------------------------- | Token & User Management (API Explorer와 공유) |-------------------------------------------------------------------------- */ /** * 현재 테넌트의 사용자 목록 */ public function users() { $tenantId = auth()->user()->tenant_id; $users = \App\Models\User::where('tenant_id', $tenantId) ->select(['id', 'name', 'email', 'tenant_id']) ->orderBy('name') ->limit(100) ->get(); return response()->json($users); } /** * 사용자 선택 (Sanctum 토큰 발급) */ public function selectUser(Request $request) { $validated = $request->validate([ 'user_id' => 'required|integer', ]); $user = \App\Models\User::find($validated['user_id']); if (! $user) { return response()->json([ 'success' => false, 'message' => '사용자를 찾을 수 없습니다.', ], 404); } // Sanctum 토큰 발급 $token = $user->createToken('flow-tester', ['*'])->plainTextToken; // 세션에 저장 (API Explorer와 공유) session([ 'api_explorer_token' => $token, 'api_explorer_user_id' => $user->id, ]); return response()->json([ 'success' => true, 'message' => "'{$user->name}' 사용자로 인증되었습니다.", 'user' => [ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'tenant_id' => $user->tenant_id, ], 'token_preview' => substr($token, 0, 20).'...', ]); } /** * Bearer 토큰 저장 (직접 입력) */ public function saveToken(Request $request) { $validated = $request->validate([ 'token' => 'required|string', ]); // API Explorer와 같은 세션 키 사용 session([ 'api_explorer_token' => $validated['token'], 'api_explorer_user_id' => null, // 직접 입력 시 사용자 정보 없음 ]); return response()->json([ 'success' => true, 'message' => '토큰이 저장되었습니다.', ]); } /** * Bearer 토큰 초기화 */ public function clearToken() { session()->forget(['api_explorer_token', 'api_explorer_user_id']); return response()->json([ 'success' => true, 'message' => '인증이 초기화되었습니다.', ]); } /** * 현재 토큰 상태 조회 */ public function tokenStatus() { $token = session('api_explorer_token'); $userId = session('api_explorer_user_id'); $user = $userId ? \App\Models\User::find($userId) : null; return response()->json([ 'has_token' => ! empty($token), 'token_preview' => $token ? substr($token, 0, 20).'...' : null, 'user' => $user ? [ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'tenant_id' => $user->tenant_id, ] : null, ]); } }