From f7a8839dedc9810e97747b1bd4a509f2da438077 Mon Sep 17 00:00:00 2001 From: hskwon Date: Wed, 17 Dec 2025 18:56:40 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20API=20=EB=A1=9C=EA=B7=B8=20=EC=9E=AC?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 리스트/상세 페이지에 재전송 버튼 추가 - 인증 방식 선택: 토큰 직접 입력 / 사용자 선택(Sanctum 토큰 발급) - 환경별 API URL 변환 (API_BASE_URL 설정) - X-API-KEY 헤더 자동 추가 (FLOW_TESTER_API_KEY 사용) - 성공/실패 상태 배너 표시 - 세션에 토큰 저장하여 다음 재전송 시 자동 입력 - 재전송 성공 시 1초 후 페이지 새로고침 - 에러만 필터 옵션 추가 (4xx+5xx) --- app/Http/Controllers/ApiLogController.php | 145 +++++++++- config/services.php | 14 + resources/views/api-logs/index.blade.php | 303 +++++++++++++++++++- resources/views/api-logs/show.blade.php | 322 +++++++++++++++++++++- routes/web.php | 1 + 5 files changed, 767 insertions(+), 18 deletions(-) diff --git a/app/Http/Controllers/ApiLogController.php b/app/Http/Controllers/ApiLogController.php index 29d9ae2b..be5da2de 100644 --- a/app/Http/Controllers/ApiLogController.php +++ b/app/Http/Controllers/ApiLogController.php @@ -3,7 +3,10 @@ namespace App\Http\Controllers; use App\Models\ApiRequestLog; +use App\Models\User; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Http; use Illuminate\View\View; class ApiLogController extends Controller @@ -35,6 +38,8 @@ public function index(Request $request): View $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') { @@ -44,7 +49,7 @@ public function index(Request $request): View // 필터: URL 검색 if ($request->filled('search')) { - $query->where('url', 'like', '%' . $request->search . '%'); + $query->where('url', 'like', '%'.$request->search.'%'); } // 필터: 그룹 ID @@ -71,7 +76,7 @@ public function index(Request $request): View // 현재 페이지 로그들의 그룹별 개수 조회 $groupIds = $logs->pluck('group_id')->filter()->unique()->values()->toArray(); $groupCounts = []; - if (!empty($groupIds)) { + if (! empty($groupIds)) { $groupCounts = ApiRequestLog::whereIn('group_id', $groupIds) ->selectRaw('group_id, COUNT(*) as count') ->groupBy('group_id') @@ -79,7 +84,11 @@ public function index(Request $request): View ->toArray(); } - return view('api-logs.index', compact('logs', 'stats', 'groupCounts')); + // 세션에 저장된 토큰 정보 + $savedToken = session('api_resend_token'); + $savedUserId = session('api_resend_user_id'); + + return view('api-logs.index', compact('logs', 'stats', 'groupCounts', 'savedToken', 'savedUserId')); } /** @@ -100,12 +109,16 @@ public function show(int $id): View // 메서드별 개수 집계 $groupMethodCounts = $groupLogs->groupBy('method') - ->map(fn($items) => $items->count()) + ->map(fn ($items) => $items->count()) ->sortKeys() ->toArray(); } - return view('api-logs.show', compact('log', 'groupLogs', 'groupMethodCounts')); + // 세션에 저장된 토큰 정보 + $savedToken = session('api_resend_token'); + $savedUserId = session('api_resend_user_id'); + + return view('api-logs.show', compact('log', 'groupLogs', 'groupMethodCounts', 'savedToken', 'savedUserId')); } /** @@ -129,4 +142,124 @@ public function truncate() return redirect()->route('dev-tools.api-logs.index') ->with('success', '모든 로그가 삭제되었습니다.'); } -} \ No newline at end of file + + /** + * 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; + } +} diff --git a/config/services.php b/config/services.php index 64f13270..48c183b6 100644 --- a/config/services.php +++ b/config/services.php @@ -49,4 +49,18 @@ 'storage_bucket' => env('GOOGLE_STORAGE_BUCKET'), ], + /* + |-------------------------------------------------------------------------- + | SAM API Server + |-------------------------------------------------------------------------- + | API 로그 재전송 시 사용할 API 서버 URL + | - 로컬 Docker: http://api:80 + | - 개발 서버: https://api.codebridge-x.com + | - 운영 서버: https://api.sam.kr + */ + 'api' => [ + 'base_url' => env('API_BASE_URL'), + 'key' => env('FLOW_TESTER_API_KEY'), + ], + ]; diff --git a/resources/views/api-logs/index.blade.php b/resources/views/api-logs/index.blade.php index 0582192b..91afb2be 100644 --- a/resources/views/api-logs/index.blade.php +++ b/resources/views/api-logs/index.blade.php @@ -22,8 +22,8 @@ - -