snp-connection-monitoring/frontend/src/pages/monitoring/ServiceStatusPage.tsx
HYOJIN 2a8723419d feat(ui): KCG 브랜딩 + 레이아웃 디자인 + 메뉴 한글화 (#48)
- S&P/SNP → KCG 텍스트 변경 (타이틀, 사이드바, 대시보드)
- 사이드 메뉴 한글화 (모니터링, 통계, API 키, 관리자, 부서)
- MainLayout/ApiHubLayout 헤더/사이드바 레퍼런스 디자인 적용
- 서비스 상태 카드 서비스 코드 제거
- 대시보드 배너 브랜드 컬러 그라디언트 적용
- 다크/라이트 테마 전환 .light 클래스 대응

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:37:31 +09:00

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;