diff --git a/php/config.php b/php/config.php index 3e886d2..2b52ac1 100644 --- a/php/config.php +++ b/php/config.php @@ -155,7 +155,7 @@ const SMS_TAG_SENSOR_STUCK = '[센서고착]'; // 측정값 고착(live-bu const SMS_TAG_ESCALATION = '[에스컬레이션]'; // 미복귀 경보 2차(야간) 통지 const SMS_TAG_LEAK_ALERT = '[누수감지]'; // 1번 모듈(누수) 누수 발생 const SMS_TAG_LEAK_RECOVERY = '[누수복구]'; // 1번 모듈(누수) 누수 해소(복구) -const SMS_TAG_HIGH_PM = '[미세먼지경보]'; // PMS7003 미세먼지(PM2.5/PM10) 임계 초과 +const SMS_TAG_HIGH_PM = '[화재의심]'; // PMS7003 PM 급상승(연기 추정) — 화재 조기징후. 서버실/항온항습 화재 프록시 // 경보 종류(metric kind) → 태그 매핑(단일 출처). evaluate_metric_thresholds 의 // breach 종류와 1:1 대응한다. @@ -164,7 +164,7 @@ const SMS_METRIC_TAGS = [ 'low_temp' => SMS_TAG_LOW_TEMP, 'high_humidity' => SMS_TAG_HIGH_HUMIDITY, 'low_humidity' => SMS_TAG_LOW_HUMIDITY, - // PMS7003 미세먼지: PM2.5·PM10 두 종류가 같은 태그([미세먼지경보])로 매핑된다. + // PMS7003 화재의심(PM): PM2.5·PM10 두 종류가 같은 태그([화재의심])로 매핑된다. 'high_pm25' => SMS_TAG_HIGH_PM, 'high_pm10' => SMS_TAG_HIGH_PM, ]; @@ -208,11 +208,16 @@ define('METRIC_ALERT_COOLDOWN_SEC', (int)cfg('METRIC_ALERT_COOLDOWN_SEC', 1800)) // ─── 미세먼지(PMS7003) 임계 경보 (PM2.5/PM10 → SMS) ──────────────────────────── // PMS7003 은 누수 노드(sensor_id 1·3)에 온습도(SHT30)와 함께 동일 sensor_id 로 통합되어 // metric_type='pms7003' 로 보고한다. 값은 대기환산 µg/m³ 정수(0..1000). 아래 운영 임계를 -// 초과하면 api/sensor_data.php 가 [미세먼지경보] SMS 를 보낸다(종류별 쿨다운은 온습도와 +// 초과하면 api/sensor_data.php 가 [화재의심] SMS 를 보낸다(종류별 쿨다운은 온습도와 // 동일하게 METRIC_ALERT_COOLDOWN_SEC 재사용). 임계 판정은 Pi 가 아니라 서버에서 하므로 // 폐쇄망에서도 재배포 없이 config.local.php 로 조정할 수 있다. -define('METRIC_PM25_HIGH', (float)cfg('METRIC_PM25_HIGH', 75)); // PM2.5 고농도 임계(µg/m³) -define('METRIC_PM10_HIGH', (float)cfg('METRIC_PM10_HIGH', 150)); // PM10 고농도 임계(µg/m³) +// +// 화재 프록시 기준(서버실/항온항습): 여과·양압 환경이라 평상 PM ≈ 0. PM 급상승 = 외부 +// 유입 아니면 연소 연기 → 화재 조기징후로 본다. 대기질 "나쁨"(75/150)까지 기다리면 연기가 +// 이미 자욱해 늦으므로, 조기감지용으로 낮춘 값을 쓴다. (PMS7003 은 광학 먼지센서지 인증 +// 화재감지기가 아니다 — 보조 조기징후로만 쓰고 인증 연기/열 감지기를 병행할 것.) +define('METRIC_PM25_HIGH', (float)cfg('METRIC_PM25_HIGH', 35)); // PM2.5 화재의심 임계(µg/m³) — 조기감지용 하향 +define('METRIC_PM10_HIGH', (float)cfg('METRIC_PM10_HIGH', 75)); // PM10 화재의심 임계(µg/m³) — 조기감지용 하향 // 복구(정상 회복) 히스테리시스: 임계 안쪽으로 이 여유만큼 들어와야 복구로 간주(경계 채터링 방지) define('METRIC_PM_HYSTERESIS', (float)cfg('METRIC_PM_HYSTERESIS', 10)); // µg/m³ @@ -299,7 +304,11 @@ define('ESCALATE_AFTER_SEC', (int)cfg('ESCALATE_AFTER_SEC', 1800)); // 센서 고착(live-but-stuck) 검출: 최근 STUCK_DETECT_SAMPLES 측정의 표준편차가 // STUCK_DETECT_EPSILON 미만(완전 평탄)이면 소자 고장(같은 값 반복)을 의심해 알린다. // (하트비트는 '수신 끊김'만 본다 — I2C 는 살아있는데 값이 고착된 무성실패를 놓침.) -define('STUCK_DETECT_ENABLED', (bool)cfg('STUCK_DETECT_ENABLED', true)); +// +// 기본 비활성: 대상이 항온항습 문서고라 온습도가 원래 거의 평탄하다. 고착 검출을 켜면 +// 정상 평탄값을 '고착'으로 오탐해 [센서고착] 문자가 반복된다. 변동이 있는 환경에서만 +// config.local.php 로 STUCK_DETECT_ENABLED=true 로 켠다. +define('STUCK_DETECT_ENABLED', (bool)cfg('STUCK_DETECT_ENABLED', false)); // 온습도 보고 주기가 1시간이므로 표본 수가 크면 고착 감지가 너무 늦다. 기본 3(= 3회 연속 // 완전 평탄 ≈ 3시간). 주기를 바꾸면 config.local.php 로 조정. define('STUCK_DETECT_SAMPLES', (int)cfg('STUCK_DETECT_SAMPLES', 3)); // 3 × 1시간 ≈ 3시간 diff --git a/php/dashboard_data.php b/php/dashboard_data.php index 6a131ce..35563ca 100644 --- a/php/dashboard_data.php +++ b/php/dashboard_data.php @@ -90,8 +90,8 @@ function metric_status_label(string $status): string { 'high_humidity' => '고습', 'low_humidity' => '저습', 'condensation_risk' => '결로주의', - 'high_pm25' => '미세먼지(PM2.5)', - 'high_pm10' => '미세먼지(PM10)', + 'high_pm25' => '화재의심(PM2.5)', + 'high_pm10' => '화재의심(PM10)', ]; return $labels[$status] ?? $status; } @@ -429,7 +429,7 @@ function dash_hero_state(array $g): array { $pm_alert = (int)($g['pm_alert_count'] ?? 0); $parts = []; if ($g['leak_active_count'] > 0) { $parts[] = "누수 {$g['leak_active_count']}개"; } - if ($pm_alert > 0) { $parts[] = "미세먼지 {$pm_alert}개"; } + if ($pm_alert > 0) { $parts[] = "화재의심 {$pm_alert}개"; } if ($g['alert_count'] > 0) { $parts[] = "임계 경보 {$g['alert_count']}개"; } if ($g['offline_count'] > 0) { $parts[] = "오프라인 {$g['offline_count']}개"; } if ($g['unrecovered_count'] > 0) { $parts[] = "미복구 사고 {$g['unrecovered_count']}건"; } @@ -445,10 +445,10 @@ function dash_hero_state(array $g): array { } // 미세먼지(누수 다음, 온습도보다 우선) if ($pm_alert > 0 && $g['alert_count'] > 0) { - return ['danger', '미세먼지·온습도 경보 발생', $detail]; + return ['danger', '화재의심·온습도 경보 발생', $detail]; } if ($pm_alert > 0) { - return ['danger', '미세먼지 경보 발생', $detail]; + return ['danger', '화재의심 경보 발생', $detail]; } if ($g['alert_count'] > 0) { return ['danger', '온습도 경보 발생', $detail]; @@ -462,7 +462,7 @@ function dash_hero_state(array $g): array { if ($g['ops_critical_count'] > 0) { return ['warning', '운영 확인 필요', "중요 진단 {$g['ops_critical_count']}건을 확인하세요."]; } - return ['normal', '정상 감시 중', '현재 누수·미세먼지·온습도 경보와 중요 운영 경고가 없습니다.']; + return ['normal', '정상 감시 중', '현재 누수·화재·온습도 경보와 중요 운영 경고가 없습니다.']; } // 히어로 상세 줄 — 경보/누수 중 센서별 "누가·어디서·얼마나(값)·언제부터·SMS 나갔는지". @@ -483,7 +483,7 @@ function dash_hero_alert_lines(PDO $db, array $g): array { WHERE sensor_id = :sid AND message LIKE '" . SMS_TAG_LEAK_ALERT . "%' ORDER BY id DESC LIMIT 1 "); - // 미세먼지 경보 SMS(가장 최근) — [미세먼지경보] 태그. + // 화재의심 경보 SMS(가장 최근) — [화재의심] 태그. $sms_pm_stmt = $db->prepare(" SELECT sent_at, status FROM leak_sms_log WHERE sensor_id = :sid AND message LIKE '" . SMS_TAG_HIGH_PM . "%' @@ -811,7 +811,7 @@ function load_tab_trend(PDO $db, array $g): array { $donut_segs = [ ['label' => '고온', 'val' => (int)($mstatus_map['high_temp'] ?? 0), 'cat' => 'hot'], ['label' => '고습', 'val' => (int)($mstatus_map['high_humidity'] ?? 0), 'cat' => 'humid'], - ['label' => '미세먼지', 'val' => $pm_30d, 'cat' => 'pm'], + ['label' => '화재의심', 'val' => $pm_30d, 'cat' => 'pm'], ['label' => '누수', 'val' => $leak_alert_30d, 'cat' => 'leak'], ['label' => '저온', 'val' => (int)($mstatus_map['low_temp'] ?? 0), 'cat' => 'cold'], ['label' => '저습', 'val' => (int)($mstatus_map['low_humidity'] ?? 0), 'cat' => 'dry'], diff --git a/php/setup_wizard.php b/php/setup_wizard.php index 48ced8b..2b4c15d 100644 --- a/php/setup_wizard.php +++ b/php/setup_wizard.php @@ -33,9 +33,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'send_ exit; } - $message = "[온습도 모니터링 테스트]\n" - . "설치/운영 점검 문자입니다.\n" - . "시각: " . date('Y-m-d H:i:s'); + $message = "[모니터링 테스트]\n" + . "설치/운영 점검 문자\n" + . date('m-d H:i'); $results = []; foreach (SMS_RECIPIENTS as $receiver) { $digits = preg_replace('/\D+/', '', $receiver); diff --git a/php/sms_send.php b/php/sms_send.php index ab0c62f..ab77236 100644 --- a/php/sms_send.php +++ b/php/sms_send.php @@ -252,9 +252,8 @@ function maybe_warn_sms_balance(PDO $db, ?int $remaining): void { } $message = SMS_TAG_BALANCE_LOW . "\n" - . "SMS 잔여 {$remaining}건 (임계 " . SMS_BALANCE_WARN_THRESHOLD . "건 이하)\n" - . "충전하지 않으면 경보 발송이 중단됩니다.\n" - . "시각: " . date('Y-m-d H:i:s'); + . "잔여 {$remaining}건 (임계 " . SMS_BALANCE_WARN_THRESHOLD . "건)\n" + . "충전 필요 · " . date('m-d H:i'); foreach (SMS_RECIPIENTS as $receiver) { $receiverDigits = preg_replace('/\D+/', '', $receiver); @@ -515,7 +514,7 @@ function send_metric_alert_sms(array $sensor_data, string $kind, float $value): $sensor_id = (int)$sensor_data['sensor_id']; // [태그, 지표명, 단위, 임계값, 방향문구] (태그는 config.php 단일 출처 상수) - // 미세먼지(high_pm25/high_pm10)는 PM2.5·PM10 이 같은 태그([미세먼지경보])를 쓰되 지표명· + // 화재의심(high_pm25/high_pm10)은 PM2.5·PM10 이 같은 태그([화재의심])를 쓰되 지표명· // 임계값으로 구분한다. 쿨다운은 태그 기준이므로 두 종류가 같은 쿨다운 창을 공유한다. $labels = [ 'high_temp' => [SMS_TAG_HIGH_TEMP, '온도', '℃', METRIC_TEMP_HIGH_C, '이상'], @@ -585,17 +584,16 @@ function send_metric_alert_sms(array $sensor_data, string $kind, float $value): return false; } - // ── 메시지 구성 ────────────────────────────────────────── - $dt = date('Y-m-d H:i:s'); + // ── 메시지 구성 (단문 90B EUC-KR 이내) ───────────────────── + $dt = date('m-d H:i'); [$loc, $name] = sensor_label((int)$sensor_data['sensor_id'], $sensor_data['device_location'] ?? null, $sensor_data['sensor_name'] ?? null); $valStr = metric_value_str($value); $thrStr = metric_value_str((float)$threshold); + $op = ($dir === '이하') ? '>' : '<'; // 정상 범위 방향(임계 미만/초과) - $message = "{$tag}\n" - . "위치: {$loc}\n" - . "관리주체: {$name}\n" - . "{$metricName}: {$valStr}{$unit} (임계 {$thrStr}{$unit} {$dir})\n" - . "시각: {$dt}"; + $message = "{$tag} {$loc}\n" + . "{$metricName} {$valStr}{$unit} 정상{$op}{$thrStr}\n" + . "{$dt} 즉시확인"; return sms_dispatch_to_recipients($db, $sensor_data, $message, "임계경보 {$kind}"); } finally { @@ -620,15 +618,12 @@ function send_metric_alert_sms(array $sensor_data, string $kind, float $value): function send_metric_recovery_sms(array $sensor_data, float $temp_c, float $rh): bool { $db = get_db(); - $dt = date('Y-m-d H:i:s'); + $dt = date('m-d H:i'); [$loc, $name] = sensor_label((int)$sensor_data['sensor_id'], $sensor_data['device_location'] ?? null, $sensor_data['sensor_name'] ?? null); - $message = SMS_TAG_RECOVERY . "\n" - . "위치: {$loc}\n" - . "관리주체: {$name}\n" - . "온도 " . metric_value_str($temp_c) . "℃ / 습도 " . metric_value_str($rh) . "%\n" - . "시각: {$dt}\n" - . "온습도가 정상 범위로 복귀했습니다."; + $message = SMS_TAG_RECOVERY . " {$loc}\n" + . "온도 " . metric_value_str($temp_c) . "℃ 습도 " . metric_value_str($rh) . "%\n" + . "{$dt} 정상복귀"; return sms_dispatch_to_recipients($db, $sensor_data, $message, "정상복귀"); } @@ -696,14 +691,12 @@ function send_leak_alert_sms(array $sensor_data): bool { } // ── 메시지 구성 ── - $dt = date('Y-m-d H:i:s'); + $dt = date('m-d H:i'); [$loc, $name] = sensor_label((int)$sensor_data['sensor_id'], $sensor_data['device_location'] ?? null, $sensor_data['sensor_name'] ?? null); - $message = SMS_TAG_LEAK_ALERT . "\n" - . "위치: {$loc}\n" - . "관리주체: {$name}\n" - . "누수가 감지되었습니다. 즉시 확인하세요!\n" - . "시각: {$dt}"; + $message = SMS_TAG_LEAK_ALERT . " {$loc}\n" + . "누수 감지! 즉시 확인\n" + . "{$dt}"; return sms_dispatch_to_recipients($db, $sensor_data, $message, "누수감지"); } finally { @@ -748,14 +741,12 @@ function send_leak_recovery_sms(array $sensor_data): bool { return false; // 직전 누수 경보 없음(또는 이미 복구 통지함) → 발송 안 함 } - $dt = date('Y-m-d H:i:s'); + $dt = date('m-d H:i'); [$loc, $name] = sensor_label((int)$sensor_data['sensor_id'], $sensor_data['device_location'] ?? null, $sensor_data['sensor_name'] ?? null); - $message = SMS_TAG_LEAK_RECOVERY . "\n" - . "위치: {$loc}\n" - . "관리주체: {$name}\n" - . "누수가 해소(복구)되었습니다.\n" - . "시각: {$dt}"; + $message = SMS_TAG_LEAK_RECOVERY . " {$loc}\n" + . "누수 해소(복구)\n" + . "{$dt}"; return sms_dispatch_to_recipients($db, $sensor_data, $message, "누수복구"); } @@ -769,16 +760,11 @@ function send_leak_recovery_sms(array $sensor_data): bool { function send_device_offline_sms(array $sensor_data): bool { $db = get_db(); - $dt = date('Y-m-d H:i:s'); + $dt = date('m-d H:i'); [$loc, $name] = sensor_label((int)$sensor_data['sensor_id'], $sensor_data['device_location'] ?? null, $sensor_data['sensor_name'] ?? null); - $last = $sensor_data['last_seen'] ?? '알 수 없음'; - - $message = SMS_TAG_DEVICE_OFFLINE . "\n" - . "위치: {$loc}\n" - . "관리주체: {$name}\n" - . "마지막 통신: {$last}\n" - . "감지 시각: {$dt}\n" - . "장비 상태를 확인하세요!"; + $message = SMS_TAG_DEVICE_OFFLINE . " {$loc}\n" + . "통신 두절 · 장비 확인\n" + . "{$dt}"; return sms_dispatch_to_recipients($db, $sensor_data, $message, "오프라인"); } @@ -792,14 +778,12 @@ function send_device_offline_sms(array $sensor_data): bool { function send_device_online_sms(array $sensor_data): bool { $db = get_db(); - $dt = date('Y-m-d H:i:s'); + $dt = date('m-d H:i'); [$loc, $name] = sensor_label((int)$sensor_data['sensor_id'], $sensor_data['device_location'] ?? null, $sensor_data['sensor_name'] ?? null); - $message = SMS_TAG_DEVICE_ONLINE . "\n" - . "위치: {$loc}\n" - . "관리주체: {$name}\n" - . "복구 시각: {$dt}\n" - . "통신이 정상 복구되었습니다."; + $message = SMS_TAG_DEVICE_ONLINE . " {$loc}\n" + . "통신 정상 복구\n" + . "{$dt}"; return sms_dispatch_to_recipients($db, $sensor_data, $message, "장비복구"); } @@ -813,7 +797,7 @@ function build_health_summary(PDO $db): string { $total = (int)$db->query("SELECT COUNT(*) FROM leak_sensor_status")->fetchColumn(); $lastSeen = $db->query("SELECT TIMESTAMPDIFF(MINUTE, MAX(last_seen), NOW()) FROM leak_sensor_status")->fetchColumn(); - $recvStr = ($lastSeen === null) ? '수신없음' : ((int)$lastSeen . '분전'); + $recvStr = ($lastSeen === null) ? '수신없음' : ((int)$lastSeen . '분'); $alerts = (int)$db->query(" SELECT COUNT(*) FROM leak_sms_log @@ -824,10 +808,10 @@ function build_health_summary(PDO $db): string { OR message LIKE '" . SMS_TAG_CONDENSATION . "%' OR message LIKE '" . SMS_TAG_DEVICE_OFFLINE . "%') ")->fetchColumn(); - $summary = "센서 {$online}/{$total} 정상, 최근수신 {$recvStr}, 24h경보 {$alerts}건"; + $summary = "센서 {$online}/{$total} 최근{$recvStr} 24h경보{$alerts}건"; $bal = latest_sms_balance($db); if ($bal !== null) { - $summary .= ", 잔여 {$bal}건"; + $summary .= " 잔여{$bal}"; } return $summary; } @@ -984,9 +968,8 @@ function maybe_escalate_alerts(PDO $db): void { [$loc, $name] = sensor_label($sid, $s['device_location'] ?? null, $s['sensor_name'] ?? null); $mins = (int)floor((time() - strtotime((string)$row['sent_at'])) / 60); $message = SMS_TAG_ESCALATION . "\n" - . "위치: {$loc}\n관리주체: {$name}\n" - . "{$firstLine} 미복귀 {$mins}분 경과\n" - . "1차 미대응 — 확인 바랍니다.\n시각: " . date('Y-m-d H:i:s'); + . "{$firstLine} 미복귀 {$mins}분\n" + . date('m-d H:i') . " 즉시확인"; $info = ['sensor_id' => $sid, 'sensor_name' => $name, 'device_id' => $s['device_id'], 'device_location' => $loc]; sms_dispatch_to_recipients($db, $info, $message, "에스컬레이션", SMS_RECIPIENTS_TIER2); error_log("[SMS] 에스컬레이션 발송 - 센서{$sid} ({$mins}분 미복귀)"); diff --git a/php/tabs/history.php b/php/tabs/history.php index 1ce7b73..80ad99f 100644 --- a/php/tabs/history.php +++ b/php/tabs/history.php @@ -98,9 +98,9 @@ if (!defined('APP_VERSION')) { exit; } // 직접 접근 차단 -

