-- ============================================================================= -- schema_sht30.sql - 통합 모니터링 시스템 신규 설치용 스키마 -- (1번 모듈=누수+카메라 sensor_id=1, 2번 모듈=SHT30 온습도 sensor_id=2 — 단일 서버) -- -- 신규 설치: 이 파일 하나만 실행합니다. -- SOURCE /path/to/sql/schema_sht30.sql; -- -- v2607: 모든 테이블에 leak_ 접두사 적용 — 같은 DB 의 다른(비관련) 테이블과 격리. -- (구버전 무접두 테이블 대상 마이그레이션 파일들은 리네임과 함께 폐기 — git 이력 참고.) -- -- 이 파일은 두 모듈을 하나의 서버에서 받는 "통합" 스키마입니다. 포함 요소: -- - 온습도(SHT30): leak_sensor_metric 테이블 + 서버 임계 판정(config.php METRIC_*). -- - 누수(leak): leak_sensor_log.is_leak / leak_sensor_status.is_leak 컬럼, -- event_type 의 leak_alert / leak_recovery, -- leak_incident(사고 추적) + leak_photo(사진 메타) 테이블. -- 임계/누수 경보 SMS 발송은 서버 PHP(api/sensor_data.php + sms_send.php)에서 수행하며, -- 발송 이력은 leak_sms_log 에 남깁니다(leak_sms_log 기준 쿨다운/복구 판정 — 별도 경보 상태 테이블 없음). -- 누수 사진 실제 파일은 php/var/photos/ 에 저장되고, leak_photo 에는 메타만 둡니다. -- ============================================================================= SET NAMES utf8mb4; SET time_zone = '+09:00'; CREATE TABLE IF NOT EXISTS `leak_sensor_log` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `device_id` VARCHAR(64) NOT NULL COMMENT 'STM32 기기 ID', `device_location` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '설치 위치', `sensor_id` TINYINT UNSIGNED NOT NULL COMMENT '센서 번호', `sensor_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '센서 이름', `event_type` ENUM('startup','periodic','leak_alert','leak_recovery') NOT NULL DEFAULT 'periodic', `is_leak` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '누수 여부 (1=누수, 0=정상)', `sensor_time` DATETIME NOT NULL COMMENT '센서 측정 시각 (기기 기준)', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'DB 저장 시각', PRIMARY KEY (`id`), INDEX `idx_sensor_time` (`sensor_time`), INDEX `idx_device_sensor` (`device_id`, `sensor_id`), INDEX `idx_event_type` (`event_type`, `created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='센서 이벤트 전체 로그'; CREATE TABLE IF NOT EXISTS `leak_sensor_status` ( `sensor_id` TINYINT UNSIGNED NOT NULL, `device_id` VARCHAR(64) NOT NULL, `sensor_name` VARCHAR(100) NOT NULL DEFAULT '', `device_location` VARCHAR(100) NOT NULL DEFAULT '', `last_event_type` VARCHAR(20) NOT NULL DEFAULT '', `is_leak` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '최신 누수 상태 (1=누수, 0=정상)', `last_seen` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `offline_alerted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '오프라인 알림 발송 여부 (1=발송됨, 0=미발송)', `leak_confirmed` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '디바운스 확정 누수 상태(SMS/사고추적 기준; is_leak 은 raw 최신)', `leak_pending_since` DATETIME NULL DEFAULT NULL COMMENT '미확정 전이 시작 시각(NULL=안정, raw==confirmed)', PRIMARY KEY (`sensor_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='센서별 최신 상태 (업서트)'; CREATE TABLE IF NOT EXISTS `leak_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' COMMENT '서버 임계 판정 결과: normal/high_temp/low_temp/high_humidity/low_humidity/condensation_risk', `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 COMMENT='SHT30 온습도 측정 이력 (온도/습도/임계 상태)'; CREATE TABLE IF NOT EXISTS `leak_sms_log` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `sensor_id` TINYINT UNSIGNED NOT NULL, `sensor_name` VARCHAR(100) NOT NULL DEFAULT '', `device_id` VARCHAR(64) NOT NULL DEFAULT '', `message` TEXT NOT NULL, `receivers` VARCHAR(500) NOT NULL COMMENT '쉼표 구분 수신자 번호', `status` ENUM('success','fail') NOT NULL, `result_code` VARCHAR(20) NOT NULL DEFAULT '', `result_message` VARCHAR(255) NOT NULL DEFAULT '', `sent_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), INDEX `idx_sent_at` (`sent_at`), INDEX `idx_sensor_sent` (`sensor_id`, `sent_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='SMS 발송 이력 (온습도 임계 경보/복구, 장비 오프라인/복구)'; -- 수집 재전송(replay) 멱등 nonce. api/sensor_data.php 가 인증·신선도를 통과한 요청의 -- 서명(X-Signature)을 여기에 원자적으로 기록한다. 시계오차 창(INGEST_MAX_CLOCK_SKEW_SEC) -- 안에서 같은 서명 본문이 다시 오면(펌웨어 재시도/캡처 재생) 중복 저장·중복 SMS 없이 -- 멱등 처리된다. 오래된 행은 서버가 주기적으로 정리한다(이 테이블이 없어도 수집은 -- 동작하며 멱등 방어만 비활성화된다 — graceful degrade). CREATE TABLE IF NOT EXISTS `leak_ingest_nonce` ( `signature` VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '요청 서명(X-Signature) — 멱등 키', `seen_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '최초 수신 시각(정리 기준)', PRIMARY KEY (`signature`), INDEX `idx_seen_at` (`seen_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='수집 재전송 멱등 nonce (시계오차 창 내 중복 차단; 주기적 정리)'; -- 누수 사고(에피소드) 추적. leak_alert 시 행을 열고(open), 같은 센서의 leak_recovery -- 시 가장 최근 미복구(recovered_at IS NULL) 행을 닫는다. 대시보드의 누수 상태/이력 표시에 사용. 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 leak_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, 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='누수 사고(에피소드) 추적 (시작/복구)'; -- 누수 사진 메타데이터. 실제 파일은 php/var/photos/ 에 서버 생성 무작위 파일명으로 저장 -- (클라이언트 파일명/경로 미사용 — 경로주입 방지). api/photo_upload.php 가 기록한다. CREATE TABLE IF NOT EXISTS `leak_photo` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `log_id` BIGINT UNSIGNED NOT NULL COMMENT 'leak_alert leak_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/)';