feat: API 로그 재전송 기능 추가
- 리스트/상세 페이지에 재전송 버튼 추가 - 인증 방식 선택: 토큰 직접 입력 / 사용자 선택(Sanctum 토큰 발급) - 환경별 API URL 변환 (API_BASE_URL 설정) - X-API-KEY 헤더 자동 추가 (FLOW_TESTER_API_KEY 사용) - 성공/실패 상태 배너 표시 - 세션에 토큰 저장하여 다음 재전송 시 자동 입력 - 재전송 성공 시 1초 후 페이지 새로고침 - 에러만 필터 옵션 추가 (4xx+5xx)
This commit is contained in:
@@ -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', '모든 로그가 삭제되었습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user