snp-connection-monitoring/frontend/src/pages/monitoring/RequestLogDetailPage.tsx
HYOJIN 071489ced6 feat(phase4): API Gateway 프록시 + 요청 로깅
백엔드:
- 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>
2026-04-08 11:20:08 +09:00

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"
>
&larr;
</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"
>
&larr;
</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;