- 37개 파일 IP → zioinfo.co.kr 치환 (소스/매뉴얼/설정/하네스) - Manager DrConsole/NetworkConsole/CsapConsole 빌드 + /var/www/manager/ 배포 - 테스트: Manager HTTP 200, ITSM 신규 API 7개 전체 200 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
13 KiB
13 KiB
메인 대시보드 차트 구성 가이드
관리자 시스템 메인화면은 대시보드 차트로 구성한다. 네이버 클라우드 콘솔의 "서비스 사용 현황" 및 "리소스 모니터링" 화면을 참조한다.
메인 대시보드 레이아웃
┌─────────────────────────────────────────────────────────────────────────┐
│ GUARDiA Manager — 통합 운영 대시보드 마지막 갱신: 2분 전 [↺ 새로고침] │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌── 핵심 지표 카드 (4개) ─────────────────────────────────────────────┐ │
│ │ [SR 현황] [인시던트] [서버 가용률] [SLA 달성률] │ │
│ │ 24건 긴급 2건 91.7% 98.7% │ │
│ │ 진행중 8↗ 해결중 3 11/12대 실행 ▲ 0.3% │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌── SR 추이 꺾은선 차트 ─────────┐ ┌── 서버 상태 도넛 차트 ──────────┐ │
│ │ 7일간 SR 생성 vs 완료 │ │ │ │
│ │ │ │ 실행중 10 │ │
│ │ ╭──╮ │ │ ●────────── │ │
│ │ │ ╰──╮ ╭─── │ │ ○ 중지 1 │ │
│ │ │ ╰────╯ │ │ ● 오류 1 │ │
│ │ └────────────────────────── │ │ │ │
│ │ 월 화 수 목 금 토 일 │ │ 12대 서버 │ │
│ └────────────────────────────────┘ └──────────────────────────────┘ │
│ │
│ ┌── 리소스 모니터링 막대 차트 ──────────────────────────────────────────┐ │
│ │ CPU ██████████░░ 62% 메모리 ████████████░ 78% 디스크 ████░ 16% │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌── 최근 배포 이력 타임라인 ────────┐ ┌── AI/LLM 사용 현황 ───────────┐ │
│ │ 5분전 ✅ zioinfo-web v1.2.1 │ │ llama3:8b 응답시간 2.3s │ │
│ │ 1시간 ✅ guardia-itsm v2.0.4 │ │ 요청 124건/오늘 │ │
│ │ 3시간 ❌ zioinfo-web (실패) │ │ [████████░░] 메모리 4.7GB │ │
│ └────────────────────────────────┘ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
차트 라이브러리 선택: Recharts
npm install recharts
선택 이유: NCloud 콘솔과 유사한 심플한 차트 스타일, React 친화적, 번들 크기 적당.
1. SR 추이 꺾은선 차트 (SRTrendChart)
// components/dashboard/SRTrendChart.tsx
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
interface SRTrendData {
date: string; // 'MM/DD'
created: number;
completed: number;
}
export function SRTrendChart({ data }: { data: SRTrendData[] }) {
return (
<div style={{ background: '#fff', borderRadius: 10, padding: '20px 24px',
border: '1px solid var(--border)' }}>
<h3 style={{ margin: '0 0 16px', fontSize: 14, fontWeight: 600 }}>
SR 생성/완료 추이 (7일)
</h3>
<ResponsiveContainer width="100%" height={200}>
<LineChart data={data} margin={{ top: 5, right: 10, left: -20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f2f5" />
<XAxis dataKey="date" tick={{ fontSize: 11, fill: '#94a3b8' }} />
<YAxis tick={{ fontSize: 11, fill: '#94a3b8' }} />
<Tooltip contentStyle={{ borderRadius: 8, border: '1px solid #e2e8f0',
fontSize: 12 }} />
<Legend wrapperStyle={{ fontSize: 12 }} />
<Line type="monotone" dataKey="created" name="신규 SR"
stroke="#4f6ef7" strokeWidth={2} dot={{ r: 3 }} />
<Line type="monotone" dataKey="completed" name="완료 SR"
stroke="#22c55e" strokeWidth={2} dot={{ r: 3 }} />
</LineChart>
</ResponsiveContainer>
</div>
);
}
2. 서버 상태 도넛 차트 (ServerStatusDonut)
// components/dashboard/ServerStatusDonut.tsx
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from 'recharts';
const COLORS = {
running: '#22c55e',
stopped: '#94a3b8',
error: '#ef4444',
pending: '#f59e0b',
};
export function ServerStatusDonut({ servers }: { servers: { status: string }[] }) {
const counts = servers.reduce((acc, s) => {
acc[s.status] = (acc[s.status] ?? 0) + 1; return acc;
}, {} as Record<string, number>);
const data = Object.entries(counts).map(([status, value]) => ({
name: { running: '실행중', stopped: '중지', error: '오류', pending: '진행중' }[status] ?? status,
value,
status,
}));
return (
<div style={{ background: '#fff', borderRadius: 10, padding: '20px 24px',
border: '1px solid var(--border)' }}>
<h3 style={{ margin: '0 0 8px', fontSize: 14, fontWeight: 600 }}>
서버 상태 ({servers.length}대)
</h3>
<ResponsiveContainer width="100%" height={180}>
<PieChart>
<Pie data={data} cx="50%" cy="50%" innerRadius={50} outerRadius={75}
paddingAngle={2} dataKey="value">
{data.map((entry) => (
<Cell key={entry.status}
fill={COLORS[entry.status as keyof typeof COLORS] ?? '#94a3b8'} />
))}
</Pie>
<Tooltip formatter={(value, name) => [`${value}대`, name]} />
<Legend iconType="circle" iconSize={8} wrapperStyle={{ fontSize: 12 }} />
</PieChart>
</ResponsiveContainer>
</div>
);
}
3. 리소스 모니터링 바 차트 (ResourceGauge)
// components/dashboard/ResourceGauge.tsx
// NCloud 콘솔의 리소스 게이지 UI 참조
interface GaugeProps {
label: string;
percent: number;
detail?: string;
}
function Gauge({ label, percent, detail }: GaugeProps) {
const color = percent > 90 ? '#ef4444' : percent > 70 ? '#f59e0b' : '#22c55e';
return (
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6,
fontSize: 12 }}>
<span style={{ fontWeight: 600 }}>{label}</span>
<span style={{ color, fontWeight: 700 }}>{percent}%</span>
</div>
<div style={{ height: 8, background: '#f0f2f5', borderRadius: 4, overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${percent}%`,
background: color, borderRadius: 4,
transition: 'width 0.5s ease' }} />
</div>
{detail && <div style={{ fontSize: 11, color: '#94a3b8', marginTop: 4 }}>{detail}</div>}
</div>
);
}
export function ResourceGauge({ resources }: { resources: SystemResources }) {
return (
<div style={{ background: '#fff', borderRadius: 10, padding: '20px 24px',
border: '1px solid var(--border)' }}>
<h3 style={{ margin: '0 0 16px', fontSize: 14, fontWeight: 600 }}>
서버 리소스
</h3>
<div style={{ display: 'flex', gap: 24 }}>
<Gauge label="CPU" percent={resources.cpu_percent} />
<Gauge label="메모리"
percent={resources.memory.percent}
detail={`${resources.memory.used_gb}GB / ${resources.memory.total_gb}GB`} />
<Gauge label="디스크"
percent={resources.disk.percent}
detail={`${resources.disk.used_gb}GB / ${resources.disk.total_gb}GB`} />
</div>
</div>
);
}
4. 배포 이력 타임라인 (DeployTimeline)
// components/dashboard/DeployTimeline.tsx
import { useGuardiaApi } from '../../hooks/useGuardiaApi';
function timeAgo(ts: string) {
const diff = Date.now() - new Date(ts).getTime();
if (diff < 60000) return '방금 전';
if (diff < 3600000) return `${Math.floor(diff/60000)}분 전`;
return `${Math.floor(diff/3600000)}시간 전`;
}
export function DeployTimeline() {
const { data: logs } = useGuardiaApi<string[]>('/api/deploy/history');
const parsed = (logs ?? []).slice(-5).reverse().map(line => ({
time: line.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/)?.[0] ?? '',
ok: line.includes('완료') || line.includes('success'),
msg: line.replace(/^\d{4}.*?INFO\s+/, '').trim().slice(0, 60),
}));
return (
<div style={{ background: '#fff', borderRadius: 10, padding: '20px 24px',
border: '1px solid var(--border)' }}>
<h3 style={{ margin: '0 0 14px', fontSize: 14, fontWeight: 600 }}>
최근 배포 이력
</h3>
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
{parsed.map((item, i) => (
<li key={i} style={{ display: 'flex', gap: 10, padding: '8px 0',
borderBottom: i < parsed.length - 1 ? '1px solid #f1f5f9' : 'none',
fontSize: 13 }}>
<span>{item.ok ? '✅' : '❌'}</span>
<span style={{ flex: 1, color: '#1e293b' }}>{item.msg}</span>
<span style={{ color: '#94a3b8', fontSize: 11, whiteSpace: 'nowrap' }}>
{item.time}
</span>
</li>
))}
{!parsed.length && (
<li style={{ color: '#94a3b8', fontSize: 13 }}>배포 이력이 없습니다.</li>
)}
</ul>
</div>
);
}
Dashboard.tsx 전체 조합
// pages/Dashboard.tsx
import { StatCard } from '../components/common/StatCard';
import { SRTrendChart } from '../components/dashboard/SRTrendChart';
import { ServerStatusDonut } from '../components/dashboard/ServerStatusDonut';
import { ResourceGauge } from '../components/dashboard/ResourceGauge';
import { DeployTimeline } from '../components/dashboard/DeployTimeline';
import { useGuardiaApi } from '../hooks/useGuardiaApi';
import { useManagerApi } from '../hooks/useManagerApi';
export function Dashboard() {
const { data: stats } = useGuardiaApi('/api/dashboard');
const { data: resources } = useManagerApi('/api/system/resources');
const { data: servers } = useGuardiaApi('/api/cmdb/servers?limit=100');
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
{/* 핵심 지표 카드 */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16 }}>
<StatCard title="전체 SR" value={stats?.total_sr ?? '-'}
sub={`진행중 ${stats?.open_sr ?? 0}건`} icon="📋" color="#e8ecff" />
<StatCard title="인시던트" value={stats?.critical_incidents ?? '-'}
sub="긴급" icon="🚨" color="#fff1f2" />
<StatCard title="서버 가용률"
value={servers ? `${((servers.filter((s:any) => s.status==='running').length / servers.length)*100).toFixed(1)}%` : '-'}
icon="🖥️" color="#f0fdf4" />
<StatCard title="SLA 달성률" value={stats?.sla_achievement ? `${stats.sla_achievement}%` : '-'}
icon="📈" color="#fff7ed" />
</div>
{/* 차트 행 1: SR 추이 + 서버 상태 */}
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 16 }}>
<SRTrendChart data={stats?.sr_trend ?? []} />
<ServerStatusDonut servers={servers ?? []} />
</div>
{/* 리소스 게이지 */}
{resources && <ResourceGauge resources={resources} />}
{/* 차트 행 2: 배포 이력 + LLM */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<DeployTimeline />
{/* LLM 상태 카드 추가 가능 */}
</div>
</div>
);
}