generated from gc/template-java-maven
백엔드:
- GatewayController (ANY /gateway/{serviceCode}/**)
- GatewayService (API Key prefix 매칭 + AES 복호화, 권한 확인, WebClient 프록시)
- AsyncConfig + RequestLogService (@Async 비동기 로깅)
- RequestLogController (검색 + 상세 API, JPA Specification)
- request_url을 gateway 경로로 통일 저장
- tenant_id 로그 기록 추가
- ErrorCode 6개 (GW001-GW006)
프론트엔드:
- RequestLogsPage (검색 폼 + 결과 테이블 + 페이지네이션)
- RequestLogDetailPage (요청/응답 상세)
- 날짜 검색 LocalDate 변환 수정
Closes #9
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
211 lines
7.2 KiB
TypeScript
211 lines
7.2 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import type { RequestLog } from '../../types/monitoring';
|
|
import { getLogDetail } from '../../services/monitoringService';
|
|
|
|
const METHOD_COLOR: Record<string, string> = {
|
|
GET: 'bg-green-100 text-green-800',
|
|
POST: 'bg-blue-100 text-blue-800',
|
|
PUT: 'bg-orange-100 text-orange-800',
|
|
DELETE: 'bg-red-100 text-red-800',
|
|
};
|
|
|
|
const STATUS_BADGE: Record<string, string> = {
|
|
SUCCESS: 'bg-green-100 text-green-800',
|
|
DENIED: 'bg-red-100 text-red-800',
|
|
EXPIRED: 'bg-orange-100 text-orange-800',
|
|
INVALID_KEY: 'bg-red-100 text-red-800',
|
|
FAILED: 'bg-gray-100 text-gray-800',
|
|
};
|
|
|
|
const formatDateTime = (dateStr: string): string => {
|
|
const d = new Date(dateStr);
|
|
const year = d.getFullYear();
|
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
const day = String(d.getDate()).padStart(2, '0');
|
|
const hours = String(d.getHours()).padStart(2, '0');
|
|
const minutes = String(d.getMinutes()).padStart(2, '0');
|
|
const seconds = String(d.getSeconds()).padStart(2, '0');
|
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
};
|
|
|
|
const formatJson = (str: string | null): string | null => {
|
|
if (!str) return null;
|
|
try {
|
|
return JSON.stringify(JSON.parse(str), null, 2);
|
|
} catch {
|
|
return str;
|
|
}
|
|
};
|
|
|
|
const RequestLogDetailPage = () => {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
|
|
const [log, setLog] = useState<RequestLog | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
|
|
const fetchDetail = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const res = await getLogDetail(Number(id));
|
|
if (res.success && res.data) {
|
|
setLog(res.data);
|
|
} else {
|
|
setError(res.message || '로그 상세 조회에 실패했습니다.');
|
|
}
|
|
} catch {
|
|
setError('로그 상세 조회에 실패했습니다.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchDetail();
|
|
}, [id]);
|
|
|
|
if (loading) {
|
|
return <div className="text-center py-10 text-gray-500">로딩 중...</div>;
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div>
|
|
<button
|
|
onClick={() => navigate(-1)}
|
|
className="text-blue-600 hover:text-blue-800 font-medium mb-4"
|
|
>
|
|
← 목록으로
|
|
</button>
|
|
<div className="p-3 bg-red-50 text-red-700 rounded-lg text-sm">{error}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!log) return null;
|
|
|
|
const formattedHeaders = formatJson(log.requestHeaders);
|
|
const formattedParams = formatJson(log.requestParams);
|
|
|
|
return (
|
|
<div>
|
|
<button
|
|
onClick={() => navigate(-1)}
|
|
className="text-blue-600 hover:text-blue-800 font-medium mb-6"
|
|
>
|
|
← 목록으로
|
|
</button>
|
|
|
|
{/* 기본 정보 */}
|
|
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">기본 정보</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
|
|
<div>
|
|
<span className="block text-sm font-medium text-gray-500">요청 시간</span>
|
|
<span className="text-sm text-gray-900">{formatDateTime(log.requestedAt)}</span>
|
|
</div>
|
|
<div>
|
|
<span className="block text-sm font-medium text-gray-500">요청</span>
|
|
<span className="text-sm text-gray-900">
|
|
<span
|
|
className={`inline-block px-2 py-0.5 rounded text-xs font-bold mr-2 ${
|
|
METHOD_COLOR[log.requestMethod] || 'bg-gray-100 text-gray-800'
|
|
}`}
|
|
>
|
|
{log.requestMethod}
|
|
</span>
|
|
{log.requestUrl}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="block text-sm font-medium text-gray-500">응답 코드</span>
|
|
<span className="text-sm text-gray-900">
|
|
{log.responseStatus != null ? log.responseStatus : '-'}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="block text-sm font-medium text-gray-500">응답시간(ms)</span>
|
|
<span className="text-sm text-gray-900">
|
|
{log.responseTime != null ? log.responseTime : '-'}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="block text-sm font-medium text-gray-500">응답크기(bytes)</span>
|
|
<span className="text-sm text-gray-900">
|
|
{log.responseSize != null ? log.responseSize : '-'}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="block text-sm font-medium text-gray-500">상태</span>
|
|
<span
|
|
className={`inline-block px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
STATUS_BADGE[log.requestStatus] || 'bg-gray-100 text-gray-800'
|
|
}`}
|
|
>
|
|
{log.requestStatus}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div>
|
|
<span className="block text-sm font-medium text-gray-500">서비스</span>
|
|
<span className="text-sm text-gray-900">{log.serviceName || '-'}</span>
|
|
</div>
|
|
<div>
|
|
<span className="block text-sm font-medium text-gray-500">API Key</span>
|
|
<span className="text-sm text-gray-900 font-mono">
|
|
{log.apiKeyPrefix || '-'}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="block text-sm font-medium text-gray-500">사용자</span>
|
|
<span className="text-sm text-gray-900">{log.userName || '-'}</span>
|
|
</div>
|
|
<div>
|
|
<span className="block text-sm font-medium text-gray-500">IP</span>
|
|
<span className="text-sm text-gray-900 font-mono">{log.requestIp}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 요청 정보 */}
|
|
{(formattedHeaders || formattedParams) && (
|
|
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">요청 정보</h2>
|
|
{formattedHeaders && (
|
|
<div className="mb-4">
|
|
<span className="block text-sm font-medium text-gray-500 mb-1">Request Headers</span>
|
|
<pre className="bg-gray-50 rounded-lg p-4 text-sm text-gray-800 overflow-x-auto">
|
|
{formattedHeaders}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
{formattedParams && (
|
|
<div>
|
|
<span className="block text-sm font-medium text-gray-500 mb-1">Request Params</span>
|
|
<pre className="bg-gray-50 rounded-lg p-4 text-sm text-gray-800 overflow-x-auto">
|
|
{formattedParams}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 에러 정보 */}
|
|
{log.errorMessage && (
|
|
<div className="bg-red-50 rounded-lg shadow p-6">
|
|
<h2 className="text-lg font-semibold text-red-900 mb-2">에러 정보</h2>
|
|
<p className="text-sm text-red-800">{log.errorMessage}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RequestLogDetailPage;
|