From 70cfb1701bc2a4658235a845697078f9de9236f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=9C=EC=A4=80?= Date: Wed, 13 May 2026 01:20:13 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EC=B0=A8=ED=8A=B8=20=EB=B0=8F=20=ED=98=84=ED=99=A9=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 심각도 분포 PieChart (CRITICAL/HIGH/MEDIUM/LOW) - 이상 유형 분포 BarChart (anomalyType) - Pod 상태 요약 (이상 Pod 목록) - 최근 알람 패널 (AlertStore) - staleTime 30초 설정으로 페이지 전환 속도 개선 Co-Authored-By: Claude Sonnet 4.6 --- app/components/Providers.tsx | 2 +- app/page.tsx | 285 ++++++++++++++++++++++++++--------- 2 files changed, 216 insertions(+), 71 deletions(-) diff --git a/app/components/Providers.tsx b/app/components/Providers.tsx index 47061a7..e51f432 100644 --- a/app/components/Providers.tsx +++ b/app/components/Providers.tsx @@ -9,7 +9,7 @@ export default function Providers({ children }: { children: React.ReactNode }) { () => new QueryClient({ defaultOptions: { - queries: { retry: 1, refetchOnWindowFocus: false }, + queries: { retry: 1, refetchOnWindowFocus: false, staleTime: 30_000 }, }, }) ); diff --git a/app/page.tsx b/app/page.tsx index d2ba4eb..82df169 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -14,6 +14,17 @@ import { } from "../components/ui/table"; import { useRouter } from "next/navigation"; import type { Severity, TicketStatus } from "./lib/types"; +import { + PieChart, + Pie, + Cell, + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, +} from "recharts"; function SeverityBadge({ severity }: { severity: Severity }) { const colorMap: Record = { @@ -23,9 +34,7 @@ function SeverityBadge({ severity }: { severity: Severity }) { LOW: "bg-blue-500 text-white", }; return ( - + {severity} ); @@ -39,9 +48,7 @@ function StatusBadge({ status }: { status: TicketStatus }) { CLOSED: "bg-gray-700 text-gray-300", }; return ( - + {status} ); @@ -60,30 +67,63 @@ function SkeletonCard() { ); } +const SEVERITY_COLORS: Record = { + CRITICAL: "#ef4444", + HIGH: "#f97316", + MEDIUM: "#eab308", + LOW: "#3b82f6", +}; + +const ANOMALY_LABELS: Record = { + CPU_HIGH: "CPU", + MEMORY_HIGH: "MEM", + POD_RESTART: "재시작", + ERROR_RATE_HIGH: "에러율", + OOM_KILLED: "OOM", + CRASH_LOOP: "크래시", +}; + export default function DashboardPage() { const router = useRouter(); - const unreadCount = useAlertStore((s) => s.unreadCount); - - const { - data: pods, - isLoading: podsLoading, - isError: podsError, - } = useQuery({ queryKey: ["pods"], queryFn: () => getPods(), refetchInterval: 30000 }); - - const { - data: tickets, - isLoading: ticketsLoading, - isError: ticketsError, - } = useQuery({ queryKey: ["tickets"], queryFn: () => getTickets(), refetchInterval: 30000 }); - - const criticalCount = - tickets?.filter((t) => t.severity === "CRITICAL").length ?? 0; - const openCount = tickets?.filter((t) => t.status === "OPEN").length ?? 0; - const recentTickets = tickets?.slice(0, 5) ?? []; + const { alerts, unreadCount } = useAlertStore(); + + const { data: pods, isLoading: podsLoading, isError: podsError } = useQuery({ + queryKey: ["pods"], + queryFn: () => getPods(), + refetchInterval: 30000, + }); + + const { data: tickets, isLoading: ticketsLoading, isError: ticketsError } = useQuery({ + queryKey: ["tickets"], + queryFn: () => getTickets(), + refetchInterval: 30000, + }); const isLoading = podsLoading || ticketsLoading; const isError = podsError || ticketsError; + const criticalCount = tickets?.filter((t) => t.severity === "CRITICAL").length ?? 0; + const openCount = tickets?.filter((t) => t.status === "OPEN").length ?? 0; + const recentTickets = tickets?.slice(0, 5) ?? []; + + // 심각도 분포 + const severityData = (["CRITICAL", "HIGH", "MEDIUM", "LOW"] as Severity[]) + .map((s) => ({ name: s, value: tickets?.filter((t) => t.severity === s).length ?? 0 })) + .filter((d) => d.value > 0); + + // 이상 유형 분포 + const anomalyMap: Record = {}; + tickets?.forEach((t) => { + anomalyMap[t.anomalyType] = (anomalyMap[t.anomalyType] ?? 0) + 1; + }); + const anomalyData = Object.entries(anomalyMap) + .map(([type, count]) => ({ name: ANOMALY_LABELS[type] ?? type, count })) + .sort((a, b) => b.count - a.count); + + // Pod 상태 + const runningCount = pods?.filter((p) => p.phase === "Running").length ?? 0; + const unhealthyPods = pods?.filter((p) => p.phase !== "Running" || p.restartCount >= 3) ?? []; + return (

대시보드

@@ -96,26 +136,19 @@ export default function DashboardPage() { )} + {/* 상단 통계 카드 */}
{isLoading ? ( - <> - - - - - + <> ) : ( <> - - 전체 Pod 수 - + 전체 Pod 수 -

- {pods?.length ?? 0} -

+

{pods?.length ?? 0}

+

Running {runningCount}개

@@ -123,23 +156,17 @@ export default function DashboardPage() { CRITICAL 티켓 - {criticalCount > 0 && ( - - )} + {criticalCount > 0 && } -

- {criticalCount} -

+

{criticalCount}

- - OPEN 티켓 - + OPEN 티켓

{openCount}

@@ -148,20 +175,151 @@ export default function DashboardPage() { - - 읽지 않은 알람 - + 읽지 않은 알람 -

- {unreadCount} -

+

{unreadCount}

)}
+ {/* 차트 영역 */} +
+ {/* 심각도 분포 */} + + + 심각도 분포 + + + {!tickets || severityData.length === 0 ? ( +

티켓 데이터가 없습니다

+ ) : ( +
+ + + + {severityData.map((entry) => ( + + ))} + + + + +
+ {severityData.map((d) => ( +
+ + {d.name} + {d.value}건 +
+ ))} +
+
+ )} +
+
+ + {/* 이상 유형 분포 */} + + + 이상 유형 분포 + + + {anomalyData.length === 0 ? ( +

티켓 데이터가 없습니다

+ ) : ( + + + + + + + + + )} +
+
+
+ + {/* Pod 상태 + 최근 알람 */} +
+ {/* Pod 상태 요약 */} + + + + Pod 상태 + + {runningCount} + / {pods?.length ?? 0} Running + + + + + {podsLoading ? ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ ))} +
+ ) : unhealthyPods.length === 0 ? ( +

모든 Pod가 정상입니다

+ ) : ( +
    + {unhealthyPods.slice(0, 5).map((pod) => ( +
  • + {pod.podName} +
    + {pod.phase !== "Running" && ( + {pod.phase} + )} + {pod.restartCount >= 3 && ( + 재시작 {pod.restartCount}회 + )} +
    +
  • + ))} + {unhealthyPods.length > 5 && ( +

    외 {unhealthyPods.length - 5}개

    + )} +
+ )} + + + + {/* 최근 알람 */} + + + 최근 알람 + + + {alerts.length === 0 ? ( +

수신된 알람이 없습니다

+ ) : ( +
    + {alerts.slice(0, 6).map((alert, i) => ( +
  • + {alert.podName} + + {ANOMALY_LABELS[alert.anomalyType] ?? alert.anomalyType} + +
  • + ))} +
+ )} +
+
+
+ + {/* 최근 티켓 */} 최근 티켓 @@ -188,10 +346,7 @@ export default function DashboardPage() { {recentTickets.length === 0 ? ( - + 티켓이 없습니다 @@ -202,21 +357,11 @@ export default function DashboardPage() { className="border-gray-800 cursor-pointer hover:bg-gray-800" onClick={() => router.push(`/tickets/${ticket.id}`)} > - - {ticket.ticketNumber} - - - {ticket.podName} - - - {ticket.anomalyType} - - - - - - - + {ticket.ticketNumber} + {ticket.podName} + {ticket.anomalyType} + + {new Date(ticket.createdAt).toLocaleString("ko-KR")}