guardia-manager/.claude/skills/manager-ui/references/dashboard-charts.md
DESKTOP-TKLFCPRython 10cc76d6e6 refactor: 101.79.17.164 → zioinfo.co.kr 전체 도메인 변환 + Manager UI 배포
- 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>
2026-05-31 10:09:17 +09:00

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>
  );
}