- 테넌트/사용자 컬럼 추가 (관계 eager loading) - 그룹 ID로 연관 API 호출 필터링 및 상세 페이지에서 그룹 목록 표시 - 상태 400 이상일 때 AI 분석용 복사 버튼 추가 - Tenant 모델 네임스페이스 수정 (Tenants\Tenant)
236 lines
10 KiB
PHP
236 lines
10 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', 'API 로그 상세')
|
|
|
|
@section('content')
|
|
<!-- 페이지 헤더 -->
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div class="flex items-center gap-4">
|
|
<a href="{{ route('dev-tools.api-logs.index') }}" class="text-gray-500 hover:text-gray-700">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
|
</svg>
|
|
</a>
|
|
<h1 class="text-2xl font-bold text-gray-800">API 로그 상세</h1>
|
|
</div>
|
|
@if($log->response_status >= 400)
|
|
<button onclick="copyAiAnalysis()" class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transition flex items-center gap-2">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
|
</svg>
|
|
AI 분석용 복사
|
|
</button>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- 기본 정보 -->
|
|
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-800 mb-4">요청 정보</h2>
|
|
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
|
<div>
|
|
<div class="text-sm text-gray-500">메서드</div>
|
|
<span class="px-2 py-1 text-sm font-medium rounded {{ $log->method_color }}">
|
|
{{ $log->method }}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-gray-500">상태 코드</div>
|
|
<span class="px-2 py-1 text-sm font-medium rounded {{ $log->status_color }}">
|
|
{{ $log->response_status }}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-gray-500">응답 시간</div>
|
|
<div class="text-lg font-semibold">{{ number_format($log->duration_ms) }}ms</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-gray-500">요청 시간</div>
|
|
<div class="text-sm">{{ $log->created_at->format('Y-m-d H:i:s') }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<div class="text-sm text-gray-500 mb-1">URL</div>
|
|
<div class="bg-gray-100 rounded-lg p-3 font-mono text-sm break-all">
|
|
{{ $log->url }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
|
<div>
|
|
<div class="text-sm text-gray-500">라우트</div>
|
|
<div class="text-sm font-mono">{{ $log->route_name ?? '-' }}</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-gray-500">IP 주소</div>
|
|
<div class="text-sm">{{ $log->ip_address ?? '-' }}</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-gray-500">테넌트</div>
|
|
<div class="text-sm">
|
|
@if($log->tenant)
|
|
{{ $log->tenant->company_name }} (#{{ $log->tenant_id }})
|
|
@else
|
|
<span class="text-gray-400">{{ $log->tenant_id ?? '-' }}</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-gray-500">사용자</div>
|
|
<div class="text-sm">
|
|
@if($log->user)
|
|
{{ $log->user->name ?? $log->user->email }} (#{{ $log->user_id }})
|
|
@else
|
|
<span class="text-gray-400">{{ $log->user_id ? "#{$log->user_id}" : 'guest' }}</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-sm text-gray-500">그룹 ID</div>
|
|
<div class="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 font-mono text-xs">
|
|
{{ Str::limit($log->group_id, 20) }}
|
|
</a>
|
|
@else
|
|
<span class="text-gray-400">-</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 같은 그룹의 요청들 -->
|
|
@if($log->group_id && count($groupLogs) > 0)
|
|
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-800 mb-4">
|
|
같은 그룹의 요청 ({{ count($groupLogs) }}개)
|
|
<a href="{{ route('dev-tools.api-logs.index', ['group_id' => $log->group_id]) }}"
|
|
class="text-sm font-normal text-purple-600 hover:text-purple-800 ml-2">
|
|
전체 보기
|
|
</a>
|
|
</h2>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">시간</th>
|
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">메서드</th>
|
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">URL</th>
|
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">상태</th>
|
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">응답</th>
|
|
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
@foreach($groupLogs as $gLog)
|
|
<tr class="hover:bg-gray-50 {{ $gLog->response_status >= 400 ? 'bg-red-50' : '' }}">
|
|
<td class="px-3 py-2 whitespace-nowrap text-xs text-gray-500">
|
|
{{ $gLog->created_at->format('H:i:s.u') }}
|
|
</td>
|
|
<td class="px-3 py-2 whitespace-nowrap">
|
|
<span class="px-1.5 py-0.5 text-xs font-medium rounded {{ $gLog->method_color }}">
|
|
{{ $gLog->method }}
|
|
</span>
|
|
</td>
|
|
<td class="px-3 py-2 text-xs text-gray-900 max-w-xs truncate" title="{{ $gLog->url }}">
|
|
{{ $gLog->path }}
|
|
</td>
|
|
<td class="px-3 py-2 whitespace-nowrap">
|
|
<span class="px-1.5 py-0.5 text-xs font-medium rounded {{ $gLog->status_color }}">
|
|
{{ $gLog->response_status }}
|
|
</span>
|
|
</td>
|
|
<td class="px-3 py-2 whitespace-nowrap text-xs text-gray-500">
|
|
{{ number_format($gLog->duration_ms) }}ms
|
|
</td>
|
|
<td class="px-3 py-2 whitespace-nowrap text-right text-xs">
|
|
<a href="{{ route('dev-tools.api-logs.show', $gLog->id) }}" class="text-blue-600 hover:text-blue-800">
|
|
상세
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- User Agent -->
|
|
@if($log->user_agent)
|
|
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-800 mb-4">User Agent</h2>
|
|
<div class="bg-gray-100 rounded-lg p-3 font-mono text-sm break-all">
|
|
{{ $log->user_agent }}
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- 요청 헤더 -->
|
|
@if($log->request_headers)
|
|
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-800 mb-4">요청 헤더</h2>
|
|
<div class="bg-gray-900 rounded-lg p-4 overflow-x-auto">
|
|
<pre class="text-green-400 text-sm font-mono">{!! stripslashes(json_encode($log->request_headers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) !!}</pre>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- 쿼리 파라미터 -->
|
|
@if($log->request_query && count($log->request_query) > 0)
|
|
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-800 mb-4">쿼리 파라미터</h2>
|
|
<div class="bg-gray-900 rounded-lg p-4 overflow-x-auto">
|
|
<pre class="text-green-400 text-sm font-mono">{!! stripslashes(json_encode($log->request_query, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) !!}</pre>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- 요청 바디 -->
|
|
@if($log->request_body && count($log->request_body) > 0)
|
|
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-800 mb-4">요청 바디</h2>
|
|
<div class="bg-gray-900 rounded-lg p-4 overflow-x-auto">
|
|
<pre class="text-green-400 text-sm font-mono">{!! stripslashes(json_encode($log->request_body, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) !!}</pre>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- 응답 바디 -->
|
|
@if($log->response_body)
|
|
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-800 mb-4">응답 바디</h2>
|
|
<div class="bg-gray-900 rounded-lg p-4 overflow-x-auto max-h-96">
|
|
@php
|
|
$responseData = json_decode($log->response_body, true);
|
|
@endphp
|
|
@if($responseData)
|
|
<pre class="text-green-400 text-sm font-mono">{!! stripslashes(json_encode($responseData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) !!}</pre>
|
|
@else
|
|
<pre class="text-green-400 text-sm font-mono">{{ $log->response_body }}</pre>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if($log->response_status >= 400)
|
|
<!-- AI 분석용 텍스트 (숨김) -->
|
|
<textarea id="aiAnalysisText" class="hidden">{!! $log->ai_analysis_summary !!}</textarea>
|
|
|
|
<script>
|
|
function copyAiAnalysis() {
|
|
const text = document.getElementById('aiAnalysisText').value;
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
alert('AI 분석용 내용이 클립보드에 복사되었습니다.\nClaude나 ChatGPT에 붙여넣기 하세요.');
|
|
}).catch(err => {
|
|
console.error('복사 실패:', err);
|
|
alert('복사에 실패했습니다.');
|
|
});
|
|
}
|
|
</script>
|
|
@endif
|
|
@endsection
|