generated from gc/template-java-maven
- 디자인 시스템 가이드 문서 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>
180 lines
7.9 KiB
TypeScript
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" /> <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;
|