- 테넌트/사용자 컬럼 추가 (관계 eager loading) - 그룹 ID로 연관 API 호출 필터링 및 상세 페이지에서 그룹 목록 표시 - 상태 400 이상일 때 AI 분석용 복사 버튼 추가 - Tenant 모델 네임스페이스 수정 (Tenants\Tenant)
185 lines
9.3 KiB
PHP
185 lines
9.3 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', 'API 로그')
|
|
|
|
@section('content')
|
|
<!-- 페이지 헤더 -->
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h1 class="text-2xl font-bold text-gray-800">API 요청 로그</h1>
|
|
<div class="flex gap-2">
|
|
<form action="{{ route('dev-tools.api-logs.prune') }}" method="POST" onsubmit="return confirm('하루 지난 로그를 삭제하시겠습니까?')">
|
|
@csrf
|
|
<button type="submit" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition">
|
|
오래된 로그 삭제
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 알림 메시지 -->
|
|
@if(session('success'))
|
|
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
|
|
{{ session('success') }}
|
|
</div>
|
|
@endif
|
|
|
|
<!-- 통계 카드 -->
|
|
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6">
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
<div class="text-sm text-gray-500">전체 요청</div>
|
|
<div class="text-2xl font-bold text-gray-800">{{ number_format($stats['total']) }}</div>
|
|
</div>
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
<div class="text-sm text-gray-500">성공 (2xx)</div>
|
|
<div class="text-2xl font-bold text-green-600">{{ number_format($stats['success']) }}</div>
|
|
</div>
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
<div class="text-sm text-gray-500">클라이언트 에러 (4xx)</div>
|
|
<div class="text-2xl font-bold text-yellow-600">{{ number_format($stats['client_error']) }}</div>
|
|
</div>
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
<div class="text-sm text-gray-500">서버 에러 (5xx)</div>
|
|
<div class="text-2xl font-bold text-red-600">{{ number_format($stats['server_error']) }}</div>
|
|
</div>
|
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
|
<div class="text-sm text-gray-500">평균 응답시간</div>
|
|
<div class="text-2xl font-bold text-blue-600">{{ number_format($stats['avg_duration']) }}ms</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 필터 -->
|
|
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
|
<form method="GET" class="flex flex-wrap gap-4 items-end">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">메서드</label>
|
|
<select name="method" class="border rounded-lg px-3 py-2 text-sm">
|
|
<option value="">전체</option>
|
|
<option value="GET" {{ request('method') === 'GET' ? 'selected' : '' }}>GET</option>
|
|
<option value="POST" {{ request('method') === 'POST' ? 'selected' : '' }}>POST</option>
|
|
<option value="PUT" {{ request('method') === 'PUT' ? 'selected' : '' }}>PUT</option>
|
|
<option value="PATCH" {{ request('method') === 'PATCH' ? 'selected' : '' }}>PATCH</option>
|
|
<option value="DELETE" {{ request('method') === 'DELETE' ? 'selected' : '' }}>DELETE</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">상태</label>
|
|
<select name="status" class="border rounded-lg px-3 py-2 text-sm">
|
|
<option value="">전체</option>
|
|
<option value="2xx" {{ request('status') === '2xx' ? 'selected' : '' }}>성공 (2xx)</option>
|
|
<option value="4xx" {{ request('status') === '4xx' ? 'selected' : '' }}>클라이언트 에러 (4xx)</option>
|
|
<option value="5xx" {{ request('status') === '5xx' ? 'selected' : '' }}>서버 에러 (5xx)</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex-1">
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">URL 검색</label>
|
|
<input type="text" name="search" value="{{ request('search') }}"
|
|
placeholder="URL 검색..."
|
|
class="w-full border rounded-lg px-3 py-2 text-sm">
|
|
</div>
|
|
<div>
|
|
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm">
|
|
검색
|
|
</button>
|
|
<a href="{{ route('dev-tools.api-logs.index') }}" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-lg text-sm ml-1">
|
|
초기화
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- 그룹 필터 안내 -->
|
|
@if(request('group_id'))
|
|
<div class="bg-blue-50 border border-blue-200 text-blue-700 px-4 py-3 rounded mb-4 flex justify-between items-center">
|
|
<span>그룹 ID: <code class="bg-blue-100 px-2 py-1 rounded">{{ request('group_id') }}</code> 필터 적용 중</span>
|
|
<a href="{{ route('dev-tools.api-logs.index') }}" class="text-blue-600 hover:text-blue-800 underline">필터 해제</a>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- 로그 목록 -->
|
|
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">시간</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">메서드</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">URL</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">상태</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">응답</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">테넌트</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">사용자</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">그룹</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
@forelse($logs as $log)
|
|
<tr class="hover:bg-gray-50 {{ $log->response_status >= 400 ? 'bg-red-50' : '' }}">
|
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
|
{{ $log->created_at->format('H:i:s') }}
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap">
|
|
<span class="px-2 py-1 text-xs font-medium rounded {{ $log->method_color }}">
|
|
{{ $log->method }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-gray-900 max-w-xs truncate" title="{{ $log->url }}">
|
|
{{ $log->path }}
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap">
|
|
<span class="px-2 py-1 text-xs font-medium rounded {{ $log->status_color }}">
|
|
{{ $log->response_status }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
|
{{ number_format($log->duration_ms) }}ms
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
|
@if($log->tenant)
|
|
<span title="{{ $log->tenant->company_name }}">{{ Str::limit($log->tenant->company_name, 10) }}</span>
|
|
@else
|
|
<span class="text-gray-400">-</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
|
@if($log->user)
|
|
<span title="{{ $log->user->email }}">{{ $log->user->name ?? $log->user->email }}</span>
|
|
@else
|
|
<span class="text-gray-400">guest</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap text-sm">
|
|
@if($log->group_id)
|
|
<a href="{{ route('dev-tools.api-logs.index', ['group_id' => $log->group_id]) }}"
|
|
class="text-purple-600 hover:text-purple-800" title="{{ $log->group_id }}">
|
|
<svg class="w-4 h-4 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
|
</svg>
|
|
</a>
|
|
@else
|
|
<span class="text-gray-400">-</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-4 py-3 whitespace-nowrap text-right text-sm">
|
|
<a href="{{ route('dev-tools.api-logs.show', $log->id) }}" class="text-blue-600 hover:text-blue-800">
|
|
상세
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="9" class="px-4 py-8 text-center text-gray-500">
|
|
로그가 없습니다.
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- 페이지네이션 -->
|
|
@if($logs->hasPages())
|
|
<div class="px-4 py-3 border-t">
|
|
{{ $logs->links() }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endsection |