- 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>
298 lines
13 KiB
Markdown
298 lines
13 KiB
Markdown
# 메인 대시보드 차트 구성 가이드
|
|
|
|
> 관리자 시스템 메인화면은 대시보드 차트로 구성한다.
|
|
> 네이버 클라우드 콘솔의 "서비스 사용 현황" 및 "리소스 모니터링" 화면을 참조한다.
|
|
|
|
## 메인 대시보드 레이아웃
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ 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
|
|
|
|
```bash
|
|
npm install recharts
|
|
```
|
|
|
|
**선택 이유:** NCloud 콘솔과 유사한 심플한 차트 스타일, React 친화적, 번들 크기 적당.
|
|
|
|
---
|
|
|
|
## 1. SR 추이 꺾은선 차트 (SRTrendChart)
|
|
|
|
```tsx
|
|
// 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)
|
|
|
|
```tsx
|
|
// 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)
|
|
|
|
```tsx
|
|
// 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)
|
|
|
|
```tsx
|
|
// 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 전체 조합
|
|
|
|
```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>
|
|
);
|
|
}
|
|
```
|