최근 미세먼지 측정 30건 기준

+

최근 화재감시(PM) 측정 30건 기준

- 최근 미세먼지 측정 30건 기준 + 최근 화재감시(PM) 측정 30건 기준
@@ -125,7 +125,7 @@ if (!defined('APP_VERSION')) { exit; } // 직접 접근 차단 - +
아직 미세먼지 측정 데이터가 없습니다.
아직 화재감시(PM) 측정 데이터가 없습니다.
@@ -248,7 +248,7 @@ if (!defined('APP_VERSION')) { exit; } // 직접 접근 차단 건
- 미세먼지 경보 SMS + 화재의심 SMS 건
diff --git a/php/tabs/live.php b/php/tabs/live.php index 712fd39..96303e7 100644 --- a/php/tabs/live.php +++ b/php/tabs/live.php @@ -39,7 +39,7 @@ ksort($sensor_cards); $is_pm_node = in_array((int)$sid, array_map('intval', PM_SENSOR_IDS), true); $type_labels = []; if ($is_leak_node) { $type_labels[] = ['leak', '누수']; } - if ($is_pm_node) { $type_labels[] = ['pm', '미세먼지']; } + if ($is_pm_node) { $type_labels[] = ['pm', '화재감시']; } if ($is_metric) { $type_labels[] = ['metric', '온습도']; } // ── 미수신 placeholder ── @@ -198,7 +198,7 @@ ksort($sensor_cards);
-
미세먼지 측정값 없음
+
화재감시(PM) 측정값 없음
-

