modify('first day of next month'); return [$month, $start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s')]; } // SMS 메시지 태그(prefix) → 집계 키 매핑. sms_send.php 가 발송 시 붙이는 태그와 일치. function report_sms_tags(): array { return [ 'high_temp' => '[고온경보]', 'low_temp' => '[저온경보]', 'high_humidity' => '[고습경보]', 'low_humidity' => '[저습경보]', 'recovery' => '[정상복귀]', 'device_offline' => '[장비 오프라인]', 'device_online' => '[장비 복구]', ]; } function round_or_null($value, int $precision = 1): ?float { return $value !== null ? round((float)$value, $precision) : null; } function fmt_num(?float $value, string $unit = ''): string { if ($value === null) { return '-'; } return htmlspecialchars(rtrim(rtrim(number_format($value, 1), '0'), '.')) . $unit; } // CSV 수식 주입(formula injection) 방지: 자유 텍스트 셀이 =,+,-,@,탭,CR 로 시작하면 // 선행 작은따옴표로 무력화한다(Excel/Sheets 가 수식으로 실행하지 않도록). // 숫자 셀(음수 온도 등)은 호출하지 않는다 — 값이 깨지지 않도록. function csv_cell($value): string { $s = (string)$value; if ($s !== '' && in_array($s[0], ['=', '+', '-', '@', "\t", "\r"], true)) { return "'" . $s; } return $s; } $db = get_db(); [$month, $start_at, $end_at] = month_range($_GET['month'] ?? date('Y-m')); $report_generated_at = date('Y-m-d H:i:s'); $report_scope = '온습도 임계 경보, 장비 오프라인, SMS'; // ── 월간 요약: SMS 태그별 건수 + 실패 건수 ─────────────────────────────────── $stmt = $db->prepare(" SELECT SUM(message LIKE '[고온경보]%') AS high_temp, SUM(message LIKE '[저온경보]%') AS low_temp, SUM(message LIKE '[고습경보]%') AS high_humidity, SUM(message LIKE '[저습경보]%') AS low_humidity, SUM(message LIKE '[정상복귀]%') AS recovery, SUM(message LIKE '[장비 오프라인]%') AS device_offline, SUM(message LIKE '[장비 복구]%') AS device_online, SUM(status = 'fail') AS sms_fail FROM sms_log WHERE sent_at >= :start_at AND sent_at < :end_at "); $stmt->execute([':start_at' => $start_at, ':end_at' => $end_at]); $row = $stmt->fetch() ?: []; $summary = [ 'high_temp' => (int)($row['high_temp'] ?? 0), 'low_temp' => (int)($row['low_temp'] ?? 0), 'high_humidity' => (int)($row['high_humidity'] ?? 0), 'low_humidity' => (int)($row['low_humidity'] ?? 0), 'recovery' => (int)($row['recovery'] ?? 0), 'device_offline' => (int)($row['device_offline'] ?? 0), 'device_online' => (int)($row['device_online'] ?? 0), 'sms_fail' => (int)($row['sms_fail'] ?? 0), 'metrics' => 0, 'avg_temp' => null, 'min_temp' => null, 'max_temp' => null, 'avg_humidity' => null, 'min_humidity' => null, 'max_humidity' => null, ]; $summary['threshold_alerts'] = $summary['high_temp'] + $summary['low_temp'] + $summary['high_humidity'] + $summary['low_humidity']; // ── 월간 요약: 측정 건수 + 평균/최저/최고 온도·습도 ─────────────────────────── $stmt = $db->prepare(" SELECT COUNT(*) AS metrics, AVG(temperature_c) AS avg_temp, MIN(temperature_c) AS min_temp, MAX(temperature_c) AS max_temp, AVG(humidity_percent) AS avg_humidity, MIN(humidity_percent) AS min_humidity, MAX(humidity_percent) AS max_humidity FROM sensor_metric WHERE measured_at >= :start_at AND measured_at < :end_at "); $stmt->execute([':start_at' => $start_at, ':end_at' => $end_at]); $row = $stmt->fetch() ?: []; $summary['metrics'] = (int)($row['metrics'] ?? 0); $summary['avg_temp'] = round_or_null($row['avg_temp'] ?? null); $summary['min_temp'] = round_or_null($row['min_temp'] ?? null); $summary['max_temp'] = round_or_null($row['max_temp'] ?? null); $summary['avg_humidity'] = round_or_null($row['avg_humidity'] ?? null); $summary['min_humidity'] = round_or_null($row['min_humidity'] ?? null); $summary['max_humidity'] = round_or_null($row['max_humidity'] ?? null); // ── 센서별 SMS 태그별 건수 + 실패 건수 ─────────────────────────────────────── $stmt = $db->prepare(" SELECT sensor_id, sensor_name, SUM(message LIKE '[고온경보]%') AS high_temp, SUM(message LIKE '[저온경보]%') AS low_temp, SUM(message LIKE '[고습경보]%') AS high_humidity, SUM(message LIKE '[저습경보]%') AS low_humidity, SUM(message LIKE '[정상복귀]%') AS recovery, SUM(message LIKE '[장비 오프라인]%') AS device_offline, SUM(message LIKE '[장비 복구]%') AS device_online, SUM(status = 'fail') AS sms_fail FROM sms_log WHERE sent_at >= :start_at AND sent_at < :end_at GROUP BY sensor_id, sensor_name "); $stmt->execute([':start_at' => $start_at, ':end_at' => $end_at]); $sms_by_sensor = []; foreach ($stmt->fetchAll() as $r) { $sms_by_sensor[(int)$r['sensor_id']] = $r; } // ── 센서별 측정 집계(온습도) ───────────────────────────────────────────────── $stmt = $db->prepare(" SELECT sensor_id, sensor_name, device_location, COUNT(*) AS metrics, AVG(temperature_c) AS avg_temp, MIN(temperature_c) AS min_temp, MAX(temperature_c) AS max_temp, AVG(humidity_percent) AS avg_humidity, MIN(humidity_percent) AS min_humidity, MAX(humidity_percent) AS max_humidity, MAX(measured_at) AS last_measured_at FROM sensor_metric WHERE measured_at >= :start_at AND measured_at < :end_at GROUP BY sensor_id, sensor_name, device_location "); $stmt->execute([':start_at' => $start_at, ':end_at' => $end_at]); $metric_by_sensor = []; foreach ($stmt->fetchAll() as $r) { $metric_by_sensor[(int)$r['sensor_id']] = $r; } // ── 센서별 행 병합(측정 + SMS) ─────────────────────────────────────────────── $sensor_rows = []; foreach (array_unique(array_merge(array_keys($metric_by_sensor), array_keys($sms_by_sensor))) as $sid) { $m = $metric_by_sensor[$sid] ?? []; $s = $sms_by_sensor[$sid] ?? []; $sensor_rows[] = [ 'sensor_id' => $sid, 'sensor_name' => $m['sensor_name'] ?? $s['sensor_name'] ?? ('센서' . $sid), 'device_location' => $m['device_location'] ?? '', 'high_temp' => (int)($s['high_temp'] ?? 0), 'low_temp' => (int)($s['low_temp'] ?? 0), 'high_humidity' => (int)($s['high_humidity'] ?? 0), 'low_humidity' => (int)($s['low_humidity'] ?? 0), 'threshold_alerts' => (int)($s['high_temp'] ?? 0) + (int)($s['low_temp'] ?? 0) + (int)($s['high_humidity'] ?? 0) + (int)($s['low_humidity'] ?? 0), 'recovery' => (int)($s['recovery'] ?? 0), 'device_offline' => (int)($s['device_offline'] ?? 0), 'device_online' => (int)($s['device_online'] ?? 0), 'sms_fail' => (int)($s['sms_fail'] ?? 0), 'metrics' => (int)($m['metrics'] ?? 0), 'avg_temp' => round_or_null($m['avg_temp'] ?? null), 'min_temp' => round_or_null($m['min_temp'] ?? null), 'max_temp' => round_or_null($m['max_temp'] ?? null), 'avg_humidity' => round_or_null($m['avg_humidity'] ?? null), 'min_humidity' => round_or_null($m['min_humidity'] ?? null), 'max_humidity' => round_or_null($m['max_humidity'] ?? null), 'last_measured_at' => $m['last_measured_at'] ?? '', ]; } usort($sensor_rows, function ($a, $b) { return [$b['threshold_alerts'], $a['sensor_id']] <=> [$a['threshold_alerts'], $b['sensor_id']]; }); // ── CSV 다운로드 ───────────────────────────────────────────────────────────── if (($_GET['format'] ?? '') === 'csv') { header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="sht30-report-' . $month . '.csv"'); $out = fopen('php://output', 'w'); fputcsv($out, ['month', $month]); fputcsv($out, ['generated_at', $report_generated_at]); fputcsv($out, ['scope', $report_scope]); fputcsv($out, ['threshold_alerts', $summary['threshold_alerts']]); fputcsv($out, ['high_temp_alerts', $summary['high_temp']]); fputcsv($out, ['low_temp_alerts', $summary['low_temp']]); fputcsv($out, ['high_humidity_alerts', $summary['high_humidity']]); fputcsv($out, ['low_humidity_alerts', $summary['low_humidity']]); fputcsv($out, ['recovery_sms', $summary['recovery']]); fputcsv($out, ['device_offline_sms', $summary['device_offline']]); fputcsv($out, ['device_online_sms', $summary['device_online']]); fputcsv($out, ['sms_fail', $summary['sms_fail']]); fputcsv($out, ['metrics', $summary['metrics']]); fputcsv($out, ['avg_temp_c', $summary['avg_temp'] ?? '']); fputcsv($out, ['min_temp_c', $summary['min_temp'] ?? '']); fputcsv($out, ['max_temp_c', $summary['max_temp'] ?? '']); fputcsv($out, ['avg_humidity_percent', $summary['avg_humidity'] ?? '']); fputcsv($out, ['min_humidity_percent', $summary['min_humidity'] ?? '']); fputcsv($out, ['max_humidity_percent', $summary['max_humidity'] ?? '']); fputcsv($out, []); fputcsv($out, [ 'sensor_id', 'sensor_name', 'location', 'high_temp', 'low_temp', 'high_humidity', 'low_humidity', 'threshold_alerts', 'recovery', 'device_offline', 'device_online', 'sms_fail', 'metrics', 'avg_temp_c', 'min_temp_c', 'max_temp_c', 'avg_humidity_percent', 'min_humidity_percent', 'max_humidity_percent', 'last_measured_at', ]); foreach ($sensor_rows as $s) { fputcsv($out, [ $s['sensor_id'], csv_cell($s['sensor_name']), csv_cell($s['device_location']), $s['high_temp'], $s['low_temp'], $s['high_humidity'], $s['low_humidity'], $s['threshold_alerts'], $s['recovery'], $s['device_offline'], $s['device_online'], $s['sms_fail'], $s['metrics'], $s['avg_temp'] ?? '', $s['min_temp'] ?? '', $s['max_temp'] ?? '', $s['avg_humidity'] ?? '', $s['min_humidity'] ?? '', $s['max_humidity'] ?? '', $s['last_measured_at'], ]); } fclose($out); exit; } ?> 월간 운영 보고서 - <?= htmlspecialchars($month) ?>

월간 운영 보고서

대시보드
CSV 다운로드
보고서 생성

범위:

임계 경보(합계)
고온/저온 경보/
고습/저습 경보/
정상복귀
장비 오프라인/복구/
SMS 실패
측정 건수
평균 온도/습도 /
온도 최저/최고 /
습도 최저/최고 /

센서별 요약

센서위치 임계 경보고온/저온고습/저습 정상복귀오프라인/복구SMS 실패 측정평균 온도/습도온도 최저/최고습도 최저/최고 최근 측정
/ / / / / /
해당 월 측정·경보 내역이 없습니다.