import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import type { RequestLog, PageResponse } from '../../types/monitoring'; import type { ServiceInfo } from '../../types/service'; import { searchLogs } from '../../services/monitoringService'; import { getServices } from '../../services/serviceService'; import Badge from '../../components/ui/Badge'; import type { BadgeVariant } from '../../components/ui/Badge'; import Button from '../../components/ui/Button'; import { SERVICE_BADGE_VARIANTS } from '../../constants/chart'; const STATUS_VARIANT: Record = { SUCCESS: 'success', FAIL: 'danger', DENIED: 'warning', EXPIRED: 'warning', INVALID_KEY: 'danger', ERROR: 'danger', FAILED: 'default', }; const METHOD_VARIANT: Record = { GET: 'success', POST: 'info', PUT: 'warning', DELETE: 'danger', }; const REQUEST_STATUSES = ['SUCCESS', 'FAIL', 'DENIED', 'EXPIRED', 'INVALID_KEY', 'ERROR', 'FAILED']; const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE']; const DEFAULT_PAGE_SIZE = 20; const formatDate = (d: Date): string => { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }; const getToday = (): string => formatDate(new Date()); const getTodayString = getToday; 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 getStatusCodeVariant = (code: number): BadgeVariant => { if (code >= 500) return 'danger'; if (code >= 400) return 'warning'; if (code >= 200) return 'success'; return 'default'; }; const getResponseTimeClass = (ms: number): string => { if (ms > 1000) return 'text-[var(--color-danger)]'; if (ms > 500) return 'text-[var(--color-warning)]'; return 'text-[var(--color-text-primary)]'; }; const RequestLogsPage = () => { const navigate = useNavigate(); const [startDate, setStartDate] = useState(() => { const d = new Date(); d.setDate(d.getDate() - 6); return formatDate(d); }); const [endDate, setEndDate] = useState(getTodayString()); const [datePreset, setDatePreset] = useState('최근 7일'); const [serviceId, setServiceId] = useState(''); const [requestStatus, setRequestStatus] = useState(''); const [requestMethod, setRequestMethod] = useState(''); const [searchKeyword, setSearchKeyword] = useState(''); const [services, setServices] = useState([]); const [serviceBadgeMap, setServiceBadgeMap] = useState>({}); // key: serviceName const [result, setResult] = useState | null>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [currentPage, setCurrentPage] = useState(0); const fetchServices = async () => { try { const res = await getServices(); if (res.success && res.data) { setServices(res.data); const sorted = [...res.data].sort((a, b) => a.serviceName.localeCompare(b.serviceName)); const map: Record = {}; sorted.forEach((s, idx) => { map[s.serviceName] = SERVICE_BADGE_VARIANTS[idx % SERVICE_BADGE_VARIANTS.length]; }); setServiceBadgeMap(map); } } catch { // 서비스 목록 로딩 실패는 무시 } }; const handleSearch = async (page: number) => { setLoading(true); setError(null); setCurrentPage(page); try { const params: Record = { startDate: startDate || undefined, endDate: endDate || undefined, serviceId: serviceId ? Number(serviceId) : undefined, requestStatus: requestStatus || undefined, requestMethod: requestMethod || undefined, keyword: searchKeyword || undefined, page, size: DEFAULT_PAGE_SIZE, }; const res = await searchLogs(params); if (res.success && res.data) { setResult(res.data); } else { setError(res.message || '로그 조회에 실패했습니다.'); } } catch { setError('로그 조회에 실패했습니다.'); } finally { setLoading(false); } }; const handleReset = () => { const d = new Date(); d.setDate(d.getDate() - 6); setStartDate(formatDate(d)); setEndDate(getTodayString()); setDatePreset('최근 7일'); setServiceId(''); setRequestStatus(''); setRequestMethod(''); setSearchKeyword(''); setCurrentPage(0); }; useEffect(() => { fetchServices(); handleSearch(0); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleResetAndSearch = () => { handleReset(); setLoading(true); setError(null); setCurrentPage(0); const today = getTodayString(); const d = new Date(); d.setDate(d.getDate() - 6); const weekAgo = formatDate(d); const params: Record = { startDate: weekAgo, endDate: today, page: 0, size: DEFAULT_PAGE_SIZE, }; searchLogs(params) .then((res) => { if (res.success && res.data) { setResult(res.data); } else { setError(res.message || '로그 조회에 실패했습니다.'); } }) .catch(() => { setError('로그 조회에 실패했습니다.'); }) .finally(() => { setLoading(false); }); }; const handleRowClick = (logId: number) => { navigate(`/monitoring/request-logs/${logId}`); }; const handlePrev = () => { if (currentPage > 0) { handleSearch(currentPage - 1); } }; const handleNext = () => { if (result && currentPage < result.totalPages - 1) { handleSearch(currentPage + 1); } }; const DATE_PRESETS = [ { label: '오늘', fn: () => { const t = getToday(); setStartDate(t); setEndDate(t); setDatePreset('오늘'); }, }, { label: '어제', fn: () => { const d = new Date(); d.setDate(d.getDate() - 1); const y = formatDate(d); setStartDate(y); setEndDate(y); setDatePreset('어제'); }, }, { label: '최근 7일', fn: () => { const d = new Date(); d.setDate(d.getDate() - 6); setStartDate(formatDate(d)); setEndDate(getToday()); setDatePreset('최근 7일'); }, }, { label: '이번 달', fn: () => { const d = new Date(); setStartDate(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-01`); setEndDate(getToday()); setDatePreset('이번 달'); }, }, { label: '지난 달', fn: () => { const d = new Date(); d.setMonth(d.getMonth() - 1); const s = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-01`; const e = new Date(d.getFullYear(), d.getMonth() + 1, 0); setStartDate(s); setEndDate(formatDate(e)); setDatePreset('지난 달'); }, }, ]; const activeFilters: { label: string; onRemove: () => void }[] = []; if (serviceId) { const svc = services.find((s) => String(s.serviceId) === serviceId); activeFilters.push({ label: `서비스: ${svc?.serviceName ?? serviceId}`, onRemove: () => setServiceId('') }); } if (requestStatus) { activeFilters.push({ label: `상태: ${requestStatus}`, onRemove: () => setRequestStatus('') }); } if (requestMethod) { activeFilters.push({ label: `Method: ${requestMethod}`, onRemove: () => setRequestMethod('') }); } if (searchKeyword) { activeFilters.push({ label: `검색: ${searchKeyword}`, onRemove: () => setSearchKeyword('') }); } const totalElements = result?.totalElements ?? 0; const totalPages = result?.totalPages ?? 1; const start = totalElements === 0 ? 0 : currentPage * DEFAULT_PAGE_SIZE + 1; const end = Math.min((currentPage + 1) * DEFAULT_PAGE_SIZE, totalElements); const selectClassName = 'bg-[var(--color-bg-base)] border border-[var(--color-border)] rounded-md px-2.5 py-1.5 text-xs text-[var(--color-text-primary)] focus:outline-none'; return (
{/* 1행: 제목 */}

요청 로그

모든 API 요청/응답 로그를 조회합니다

{/* 2행: 필터 카드 */}
{/* 1줄: 기간 프리셋 + 날짜 입력 */}
{/* 세그먼트 컨트롤 */}
{DATE_PRESETS.map((preset) => ( ))}
{/* 구분선 */}
{/* 날짜 입력 */}
{ setStartDate(e.target.value); setDatePreset(''); }} className={selectClassName} /> ~ { setEndDate(e.target.value); setDatePreset(''); }} className={selectClassName} />
{/* 2줄: 필터 셀렉트 + URL 검색 + 버튼 */}
{/* 필터 아이콘 */} {/* 서비스 */} {/* 상태 */} {/* Method */} {/* URL 검색 */}
setSearchKeyword(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch(0)} placeholder="URL 또는 IP로 검색..." className="w-full pl-8 pr-3 py-1.5 bg-[var(--color-bg-base)] border border-[var(--color-border)] rounded-md text-xs text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none" />
{/* 버튼 */}
{/* 3줄: 활성 필터 칩 */} {activeFilters.length > 0 && (
적용된 필터: {activeFilters.map((f) => ( {f.label} ))}
)}
{/* 에러 메시지 */} {error && (
{error}
)} {/* 3행: 로그 테이블 */}
{loading ? (
로딩 중...
) : (
{result && result.content.length > 0 ? ( result.content.map((log) => ( handleRowClick(log.logId)} className="cursor-pointer hover:bg-[var(--color-bg-base)] transition-colors" > {/* 시간 */} {/* 서비스 — filled Badge (rounded-md, 고정 너비) */} {/* Method — pill Badge */} {/* URL */} {/* 응답코드 — pill Badge */} {/* 응답시간 */} {/* 응답결과 — pill Badge */} {/* IP */} )) ) : ( )}
시간 서비스 Method URL 응답코드 응답시간 응답결과 IP
{formatDateTime(log.requestedAt)} {log.serviceName ? ( {log.serviceName} ) : ( - )} {log.requestMethod} {log.requestUrl} {log.responseStatus != null ? ( {log.responseStatus} ) : ( - )} {log.responseTime != null ? ( {log.responseTime} ms ) : ( - )} {log.requestStatus} {log.requestIp}
검색 결과가 없습니다
)} {/* 4행: 페이지네이션 (테이블 카드 내부 하단) */} {result && result.totalElements > 0 && (
총{' '} {totalElements}건 중{' '} {start}-{end} 건 표시
{currentPage + 1} / {totalPages}
)}
); }; export default RequestLogsPage;