미세먼지 추이 +

화재감시 추이 (PM) · 최근 개

@@ -272,7 +272,7 @@ if (!defined('APP_VERSION')) { exit; } // 직접 접근 차단 ?> + aria-label="화재감시 추이 라인 차트" class="chart-svg">

임계 — PM2.5 · PM10 µg/m³ 초과 시 경보

-
미세먼지 추이를 표시할 측정 데이터가 충분하지 않습니다 (2개 이상 필요).
+
화재감시(PM) 추이를 표시할 측정 데이터가 충분하지 않습니다 (2개 이상 필요).
@@ -340,7 +340,7 @@ if (!defined('APP_VERSION')) { exit; } // 직접 접근 차단

경보 발생 추이 최근 7일 · 유형별

온습도 경보 - 미세먼지 경보 + 화재의심 누수 감지
-

단위 — 온습도·미세먼지: 비정상 측정 건수 · 누수: 감지 이벤트 건수

+

단위 — 온습도·화재의심: 비정상 측정 건수 · 누수: 감지 이벤트 건수

최근 30일 경보 없음
diff --git a/tests/pm_threshold_test.php b/tests/pm_threshold_test.php index 152bf5a..17dca7e 100644 --- a/tests/pm_threshold_test.php +++ b/tests/pm_threshold_test.php @@ -24,7 +24,7 @@ function eq($a, $b, string $msg): void { // ── 상수/함수 존재 ───────────────────────────────────────── check(function_exists('evaluate_metric_thresholds'), 'evaluate_metric_thresholds() should exist.'); check(defined('SMS_TAG_HIGH_PM'), 'SMS_TAG_HIGH_PM should be defined.'); -eq(SMS_TAG_HIGH_PM, '[미세먼지경보]', 'SMS_TAG_HIGH_PM tag value'); +eq(SMS_TAG_HIGH_PM, '[화재의심]', 'SMS_TAG_HIGH_PM tag value'); check(defined('METRIC_PM25_HIGH'), 'METRIC_PM25_HIGH should be defined.'); check(defined('METRIC_PM10_HIGH'), 'METRIC_PM10_HIGH should be defined.'); @@ -73,10 +73,10 @@ $b = evaluate_metric_thresholds((float)METRIC_TEMP_HIGH_C + 5, null, $hi25, $lo1 check(in_array('high_temp', $b, true) && in_array('high_pm25', $b, true), 'high temp + high pm25 -> both breaches present'); -// 8) 기본 임계(75/150)에서의 명시 케이스(오버라이드 없을 때만) — 사양 예시 그대로. -if ($P25 === 75.0) { - check(in_array('high_pm25', evaluate_metric_thresholds(null, null, 80.0, 40.0)['breaches'], true), - 'default: pm25=80 (>75) -> high_pm25, pm10=40 -> no high_pm10'); +// 8) 기본 임계(화재의심 35/75)에서의 명시 케이스(오버라이드 없을 때만) — 사양 예시 그대로. +if ($P25 === 35.0) { + check(in_array('high_pm25', evaluate_metric_thresholds(null, null, 40.0, 20.0)['breaches'], true), + 'default: pm25=40 (>35) -> high_pm25, pm10=20 -> no high_pm10'); } // ── 집계 SELECT 에 high_pm 컬럼 포함 ────────────────────────