snp-connection-monitoring/frontend/src/pages/monitoring/ServiceStatusDetailPage.tsx
HYOJIN c2a71c1b77 feat(design): 디자인 시스템 적용 (CSS 토큰, Button/Badge, 차트, 다크모드) (#48)
- 디자인 시스템 가이드 문서 11개 생성 (docs/design/)
- CSS 변수 토큰 시스템 (@theme + :root/.dark 전환)
- cn() 유틸리티 (clsx + tailwind-merge)
- Button/Badge 공통 컴포넌트 (variant/size, 다크모드 대응)
- 하드코딩 Tailwind 색상 → CSS 변수 토큰 리팩토링 (30개 파일)
- 차트 팔레트 다크모드 색상 업데이트 (CHART_COLORS_HEX)
- 버튼 다크모드 채도/대비 강화 (primary-600 기반)

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

180 lines
7.9 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import type { ServiceStatusDetail } from '../../types/service';
import { getServiceStatusDetail } from '../../services/heartbeatService';
const STATUS_COLOR: Record<string, string> = {
UP: 'bg-green-500',
DOWN: 'bg-red-500',
UNKNOWN: 'bg-gray-400',
};
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.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
};
const formatTime = (dateStr: string): string => {
return new Date(dateStr).toLocaleTimeString('ko-KR');
};
const ServiceStatusDetailPage = () => {
const { serviceId } = useParams<{ serviceId: string }>();
const navigate = useNavigate();
const [detail, setDetail] = useState<ServiceStatusDetail | null>(null);
const [isLoading, setIsLoading] = useState(true);
const fetchData = useCallback(async () => {
if (!serviceId) return;
try {
const res = await getServiceStatusDetail(Number(serviceId));
if (res.success && res.data) {
setDetail(res.data);
}
} catch {
// silent
} finally {
setIsLoading(false);
}
}, [serviceId]);
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, 60000);
return () => clearInterval(interval);
}, [fetchData]);
if (isLoading) {
return <div className="text-center py-20 text-[var(--color-text-secondary)]"> ...</div>;
}
if (!detail) {
return <div className="text-center py-20 text-[var(--color-text-secondary)]"> </div>;
}
return (
<div className="max-w-7xl mx-auto">
<button
onClick={() => navigate('/monitoring/service-status')}
className="text-sm text-[var(--color-primary)] hover:text-blue-800 mb-4 inline-block"
>
Status
</button>
{/* Header */}
<div className="bg-[var(--color-bg-surface)] rounded-lg shadow p-6 mb-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`w-4 h-4 rounded-full ${STATUS_COLOR[detail.currentStatus] || 'bg-gray-400'}`} />
<h1 className="text-2xl font-bold text-[var(--color-text-primary)]">{detail.serviceName}</h1>
<span className="text-[var(--color-text-secondary)]">{detail.serviceCode}</span>
</div>
<div className="text-right">
<div className={`text-lg font-semibold ${detail.currentStatus === 'UP' ? 'text-green-600' : 'text-red-600'}`}>
{detail.currentStatus === 'UP' ? 'Operational' : detail.currentStatus === 'DOWN' ? 'Down' : 'Unknown'}
</div>
{detail.lastResponseTime !== null && (
<div className="text-sm text-[var(--color-text-secondary)]">{detail.lastResponseTime}ms</div>
)}
</div>
</div>
</div>
{/* Uptime Summary */}
<div className="bg-[var(--color-bg-surface)] rounded-lg shadow p-6 mb-6">
<h2 className="text-lg font-semibold text-[var(--color-text-primary)] mb-4">90 Uptime</h2>
<div className="text-4xl font-bold text-[var(--color-text-primary)] mb-4">{detail.uptimePercent90d.toFixed(3)}%</div>
{/* 90-Day Bar */}
<div className="flex items-center gap-0.5 mb-2">
{detail.dailyUptime.map((day, idx) => (
<div key={idx} className="group relative flex-1">
<div
className={`h-10 rounded-sm ${getUptimeBarColor(day.uptimePercent)} hover:opacity-80 transition-opacity`}
/>
<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-3 py-2 whitespace-nowrap shadow-lg">
<div className="font-medium">{formatDate(day.date)}</div>
<div>Uptime: {day.uptimePercent.toFixed(1)}%</div>
<div>Checks: {day.upChecks}/{day.totalChecks}</div>
</div>
</div>
</div>
))}
{detail.dailyUptime.length === 0 && (
<div className="flex-1 h-10 bg-[var(--color-bg-base)] rounded-sm" />
)}
</div>
<div className="flex justify-between text-xs text-[var(--color-text-tertiary)]">
<span>{detail.dailyUptime.length > 0 ? formatDate(detail.dailyUptime[0].date) : ''}</span>
<span>Today</span>
</div>
{/* Daily Uptime Legend */}
<div className="flex items-center gap-4 mt-4 text-xs text-[var(--color-text-secondary)]">
<div className="flex items-center gap-1"><div className="w-3 h-3 rounded-sm bg-green-500" /> 99.9%+</div>
<div className="flex items-center gap-1"><div className="w-3 h-3 rounded-sm bg-green-400" /> 99%+</div>
<div className="flex items-center gap-1"><div className="w-3 h-3 rounded-sm bg-yellow-400" /> 95%+</div>
<div className="flex items-center gap-1"><div className="w-3 h-3 rounded-sm bg-orange-400" /> 90%+</div>
<div className="flex items-center gap-1"><div className="w-3 h-3 rounded-sm bg-red-500" /> &lt;90%</div>
</div>
</div>
{/* Recent Checks */}
<div className="bg-[var(--color-bg-surface)] rounded-lg shadow mb-6">
<div className="p-6 border-b border-[var(--color-border)]">
<h2 className="text-lg font-semibold text-[var(--color-text-primary)]"> </h2>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-[var(--color-bg-base)]">
<tr>
<th className="px-4 py-3 text-left font-medium text-[var(--color-text-secondary)]"></th>
<th className="px-4 py-3 text-left font-medium text-[var(--color-text-secondary)]"></th>
<th className="px-4 py-3 text-left font-medium text-[var(--color-text-secondary)]"></th>
<th className="px-4 py-3 text-left font-medium text-[var(--color-text-secondary)]"></th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--color-border)]">
{detail.recentChecks.map((check, idx) => (
<tr key={idx} className="hover:bg-[var(--color-bg-base)]">
<td className="px-4 py-3 text-[var(--color-text-secondary)] whitespace-nowrap">{formatTime(check.checkedAt)}</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<div className={`w-2.5 h-2.5 rounded-full ${STATUS_COLOR[check.status] || 'bg-gray-400'}`} />
<span className={check.status === 'UP' ? 'text-green-700' : 'text-red-700'}>{check.status}</span>
</div>
</td>
<td className="px-4 py-3 text-[var(--color-text-secondary)]">
{check.responseTime !== null ? `${check.responseTime}ms` : '-'}
</td>
<td className="px-4 py-3 text-red-600 text-xs max-w-xs truncate">
{check.errorMessage || '-'}
</td>
</tr>
))}
{detail.recentChecks.length === 0 && (
<tr>
<td colSpan={4} className="px-4 py-8 text-center text-[var(--color-text-tertiary)]">
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
);
};
export default ServiceStatusDetailPage;