Files
sam-manage/resources/views/api-logs/show.blade.php
hskwon 1d4725e464 API 로그 기능 개선: 정렬, UI, 커스텀 모달
- 필터 적용 시 오래된 순 정렬 (시간순 추적 용이)
- 상세 페이지 그룹 요청 섹션: 접힌 상태 기본, 메서드별 뱃지
- 리스트 아이콘 버튼 (상세, AI 복사), 세로 중앙 정렬
- 응답 바디 슬래시 이스케이프 처리
- 시스템 alert를 커스텀 Tailwind 모달로 교체
2025-12-15 22:07:34 +09:00

279 lines
13 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 mb-6">
<!-- 클릭 가능한 헤더 (접힌 상태 기본) -->
<div class="p-4 cursor-pointer hover:bg-gray-50 flex items-center justify-between"
onclick="document.getElementById('groupLogsContent').classList.toggle('hidden'); document.getElementById('groupLogsChevron').classList.toggle('rotate-180')">
<div class="flex items-center gap-3">
<h2 class="text-lg font-semibold text-gray-800">
같은 그룹의 요청
</h2>
<!-- 메서드별 개수 뱃지 -->
<div class="flex items-center gap-2">
@foreach($groupMethodCounts as $method => $count)
@php
$methodColors = [
'GET' => 'bg-blue-100 text-blue-800',
'POST' => 'bg-green-100 text-green-800',
'PUT' => 'bg-yellow-100 text-yellow-800',
'PATCH' => 'bg-orange-100 text-orange-800',
'DELETE' => 'bg-red-100 text-red-800',
];
$color = $methodColors[$method] ?? 'bg-gray-100 text-gray-800';
@endphp
<span class="px-2 py-0.5 text-xs font-medium rounded {{ $color }}">
{{ $method }} {{ $count }}
</span>
@endforeach
<span class="px-2 py-0.5 text-xs font-medium rounded bg-gray-200 text-gray-700">
{{ count($groupLogs) }}
</span>
</div>
</div>
<div class="flex items-center gap-2">
<a href="{{ route('dev-tools.api-logs.index', ['group_id' => $log->group_id]) }}"
class="text-sm text-purple-600 hover:text-purple-800"
onclick="event.stopPropagation()">
전체 보기
</a>
<svg id="groupLogsChevron" class="w-5 h-5 text-gray-500 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
</div>
<!-- 펼쳐지는 내용 (기본 숨김) -->
<div id="groupLogsContent" class="hidden border-t">
<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.v') }}
</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>
</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);
// json_decode 실패 시 유니코드 이스케이프를 한글로 변환하고 슬래시 이스케이프도 제거
if ($responseData === null) {
$displayBody = preg_replace_callback('/\\\\u([0-9a-fA-F]{4})/', function($m) {
return mb_convert_encoding(pack('H*', $m[1]), 'UTF-8', 'UTF-16BE');
}, $log->response_body);
$displayBody = str_replace('\\/', '/', $displayBody);
}
@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">{!! e($displayBody) !!}</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