Files
sam-manage/app/Http/Controllers/ApiLogController.php
kent a69fd527cf fix(mng): API Logs 세션 키를 API Explorer와 통일
- api_resend_token → api_explorer_token
- api_resend_user_id → api_explorer_user_id
- API Logs, API Explorer, Flow Tester 간 토큰 공유 가능

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 00:17:51 +09:00

272 lines
9.3 KiB
PHP

<?php
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
{
/**
* API 로그 목록 화면
*/
public function index(Request $request): View
{
// 필터가 적용되면 오래된 순, 아니면 최신순
$hasFilter = $request->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')) {
$methods = $request->input('method');
if (is_array($methods)) {
$query->whereIn('method', $methods);
} else {
$query->where('method', $methods);
}
}
// 필터: 상태 코드
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();
}
// 세션에 저장된 토큰 정보 (API Explorer와 공유)
$savedToken = session('api_explorer_token');
$savedUserId = session('api_explorer_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();
}
// 세션에 저장된 토큰 정보 (API Explorer와 공유)
$savedToken = session('api_explorer_token');
$savedUserId = session('api_explorer_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);
// 인증 토큰 결정 (API Explorer와 공유)
$token = null;
if ($request->filled('token')) {
// 직접 입력한 토큰 사용
$token = $request->input('token');
// 세션에 저장 (다음 재전송 시 재사용)
session(['api_explorer_token' => $token]);
} elseif ($request->filled('user_id')) {
// 사용자 선택 시 Sanctum 토큰 발급
$user = User::findOrFail($request->input('user_id'));
$token = $user->createToken('api-log-resend', ['*'])->plainTextToken;
// 세션에 저장 (다음 재전송 시 재사용)
session(['api_explorer_token' => $token]);
session(['api_explorer_user_id' => $user->id]);
} elseif (session('api_explorer_token')) {
// 세션에 저장된 토큰 사용
$token = session('api_explorer_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;
}
}