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(APP_VERSION) ?>

온습도 모니터링 대시보드

설치 점검 월간 보고 보안 증적 로그아웃


30초 자동 새로고침
등록 센서
개
경보 센서
개
오프라인
개
최근 온도
최근 습도
현재 시각

센서 현재 상태 30초 자동 새로고침

임계 경보 > 정상 $status_code = $s['metric_status'] ?? 'normal'; if ($s['is_offline']) { $card_class = 'offline'; $badge_class = 'offline'; $badge_text = $s['is_alerting'] ? ('오프라인 (' . metric_status_label($status_code) . ' 중 끊김)') : '오프라인'; } elseif ($s['is_alerting']) { $card_class = 'alert'; $badge_class = 'alert'; $badge_text = metric_status_label($status_code) . ' 경보!'; } else { $card_class = ''; $badge_class = 'ok'; $badge_text = '정상'; } $temp_chip_class = ''; $hum_chip_class = ''; if ($status_code === 'high_temp') { $temp_chip_class = 'hot'; } elseif ($status_code === 'low_temp') { $temp_chip_class = 'cold'; } elseif ($status_code === 'high_humidity' || $status_code === 'low_humidity') { $hum_chip_class = 'humid'; } ?>

/
온도 습도
측정:
측정값 없음
최종확인: (분 전)

아직 수신된 센서 데이터가 없습니다.

온습도 추이 0): ?> · 최근 개

= 2): ?>
온도(°C) 습도(%)
$p['temp'], $chart_points), fn($v) => $v !== null)); $hums = array_values(array_filter(array_map(fn($p) => $p['hum'], $chart_points), fn($v) => $v !== null)); // 온도 축 범위 (여유 패딩) $t_min = !empty($temps) ? min($temps) : 0; $t_max = !empty($temps) ? max($temps) : 1; if ($t_min === $t_max) { $t_min -= 1; $t_max += 1; } $t_pad = ($t_max - $t_min) * 0.1; $t_min -= $t_pad; $t_max += $t_pad; // 습도 축 범위 $h_min = !empty($hums) ? min($hums) : 0; $h_max = !empty($hums) ? max($hums) : 1; if ($h_min === $h_max) { $h_min -= 1; $h_max += 1; } $h_pad = ($h_max - $h_min) * 0.1; $h_min -= $h_pad; $h_max += $h_pad; $x_at = function (int $i) use ($pad_l, $plot_w, $n): float { if ($n <= 1) { return $pad_l; } return $pad_l + ($plot_w * $i / ($n - 1)); }; $y_temp = function (?float $v) use ($pad_t, $plot_h, $t_min, $t_max): ?float { if ($v === null) { return null; } $r = ($t_max - $t_min) ?: 1; return $pad_t + $plot_h * (1 - (($v - $t_min) / $r)); }; $y_hum = function (?float $v) use ($pad_t, $plot_h, $h_min, $h_max): ?float { if ($v === null) { return null; } $r = ($h_max - $h_min) ?: 1; return $pad_t + $plot_h * (1 - (($v - $h_min) / $r)); }; // 결측(null)은 구간을 끊어 polyline 들을 만든다 $build_segments = function (string $key, callable $y_fn) use ($chart_points, $x_at): array { $segments = []; $current = []; foreach ($chart_points as $i => $p) { $val = $p[$key]; if ($val === null) { if (count($current) >= 2) { $segments[] = $current; } $current = []; continue; } $current[] = sprintf('%.1f,%.1f', $x_at($i), $y_fn((float)$val)); } if (count($current) >= 2) { $segments[] = $current; } return $segments; }; $temp_segments = $build_segments('temp', $y_temp); $hum_segments = $build_segments('hum', $y_hum); $fmt = fn($v) => rtrim(rtrim(number_format($v, 1, '.', ''), '0'), '.'); ?> 1 ? [0, intdiv($n - 1, 2), $n - 1] : [0]; foreach ($label_idx as $li): $lx = $x_at($li); $anchor = $li === 0 ? 'start' : ($li === $n - 1 ? 'end' : 'middle'); $tlabel = substr($chart_points[$li]['t'], 5, 11); // MM-DD HH:MM ?>
추이를 표시할 측정 데이터가 충분하지 않습니다 (2개 이상 필요).

30일 운영 요약

고온 경보 SMS 건
저온 경보 SMS 건
고습 경보 SMS 건
저습 경보 SMS 건
정상 복귀 SMS 건
오프라인 SMS 건
장비 복구 SMS 건
SMS 실패 건
평균 온도
평균 습도
최근 온도
최근 습도

최근 온습도 측정 (30건)

최근 온습도 측정 30건 기준
측정시각센서위치 온도습도상태
아직 온습도 측정 데이터가 없습니다.

센서 이벤트 로그 (30건)

센서 이벤트 로그 30건 기준
'장비시작', 'periodic' => '정기보고', ]; $evt = $row['event_type']; ?>
#시각센서 위치이벤트
아직 센서 이벤트가 없습니다.

SMS 발송 이력 (최근 20건)

SMS 발송 이력 최근 20건
#발송시각센서수신자상태결과
성공 실패
아직 SMS 발송 이력이 없습니다.