refactor(db)!: DB 테이블 전체 leak_ 접두사 통일 + 구 마이그레이션 폐기
같은 cafe24 DB의 다른 시스템 테이블과 혼동/충돌을 막기 위해 5개 테이블을 leak_ 네임스페이스로 격리: sensor_log→leak_sensor_log, sensor_status→leak_sensor_status, sensor_metric→leak_sensor_metric, sms_log→leak_sms_log, ingest_nonce→leak_ingest_nonce (leak_incident/leak_photo는 이미 접두사). 컬럼명은 불변(sensor_id, is_leak 등) — 테이블명만 변경. sql/migration_add_leak.sql, migration_drop_leak.sql 삭제: 리네임 후 구버전 무접두 테이블 대상이라 실행 즉시 에러. 신규 설치는 schema_sht30.sql 하나로 완결. BREAKING CHANGE: 기존 무접두 테이블 배포와 호환 안 됨(신규 설치 전용). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821XP36Q6t9eWKTPSEumnC
This commit is contained in:
parent
c8b448c225
commit
9fef7031fc
13 changed files with 70 additions and 282 deletions
|
|
@ -23,7 +23,7 @@ function check_offline_sensors(): int {
|
||||||
$db = get_db();
|
$db = get_db();
|
||||||
|
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
SELECT * FROM sensor_status
|
SELECT * FROM leak_sensor_status
|
||||||
WHERE last_seen < DATE_SUB(NOW(), INTERVAL :timeout SECOND)
|
WHERE last_seen < DATE_SUB(NOW(), INTERVAL :timeout SECOND)
|
||||||
AND offline_alerted = 0
|
AND offline_alerted = 0
|
||||||
");
|
");
|
||||||
|
|
@ -36,7 +36,7 @@ function check_offline_sensors(): int {
|
||||||
// last_seen 조건을 함께 둬, SELECT 와 UPDATE 사이에 장비가 복귀 보고를 하면
|
// last_seen 조건을 함께 둬, SELECT 와 UPDATE 사이에 장비가 복귀 보고를 하면
|
||||||
// (last_seen 갱신) 선점이 실패하여 오탐 오프라인 SMS 가 나가지 않도록 한다.
|
// (last_seen 갱신) 선점이 실패하여 오탐 오프라인 SMS 가 나가지 않도록 한다.
|
||||||
$claim = $db->prepare("
|
$claim = $db->prepare("
|
||||||
UPDATE sensor_status SET offline_alerted = 1
|
UPDATE leak_sensor_status SET offline_alerted = 1
|
||||||
WHERE sensor_id = :sensor_id
|
WHERE sensor_id = :sensor_id
|
||||||
AND offline_alerted = 0
|
AND offline_alerted = 0
|
||||||
AND last_seen < DATE_SUB(NOW(), INTERVAL :timeout SECOND)
|
AND last_seen < DATE_SUB(NOW(), INTERVAL :timeout SECOND)
|
||||||
|
|
@ -66,7 +66,7 @@ function check_offline_sensors(): int {
|
||||||
if (!$sent && SMS_RETRY_OFFLINE) {
|
if (!$sent && SMS_RETRY_OFFLINE) {
|
||||||
// 일회성 오프라인 SMS 가 실패하면 선점(offline_alerted=1)을 되돌려, 다음 주기에
|
// 일회성 오프라인 SMS 가 실패하면 선점(offline_alerted=1)을 되돌려, 다음 주기에
|
||||||
// 재시도되게 한다(critical: 게이트웨이 일시오류로 장비 다운을 영영 못 알리던 갭).
|
// 재시도되게 한다(critical: 게이트웨이 일시오류로 장비 다운을 영영 못 알리던 갭).
|
||||||
$db->prepare("UPDATE sensor_status SET offline_alerted = 0 WHERE sensor_id = :sid")
|
$db->prepare("UPDATE leak_sensor_status SET offline_alerted = 0 WHERE sensor_id = :sid")
|
||||||
->execute([':sid' => $sensor['sensor_id']]);
|
->execute([':sid' => $sensor['sensor_id']]);
|
||||||
error_log("[Heartbeat] 오프라인 SMS 실패 - 센서{$sensor['sensor_id']} 재시도 예약");
|
error_log("[Heartbeat] 오프라인 SMS 실패 - 센서{$sensor['sensor_id']} 재시도 예약");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
//
|
//
|
||||||
// 집계 범위: 온습도 임계 경보(고온/저온/고습/저습), 정상복귀, 장비 오프라인/복구,
|
// 집계 범위: 온습도 임계 경보(고온/저온/고습/저습), 정상복귀, 장비 오프라인/복구,
|
||||||
// SMS 실패, 측정 건수, 평균/최저/최고 온도·습도.
|
// SMS 실패, 측정 건수, 평균/최저/최고 온도·습도.
|
||||||
// 데이터 출처: sms_log(message 태그 prefix 기준), sensor_metric(온습도 측정 이력).
|
// 데이터 출처: leak_sms_log(message 태그 prefix 기준), leak_sensor_metric(온습도 측정 이력).
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
require_once __DIR__ . '/config.php';
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
@ -80,7 +80,7 @@ $report_scope = '온습도 임계 경보, 장비 오프라인, SMS';
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
SELECT
|
SELECT
|
||||||
" . sms_summary_select_sql() . "
|
" . sms_summary_select_sql() . "
|
||||||
FROM sms_log
|
FROM leak_sms_log
|
||||||
WHERE sent_at >= :start_at AND sent_at < :end_at
|
WHERE sent_at >= :start_at AND sent_at < :end_at
|
||||||
");
|
");
|
||||||
$stmt->execute([':start_at' => $start_at, ':end_at' => $end_at]);
|
$stmt->execute([':start_at' => $start_at, ':end_at' => $end_at]);
|
||||||
|
|
@ -116,7 +116,7 @@ $stmt = $db->prepare("
|
||||||
AVG(humidity_percent) AS avg_humidity,
|
AVG(humidity_percent) AS avg_humidity,
|
||||||
MIN(humidity_percent) AS min_humidity,
|
MIN(humidity_percent) AS min_humidity,
|
||||||
MAX(humidity_percent) AS max_humidity
|
MAX(humidity_percent) AS max_humidity
|
||||||
FROM sensor_metric
|
FROM leak_sensor_metric
|
||||||
WHERE measured_at >= :start_at AND measured_at < :end_at
|
WHERE measured_at >= :start_at AND measured_at < :end_at
|
||||||
");
|
");
|
||||||
$stmt->execute([':start_at' => $start_at, ':end_at' => $end_at]);
|
$stmt->execute([':start_at' => $start_at, ':end_at' => $end_at]);
|
||||||
|
|
@ -133,7 +133,7 @@ $summary['max_humidity'] = round_or_null($row['max_humidity'] ?? null);
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
SELECT sensor_id, sensor_name,
|
SELECT sensor_id, sensor_name,
|
||||||
" . sms_summary_select_sql() . "
|
" . sms_summary_select_sql() . "
|
||||||
FROM sms_log
|
FROM leak_sms_log
|
||||||
WHERE sent_at >= :start_at AND sent_at < :end_at
|
WHERE sent_at >= :start_at AND sent_at < :end_at
|
||||||
GROUP BY sensor_id, sensor_name
|
GROUP BY sensor_id, sensor_name
|
||||||
");
|
");
|
||||||
|
|
@ -154,7 +154,7 @@ $stmt = $db->prepare("
|
||||||
MIN(humidity_percent) AS min_humidity,
|
MIN(humidity_percent) AS min_humidity,
|
||||||
MAX(humidity_percent) AS max_humidity,
|
MAX(humidity_percent) AS max_humidity,
|
||||||
MAX(measured_at) AS last_measured_at
|
MAX(measured_at) AS last_measured_at
|
||||||
FROM sensor_metric
|
FROM leak_sensor_metric
|
||||||
WHERE measured_at >= :start_at AND measured_at < :end_at
|
WHERE measured_at >= :start_at AND measured_at < :end_at
|
||||||
GROUP BY sensor_id, sensor_name, device_location
|
GROUP BY sensor_id, sensor_name, device_location
|
||||||
");
|
");
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ function collect_ops_checks(): array {
|
||||||
$db = get_db();
|
$db = get_db();
|
||||||
$checks[] = ops_check('서버', 'DB 연결', true, DB_NAME . ' 연결 성공', '', 'critical', 'setup_wizard.php 또는 security_evidence.php 화면');
|
$checks[] = ops_check('서버', 'DB 연결', true, DB_NAME . ' 연결 성공', '', 'critical', 'setup_wizard.php 또는 security_evidence.php 화면');
|
||||||
|
|
||||||
$required_tables = ['sensor_log', 'sensor_status', 'sensor_metric', 'sms_log'];
|
$required_tables = ['leak_sensor_log', 'leak_sensor_status', 'leak_sensor_metric', 'leak_sms_log'];
|
||||||
foreach ($required_tables as $table) {
|
foreach ($required_tables as $table) {
|
||||||
$exists = ops_table_exists($db, $table);
|
$exists = ops_table_exists($db, $table);
|
||||||
$checks[] = ops_check(
|
$checks[] = ops_check(
|
||||||
|
|
@ -63,30 +63,30 @@ function collect_ops_checks(): array {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 최근 온습도 측정 수신 여부(센서 동작/수집 확인)
|
// 최근 온습도 측정 수신 여부(센서 동작/수집 확인)
|
||||||
if (ops_table_exists($db, 'sensor_metric')) {
|
if (ops_table_exists($db, 'leak_sensor_metric')) {
|
||||||
$recent_metric_stmt = $db->query("
|
$recent_metric_stmt = $db->query("
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM sensor_metric
|
FROM leak_sensor_metric
|
||||||
WHERE measured_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
WHERE measured_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
||||||
");
|
");
|
||||||
$recent_metric = (int)$recent_metric_stmt->fetchColumn();
|
$recent_metric = (int)$recent_metric_stmt->fetchColumn();
|
||||||
$checks[] = ops_check('센서', '최근 1시간 온습도 수신', $recent_metric > 0, "{$recent_metric}건", 'SHT30 기기 전원/네트워크/배선 및 sensor_data.php 수신 확인', 'warn', 'sensor_metric 최근 수신');
|
$checks[] = ops_check('센서', '최근 1시간 온습도 수신', $recent_metric > 0, "{$recent_metric}건", 'SHT30 기기 전원/네트워크/배선 및 sensor_data.php 수신 확인', 'warn', 'leak_sensor_metric 최근 수신');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ops_table_exists($db, 'sms_log')) {
|
if (ops_table_exists($db, 'leak_sms_log')) {
|
||||||
$recent_sms_fail_stmt = $db->query("
|
$recent_sms_fail_stmt = $db->query("
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM sms_log
|
FROM leak_sms_log
|
||||||
WHERE status = 'fail'
|
WHERE status = 'fail'
|
||||||
AND sent_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
AND sent_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||||
");
|
");
|
||||||
$recent_sms_fail = (int)$recent_sms_fail_stmt->fetchColumn();
|
$recent_sms_fail = (int)$recent_sms_fail_stmt->fetchColumn();
|
||||||
$checks[] = ops_check('SMS', '최근 30일 SMS 실패', $recent_sms_fail === 0, "{$recent_sms_fail}건", 'Cafe24 SMS 계정, 잔액, 발신번호, 수신자 확인', 'warn', 'sms_log 최근 실패 건수');
|
$checks[] = ops_check('SMS', '최근 30일 SMS 실패', $recent_sms_fail === 0, "{$recent_sms_fail}건", 'Cafe24 SMS 계정, 잔액, 발신번호, 수신자 확인', 'warn', 'leak_sms_log 최근 실패 건수');
|
||||||
|
|
||||||
// SMS 잔여 건수(최근 성공 발송 기준). 임계 이하이면 충전 필요(경보 채널 보호).
|
// SMS 잔여 건수(최근 성공 발송 기준). 임계 이하이면 충전 필요(경보 채널 보호).
|
||||||
$sms_balance = latest_sms_balance($db);
|
$sms_balance = latest_sms_balance($db);
|
||||||
if ($sms_balance !== null) {
|
if ($sms_balance !== null) {
|
||||||
$checks[] = ops_check('SMS', 'SMS 잔여 건수', $sms_balance > SMS_BALANCE_WARN_THRESHOLD, "{$sms_balance}건 (임계 " . SMS_BALANCE_WARN_THRESHOLD . "건)", 'Cafe24 SMS 충전 — 잔액 소진 시 임계/오프라인 경보 발송이 중단됩니다', 'warn', 'sms_log 최근 발송 잔여');
|
$checks[] = ops_check('SMS', 'SMS 잔여 건수', $sms_balance > SMS_BALANCE_WARN_THRESHOLD, "{$sms_balance}건 (임계 " . SMS_BALANCE_WARN_THRESHOLD . "건)", 'Cafe24 SMS 충전 — 잔액 소진 시 임계/오프라인 경보 발송이 중단됩니다', 'warn', 'leak_sms_log 최근 발송 잔여');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,9 @@ $summary = [
|
||||||
'sensor_log_retention_days' => SENSOR_LOG_RETENTION_DAYS,
|
'sensor_log_retention_days' => SENSOR_LOG_RETENTION_DAYS,
|
||||||
'sensor_metric_retention_days' => SENSOR_METRIC_RETENTION_DAYS,
|
'sensor_metric_retention_days' => SENSOR_METRIC_RETENTION_DAYS,
|
||||||
'admin_audit_retention_days' => ADMIN_AUDIT_RETENTION_DAYS,
|
'admin_audit_retention_days' => ADMIN_AUDIT_RETENTION_DAYS,
|
||||||
'sms_log' => ['matched' => 0, 'rows_deleted' => 0],
|
'leak_sms_log' => ['matched' => 0, 'rows_deleted' => 0],
|
||||||
'sensor_log' => ['matched' => 0, 'rows_deleted' => 0],
|
'leak_sensor_log' => ['matched' => 0, 'rows_deleted' => 0],
|
||||||
'sensor_metric' => ['matched' => 0, 'rows_deleted' => 0],
|
'leak_sensor_metric' => ['matched' => 0, 'rows_deleted' => 0],
|
||||||
'admin_audit' => ['kept_lines' => null, 'removed_lines' => null],
|
'admin_audit' => ['kept_lines' => null, 'removed_lines' => null],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -62,16 +62,16 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
$smsCutoff = retention_cutoff(SMS_LOG_RETENTION_DAYS);
|
$smsCutoff = retention_cutoff(SMS_LOG_RETENTION_DAYS);
|
||||||
$summary['sms_log']['matched'] = retention_count($db, "SELECT COUNT(*) FROM sms_log WHERE sent_at < :cutoff", [':cutoff' => $smsCutoff]);
|
$summary['leak_sms_log']['matched'] = retention_count($db, "SELECT COUNT(*) FROM leak_sms_log WHERE sent_at < :cutoff", [':cutoff' => $smsCutoff]);
|
||||||
$summary['sms_log']['rows_deleted'] = retention_delete($db, "DELETE FROM sms_log WHERE sent_at < :cutoff", [':cutoff' => $smsCutoff], $dryRun);
|
$summary['leak_sms_log']['rows_deleted'] = retention_delete($db, "DELETE FROM leak_sms_log WHERE sent_at < :cutoff", [':cutoff' => $smsCutoff], $dryRun);
|
||||||
|
|
||||||
$sensorCutoff = retention_cutoff(SENSOR_LOG_RETENTION_DAYS);
|
$sensorCutoff = retention_cutoff(SENSOR_LOG_RETENTION_DAYS);
|
||||||
$summary['sensor_log']['matched'] = retention_count($db, "SELECT COUNT(*) FROM sensor_log WHERE created_at < :cutoff", [':cutoff' => $sensorCutoff]);
|
$summary['leak_sensor_log']['matched'] = retention_count($db, "SELECT COUNT(*) FROM leak_sensor_log WHERE created_at < :cutoff", [':cutoff' => $sensorCutoff]);
|
||||||
$summary['sensor_log']['rows_deleted'] = retention_delete($db, "DELETE FROM sensor_log WHERE created_at < :cutoff", [':cutoff' => $sensorCutoff], $dryRun);
|
$summary['leak_sensor_log']['rows_deleted'] = retention_delete($db, "DELETE FROM leak_sensor_log WHERE created_at < :cutoff", [':cutoff' => $sensorCutoff], $dryRun);
|
||||||
|
|
||||||
$metricCutoff = retention_cutoff(SENSOR_METRIC_RETENTION_DAYS);
|
$metricCutoff = retention_cutoff(SENSOR_METRIC_RETENTION_DAYS);
|
||||||
$summary['sensor_metric']['matched'] = retention_count($db, "SELECT COUNT(*) FROM sensor_metric WHERE created_at < :cutoff", [':cutoff' => $metricCutoff]);
|
$summary['leak_sensor_metric']['matched'] = retention_count($db, "SELECT COUNT(*) FROM leak_sensor_metric WHERE created_at < :cutoff", [':cutoff' => $metricCutoff]);
|
||||||
$summary['sensor_metric']['rows_deleted'] = retention_delete($db, "DELETE FROM sensor_metric WHERE created_at < :cutoff", [':cutoff' => $metricCutoff], $dryRun);
|
$summary['leak_sensor_metric']['rows_deleted'] = retention_delete($db, "DELETE FROM leak_sensor_metric WHERE created_at < :cutoff", [':cutoff' => $metricCutoff], $dryRun);
|
||||||
|
|
||||||
$auditPath = admin_audit_log_path();
|
$auditPath = admin_audit_log_path();
|
||||||
if (is_file($auditPath)) {
|
if (is_file($auditPath)) {
|
||||||
|
|
@ -113,9 +113,9 @@ if (is_file($auditPath)) {
|
||||||
|
|
||||||
admin_audit_log('retention_cleanup', 'system', true, [
|
admin_audit_log('retention_cleanup', 'system', true, [
|
||||||
'mode' => $summary['mode'],
|
'mode' => $summary['mode'],
|
||||||
'sms_matches' => $summary['sms_log']['matched'],
|
'sms_matches' => $summary['leak_sms_log']['matched'],
|
||||||
'sensor_matches' => $summary['sensor_log']['matched'],
|
'sensor_matches' => $summary['leak_sensor_log']['matched'],
|
||||||
'metric_matches' => $summary['sensor_metric']['matched'],
|
'metric_matches' => $summary['leak_sensor_metric']['matched'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,8 @@ $control_rows = [
|
||||||
['비밀값', 'config.local.php와 /etc/sht30-sensor.env로 분리', 'config.php', '설정 파일 권한 화면'],
|
['비밀값', 'config.local.php와 /etc/sht30-sensor.env로 분리', 'config.php', '설정 파일 권한 화면'],
|
||||||
['관리자 인증', '비밀번호 해시, CSRF, 세션 보호, 실패 제한, 관리자 MFA', 'login.php, admin_security.php', '로그인 화면, MFA 등록 확인표'],
|
['관리자 인증', '비밀번호 해시, CSRF, 세션 보호, 실패 제한, 관리자 MFA', 'login.php, admin_security.php', '로그인 화면, MFA 등록 확인표'],
|
||||||
['감사로그', '로그인 성공/실패, 로그아웃, 관리자 활동 접속기록 저장', 'admin_security.php, php/var/admin_audit.log', '감사로그 파일'],
|
['감사로그', '로그인 성공/실패, 로그아웃, 관리자 활동 접속기록 저장', 'admin_security.php, php/var/admin_audit.log', '감사로그 파일'],
|
||||||
['로그관리', '센서 측정 이력(sensor_metric)·SMS 발송 이력(sms_log)·관리자 감사로그 저장 및 보관기간 정리', 'MySQL, retention_cleanup.php', '보관기간 정리 dry-run 결과'],
|
['로그관리', '센서 측정 이력(leak_sensor_metric)·SMS 발송 이력(leak_sms_log)·관리자 감사로그 저장 및 보관기간 정리', 'MySQL, retention_cleanup.php', '보관기간 정리 dry-run 결과'],
|
||||||
['장애대응', '온습도 임계 경보 SMS + 장비 오프라인/복구 알림', 'cron_heartbeat.php', 'SMS 로그, sensor_metric 이력'],
|
['장애대응', '온습도 임계 경보 SMS + 장비 오프라인/복구 알림', 'cron_heartbeat.php', 'SMS 로그, leak_sensor_metric 이력'],
|
||||||
['백업', 'DB/설정 백업 목록과 복구 테스트 증적 관리', 'scripts/backup_evidence.php', 'backup-evidence 보고서'],
|
['백업', 'DB/설정 백업 목록과 복구 테스트 증적 관리', 'scripts/backup_evidence.php', 'backup-evidence 보고서'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ function send_sms_cafe24(string $receiver, string $message): array {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 완성된 메시지를 모든 수신자(SMS_RECIPIENTS)에게 발송하고 sms_log 에 기록한다.
|
* 완성된 메시지를 모든 수신자(SMS_RECIPIENTS)에게 발송하고 leak_sms_log 에 기록한다.
|
||||||
*
|
*
|
||||||
* @param PDO $db
|
* @param PDO $db
|
||||||
* @param array $sensor_data ['sensor_id','sensor_name','device_id', ...]
|
* @param array $sensor_data ['sensor_id','sensor_name','device_id', ...]
|
||||||
|
|
@ -191,7 +191,7 @@ function sms_dispatch_to_recipients(PDO $db, array $sensor_data, string $message
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
INSERT INTO sms_log
|
INSERT INTO leak_sms_log
|
||||||
(sensor_id, sensor_name, device_id, message, receivers,
|
(sensor_id, sensor_name, device_id, message, receivers,
|
||||||
status, result_code, result_message, sent_at)
|
status, result_code, result_message, sent_at)
|
||||||
VALUES
|
VALUES
|
||||||
|
|
@ -234,7 +234,7 @@ function maybe_warn_sms_balance(PDO $db, ?int $remaining): void {
|
||||||
}
|
}
|
||||||
// 쿨다운: 최근 '[SMS 잔액부족]' 성공/실패 불문 발송이 있으면 생략(반복 폭주 방지).
|
// 쿨다운: 최근 '[SMS 잔액부족]' 성공/실패 불문 발송이 있으면 생략(반복 폭주 방지).
|
||||||
$recent = $db->prepare("
|
$recent = $db->prepare("
|
||||||
SELECT sent_at FROM sms_log
|
SELECT sent_at FROM leak_sms_log
|
||||||
WHERE message LIKE :tag
|
WHERE message LIKE :tag
|
||||||
ORDER BY sent_at DESC
|
ORDER BY sent_at DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
|
|
@ -254,7 +254,7 @@ function maybe_warn_sms_balance(PDO $db, ?int $remaining): void {
|
||||||
$receiverDigits = preg_replace('/\D+/', '', $receiver);
|
$receiverDigits = preg_replace('/\D+/', '', $receiver);
|
||||||
$result = send_sms_cafe24($receiverDigits, $message);
|
$result = send_sms_cafe24($receiverDigits, $message);
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
INSERT INTO sms_log
|
INSERT INTO leak_sms_log
|
||||||
(sensor_id, sensor_name, device_id, message, receivers,
|
(sensor_id, sensor_name, device_id, message, receivers,
|
||||||
status, result_code, result_message, sent_at)
|
status, result_code, result_message, sent_at)
|
||||||
VALUES
|
VALUES
|
||||||
|
|
@ -329,7 +329,7 @@ function condensation_is_clear(?float $temp_c, ?float $rh): bool {
|
||||||
/**
|
/**
|
||||||
* 온습도 측정값을 운영 임계(config.php METRIC_*)와 비교해 경보 종류를 산출한다.
|
* 온습도 측정값을 운영 임계(config.php METRIC_*)와 비교해 경보 종류를 산출한다.
|
||||||
* @return array{breaches: string[], status: string}
|
* @return array{breaches: string[], status: string}
|
||||||
* breaches: 발생한 경보 종류 목록, status: sensor_metric 저장용 대표 상태
|
* breaches: 발생한 경보 종류 목록, status: leak_sensor_metric 저장용 대표 상태
|
||||||
*/
|
*/
|
||||||
function evaluate_metric_thresholds(?float $temp_c, ?float $rh): array {
|
function evaluate_metric_thresholds(?float $temp_c, ?float $rh): array {
|
||||||
$breaches = [];
|
$breaches = [];
|
||||||
|
|
@ -379,7 +379,7 @@ function metric_is_recovered(?float $temp_c, ?float $rh): bool {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 센서가 현재 임계 경보 상태인지 판정한다(최근 metric SMS가 경보이고 그 뒤 복귀가
|
* 센서가 현재 임계 경보 상태인지 판정한다(최근 metric SMS가 경보이고 그 뒤 복귀가
|
||||||
* 없었는지). 복귀 SMS 를 한 번만 보내기 위한 상태 추적(sms_log 기준).
|
* 없었는지). 복귀 SMS 를 한 번만 보내기 위한 상태 추적(leak_sms_log 기준).
|
||||||
*/
|
*/
|
||||||
function metric_was_alerting(PDO $db, int $sensor_id): bool {
|
function metric_was_alerting(PDO $db, int $sensor_id): bool {
|
||||||
// 태그는 config.php 상수(단일 출처)에서 가져와 바인딩한다. 문자열 하드코딩 금지.
|
// 태그는 config.php 상수(단일 출처)에서 가져와 바인딩한다. 문자열 하드코딩 금지.
|
||||||
|
|
@ -397,7 +397,7 @@ function metric_was_alerting(PDO $db, int $sensor_id): bool {
|
||||||
$likes[] = "message LIKE {$key}";
|
$likes[] = "message LIKE {$key}";
|
||||||
$params[$key] = $tag . '%';
|
$params[$key] = $tag . '%';
|
||||||
}
|
}
|
||||||
$sql = "SELECT message FROM sms_log
|
$sql = "SELECT message FROM leak_sms_log
|
||||||
WHERE sensor_id = :sid
|
WHERE sensor_id = :sid
|
||||||
AND status = 'success'
|
AND status = 'success'
|
||||||
AND (" . implode(' OR ', $likes) . ")
|
AND (" . implode(' OR ', $likes) . ")
|
||||||
|
|
@ -442,7 +442,7 @@ function send_metric_alert_sms(array $sensor_data, string $kind, float $value):
|
||||||
[$tag, $metricName, $unit, $threshold, $dir] = $labels[$kind];
|
[$tag, $metricName, $unit, $threshold, $dir] = $labels[$kind];
|
||||||
|
|
||||||
// ── 동시 발송 직렬화 (중복 SMS 방지) ────────────────────────
|
// ── 동시 발송 직렬화 (중복 SMS 방지) ────────────────────────
|
||||||
// 쿨다운 검사→발송→sms_log 기록 사이에 다른 요청(서로 다른 측정의 동시 수신 등)이
|
// 쿨다운 검사→발송→leak_sms_log 기록 사이에 다른 요청(서로 다른 측정의 동시 수신 등)이
|
||||||
// 끼어들면 둘 다 쿨다운을 통과해 같은 경보를 중복 발송할 수 있다. (센서,종류) 단위
|
// 끼어들면 둘 다 쿨다운을 통과해 같은 경보를 중복 발송할 수 있다. (센서,종류) 단위
|
||||||
// MySQL 네임드 락으로 직렬화한다. 락이 잡혀 있으면(busy) 다른 프로세스가 같은 경보를
|
// MySQL 네임드 락으로 직렬화한다. 락이 잡혀 있으면(busy) 다른 프로세스가 같은 경보를
|
||||||
// 처리 중이므로 생략한다. GET_LOCK 미지원/오류(NULL)면 종전 동작(락 없이 진행)으로
|
// 처리 중이므로 생략한다. GET_LOCK 미지원/오류(NULL)면 종전 동작(락 없이 진행)으로
|
||||||
|
|
@ -466,9 +466,9 @@ function send_metric_alert_sms(array $sensor_data, string $kind, float $value):
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ── 종류별 쿨다운 (sms_log 메시지 prefix 기준) ──────────────
|
// ── 종류별 쿨다운 (leak_sms_log 메시지 prefix 기준) ──────────────
|
||||||
$recent = $db->prepare("
|
$recent = $db->prepare("
|
||||||
SELECT sent_at FROM sms_log
|
SELECT sent_at FROM leak_sms_log
|
||||||
WHERE sensor_id = :sensor_id
|
WHERE sensor_id = :sensor_id
|
||||||
AND status = 'success'
|
AND status = 'success'
|
||||||
AND message LIKE :tag
|
AND message LIKE :tag
|
||||||
|
|
@ -567,9 +567,9 @@ function send_leak_alert_sms(array $sensor_data): bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ── 쿨다운 (sms_log 메시지 prefix 기준) ──
|
// ── 쿨다운 (leak_sms_log 메시지 prefix 기준) ──
|
||||||
$recent = $db->prepare("
|
$recent = $db->prepare("
|
||||||
SELECT sent_at FROM sms_log
|
SELECT sent_at FROM leak_sms_log
|
||||||
WHERE sensor_id = :sensor_id
|
WHERE sensor_id = :sensor_id
|
||||||
AND status = 'success'
|
AND status = 'success'
|
||||||
AND message LIKE :tag
|
AND message LIKE :tag
|
||||||
|
|
@ -621,7 +621,7 @@ function send_leak_recovery_sms(array $sensor_data): bool {
|
||||||
// 직전 누수 상태 SMS([누수감지]/[누수복구]) 중 최신 1건을 본다. 최신이 [누수감지]일
|
// 직전 누수 상태 SMS([누수감지]/[누수복구]) 중 최신 1건을 본다. 최신이 [누수감지]일
|
||||||
// 때만(=아직 복구 통지 전) 복구 SMS 를 보낸다.
|
// 때만(=아직 복구 통지 전) 복구 SMS 를 보낸다.
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
SELECT message FROM sms_log
|
SELECT message FROM leak_sms_log
|
||||||
WHERE sensor_id = :sid
|
WHERE sensor_id = :sid
|
||||||
AND status = 'success'
|
AND status = 'success'
|
||||||
AND (message LIKE :alert OR message LIKE :recovery)
|
AND (message LIKE :alert OR message LIKE :recovery)
|
||||||
|
|
@ -702,14 +702,14 @@ function send_device_online_sms(array $sensor_data): bool {
|
||||||
* 예: "센서 2/2 정상, 최근수신 3분전, 24h경보 0건, 잔여 95건"
|
* 예: "센서 2/2 정상, 최근수신 3분전, 24h경보 0건, 잔여 95건"
|
||||||
*/
|
*/
|
||||||
function build_health_summary(PDO $db): string {
|
function build_health_summary(PDO $db): string {
|
||||||
$online = (int)$db->query("SELECT COUNT(*) FROM sensor_status WHERE offline_alerted = 0")->fetchColumn();
|
$online = (int)$db->query("SELECT COUNT(*) FROM leak_sensor_status WHERE offline_alerted = 0")->fetchColumn();
|
||||||
$total = (int)$db->query("SELECT COUNT(*) FROM sensor_status")->fetchColumn();
|
$total = (int)$db->query("SELECT COUNT(*) FROM leak_sensor_status")->fetchColumn();
|
||||||
|
|
||||||
$lastSeen = $db->query("SELECT TIMESTAMPDIFF(MINUTE, MAX(last_seen), NOW()) FROM 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("
|
$alerts = (int)$db->query("
|
||||||
SELECT COUNT(*) FROM sms_log
|
SELECT COUNT(*) FROM leak_sms_log
|
||||||
WHERE sent_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
WHERE sent_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||||
AND status = 'success'
|
AND status = 'success'
|
||||||
AND (message LIKE '" . SMS_TAG_HIGH_TEMP . "%' OR message LIKE '" . SMS_TAG_LOW_TEMP . "%'
|
AND (message LIKE '" . SMS_TAG_HIGH_TEMP . "%' OR message LIKE '" . SMS_TAG_LOW_TEMP . "%'
|
||||||
|
|
@ -735,7 +735,7 @@ function maybe_send_daily_health_sms(PDO $db): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$today = $db->prepare("
|
$today = $db->prepare("
|
||||||
SELECT 1 FROM sms_log
|
SELECT 1 FROM leak_sms_log
|
||||||
WHERE message LIKE :tag AND DATE(sent_at) = CURDATE()
|
WHERE message LIKE :tag AND DATE(sent_at) = CURDATE()
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
");
|
");
|
||||||
|
|
@ -785,7 +785,7 @@ function maybe_send_daily_health_sms(PDO $db): void {
|
||||||
*/
|
*/
|
||||||
function recent_tag_within(PDO $db, int $sensor_id, string $tag, int $seconds): bool {
|
function recent_tag_within(PDO $db, int $sensor_id, string $tag, int $seconds): bool {
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
SELECT sent_at FROM sms_log
|
SELECT sent_at FROM leak_sms_log
|
||||||
WHERE sensor_id = :sid AND message LIKE :tag
|
WHERE sensor_id = :sid AND message LIKE :tag
|
||||||
ORDER BY sent_at DESC LIMIT 1
|
ORDER BY sent_at DESC LIMIT 1
|
||||||
");
|
");
|
||||||
|
|
@ -828,7 +828,7 @@ function latest_alert_state_row(PDO $db, int $sensor_id): ?array {
|
||||||
}
|
}
|
||||||
// id(AUTO_INCREMENT) 를 2차 정렬키로 둬, 같은 초에 경보·복귀가 기록돼도 최신 행이
|
// id(AUTO_INCREMENT) 를 2차 정렬키로 둬, 같은 초에 경보·복귀가 기록돼도 최신 행이
|
||||||
// 결정적으로 선택되게 한다(sent_at 은 1초 해상도).
|
// 결정적으로 선택되게 한다(sent_at 은 1초 해상도).
|
||||||
$sql = "SELECT message, sent_at FROM sms_log
|
$sql = "SELECT message, sent_at FROM leak_sms_log
|
||||||
WHERE sensor_id = :sid AND status = 'success'
|
WHERE sensor_id = :sid AND status = 'success'
|
||||||
AND (" . implode(' OR ', $likes) . ")
|
AND (" . implode(' OR ', $likes) . ")
|
||||||
ORDER BY sent_at DESC, id DESC LIMIT 1";
|
ORDER BY sent_at DESC, id DESC LIMIT 1";
|
||||||
|
|
@ -848,7 +848,7 @@ function maybe_escalate_alerts(PDO $db): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$alertTags = [SMS_TAG_HIGH_TEMP, SMS_TAG_LOW_TEMP, SMS_TAG_HIGH_HUMIDITY, SMS_TAG_LOW_HUMIDITY, SMS_TAG_CONDENSATION];
|
$alertTags = [SMS_TAG_HIGH_TEMP, SMS_TAG_LOW_TEMP, SMS_TAG_HIGH_HUMIDITY, SMS_TAG_LOW_HUMIDITY, SMS_TAG_CONDENSATION];
|
||||||
$sensors = $db->query("SELECT sensor_id, sensor_name, device_id, device_location FROM sensor_status")->fetchAll();
|
$sensors = $db->query("SELECT sensor_id, sensor_name, device_id, device_location FROM leak_sensor_status")->fetchAll();
|
||||||
foreach ($sensors as $s) {
|
foreach ($sensors as $s) {
|
||||||
$sid = (int)$s['sensor_id'];
|
$sid = (int)$s['sensor_id'];
|
||||||
$row = latest_alert_state_row($db, $sid);
|
$row = latest_alert_state_row($db, $sid);
|
||||||
|
|
@ -897,12 +897,12 @@ function maybe_detect_stuck_sensors(PDO $db): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$n = (int)STUCK_DETECT_SAMPLES; // 정수 상수 — LIMIT 에 안전하게 인라인
|
$n = (int)STUCK_DETECT_SAMPLES; // 정수 상수 — LIMIT 에 안전하게 인라인
|
||||||
$sensors = $db->query("SELECT sensor_id, sensor_name, device_id, device_location FROM sensor_status")->fetchAll();
|
$sensors = $db->query("SELECT sensor_id, sensor_name, device_id, device_location FROM leak_sensor_status")->fetchAll();
|
||||||
foreach ($sensors as $s) {
|
foreach ($sensors as $s) {
|
||||||
$sid = (int)$s['sensor_id'];
|
$sid = (int)$s['sensor_id'];
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
SELECT temperature_c, humidity_percent
|
SELECT temperature_c, humidity_percent
|
||||||
FROM sensor_metric
|
FROM leak_sensor_metric
|
||||||
WHERE sensor_id = :sid
|
WHERE sensor_id = :sid
|
||||||
ORDER BY measured_at DESC
|
ORDER BY measured_at DESC
|
||||||
LIMIT {$n}
|
LIMIT {$n}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
# 센서 식별 (서버 대시보드 표시용; 비밀 아님)
|
# 센서 식별 (서버 대시보드 표시용; 비밀 아님)
|
||||||
# SENSOR_ID 는 서버 sensor_metric / sensor_status 의 키. 대시보드 기본 차트=2.
|
# SENSOR_ID 는 서버 leak_sensor_metric / leak_sensor_status 의 키. 대시보드 기본 차트=2.
|
||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
SENSOR_ID = 2
|
SENSOR_ID = 2
|
||||||
SENSOR_NAME = "2번 센서 (SHT30)"
|
SENSOR_NAME = "2번 센서 (SHT30)"
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ def post_reading(event_type: str, temp_c: float, rh: float, metric_status: str)
|
||||||
# 측정 루프 (firmware sht30_measure_task 대응)
|
# 측정 루프 (firmware sht30_measure_task 대응)
|
||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
def measure_loop(sensor: SHT30):
|
def measure_loop(sensor: SHT30):
|
||||||
# 최초 성공 측정만 startup, 이후 periodic (서버 sensor_log 1회 startup 기록)
|
# 최초 성공 측정만 startup, 이후 periodic (서버 leak_sensor_log 1회 startup 기록)
|
||||||
first_success = True
|
first_success = True
|
||||||
|
|
||||||
log.info(f"[LOOP] 측정 시작 (간격={REPORT_INTERVAL}s, addr=0x{I2C_ADDR:02X}, bus={I2C_BUS})")
|
log.info(f"[LOOP] 측정 시작 (간격={REPORT_INTERVAL}s, addr=0x{I2C_ADDR:02X}, bus={I2C_BUS})")
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ def save_data_flow():
|
||||||
("1. 온습도 측정", ["SHT30 측정", "온도/습도 5분 주기"]),
|
("1. 온습도 측정", ["SHT30 측정", "온도/습도 5분 주기"]),
|
||||||
("2. 단말 보고", ["측정값 수집", "periodic/startup", "정상 보고"]),
|
("2. 단말 보고", ["측정값 수집", "periodic/startup", "정상 보고"]),
|
||||||
("3. API 전송", ["HTTPS/TLS", "HMAC/서명 검증", "API 키 일치"]),
|
("3. API 전송", ["HTTPS/TLS", "HMAC/서명 검증", "API 키 일치"]),
|
||||||
("4. 서버 판정/저장", ["임계 판정(METRIC_*)", "sensor_metric", "sensor_log", "sms_log"]),
|
("4. 서버 판정/저장", ["임계 판정(METRIC_*)", "leak_sensor_metric", "leak_sensor_log", "leak_sms_log"]),
|
||||||
("5. 알림/대응", ["임계 SMS 발송", "정상복귀 SMS", "대시보드 확인"]),
|
("5. 알림/대응", ["임계 SMS 발송", "정상복귀 SMS", "대시보드 확인"]),
|
||||||
]
|
]
|
||||||
xs = [45, 275, 505, 735, 965]
|
xs = [45, 275, 505, 735, 965]
|
||||||
|
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
-- =============================================================================
|
|
||||||
-- migration_add_leak.sql - SHT30 전용 서버에 누수(leak) 수신을 다시 추가(통합)
|
|
||||||
--
|
|
||||||
-- [목적] 1번 모듈(누수+카메라, sensor_id=1)과 2번 모듈(SHT30 온습도, sensor_id=2)이
|
|
||||||
-- "하나의 cafe24 서버 php/" 로 함께 보고하도록 통합한다. 이 마이그레이션은
|
|
||||||
-- sql/migration_drop_leak.sql 가 제거했던 누수 요소를 "가산(additive)·멱등"으로
|
|
||||||
-- 되돌린다(= migration_drop_leak.sql 의 역방향, 단 데이터 삭제 없이 가산만 수행).
|
|
||||||
--
|
|
||||||
-- [안전성] 운영 중인 SHT30 서버에서 그대로 실행해도 안전하다:
|
|
||||||
-- - sensor_metric(SHT30 측정) / sms_log / sensor_status / ingest_nonce 등 기존 테이블·
|
|
||||||
-- 데이터는 전혀 건드리지 않는다.
|
|
||||||
-- - 모든 변경은 "없을 때만 추가"(IF NOT EXISTS / information_schema 가드)이므로
|
|
||||||
-- 여러 번 재실행해도 오류가 나지 않는다.
|
|
||||||
--
|
|
||||||
-- [되돌리는 항목]
|
|
||||||
-- - sensor_log.event_type ENUM 에 leak_alert / leak_recovery 재추가
|
|
||||||
-- - sensor_log.is_leak 컬럼(없으면 추가)
|
|
||||||
-- - sensor_status.is_leak 컬럼(없으면 추가)
|
|
||||||
-- - leak_incident 테이블(없으면 생성) — 누수 사고(에피소드) 시작/복구 추적
|
|
||||||
-- - leak_photo 테이블(없으면 생성) — 누수 시 촬영 사진 메타데이터
|
|
||||||
--
|
|
||||||
-- 신규(통합) 설치라면 이 파일이 아니라 sql/schema_sht30.sql 하나만 실행하면 된다
|
|
||||||
-- (schema_sht30.sql 이 이미 누수 요소를 포함하도록 통합되어 있다).
|
|
||||||
-- =============================================================================
|
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
|
||||||
|
|
||||||
-- 1) sensor_log.event_type: 누수 enum 값을 다시 허용(멱등 — 동일 enum 재적용은 안전).
|
|
||||||
ALTER TABLE `sensor_log`
|
|
||||||
MODIFY `event_type` ENUM('startup','periodic','leak_alert','leak_recovery')
|
|
||||||
NOT NULL DEFAULT 'periodic';
|
|
||||||
|
|
||||||
-- 2~3) 컬럼/인덱스 추가는 "없을 때만" 수행해 재실행해도 오류가 없게 한다.
|
|
||||||
-- (MySQL/MariaDB 는 ADD COLUMN IF NOT EXISTS 호환이 일정치 않으므로
|
|
||||||
-- information_schema 로 가드하는 멱등 프로시저를 사용한다.)
|
|
||||||
DROP PROCEDURE IF EXISTS _mig_add_leak;
|
|
||||||
DELIMITER //
|
|
||||||
CREATE PROCEDURE _mig_add_leak()
|
|
||||||
BEGIN
|
|
||||||
DECLARE db VARCHAR(64);
|
|
||||||
SELECT DATABASE() INTO db;
|
|
||||||
|
|
||||||
-- sensor_log.is_leak: 누수 여부(0/1) — 없으면 추가
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = db AND TABLE_NAME = 'sensor_log' AND COLUMN_NAME = 'is_leak') THEN
|
|
||||||
ALTER TABLE `sensor_log`
|
|
||||||
ADD COLUMN `is_leak` TINYINT(1) NOT NULL DEFAULT 0
|
|
||||||
COMMENT '누수 여부 (1=누수, 0=정상)' AFTER `event_type`;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- sensor_status.is_leak: 최신 누수 상태(0/1) — 없으면 추가
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = db AND TABLE_NAME = 'sensor_status' AND COLUMN_NAME = 'is_leak') THEN
|
|
||||||
ALTER TABLE `sensor_status`
|
|
||||||
ADD COLUMN `is_leak` TINYINT(1) NOT NULL DEFAULT 0
|
|
||||||
COMMENT '최신 누수 상태 (1=누수, 0=정상)' AFTER `last_event_type`;
|
|
||||||
END IF;
|
|
||||||
END //
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
CALL _mig_add_leak();
|
|
||||||
DROP PROCEDURE IF EXISTS _mig_add_leak;
|
|
||||||
|
|
||||||
-- 4) leak_incident: 누수 사고(에피소드) 추적. leak_alert 시 행을 열고(open), 같은
|
|
||||||
-- 센서의 leak_recovery 시 가장 최근 미복구 행을 닫는다(recovered_at 채움).
|
|
||||||
CREATE TABLE IF NOT EXISTS `leak_incident` (
|
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
||||||
`sensor_id` TINYINT UNSIGNED NOT NULL COMMENT '센서 번호(누수=1)',
|
|
||||||
`device_id` VARCHAR(64) NOT NULL DEFAULT '',
|
|
||||||
`sensor_name` VARCHAR(100) NOT NULL DEFAULT '',
|
|
||||||
`device_location` VARCHAR(100) NOT NULL DEFAULT '',
|
|
||||||
`started_at` DATETIME NOT NULL COMMENT '누수 감지(시작) 시각',
|
|
||||||
`recovered_at` DATETIME NULL DEFAULT NULL COMMENT '누수 복구 시각(미복구면 NULL)',
|
|
||||||
`log_id_alert` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT 'leak_alert sensor_log.id',
|
|
||||||
`log_id_recovery` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT 'leak_recovery sensor_log.id',
|
|
||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
INDEX `idx_sensor_open` (`sensor_id`, `recovered_at`),
|
|
||||||
INDEX `idx_started_at` (`started_at`)
|
|
||||||
) ENGINE=InnoDB
|
|
||||||
DEFAULT CHARSET=utf8mb4
|
|
||||||
COLLATE=utf8mb4_unicode_ci
|
|
||||||
COMMENT='누수 사고(에피소드) 추적 (시작/복구)';
|
|
||||||
|
|
||||||
-- 5) leak_photo: 누수 시 촬영한 사진 메타데이터. 실제 파일은 php/var/photos/ 에 저장되고
|
|
||||||
-- filename 은 서버가 생성한 무작위 파일명(클라이언트 경로 미사용 — 경로주입 방지).
|
|
||||||
CREATE TABLE IF NOT EXISTS `leak_photo` (
|
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
||||||
`log_id` BIGINT UNSIGNED NOT NULL COMMENT 'leak_alert sensor_log.id',
|
|
||||||
`sensor_id` TINYINT UNSIGNED NOT NULL,
|
|
||||||
`device_id` VARCHAR(64) NOT NULL DEFAULT '',
|
|
||||||
`filename` VARCHAR(128) NOT NULL COMMENT '서버 생성 파일명(var/photos/ 하위)',
|
|
||||||
`mime` VARCHAR(40) NOT NULL DEFAULT 'image/jpeg',
|
|
||||||
`bytes` INT UNSIGNED NOT NULL DEFAULT 0,
|
|
||||||
`captured_at` DATETIME NOT NULL COMMENT '촬영(업로드) 시각',
|
|
||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
INDEX `idx_log_id` (`log_id`),
|
|
||||||
INDEX `idx_captured_at` (`captured_at`)
|
|
||||||
) ENGINE=InnoDB
|
|
||||||
DEFAULT CHARSET=utf8mb4
|
|
||||||
COLLATE=utf8mb4_unicode_ci
|
|
||||||
COMMENT='누수 사진 메타데이터 (파일은 var/photos/)';
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
-- =============================================================================
|
|
||||||
-- migration_drop_leak.sql - 기존 누수(v2605) 설치 → SHT30 온습도 전용 전환
|
|
||||||
--
|
|
||||||
-- ⚠️ 비가역(DROP COLUMN/TABLE) 변경입니다. 반드시 DB 전체 백업 후 실행하세요.
|
|
||||||
-- 운영 데이터(과거 누수 이벤트/사진/사고 이력)가 삭제됩니다.
|
|
||||||
--
|
|
||||||
-- 이미 v2605 스키마(sensor_log/sensor_status/sensor_metric/sms_log + leak_*)가
|
|
||||||
-- 적용된 운영 DB를 SHT30 전용으로 정리합니다. 측정 이력(sensor_metric)과 SMS
|
|
||||||
-- 이력(sms_log)은 보존합니다.
|
|
||||||
--
|
|
||||||
-- 신규 설치라면 이 파일이 아니라 sql/schema_sht30.sql 하나만 실행하세요.
|
|
||||||
-- =============================================================================
|
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
|
||||||
|
|
||||||
-- 0) sensor_metric 이 없던 구버전 대비: 없으면 생성(있으면 무시).
|
|
||||||
CREATE TABLE IF NOT EXISTS `sensor_metric` (
|
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
||||||
`device_id` VARCHAR(64) NOT NULL DEFAULT '',
|
|
||||||
`device_location` VARCHAR(100) NOT NULL DEFAULT '',
|
|
||||||
`sensor_id` TINYINT UNSIGNED NOT NULL,
|
|
||||||
`sensor_name` VARCHAR(100) NOT NULL DEFAULT '',
|
|
||||||
`metric_type` VARCHAR(30) NOT NULL DEFAULT 'sht30',
|
|
||||||
`temperature_c` DECIMAL(6,2) NULL,
|
|
||||||
`humidity_percent` DECIMAL(5,2) NULL,
|
|
||||||
`metric_status` VARCHAR(30) NOT NULL DEFAULT 'normal',
|
|
||||||
`measured_at` DATETIME NOT NULL,
|
|
||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
INDEX `idx_sensor_measured` (`sensor_id`, `measured_at`),
|
|
||||||
INDEX `idx_device_sensor` (`device_id`, `sensor_id`),
|
|
||||||
INDEX `idx_metric_type` (`metric_type`, `created_at`),
|
|
||||||
INDEX `idx_status_measured` (`metric_status`, `measured_at`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- 1) 누수 전용 테이블 제거
|
|
||||||
DROP TABLE IF EXISTS `leak_photo`;
|
|
||||||
DROP TABLE IF EXISTS `leak_incident`;
|
|
||||||
|
|
||||||
-- 2) sensor_log: 누수 enum 값 정리 → event_type 재정의 (둘 다 멱등)
|
|
||||||
-- (leak_alert/leak_recovery 행이 있으면 periodic 으로 강등 후 enum 축소.
|
|
||||||
-- 이미 정리된 DB면 매칭 행이 없어 no-op, MODIFY 도 동일 enum 재적용이라 안전.)
|
|
||||||
UPDATE `sensor_log` SET `event_type` = 'periodic'
|
|
||||||
WHERE `event_type` IN ('leak_alert', 'leak_recovery');
|
|
||||||
ALTER TABLE `sensor_log`
|
|
||||||
MODIFY `event_type` ENUM('startup','periodic') NOT NULL DEFAULT 'periodic';
|
|
||||||
|
|
||||||
-- 3~4) 인덱스/컬럼 제거·추가는 "존재할 때만" 수행하여 재실행해도 오류가 없게 한다.
|
|
||||||
-- (MySQL/MariaDB 는 DROP COLUMN/INDEX 에 IF EXISTS 호환이 일정치 않으므로
|
|
||||||
-- information_schema 로 가드하는 멱등 프로시저를 사용한다.)
|
|
||||||
DROP PROCEDURE IF EXISTS _mig_drop_leak;
|
|
||||||
DELIMITER //
|
|
||||||
CREATE PROCEDURE _mig_drop_leak()
|
|
||||||
BEGIN
|
|
||||||
DECLARE db VARCHAR(64);
|
|
||||||
SELECT DATABASE() INTO db;
|
|
||||||
|
|
||||||
-- sensor_log: is_leak 인덱스/컬럼 제거, event_type 인덱스 추가
|
|
||||||
IF EXISTS (SELECT 1 FROM information_schema.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = db AND TABLE_NAME = 'sensor_log' AND INDEX_NAME = 'idx_is_leak') THEN
|
|
||||||
ALTER TABLE `sensor_log` DROP INDEX `idx_is_leak`;
|
|
||||||
END IF;
|
|
||||||
IF EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = db AND TABLE_NAME = 'sensor_log' AND COLUMN_NAME = 'is_leak') THEN
|
|
||||||
ALTER TABLE `sensor_log` DROP COLUMN `is_leak`;
|
|
||||||
END IF;
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = db AND TABLE_NAME = 'sensor_log' AND INDEX_NAME = 'idx_event_type') THEN
|
|
||||||
ALTER TABLE `sensor_log` ADD INDEX `idx_event_type` (`event_type`, `created_at`);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- sensor_status: 누수 컬럼/인덱스 제거 (offline_alerted 는 오프라인 SMS에 필요 → 유지)
|
|
||||||
IF EXISTS (SELECT 1 FROM information_schema.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = db AND TABLE_NAME = 'sensor_status' AND INDEX_NAME = 'idx_is_leak') THEN
|
|
||||||
ALTER TABLE `sensor_status` DROP INDEX `idx_is_leak`;
|
|
||||||
END IF;
|
|
||||||
IF EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = db AND TABLE_NAME = 'sensor_status' AND COLUMN_NAME = 'is_leak') THEN
|
|
||||||
ALTER TABLE `sensor_status` DROP COLUMN `is_leak`;
|
|
||||||
END IF;
|
|
||||||
IF EXISTS (SELECT 1 FROM information_schema.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = db AND TABLE_NAME = 'sensor_status' AND COLUMN_NAME = 'leak_periodic_count') THEN
|
|
||||||
ALTER TABLE `sensor_status` DROP COLUMN `leak_periodic_count`;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- sensor_metric: 임계 경보 조회용 인덱스 추가(없을 때만)
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = db AND TABLE_NAME = 'sensor_metric' AND INDEX_NAME = 'idx_status_measured') THEN
|
|
||||||
ALTER TABLE `sensor_metric` ADD INDEX `idx_status_measured` (`metric_status`, `measured_at`);
|
|
||||||
END IF;
|
|
||||||
END //
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
CALL _mig_drop_leak();
|
|
||||||
DROP PROCEDURE IF EXISTS _mig_drop_leak;
|
|
||||||
|
|
||||||
-- 5) 수집 재전송(replay) 멱등 nonce 테이블 추가(없을 때만 — 가산/멱등).
|
|
||||||
-- api/sensor_data.php 가 서명을 nonce 로 기록해 시계오차 창 내 중복 수신을 멱등
|
|
||||||
-- 처리한다(중복 저장·중복 SMS 방지). 이 테이블이 없어도 수집은 동작한다.
|
|
||||||
CREATE TABLE IF NOT EXISTS `ingest_nonce` (
|
|
||||||
`signature` VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
|
||||||
`seen_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (`signature`),
|
|
||||||
INDEX `idx_seen_at` (`seen_at`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
||||||
COMMENT='수집 재전송 멱등 nonce (시계오차 창 내 중복 차단; 주기적 정리)';
|
|
||||||
|
|
@ -5,24 +5,23 @@
|
||||||
-- 신규 설치: 이 파일 하나만 실행합니다.
|
-- 신규 설치: 이 파일 하나만 실행합니다.
|
||||||
-- SOURCE /path/to/sql/schema_sht30.sql;
|
-- SOURCE /path/to/sql/schema_sht30.sql;
|
||||||
--
|
--
|
||||||
-- 기존(누수 v2605) 설치를 정리·재설치할 때는 운영 데이터를 보존하려면 마이그레이션을
|
-- v2607: 모든 테이블에 leak_ 접두사 적용 — 같은 DB 의 다른(비관련) 테이블과 격리.
|
||||||
-- 사용합니다: 누수를 빼려면 sql/migration_drop_leak.sql, 빠진 서버에 누수를 다시
|
-- (구버전 무접두 테이블 대상 마이그레이션 파일들은 리네임과 함께 폐기 — git 이력 참고.)
|
||||||
-- 더하려면 sql/migration_add_leak.sql(가산·멱등).
|
|
||||||
--
|
--
|
||||||
-- 이 파일은 두 모듈을 하나의 서버에서 받는 "통합" 스키마입니다. 포함 요소:
|
-- 이 파일은 두 모듈을 하나의 서버에서 받는 "통합" 스키마입니다. 포함 요소:
|
||||||
-- - 온습도(SHT30): sensor_metric 테이블 + 서버 임계 판정(config.php METRIC_*).
|
-- - 온습도(SHT30): leak_sensor_metric 테이블 + 서버 임계 판정(config.php METRIC_*).
|
||||||
-- - 누수(leak): sensor_log.is_leak / sensor_status.is_leak 컬럼,
|
-- - 누수(leak): leak_sensor_log.is_leak / leak_sensor_status.is_leak 컬럼,
|
||||||
-- event_type 의 leak_alert / leak_recovery,
|
-- event_type 의 leak_alert / leak_recovery,
|
||||||
-- leak_incident(사고 추적) + leak_photo(사진 메타) 테이블.
|
-- leak_incident(사고 추적) + leak_photo(사진 메타) 테이블.
|
||||||
-- 임계/누수 경보 SMS 발송은 서버 PHP(api/sensor_data.php + sms_send.php)에서 수행하며,
|
-- 임계/누수 경보 SMS 발송은 서버 PHP(api/sensor_data.php + sms_send.php)에서 수행하며,
|
||||||
-- 발송 이력은 sms_log 에 남깁니다(sms_log 기준 쿨다운/복구 판정 — 별도 경보 상태 테이블 없음).
|
-- 발송 이력은 leak_sms_log 에 남깁니다(leak_sms_log 기준 쿨다운/복구 판정 — 별도 경보 상태 테이블 없음).
|
||||||
-- 누수 사진 실제 파일은 php/var/photos/ 에 저장되고, leak_photo 에는 메타만 둡니다.
|
-- 누수 사진 실제 파일은 php/var/photos/ 에 저장되고, leak_photo 에는 메타만 둡니다.
|
||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
SET NAMES utf8mb4;
|
||||||
SET time_zone = '+09:00';
|
SET time_zone = '+09:00';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `sensor_log` (
|
CREATE TABLE IF NOT EXISTS `leak_sensor_log` (
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
`device_id` VARCHAR(64) NOT NULL COMMENT 'STM32 기기 ID',
|
`device_id` VARCHAR(64) NOT NULL COMMENT 'STM32 기기 ID',
|
||||||
`device_location` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '설치 위치',
|
`device_location` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '설치 위치',
|
||||||
|
|
@ -43,7 +42,7 @@ CREATE TABLE IF NOT EXISTS `sensor_log` (
|
||||||
COLLATE=utf8mb4_unicode_ci
|
COLLATE=utf8mb4_unicode_ci
|
||||||
COMMENT='센서 이벤트 전체 로그';
|
COMMENT='센서 이벤트 전체 로그';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `sensor_status` (
|
CREATE TABLE IF NOT EXISTS `leak_sensor_status` (
|
||||||
`sensor_id` TINYINT UNSIGNED NOT NULL,
|
`sensor_id` TINYINT UNSIGNED NOT NULL,
|
||||||
`device_id` VARCHAR(64) NOT NULL,
|
`device_id` VARCHAR(64) NOT NULL,
|
||||||
`sensor_name` VARCHAR(100) NOT NULL DEFAULT '',
|
`sensor_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
|
@ -61,7 +60,7 @@ CREATE TABLE IF NOT EXISTS `sensor_status` (
|
||||||
COLLATE=utf8mb4_unicode_ci
|
COLLATE=utf8mb4_unicode_ci
|
||||||
COMMENT='센서별 최신 상태 (업서트)';
|
COMMENT='센서별 최신 상태 (업서트)';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `sensor_metric` (
|
CREATE TABLE IF NOT EXISTS `leak_sensor_metric` (
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
`device_id` VARCHAR(64) NOT NULL DEFAULT '',
|
`device_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
`device_location` VARCHAR(100) NOT NULL DEFAULT '',
|
`device_location` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
|
@ -85,7 +84,7 @@ CREATE TABLE IF NOT EXISTS `sensor_metric` (
|
||||||
COLLATE=utf8mb4_unicode_ci
|
COLLATE=utf8mb4_unicode_ci
|
||||||
COMMENT='SHT30 온습도 측정 이력 (온도/습도/임계 상태)';
|
COMMENT='SHT30 온습도 측정 이력 (온도/습도/임계 상태)';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `sms_log` (
|
CREATE TABLE IF NOT EXISTS `leak_sms_log` (
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
`sensor_id` TINYINT UNSIGNED NOT NULL,
|
`sensor_id` TINYINT UNSIGNED NOT NULL,
|
||||||
`sensor_name` VARCHAR(100) NOT NULL DEFAULT '',
|
`sensor_name` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
|
@ -110,7 +109,7 @@ CREATE TABLE IF NOT EXISTS `sms_log` (
|
||||||
-- 안에서 같은 서명 본문이 다시 오면(펌웨어 재시도/캡처 재생) 중복 저장·중복 SMS 없이
|
-- 안에서 같은 서명 본문이 다시 오면(펌웨어 재시도/캡처 재생) 중복 저장·중복 SMS 없이
|
||||||
-- 멱등 처리된다. 오래된 행은 서버가 주기적으로 정리한다(이 테이블이 없어도 수집은
|
-- 멱등 처리된다. 오래된 행은 서버가 주기적으로 정리한다(이 테이블이 없어도 수집은
|
||||||
-- 동작하며 멱등 방어만 비활성화된다 — graceful degrade).
|
-- 동작하며 멱등 방어만 비활성화된다 — graceful degrade).
|
||||||
CREATE TABLE IF NOT EXISTS `ingest_nonce` (
|
CREATE TABLE IF NOT EXISTS `leak_ingest_nonce` (
|
||||||
`signature` VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL
|
`signature` VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL
|
||||||
COMMENT '요청 서명(X-Signature) — 멱등 키',
|
COMMENT '요청 서명(X-Signature) — 멱등 키',
|
||||||
`seen_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
`seen_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
|
@ -133,8 +132,8 @@ CREATE TABLE IF NOT EXISTS `leak_incident` (
|
||||||
`device_location` VARCHAR(100) NOT NULL DEFAULT '',
|
`device_location` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
`started_at` DATETIME NOT NULL COMMENT '누수 감지(시작) 시각',
|
`started_at` DATETIME NOT NULL COMMENT '누수 감지(시작) 시각',
|
||||||
`recovered_at` DATETIME NULL DEFAULT NULL COMMENT '누수 복구 시각(미복구면 NULL)',
|
`recovered_at` DATETIME NULL DEFAULT NULL COMMENT '누수 복구 시각(미복구면 NULL)',
|
||||||
`log_id_alert` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT 'leak_alert sensor_log.id',
|
`log_id_alert` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT 'leak_alert leak_sensor_log.id',
|
||||||
`log_id_recovery` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT 'leak_recovery sensor_log.id',
|
`log_id_recovery` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT 'leak_recovery leak_sensor_log.id',
|
||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
|
|
@ -149,7 +148,7 @@ CREATE TABLE IF NOT EXISTS `leak_incident` (
|
||||||
-- (클라이언트 파일명/경로 미사용 — 경로주입 방지). api/photo_upload.php 가 기록한다.
|
-- (클라이언트 파일명/경로 미사용 — 경로주입 방지). api/photo_upload.php 가 기록한다.
|
||||||
CREATE TABLE IF NOT EXISTS `leak_photo` (
|
CREATE TABLE IF NOT EXISTS `leak_photo` (
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
`log_id` BIGINT UNSIGNED NOT NULL COMMENT 'leak_alert sensor_log.id',
|
`log_id` BIGINT UNSIGNED NOT NULL COMMENT 'leak_alert leak_sensor_log.id',
|
||||||
`sensor_id` TINYINT UNSIGNED NOT NULL,
|
`sensor_id` TINYINT UNSIGNED NOT NULL,
|
||||||
`device_id` VARCHAR(64) NOT NULL DEFAULT '',
|
`device_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
`filename` VARCHAR(128) NOT NULL COMMENT '서버 생성 파일명(var/photos/ 하위)',
|
`filename` VARCHAR(128) NOT NULL COMMENT '서버 생성 파일명(var/photos/ 하위)',
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ $opsChecks = file_text($root . '/php/ops_checks.php');
|
||||||
check(str_contains($config, 'INGEST_NONCE_ENABLED'), 'config.php should define INGEST_NONCE_ENABLED.');
|
check(str_contains($config, 'INGEST_NONCE_ENABLED'), 'config.php should define INGEST_NONCE_ENABLED.');
|
||||||
check(str_contains($sensorApi, 'ingest_nonce_claim'), 'sensor_data.php should claim a replay nonce.');
|
check(str_contains($sensorApi, 'ingest_nonce_claim'), 'sensor_data.php should claim a replay nonce.');
|
||||||
check(str_contains($sensorApi, 'ingest_nonce_release'), 'sensor_data.php should release the nonce on failure.');
|
check(str_contains($sensorApi, 'ingest_nonce_release'), 'sensor_data.php should release the nonce on failure.');
|
||||||
check(str_contains($schemaSql, 'ingest_nonce'), 'schema_sht30.sql should create the ingest_nonce table.');
|
check(str_contains($schemaSql, 'leak_ingest_nonce'), 'schema_sht30.sql should create the leak_ingest_nonce table.');
|
||||||
// sensor_id 범위 검증 (TINYINT 경계 → 깔끔한 400)
|
// sensor_id 범위 검증 (TINYINT 경계 → 깔끔한 400)
|
||||||
check(str_contains($sensorApi, 'sensor_id 범위'), 'sensor_data.php should validate sensor_id range.');
|
check(str_contains($sensorApi, 'sensor_id 범위'), 'sensor_data.php should validate sensor_id range.');
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue