generated from gc/template-java-maven
- S&P/SNP → KCG 텍스트 변경 (타이틀, 사이드바, 대시보드) - 사이드 메뉴 한글화 (모니터링, 통계, API 키, 관리자, 부서) - MainLayout/ApiHubLayout 헤더/사이드바 레퍼런스 디자인 적용 - 서비스 상태 카드 서비스 코드 제거 - 대시보드 배너 브랜드 컬러 그라디언트 적용 - 다크/라이트 테마 전환 .light 클래스 대응 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
151 lines
6.1 KiB
TypeScript
151 lines
6.1 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import type { ServiceStatusDetail } from '../../types/service';
|
|
import { getAllStatusDetail } from '../../services/heartbeatService';
|
|
|
|
const STATUS_COLOR: Record<string, string> = {
|
|
UP: 'bg-green-500',
|
|
DOWN: 'bg-red-500',
|
|
UNKNOWN: 'bg-gray-400',
|
|
};
|
|
|
|
const STATUS_TEXT: Record<string, string> = {
|
|
UP: 'Operational',
|
|
DOWN: 'Down',
|
|
UNKNOWN: 'Unknown',
|
|
};
|
|
|
|
const getUptimeBarColor = (pct: number): string => {
|
|
if (pct >= 99.9) return 'bg-green-500';
|
|
if (pct >= 99) return 'bg-green-400';
|
|
if (pct >= 95) return 'bg-yellow-400';
|
|
if (pct >= 90) return 'bg-orange-400';
|
|
return 'bg-red-500';
|
|
};
|
|
|
|
const formatDate = (dateStr: string): string => {
|
|
const d = new Date(dateStr);
|
|
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
};
|
|
|
|
const ServiceStatusPage = () => {
|
|
const navigate = useNavigate();
|
|
const [services, setServices] = useState<ServiceStatusDetail[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [lastUpdated, setLastUpdated] = useState('');
|
|
|
|
const fetchData = useCallback(async () => {
|
|
try {
|
|
const res = await getAllStatusDetail();
|
|
if (res.success && res.data) {
|
|
setServices(res.data);
|
|
}
|
|
setLastUpdated(new Date().toLocaleTimeString('ko-KR'));
|
|
} catch {
|
|
// silent
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
const interval = setInterval(fetchData, 60000);
|
|
return () => clearInterval(interval);
|
|
}, [fetchData]);
|
|
|
|
const allOperational = services.length > 0 && services.every((s) => s.currentStatus === 'UP');
|
|
|
|
if (isLoading) {
|
|
return <div className="text-center py-20 text-[var(--color-text-secondary)]">로딩 중...</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="flex items-center justify-between mb-8">
|
|
<h1 className="text-2xl font-bold text-[var(--color-text-primary)]">Service Status</h1>
|
|
<span className="text-sm text-[var(--color-text-secondary)]">마지막 갱신: {lastUpdated}</span>
|
|
</div>
|
|
|
|
{/* Overall Status Banner */}
|
|
<div className={`rounded-lg p-6 mb-8 ${allOperational ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}>
|
|
<div className="flex items-center gap-3">
|
|
<div className={`w-4 h-4 rounded-full ${allOperational ? 'bg-green-500' : 'bg-red-500'}`} />
|
|
<span className={`text-lg font-semibold ${allOperational ? 'text-green-800' : 'text-red-800'}`}>
|
|
{allOperational ? 'All Systems Operational' : 'Some Systems Down'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Service List */}
|
|
<div className="space-y-6">
|
|
{services.map((svc) => (
|
|
<div key={svc.serviceId} className="bg-[var(--color-bg-surface)] rounded-lg shadow">
|
|
{/* Service Header */}
|
|
<div className="p-6 border-b border-[var(--color-border)]">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`w-3 h-3 rounded-full ${STATUS_COLOR[svc.currentStatus] || 'bg-gray-400'}`} />
|
|
<h2
|
|
className="text-lg font-semibold text-[var(--color-text-primary)] cursor-pointer hover:text-[var(--color-primary)]"
|
|
onClick={() => navigate(`/monitoring/service-status/${svc.serviceId}`)}
|
|
>
|
|
{svc.serviceName}
|
|
</h2>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<span className={`text-sm font-medium ${svc.currentStatus === 'UP' ? 'text-green-600' : svc.currentStatus === 'DOWN' ? 'text-red-600' : 'text-[var(--color-text-secondary)]'}`}>
|
|
{STATUS_TEXT[svc.currentStatus] || svc.currentStatus}
|
|
</span>
|
|
{svc.lastResponseTime !== null && (
|
|
<span className="text-sm text-[var(--color-text-tertiary)]">{svc.lastResponseTime}ms</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="mt-1 text-sm text-[var(--color-text-secondary)]">
|
|
90일 Uptime: <span className="font-medium text-[var(--color-text-primary)]">{svc.uptimePercent90d.toFixed(2)}%</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 90-Day Uptime Bar */}
|
|
<div className="px-6 py-4">
|
|
<div className="flex items-center gap-0.5">
|
|
{svc.dailyUptime.length > 0 ? (
|
|
svc.dailyUptime.map((day, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="group relative flex-1"
|
|
>
|
|
<div
|
|
className={`h-8 rounded-sm ${getUptimeBarColor(day.uptimePercent)} hover:opacity-80 transition-opacity`}
|
|
title={`${formatDate(day.date)}: ${day.uptimePercent.toFixed(1)}% (${day.upChecks}/${day.totalChecks})`}
|
|
/>
|
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block z-10">
|
|
<div className="bg-gray-900 text-white text-xs rounded px-2 py-1 whitespace-nowrap">
|
|
{formatDate(day.date)}: {day.uptimePercent.toFixed(1)}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="flex-1 h-8 bg-[var(--color-bg-base)] rounded-sm" />
|
|
)}
|
|
</div>
|
|
<div className="flex justify-between mt-1 text-xs text-[var(--color-text-tertiary)]">
|
|
<span>{svc.dailyUptime.length > 0 ? formatDate(svc.dailyUptime[0].date) : ''}</span>
|
|
<span>Today</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{services.length === 0 && (
|
|
<div className="text-center py-20 text-[var(--color-text-tertiary)]">등록된 서비스가 없습니다</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ServiceStatusPage;
|