SESSION_TIMEOUT) { session_destroy(); header('Location: login.php?expired'); exit; } // 활동 시 로그인 시간 갱신 $_SESSION['login_time'] = time(); // ── Heartbeat: 오프라인 센서 체크 (HEARTBEAT_CHECK_INTERVAL_SEC 쓰로틀) ────── check_offline_sensors_throttled(); $db = get_db(); // ── 온습도 경보 상태 라벨/표시 헬퍼 ───────────────────────── // metric_status: normal|high_temp|low_temp|high_humidity|low_humidity function metric_status_label(string $status): string { $labels = [ 'normal' => '정상', 'high_temp' => '고온', 'low_temp' => '저온', 'high_humidity' => '고습', 'low_humidity' => '저습', ]; return $labels[$status] ?? $status; } // 측정값을 사람이 읽는 짧은 문자열로 (소수점 1자리) function metric_display(?string $value, string $unit): string { if ($value === null || $value === '') { return '-'; } return number_format((float)$value, 1) . $unit; } // ── 센서별 현재 상태 + 최신 측정값 ────────────────────────── $statuses = $db->query("SELECT * FROM sensor_status ORDER BY sensor_id")->fetchAll(); // 센서별 최신 sensor_metric (최대 id 기준) $latest_metrics = []; $metric_rows = $db->query(" SELECT sm.* FROM sensor_metric sm INNER JOIN ( SELECT sensor_id, MAX(id) AS latest_id FROM sensor_metric GROUP BY sensor_id ) latest ON latest.latest_id = sm.id ORDER BY sm.sensor_id ")->fetchAll(); foreach ($metric_rows as $metric_row) { $latest_metrics[(int)$metric_row['sensor_id']] = $metric_row; } // 각 센서에 대해 실시간 오프라인/경보 상태 계산 $now = time(); foreach ($statuses as &$s) { $sid = (int)$s['sensor_id']; $metric = $latest_metrics[$sid] ?? null; $s['latest_metric'] = $metric; $last_seen_ts = strtotime($s['last_seen'] ?? '2000-01-01'); $elapsed = $now - $last_seen_ts; // offline_alerted 플래그가 1이거나, last_seen이 HEARTBEAT_TIMEOUT_SEC 이상 경과했으면 오프라인 $s['is_offline'] = ($s['offline_alerted'] == 1) || ($elapsed >= HEARTBEAT_TIMEOUT_SEC); $s['offline_elapsed_min'] = (int)round($elapsed / 60); $status_code = $metric['metric_status'] ?? 'normal'; $s['metric_status'] = $status_code; $s['is_alerting'] = ($status_code !== 'normal'); } unset($s); // 참조 해제 // ── 온습도 추이 차트용: 한 센서의 최근 100개 측정 ─────────── // 기본 sensor_id=2, 없으면 첫 센서 $chart_sensor_ids = array_map(fn($s) => (int)$s['sensor_id'], $statuses); $chart_sensor_id = in_array(2, $chart_sensor_ids, true) ? 2 : ($chart_sensor_ids[0] ?? 0); $chart_sensor_name = ''; foreach ($statuses as $s) { if ((int)$s['sensor_id'] === $chart_sensor_id) { $chart_sensor_name = $s['sensor_name']; break; } } $chart_points = []; if ($chart_sensor_id > 0) { $chart_stmt = $db->prepare(" SELECT measured_at, temperature_c, humidity_percent FROM sensor_metric WHERE sensor_id = :sid ORDER BY id DESC LIMIT 100 "); $chart_stmt->execute([':sid' => $chart_sensor_id]); $chart_rows = $chart_stmt->fetchAll(); // 오래된 → 최신 순으로 (그래프 좌→우) $chart_rows = array_reverse($chart_rows); foreach ($chart_rows as $cr) { $chart_points[] = [ 't' => $cr['measured_at'], 'temp' => $cr['temperature_c'] !== null ? (float)$cr['temperature_c'] : null, 'hum' => $cr['humidity_percent'] !== null ? (float)$cr['humidity_percent'] : null, ]; } } // ── 최근 측정 이력 (sensor_metric 30건) ───────────────────── $recent_metrics = $db->query(" SELECT sensor_id, sensor_name, device_location, temperature_c, humidity_percent, metric_status, measured_at FROM sensor_metric ORDER BY id DESC LIMIT 30 ")->fetchAll(); // ── 최근 센서 이벤트 로그 (sensor_log: startup/periodic) ──── $logs = $db->query(" SELECT id, sensor_name, device_location, event_type, sensor_time FROM sensor_log ORDER BY id DESC LIMIT 30 ")->fetchAll(); // ── 최근 SMS 발송 이력 ───────────────────────────────────── $sms_logs = $db->query(" SELECT * FROM sms_log ORDER BY id DESC LIMIT 20 ")->fetchAll(); // ── 30일 운영 요약 ───────────────────────────────────────── // 종류별 임계 경보 SMS 건수 (sms_log message prefix 기준) $sms_summary_rows = $db->query(" 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 offline, SUM(message LIKE '[장비 복구]%') AS device_online, SUM(status = 'fail') AS sms_fail FROM sms_log WHERE sent_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) ")->fetch(); $summary = [ 'high_temp' => (int)($sms_summary_rows['high_temp'] ?? 0), 'low_temp' => (int)($sms_summary_rows['low_temp'] ?? 0), 'high_humidity' => (int)($sms_summary_rows['high_humidity'] ?? 0), 'low_humidity' => (int)($sms_summary_rows['low_humidity'] ?? 0), 'recovery' => (int)($sms_summary_rows['recovery'] ?? 0), 'offline' => (int)($sms_summary_rows['offline'] ?? 0), 'device_online' => (int)($sms_summary_rows['device_online'] ?? 0), 'sms_fail' => (int)($sms_summary_rows['sms_fail'] ?? 0), ]; $summary['alert_total'] = $summary['high_temp'] + $summary['low_temp'] + $summary['high_humidity'] + $summary['low_humidity']; // 30일 평균 온습도 + 최근 온습도(차트 센서 기준 최신값) $metric_avg = $db->query(" SELECT AVG(temperature_c) AS avg_temp, AVG(humidity_percent) AS avg_hum FROM sensor_metric WHERE measured_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) ")->fetch(); $avg_temp = $metric_avg['avg_temp'] !== null ? round((float)$metric_avg['avg_temp'], 1) : null; $avg_hum = $metric_avg['avg_hum'] !== null ? round((float)$metric_avg['avg_hum'], 1) : null; $latest_overall = !empty($chart_points) ? end($chart_points) : null; $latest_temp = $latest_overall['temp'] ?? null; $latest_hum = $latest_overall['hum'] ?? null; // ── 운영 자가진단 (ops_checks.php) ───────────────────────── $ops_checks = collect_ops_checks(); $ops_failed = ops_failed_count($ops_checks); $ops_critical_count = count(array_filter( $ops_checks, fn($c) => !$c['ok'] && $c['severity'] === 'critical' )); $ops_warn_count = count(array_filter( $ops_checks, fn($c) => !$c['ok'] && $c['severity'] !== 'critical' )); // ── 상단 시스템 상태 집계 ────────────────────────────────── $offline_count = count(array_filter($statuses, fn($s) => $s['is_offline'])); $alert_count = count(array_filter($statuses, fn($s) => $s['is_alerting'] && !$s['is_offline'])); $normal_count = count($statuses) - $offline_count - $alert_count; if ($alert_count > 0) { $system_state = 'danger'; $system_title = '온습도 경보 발생'; $system_detail = "임계 경보 센서 {$alert_count}개, 오프라인 {$offline_count}개"; } elseif ($offline_count > 0) { $system_state = 'warning'; $system_title = '장비 오프라인 확인 필요'; $system_detail = "오프라인 {$offline_count}개, 중요 진단 {$ops_critical_count}건"; } elseif ($ops_critical_count > 0) { $system_state = 'warning'; $system_title = '운영 확인 필요'; $system_detail = "중요 진단 {$ops_critical_count}건을 확인하세요."; } else { $system_state = 'normal'; $system_title = '정상 감시 중'; $system_detail = '현재 온습도 경보와 중요 운영 경고가 없습니다.'; } ?>
= htmlspecialchars($system_detail) ?>
아직 수신된 센서 데이터가 없습니다.
| 측정시각 | 센서 | 온도 | 습도 | 상태 | |
|---|---|---|---|---|---|
| = htmlspecialchars($row['measured_at']) ?> | = htmlspecialchars($row['sensor_name']) ?> | = htmlspecialchars(metric_display($row['temperature_c'], '°C')) ?> | = htmlspecialchars(metric_display($row['humidity_percent'], '%')) ?> | = htmlspecialchars(metric_status_label($status_code)) ?> | |
| 아직 온습도 측정 데이터가 없습니다. | |||||
| 시각 | 센서 | 이벤트 | ||
|---|---|---|---|---|
| = htmlspecialchars($row['sensor_time']) ?> | = htmlspecialchars($row['sensor_name']) ?> | = htmlspecialchars($event_labels[$evt] ?? $evt) ?> | ||
| 아직 센서 이벤트가 없습니다. | ||||
| 발송시각 | 센서 | 수신자 | 상태 | ||
|---|---|---|---|---|---|
| = htmlspecialchars($row['sent_at']) ?> | = htmlspecialchars($row['sensor_name']) ?> | = htmlspecialchars($row['receivers']) ?> | 성공 실패 | ||
| 아직 SMS 발송 이력이 없습니다. | |||||