fix: 개요 경보 집계를 측정 기준으로 전환 — 하단 리스트와 정합
상단 개요 위젯이 SMS 발송(leak_sms_log)을 세어 하단 측정 이력·이벤트 로그와 숫자가 어긋나던 문제 수정. 실제 센서 데이터로 소스 일원화: - 도넛/리더보드/게이지 스파크/추이: leak_sensor_metric.metric_status(비정상 측정) + leak_sensor_log leak_alert(누수 이벤트) 기준 - "경보 발생 vs 복구" → "경보 발생 추이(온습도 vs 누수)"로 재정의 (측정 기준엔 복구 측정 개념이 없어 두 위험 유형 발생량 비교로 변경) - 30일 운영 요약은 SMS 기준 유지(라벨에 "SMS" 명시 — 발송 통계는 별개 지표) 검증: php -l 통과, 스텁DB 전체 렌더 exit 0(경고 0)·태그 균형(div 61/61)·존 4개.
This commit is contained in:
parent
e392d6475b
commit
baf5b68fb8
2 changed files with 75 additions and 66 deletions
|
|
@ -378,7 +378,9 @@ tbody tr:hover td { background: var(--surface-2); }
|
||||||
.ov-trend .ax { fill: var(--ink-3); }
|
.ov-trend .ax { fill: var(--ink-3); }
|
||||||
.ov-trend .line-alert { stroke: var(--leak); }
|
.ov-trend .line-alert { stroke: var(--leak); }
|
||||||
.ov-trend .line-recover { stroke: var(--normal); }
|
.ov-trend .line-recover { stroke: var(--normal); }
|
||||||
|
.ov-trend .line-metric { stroke: var(--warn); }
|
||||||
.ov-trend .dot-alert { fill: var(--leak); }
|
.ov-trend .dot-alert { fill: var(--leak); }
|
||||||
|
.ov-trend .dot-metric { fill: var(--warn); }
|
||||||
.mini-legend { display: flex; gap: var(--sp-4); flex-wrap: wrap; font-size: .78rem; color: var(--ink-2); margin-bottom: var(--sp-2); }
|
.mini-legend { display: flex; gap: var(--sp-4); flex-wrap: wrap; font-size: .78rem; color: var(--ink-2); margin-bottom: var(--sp-2); }
|
||||||
.mini-legend span { display: inline-flex; align-items: center; gap: 6px; }
|
.mini-legend span { display: inline-flex; align-items: center; gap: 6px; }
|
||||||
.mini-legend i { width: 15px; height: 3px; border-radius: 2px; }
|
.mini-legend i { width: 15px; height: 3px; border-radius: 2px; }
|
||||||
|
|
|
||||||
|
|
@ -409,81 +409,88 @@ if (FORECAST_ENABLED) {
|
||||||
usort($risk_items_all, fn($a, $b) => ($a['severity'] === 'critical' ? 0 : 1) <=> ($b['severity'] === 'critical' ? 0 : 1));
|
usort($risk_items_all, fn($a, $b) => ($a['severity'] === 'critical' ? 0 : 1) <=> ($b['severity'] === 'critical' ? 0 : 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 개요 벤토 위젯 데이터 (샘플 대시보드 콘텐츠 반영: 게이지·리더보드·이벤트율·도넛) ──
|
// ── 개요 벤토 위젯 데이터 (측정 기준 — 하단 측정 이력·이벤트 로그와 동일 소스로 정합) ──
|
||||||
// 경보성/복구성 SMS 태그 묶음 — 태그 상수에서만 파생(집계 일원화). 상수는 신뢰값이므로
|
// 상단 경보 집계는 SMS 발송(leak_sms_log)이 아니라 실제 센서 데이터 기준으로 센다:
|
||||||
// $recent_alert_fail 쿼리와 동일하게 LIKE 절에 직접 삽입한다(사용자 입력 아님).
|
// · 온습도 경보 = leak_sensor_metric.metric_status (비정상 측정) — 하단 "측정 이력"과 동일 소스
|
||||||
$OV_ALERT_TAGS = [
|
// · 누수 = leak_sensor_log.event_type='leak_alert' (누수 감지 이벤트) — "이벤트 로그"와 동일
|
||||||
SMS_TAG_HIGH_TEMP, SMS_TAG_LOW_TEMP, SMS_TAG_HIGH_HUMIDITY, SMS_TAG_LOW_HUMIDITY,
|
// (SMS 발송 통계는 하단 "30일 운영 요약"에 별도로, 명시적 SMS 라벨로 유지)
|
||||||
SMS_TAG_CONDENSATION, SMS_TAG_DEVICE_OFFLINE, SMS_TAG_LEAK_ALERT, SMS_TAG_ESCALATION,
|
|
||||||
];
|
|
||||||
$OV_RECOVER_TAGS = [SMS_TAG_RECOVERY, SMS_TAG_DEVICE_ONLINE, SMS_TAG_LEAK_RECOVERY];
|
|
||||||
$ov_like = function (array $tags): string {
|
|
||||||
return '(' . implode(' OR ', array_map(
|
|
||||||
fn($t) => "message LIKE '" . $t . "%'", $tags
|
|
||||||
)) . ')';
|
|
||||||
};
|
|
||||||
|
|
||||||
// (1) 가동률 게이지 — 온라인 센서 비율 + 7일 일별 경보건수 스파크라인 + 상태 배지
|
// (1) 가동률 게이지 — 온라인 센서 비율 (last_seen 기준, 경보 소스와 무관)
|
||||||
$sensor_total = count($statuses);
|
$sensor_total = count($statuses);
|
||||||
$online_count = $sensor_total - $offline_count;
|
$online_count = $sensor_total - $offline_count;
|
||||||
$uptime_pct = $sensor_total > 0 ? (int)round($online_count / $sensor_total * 100) : 100;
|
$uptime_pct = $sensor_total > 0 ? (int)round($online_count / $sensor_total * 100) : 100;
|
||||||
|
|
||||||
$spark_map = $db->query("
|
// (2) 7일 일별 온습도 경보 측정 / 누수 감지 — 게이지 스파크라인 + 발생 추이 라인차트 공용
|
||||||
SELECT DATE(sent_at) d, COUNT(*) c
|
$mtrend_map = $db->query("
|
||||||
FROM leak_sms_log
|
SELECT DATE(measured_at) d, COUNT(*) c
|
||||||
WHERE sent_at >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) AND " . $ov_like($OV_ALERT_TAGS) . "
|
FROM leak_sensor_metric
|
||||||
GROUP BY DATE(sent_at)
|
WHERE measured_at >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) AND metric_status <> 'normal'
|
||||||
|
GROUP BY DATE(measured_at)
|
||||||
")->fetchAll(PDO::FETCH_KEY_PAIR);
|
")->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
$spark_alerts = [];
|
$ltrend_map = $db->query("
|
||||||
|
SELECT DATE(sensor_time) d, COUNT(*) c
|
||||||
|
FROM leak_sensor_log
|
||||||
|
WHERE event_type = 'leak_alert' AND sensor_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY)
|
||||||
|
GROUP BY DATE(sensor_time)
|
||||||
|
")->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
$trend_days = []; $trend_metric = []; $trend_leak = []; $spark_alerts = [];
|
||||||
for ($i = 6; $i >= 0; $i--) {
|
for ($i = 6; $i >= 0; $i--) {
|
||||||
$spark_alerts[] = (int)($spark_map[date('Y-m-d', strtotime("-$i day"))] ?? 0);
|
$d = date('Y-m-d', strtotime("-$i day"));
|
||||||
|
$tm = (int)($mtrend_map[$d] ?? 0);
|
||||||
|
$tl = (int)($ltrend_map[$d] ?? 0);
|
||||||
|
$trend_days[] = $d;
|
||||||
|
$trend_metric[] = $tm;
|
||||||
|
$trend_leak[] = $tl;
|
||||||
|
$spark_alerts[] = $tm + $tl;
|
||||||
}
|
}
|
||||||
|
$trend_max = max(1, max(array_merge($trend_metric, $trend_leak)));
|
||||||
$spark_today = end($spark_alerts);
|
$spark_today = end($spark_alerts);
|
||||||
reset($spark_alerts);
|
reset($spark_alerts);
|
||||||
if ($alert_count > 0) { $ov_badge = 'crit'; $ov_badge_txt = '경보'; }
|
if ($alert_count > 0) { $ov_badge = 'crit'; $ov_badge_txt = '경보'; }
|
||||||
elseif ($offline_count > 0 || $spark_today > 0) { $ov_badge = 'warn'; $ov_badge_txt = '주의'; }
|
elseif ($offline_count > 0 || $spark_today > 0) { $ov_badge = 'warn'; $ov_badge_txt = '주의'; }
|
||||||
else { $ov_badge = 'ok'; $ov_badge_txt = '정상'; }
|
else { $ov_badge = 'ok'; $ov_badge_txt = '정상'; }
|
||||||
|
|
||||||
// (2) 경보 최다 센서 리더보드 (30일)
|
// (3) 경보 최다 센서 리더보드 (30일) — 온습도 경보 측정 + 누수 감지, 센서별 합산
|
||||||
$board_rows = $db->query("
|
$board_metric = $db->query("
|
||||||
SELECT sensor_name, COUNT(*) c
|
SELECT sensor_name, COUNT(*) c
|
||||||
FROM leak_sms_log
|
FROM leak_sensor_metric
|
||||||
WHERE sent_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND " . $ov_like($OV_ALERT_TAGS) . "
|
WHERE measured_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND metric_status <> 'normal'
|
||||||
GROUP BY sensor_name
|
GROUP BY sensor_name
|
||||||
ORDER BY c DESC, sensor_name ASC
|
")->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
LIMIT 6
|
$board_leak = $db->query("
|
||||||
")->fetchAll();
|
SELECT sensor_name, COUNT(*) c
|
||||||
$board_max = 0;
|
FROM leak_sensor_log
|
||||||
foreach ($board_rows as $br) { $board_max = max($board_max, (int)$br['c']); }
|
WHERE event_type = 'leak_alert' AND sensor_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||||
|
GROUP BY sensor_name
|
||||||
// (3) 경보 발생 vs 복구 (7일 일별) — 이벤트율 라인차트
|
")->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
$trend_map = [];
|
$board_combined = [];
|
||||||
foreach ($db->query("
|
foreach ([$board_metric, $board_leak] as $src) {
|
||||||
SELECT DATE(sent_at) d,
|
foreach ($src as $name => $c) { $board_combined[$name] = ($board_combined[$name] ?? 0) + (int)$c; }
|
||||||
SUM(" . $ov_like($OV_ALERT_TAGS) . ") alerts,
|
|
||||||
SUM(" . $ov_like($OV_RECOVER_TAGS) . ") recovers
|
|
||||||
FROM leak_sms_log
|
|
||||||
WHERE sent_at >= DATE_SUB(CURDATE(), INTERVAL 6 DAY)
|
|
||||||
GROUP BY DATE(sent_at)
|
|
||||||
")->fetchAll() as $tr) {
|
|
||||||
$trend_map[$tr['d']] = $tr;
|
|
||||||
}
|
}
|
||||||
$trend_days = []; $trend_alerts = []; $trend_recovers = [];
|
arsort($board_combined);
|
||||||
for ($i = 6; $i >= 0; $i--) {
|
$board_rows = [];
|
||||||
$d = date('Y-m-d', strtotime("-$i day"));
|
foreach (array_slice($board_combined, 0, 6, true) as $name => $c) {
|
||||||
$trend_days[] = $d;
|
$board_rows[] = ['sensor_name' => $name, 'c' => $c];
|
||||||
$trend_alerts[] = (int)($trend_map[$d]['alerts'] ?? 0);
|
|
||||||
$trend_recovers[] = (int)($trend_map[$d]['recovers'] ?? 0);
|
|
||||||
}
|
}
|
||||||
$trend_max = max(1, max(array_merge($trend_alerts, $trend_recovers)));
|
$board_max = $board_rows ? max(array_column($board_rows, 'c')) : 0;
|
||||||
|
|
||||||
// (4) 경보 종류 도넛 — $summary(30일) 재사용(추가 쿼리 없음)
|
// (4) 경보 종류 도넛 (30일) — 온습도 경보 상태별 측정 건수 + 누수 감지 건수
|
||||||
|
$mstatus_map = $db->query("
|
||||||
|
SELECT metric_status, COUNT(*) c
|
||||||
|
FROM leak_sensor_metric
|
||||||
|
WHERE measured_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND metric_status <> 'normal'
|
||||||
|
GROUP BY metric_status
|
||||||
|
")->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
$leak_alert_30d = (int)$db->query("
|
||||||
|
SELECT COUNT(*) FROM leak_sensor_log
|
||||||
|
WHERE event_type = 'leak_alert' AND sensor_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||||
|
")->fetchColumn();
|
||||||
$donut_segs = [
|
$donut_segs = [
|
||||||
['label' => '고온', 'val' => (int)$summary['high_temp'], 'color' => '#ff8a5c'],
|
['label' => '고온', 'val' => (int)($mstatus_map['high_temp'] ?? 0), 'color' => '#ff8a5c'],
|
||||||
['label' => '고습', 'val' => (int)$summary['high_humidity'], 'color' => '#ffb23e'],
|
['label' => '고습', 'val' => (int)($mstatus_map['high_humidity'] ?? 0), 'color' => '#ffb23e'],
|
||||||
['label' => '누수', 'val' => (int)$summary['leak_alert'], 'color' => '#ff5470'],
|
['label' => '누수', 'val' => $leak_alert_30d, 'color' => '#ff5470'],
|
||||||
['label' => '저온', 'val' => (int)$summary['low_temp'], 'color' => '#5fd6ee'],
|
['label' => '저온', 'val' => (int)($mstatus_map['low_temp'] ?? 0), 'color' => '#5fd6ee'],
|
||||||
['label' => '저습', 'val' => (int)$summary['low_humidity'], 'color' => '#b0b8ff'],
|
['label' => '저습', 'val' => (int)($mstatus_map['low_humidity'] ?? 0), 'color' => '#b0b8ff'],
|
||||||
];
|
];
|
||||||
usort($donut_segs, fn($a, $b) => $b['val'] <=> $a['val']);
|
usort($donut_segs, fn($a, $b) => $b['val'] <=> $a['val']);
|
||||||
$donut_total = array_sum(array_column($donut_segs, 'val'));
|
$donut_total = array_sum(array_column($donut_segs, 'val'));
|
||||||
|
|
@ -717,7 +724,7 @@ foreach ($chart_points as $ci => $cp) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- (3) 경보 종류 도넛 (30일, $summary 재사용) -->
|
<!-- (3) 경보 종류 도넛 (30일 · 온습도 경보 측정 상태별 + 누수 감지) -->
|
||||||
<div class="ov-tile ov-donut">
|
<div class="ov-tile ov-donut">
|
||||||
<h3>경보 종류 <span class="sub">최근 30일</span></h3>
|
<h3>경보 종류 <span class="sub">최근 30일</span></h3>
|
||||||
<?php if ($donut_total > 0): ?>
|
<?php if ($donut_total > 0): ?>
|
||||||
|
|
@ -771,12 +778,12 @@ foreach ($chart_points as $ci => $cp) {
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- (5) 경보 발생 vs 복구 (7일 일별 이벤트율) -->
|
<!-- (5) 경보 발생 추이 (7일 · 온습도 경보 측정 vs 누수 감지 이벤트) -->
|
||||||
<div class="ov-tile ov-trend">
|
<div class="ov-tile ov-trend">
|
||||||
<h3>경보 발생 vs 복구 <span class="sub">최근 7일 · 일별</span></h3>
|
<h3>경보 발생 추이 <span class="sub">최근 7일 · 유형별</span></h3>
|
||||||
<div class="mini-legend">
|
<div class="mini-legend">
|
||||||
<span><i style="background:var(--leak)"></i>경보 발생</span>
|
<span><i style="background:var(--warn)"></i>온습도 경보</span>
|
||||||
<span><i style="background:var(--normal)"></i>복구</span>
|
<span><i style="background:var(--leak)"></i>누수 감지</span>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
$tw = 640; $th = 200; $tpl = 40; $tpr = 16; $tpt = 16; $tpb = 34;
|
$tw = 640; $th = 200; $tpl = 40; $tpr = 16; $tpt = 16; $tpb = 34;
|
||||||
|
|
@ -789,19 +796,19 @@ foreach ($chart_points as $ci => $cp) {
|
||||||
return implode(' ', $pts);
|
return implode(' ', $pts);
|
||||||
};
|
};
|
||||||
$t_peak_i = 0;
|
$t_peak_i = 0;
|
||||||
foreach ($trend_alerts as $i => $v) { if ($v > $trend_alerts[$t_peak_i]) { $t_peak_i = $i; } }
|
foreach ($trend_metric as $i => $v) { if ($v > $trend_metric[$t_peak_i]) { $t_peak_i = $i; } }
|
||||||
?>
|
?>
|
||||||
<svg class="trend-svg" viewBox="0 0 <?= $tw ?> <?= $th ?>" preserveAspectRatio="xMidYMid meet" role="img" aria-label="경보 발생 vs 복구 7일 추이">
|
<svg class="trend-svg" viewBox="0 0 <?= $tw ?> <?= $th ?>" preserveAspectRatio="xMidYMid meet" role="img" aria-label="온습도 경보 vs 누수 감지 7일 추이">
|
||||||
<rect x="<?= $tpl ?>" y="<?= $tpt ?>" width="<?= $tplot_w ?>" height="<?= $tplot_h ?>" class="plot-bg"></rect>
|
<rect x="<?= $tpl ?>" y="<?= $tpt ?>" width="<?= $tplot_w ?>" height="<?= $tplot_h ?>" class="plot-bg"></rect>
|
||||||
<?php for ($g = 0; $g <= 2; $g++):
|
<?php for ($g = 0; $g <= 2; $g++):
|
||||||
$gy = $tpt + $tplot_h * $g / 2; $gv = $trend_max - $trend_max * $g / 2; ?>
|
$gy = $tpt + $tplot_h * $g / 2; $gv = $trend_max - $trend_max * $g / 2; ?>
|
||||||
<line x1="<?= $tpl ?>" y1="<?= sprintf('%.1f', $gy) ?>" x2="<?= $tpl + $tplot_w ?>" y2="<?= sprintf('%.1f', $gy) ?>" class="grid-line" stroke-dasharray="3,3"></line>
|
<line x1="<?= $tpl ?>" y1="<?= sprintf('%.1f', $gy) ?>" x2="<?= $tpl + $tplot_w ?>" y2="<?= sprintf('%.1f', $gy) ?>" class="grid-line" stroke-dasharray="3,3"></line>
|
||||||
<text x="<?= $tpl - 6 ?>" y="<?= sprintf('%.1f', $gy + 3) ?>" text-anchor="end" font-size="10" class="ax"><?= htmlspecialchars(rtrim(rtrim(number_format($gv, 1, '.', ''), '0'), '.')) ?></text>
|
<text x="<?= $tpl - 6 ?>" y="<?= sprintf('%.1f', $gy + 3) ?>" text-anchor="end" font-size="10" class="ax"><?= htmlspecialchars(rtrim(rtrim(number_format($gv, 1, '.', ''), '0'), '.')) ?></text>
|
||||||
<?php endfor; ?>
|
<?php endfor; ?>
|
||||||
<polyline points="<?= htmlspecialchars($t_line($trend_recovers)) ?>" class="line-recover" fill="none" stroke-width="2" stroke-dasharray="6,3" stroke-linejoin="round" stroke-linecap="round"></polyline>
|
<polyline points="<?= htmlspecialchars($t_line($trend_leak)) ?>" class="line-alert" fill="none" stroke-width="2" stroke-dasharray="6,3" stroke-linejoin="round" stroke-linecap="round"></polyline>
|
||||||
<polyline points="<?= htmlspecialchars($t_line($trend_alerts)) ?>" class="line-alert" fill="none" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"></polyline>
|
<polyline points="<?= htmlspecialchars($t_line($trend_metric)) ?>" class="line-metric" fill="none" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"></polyline>
|
||||||
<?php if ($trend_alerts[$t_peak_i] > 0): ?>
|
<?php if ($trend_metric[$t_peak_i] > 0): ?>
|
||||||
<circle cx="<?= sprintf('%.1f', $t_x($t_peak_i)) ?>" cy="<?= sprintf('%.1f', $t_y($trend_alerts[$t_peak_i])) ?>" r="3.2" class="dot-alert"></circle>
|
<circle cx="<?= sprintf('%.1f', $t_x($t_peak_i)) ?>" cy="<?= sprintf('%.1f', $t_y($trend_metric[$t_peak_i])) ?>" r="3.2" class="dot-metric"></circle>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php
|
<?php
|
||||||
$t_lbl_idx = $tn > 1 ? [0, intdiv($tn - 1, 2), $tn - 1] : [0];
|
$t_lbl_idx = $tn > 1 ? [0, intdiv($tn - 1, 2), $tn - 1] : [0];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue