hasAny(['method', 'status', 'search', 'group_id', 'tenant_id']); $query = ApiRequestLog::query() ->with(['tenant', 'user']); if ($hasFilter) { $query->orderBy('created_at'); // 오래된 순 } else { $query->orderByDesc('created_at'); // 최신순 } // 필터: HTTP 메서드 if ($request->filled('method')) { $query->where('method', $request->method); } // 필터: 상태 코드 if ($request->filled('status')) { $status = $request->status; if ($status === '2xx') { $query->whereBetween('response_status', [200, 299]); } elseif ($status === 'error') { $query->where('response_status', '>=', 400); } elseif ($status === '4xx') { $query->whereBetween('response_status', [400, 499]); } elseif ($status === '5xx') { $query->whereBetween('response_status', [500, 599]); } } // 필터: URL 검색 if ($request->filled('search')) { $query->where('url', 'like', '%'.$request->search.'%'); } // 필터: 그룹 ID if ($request->filled('group_id')) { $query->where('group_id', $request->group_id); } // 필터: 테넌트 if ($request->filled('tenant_id')) { $query->where('tenant_id', $request->tenant_id); } // 통계 $stats = [ 'total' => ApiRequestLog::count(), 'success' => ApiRequestLog::whereBetween('response_status', [200, 299])->count(), 'client_error' => ApiRequestLog::whereBetween('response_status', [400, 499])->count(), 'server_error' => ApiRequestLog::whereBetween('response_status', [500, 599])->count(), 'avg_duration' => (int) ApiRequestLog::avg('duration_ms'), ]; $logs = $query->paginate(50)->withQueryString(); // 현재 페이지 로그들의 그룹별 개수 조회 $groupIds = $logs->pluck('group_id')->filter()->unique()->values()->toArray(); $groupCounts = []; if (! empty($groupIds)) { $groupCounts = ApiRequestLog::whereIn('group_id', $groupIds) ->selectRaw('group_id, COUNT(*) as count') ->groupBy('group_id') ->pluck('count', 'group_id') ->toArray(); } // 세션에 저장된 토큰 정보 $savedToken = session('api_resend_token'); $savedUserId = session('api_resend_user_id'); return view('api-logs.index', compact('logs', 'stats', 'groupCounts', 'savedToken', 'savedUserId')); } /** * API 로그 상세 화면 */ public function show(int $id): View { $log = ApiRequestLog::with(['tenant', 'user'])->findOrFail($id); // 같은 그룹의 다른 요청들 (오래된 순) $groupLogs = collect(); $groupMethodCounts = []; if ($log->group_id) { $groupLogs = ApiRequestLog::where('group_id', $log->group_id) ->where('id', '!=', $log->id) ->orderBy('created_at') // 오래된 순 ->get(); // 메서드별 개수 집계 $groupMethodCounts = $groupLogs->groupBy('method') ->map(fn ($items) => $items->count()) ->sortKeys() ->toArray(); } // 세션에 저장된 토큰 정보 $savedToken = session('api_resend_token'); $savedUserId = session('api_resend_user_id'); return view('api-logs.show', compact('log', 'groupLogs', 'groupMethodCounts', 'savedToken', 'savedUserId')); } /** * 오래된 로그 삭제 (수동) */ public function prune() { $deleted = ApiRequestLog::pruneOldLogs(); return redirect()->route('dev-tools.api-logs.index') ->with('success', "{$deleted}개의 로그가 삭제되었습니다."); } /** * 모든 로그 삭제 */ public function truncate() { $deleted = ApiRequestLog::truncate(); return redirect()->route('dev-tools.api-logs.index') ->with('success', '모든 로그가 삭제되었습니다.'); } /** * API 로그 재전송 */ public function resend(Request $request, int $id): JsonResponse { $log = ApiRequestLog::findOrFail($id); // URL 변환: api.sam.kr → API_BASE_URL (환경별 설정) $url = $this->convertApiUrl($log->url); // 인증 토큰 결정 $token = null; if ($request->filled('token')) { // 직접 입력한 토큰 사용 $token = $request->input('token'); // 세션에 저장 (다음 재전송 시 재사용) session(['api_resend_token' => $token]); } elseif ($request->filled('user_id')) { // 사용자 선택 시 Sanctum 토큰 발급 $user = User::findOrFail($request->input('user_id')); $token = $user->createToken('api-log-resend', ['*'])->plainTextToken; // 세션에 저장 (다음 재전송 시 재사용) session(['api_resend_token' => $token]); } elseif (session('api_resend_token')) { // 세션에 저장된 토큰 사용 $token = session('api_resend_token'); } // HTTP 요청 빌더 생성 $httpClient = Http::timeout(30); // Authorization 헤더 설정 if ($token) { $httpClient = $httpClient->withToken($token); } // Content-Type 설정 (원본 헤더에서 추출) $contentType = 'application/json'; if (isset($log->request_headers['content-type'])) { $contentType = is_array($log->request_headers['content-type']) ? $log->request_headers['content-type'][0] : $log->request_headers['content-type']; } $httpClient = $httpClient->contentType($contentType); // Accept 헤더 설정 $httpClient = $httpClient->accept('application/json'); // X-API-KEY 헤더 설정 (.env에서 가져옴) $apiKey = config('services.api.key'); if ($apiKey) { $httpClient = $httpClient->withHeaders(['X-API-KEY' => $apiKey]); } // 요청 실행 및 시간 측정 $startTime = microtime(true); try { $response = match (strtoupper($log->method)) { 'GET' => $httpClient->get($url, $log->request_query ?? []), 'POST' => $httpClient->post($url, $log->request_body ?? []), 'PUT' => $httpClient->put($url, $log->request_body ?? []), 'PATCH' => $httpClient->patch($url, $log->request_body ?? []), 'DELETE' => $httpClient->delete($url, $log->request_body ?? []), default => throw new \InvalidArgumentException("Unsupported HTTP method: {$log->method}"), }; $durationMs = (int) ((microtime(true) - $startTime) * 1000); // 응답 파싱 $responseBody = $response->json() ?? $response->body(); return response()->json([ 'success' => $response->successful(), 'status' => $response->status(), 'duration_ms' => $durationMs, 'response' => $responseBody, ]); } catch (\Exception $e) { $durationMs = (int) ((microtime(true) - $startTime) * 1000); return response()->json([ 'success' => false, 'status' => 0, 'duration_ms' => $durationMs, 'response' => [ 'error' => $e->getMessage(), 'type' => get_class($e), ], ], 500); } } /** * API URL 변환 (환경별 설정 적용) * 로그에 저장된 외부 URL을 현재 환경에서 접근 가능한 URL로 변환 */ private function convertApiUrl(string $originalUrl): string { $apiBaseUrl = config('services.api.base_url'); if (empty($apiBaseUrl)) { return $originalUrl; } // 기존 API 도메인 패턴 (api.sam.kr, api.codebridge-x.com 등) $patterns = [ '#^https?://api\.sam\.kr#', '#^https?://api\.codebridge-x\.com#', ]; foreach ($patterns as $pattern) { if (preg_match($pattern, $originalUrl)) { return preg_replace($pattern, rtrim($apiBaseUrl, '/'), $originalUrl); } } return $originalUrl; } }