From 0cae92ed63996827aa2af98ddcac52280faf0b58 Mon Sep 17 00:00:00 2001 From: Cloud User Date: Tue, 17 Mar 2026 13:06:29 +0900 Subject: [PATCH] feat: move alert panels next to corresponding charts with scroll - Replace bottom AlertSection with per-chart AlertPanel - Each chart row: chart (flex) + alert panel (280px) side by side - Alert panel matches chart height, scrollable when content overflows Co-Authored-By: Claude Opus 4.6 --- src/app/page.tsx | 45 +++++++------ src/components/report/AlertPanel.tsx | 99 ++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 src/components/report/AlertPanel.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index e3bedc6..b424696 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -9,7 +9,7 @@ import NetworkChart from '@/components/charts/NetworkChart'; import NodeTable from '@/components/report/NodeTable'; import NasTable from '@/components/report/NasTable'; import NetworkTable from '@/components/report/NetworkTable'; -import AlertSection from '@/components/report/AlertSection'; +import AlertPanel from '@/components/report/AlertPanel'; import ErrorBoundary from '@/components/ErrorBoundary'; function getDefaultRange() { @@ -146,15 +146,30 @@ export default function Home() { /> - - - - - - - - - +
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + +
{data.metrics.nas && data.metrics.nas.length > 0 && ( @@ -205,16 +220,6 @@ export default function Home() { - -
-

주의 구간

- -
-
); diff --git a/src/components/report/AlertPanel.tsx b/src/components/report/AlertPanel.tsx new file mode 100644 index 0000000..91648db --- /dev/null +++ b/src/components/report/AlertPanel.tsx @@ -0,0 +1,99 @@ +'use client'; + +import type { MetricInstance } from '@/types/metrics'; +import { formatTimestamp, formatPercent } from '@/lib/formatters'; + +interface AlertPanelProps { + instances: MetricInstance[]; + type: 'cpu' | 'memory' | 'disk'; + height?: number; +} + +interface Alert { + level: 'warning' | 'critical'; + instance: string; + message: string; +} + +function generateAlerts(instances: MetricInstance[], type: string): Alert[] { + const alerts: Alert[] = []; + + instances.forEach((inst) => { + if (type === 'cpu') { + if (inst.stats.max > 90) { + alerts.push({ + level: 'critical', + instance: inst.instance, + message: `최대 ${formatPercent(inst.stats.max)} 도달`, + }); + } + inst.peaks.peaks.forEach((peak) => { + alerts.push({ + level: 'warning', + instance: inst.instance, + message: `${formatTimestamp(peak.startTime)} ~ ${formatTimestamp(peak.endTime)} Peak (max: ${formatPercent(peak.maxValue)})`, + }); + }); + } else if (type === 'memory') { + if (inst.stats.max > 85) { + alerts.push({ + level: inst.stats.max > 95 ? 'critical' : 'warning', + instance: inst.instance, + message: `최대 ${formatPercent(inst.stats.max)} 도달`, + }); + } + inst.peaks.peaks.forEach((peak) => { + alerts.push({ + level: 'warning', + instance: inst.instance, + message: `${formatTimestamp(peak.startTime)} ~ ${formatTimestamp(peak.endTime)} Peak (max: ${formatPercent(peak.maxValue)})`, + }); + }); + } else if (type === 'disk') { + if (inst.stats.max > 80) { + alerts.push({ + level: inst.stats.max > 90 ? 'critical' : 'warning', + instance: inst.instance, + message: `사용률 ${formatPercent(inst.stats.max)} (증설 검토 필요)`, + }); + } + } + }); + + return alerts; +} + +export default function AlertPanel({ instances, type, height = 300 }: AlertPanelProps) { + const alerts = generateAlerts(instances, type); + + if (alerts.length === 0) { + return ( +
+

특이사항 없음

+
+ ); + } + + return ( +
+
+

주의 구간 ({alerts.length}건)

+
+
    + {alerts.map((alert, idx) => ( +
  • + +
    + {alert.instance} +

    {alert.message}

    +
    +
  • + ))} +
+
+ ); +}