docs: 문서 현행화(leak_ 접두사·경로 스크럽) + 코드 투어 + settings deny 수정

- 전 운영 문서 테이블명 leak_ 접두사 반영, 삭제된 마이그레이션 참조 정리,
  구경로 raspi_leck_detecter→<WEB_DIR> 스크럽(DEPLOYMENT_PLAN).
- docs/DEPLOYMENT_PLAN_RPI4x3.md(신규): 3대 Pi + 신규 서버 배포 로드맵.
- .tours/architect-full-system.tour(신규): 19스텝 전체 아키텍처 투어.
- README.md(루트, 0바이트였음): 시스템 인덱스.
- .claude/settings.local.json: /doctor 지적 deny 규칙 5건 수정
  (파이프 4건 와일드카드형, 포크밤 삭제) + 죽은 allow 항목 3건 정리.

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:
cw2007 2026-07-02 18:52:15 +09:00
parent 7a3b571b05
commit fb3630d868
25 changed files with 562 additions and 152 deletions

View file

@ -1,10 +1,7 @@
{
"permissions": {
"allow": [
"Bash(tail:*)",
"Bash(cd \"C:/Users/Posafe/leak_sms_v2/windows_checker\" && pyinstaller --onefile --noconsole --name sensor_checker sensor_checker.py 2>&1)",
"Bash(cd \"C:/Users/Posafe/leak_sms_v2/windows_checker\" && python sensor_checker.py 2>&1)",
"Bash(sort -k2)"
"Bash(tail:*)"
],
"deny": [
"Bash(rm -rf:*)",
@ -14,13 +11,12 @@
"Bash(chmod 777:*)",
"Bash(dd:*)",
"Bash(mkfs:*)",
"Bash(:(){ :|:& };:)",
"Bash(git push --force:*)",
"Bash(git push -f:*)",
"Bash(curl:* | sh)",
"Bash(curl:* | bash)",
"Bash(wget:* | sh)",
"Bash(wget:* | bash)",
"Bash(curl* | sh)",
"Bash(curl* | bash)",
"Bash(wget* | sh)",
"Bash(wget* | bash)",
"Bash(ssh:*)",
"Bash(scp:*)",
"Read(./**/config.local.php)",

View file

@ -0,0 +1,119 @@
{
"$schema": "https://aka.ms/codetour-schema",
"title": "전체 시스템 아키텍처 투어 — 누수+온습도 모니터링 (RPi4×3 → cafe24)",
"description": "센서 노드(RPi4) → HMAC 서명 HTTPS 수집 → MySQL(leak_ 접두사 테이블) → SMS 경보/대시보드까지 전체 데이터 경로를 따라가는 아키텍처 투어. v2607 기준 (테이블 leak_ 접두사, sensor_id 3 신규 지점, 턴키 배포 번들).",
"ref": "feat/pi-c-sensor3-leak-node",
"steps": [
{
"file": "SYSTEM_OVERVIEW.md",
"line": 1,
"title": "1. 전체 그림",
"description": "**상황**: 2종 센서 모듈(누수+카메라 / SHT30 온습도)이 단일 cafe24 PHP 서버로 보고하는 통합 시스템. **구조**: 모든 모듈이 동일한 raw-body HMAC-SHA256 X-Signature 스킴 사용. **왜 중요**: 이 문서가 모듈↔서버 계약(서명·sensor_id·엔드포인트)의 단일 출처. **주의**: v2607부터 DB 테이블 전체가 leak_ 접두사 — 같은 DB의 타 시스템 테이블과 격리."
},
{
"directory": "raspberry_pi4",
"title": "2. 모듈 1 — 누수 + 카메라 (sensor_id 1, 3)",
"description": "**상황**: e-con 5C-08 저항형 누수센서(GPIO17, 외부 4.7MΩ 풀다운) + Pi Camera. **구조**: leak_sensor.py 단일 데몬 + config.py(비밀 아님) + config_local.py(비밀, 미커밋). **왜 중요**: Pi-A(sensor 1)와 Pi-C(sensor 3, 신규 지점)가 같은 코드 폴더를 씀 — 기기별 차이는 전부 config_local.py로."
},
{
"file": "raspberry_pi4/config.py",
"line": 13,
"title": "3. 센서 정의 + 보고 주기",
"description": "**상황**: SENSORS 리스트가 이 기기의 sensor_id/GPIO를 정의. **구조**: REPORT_INTERVAL=600초 — 서버 HEARTBEAT_TIMEOUT_SEC(1200초)의 절반이라 2회 누락 시 오프라인 판정. **주의**: 예전 3600초는 매시간 오프라인 오탐을 냈던 버그 — 주기를 늘릴 땐 반드시 서버 타임아웃과 함께 판단."
},
{
"file": "raspberry_pi4/config.py",
"line": 92,
"title": "4. 기기별 오버라이드 (Pi-C 핵심)",
"description": "**상황**: config_local.py에 SENSORS가 있으면 기본값을 덮어씀. **구조**: 2단 import — 필수 비밀값은 fail-closed(없으면 기동 거부), SENSORS는 선택. **왜 중요**: Pi-C(sensor_id 3)는 추적 파일(config.py)을 건드리지 않고 배포 가능 — 기기별 drift 방지. **주의**: sensor_id 1/2 재사용 시 기존 지점 상태를 조용히 덮어씀(leak_sensor_status는 sensor_id 단독 키)."
},
{
"file": "raspberry_pi4/leak_sensor.py",
"line": 194,
"title": "5. 클라이언트 서명 — raw-body HMAC",
"description": "**상황**: 요청 본문 바이트에 HMAC-SHA256(API_KEY)을 계산해 X-Signature 헤더로 전송. **구조**: 본문을 고정 필드 순서로 직렬화한 뒤 그 바이트 그대로 서명·전송 — 서버도 같은 바이트로 검증. **주의**: 구버전 sha256(API_KEY+정렬JSON) 스킴은 폐기 — 서버가 기본 거부(ALLOW_LEGACY_BODY_SIGNATURE=false)."
},
{
"file": "raspberry_pi4/leak_sensor.py",
"line": 451,
"title": "6. 보고 상태머신",
"description": "**상황**: 메인 루프의 주기 선택 — 정상 600초, 누수 중 10초(LEAK_REPORT_INTERVAL). **구조**: 누수 감지 → leak_alert 즉시 보고 + 사진 촬영/업로드 → 지속 재보고 → 복구 시 leak_recovery. **왜 중요**: SMS 중복 방지는 클라이언트가 아니라 서버 쿨다운이 담당 — 클라이언트는 상태만 계속 밀어넣는 단순 설계."
},
{
"file": "raspberry_pi4_sht30/sht30_sensor.py",
"line": 179,
"title": "7. 모듈 2 — SHT30 온습도 (sensor_id 2)",
"description": "**상황**: I2C(0x44) SHT30을 300초 주기로 측정·보고하는 메인 루프. **구조**: 측정 → CRC 검증 → 같은 HMAC 서명 → 같은 엔드포인트. 임계 판정은 전부 서버 몫. **주의**: smbus2 부재/I2C 실패 시 시뮬레이션 값으로 폴백(WARN 로그만) — 배포 검증 때 journalctl에서 시뮬레이션 경고 부재를 반드시 확인."
},
{
"file": "php/api/sensor_data.php",
"line": 73,
"title": "8. 수집 경계 — fail-closed 인증",
"description": "**상황**: 모든 센서 보고가 들어오는 단일 진입점. **구조**: API_KEY가 기본값이면 503 거부 → 크기 상한 → 타임스탬프 ±600초 → raw-body HMAC 검증 순서. **왜 중요**: 여기가 시스템의 유일한 신뢰 경계 — 이 파일 통과 = DB 기록 + SMS 발송 권한."
},
{
"file": "php/api/sensor_data.php",
"line": 112,
"title": "9. 재전송(replay) 멱등 방어",
"description": "**상황**: 서명 자체를 nonce로 leak_ingest_nonce에 기록. **구조**: 같은 서명 재수신 = 중복 → 재저장/재발송 생략. 시계오차 창 지나면 자동 정리. **주의**: 테이블 없으면 조용히 비활성(graceful degrade) — setup_wizard가 이 테이블 누락을 검사하지 않으므로 신규 설치 후 7개 테이블 존재를 직접 확인."
},
{
"file": "php/api/sensor_data.php",
"line": 371,
"title": "10. 누수 사고(incident) 추적",
"description": "**상황**: leak_alert 수신 시 leak_incident 에피소드 개시, leak_recovery 시 종료. **구조**: sensor_id 단위로 열린 사고를 찾아 갱신 — sensor_id 3도 코드 수정 없이 동작(자동 등록 upsert). **왜 중요**: 사고 단위 추적이라 SMS·사진·대시보드 이력이 에피소드로 묶임."
},
{
"file": "php/api/photo_upload.php",
"line": 221,
"title": "11. 사진 업로드 검증 체인",
"description": "**상황**: 누수 사진 전용 업로드 엔드포인트. **구조**: canonical 문자열(device_id|sensor_id|log_id|timestamp) HMAC 서명 → log_id 실존 확인 → finfo MIME(image/jpeg만) → 크기 상한 → 서버 생성 무작위 파일명. **주의**: 파일명을 클라이언트가 정하지 않음 — 경로 조작 원천 차단. 저장 위치 php/var/photos/는 웹에서 403."
},
{
"file": "php/sms_send.php",
"line": 546,
"title": "12. SMS 경보 상태머신",
"description": "**상황**: 누수 SMS 발송 함수 — 쿨다운(기본 30분) 적용, 복구 SMS는 경보 후에만. **구조**: 별도 상태 테이블 없이 leak_sms_log의 메시지 접두 태그([누수감지] 등)가 사실상 상태 키 — 태그는 config.php SMS_TAG_* 상수가 단일 출처. **주의**: 태그 문자열을 직접 쓰면 여러 파일의 LIKE 검색이 어긋나 무성 장애 — 반드시 상수 참조."
},
{
"file": "php/cron_heartbeat.php",
"line": 22,
"title": "13. 오프라인 감지 (cron 1분)",
"description": "**상황**: last_seen이 HEARTBEAT_TIMEOUT_SEC(1200초) 초과한 센서를 찾아 오프라인 SMS. **구조**: offline_alerted 0→1 원자적 선점(UPDATE ... WHERE offline_alerted=0)으로 다중 진입점 동시 실행에도 중복 발송 방지. **주의**: 타임아웃은 전 센서 공통 — 어떤 모듈의 보고 주기보다 길어야 오탐 없음."
},
{
"file": "php/config.php",
"line": 217,
"title": "14. LEAK_SENSOR_IDS — 지점 확장점",
"description": "**상황**: 대시보드 누수 카드 표시 대상 sensor_id 목록 (기본 [1]). **구조**: config.local.php에서 [1,3] 오버라이드 — 1..255 정수만 통과, 잘못된 값은 버리고 비면 [1] 복귀. **왜 중요**: 신규 누수 지점 추가 시 서버 코드 수정 없이 이 설정 하나로 대시보드 노출."
},
{
"file": "php/dashboard.php",
"line": 700,
"title": "15. 대시보드 — 누수 카드 렌더",
"description": "**상황**: LEAK_SENSOR_IDS 루프로 지점별 누수 카드 렌더 (v2607 이전엔 sensor_id 1 하드코딩). **구조**: 우선순위 오프라인 > 누수 > 정상, 미수신이면 '데이터 없음' 카드. **주의**: 온습도 차트는 여전히 sensor 2 고정 — SHT30 지점을 추가하면 차트 선택 UI 작업 필요(누수 지점은 무관)."
},
{
"file": "php/login.php",
"line": 110,
"title": "16. 관리자 인증 — 비밀번호 + TOTP",
"description": "**상황**: bcrypt 해시 검증 + TOTP MFA. **구조**: ADMIN_TOTP_SECRET 미설정이면 로그인 자체가 불가(fail-closed) — setup_mfa.php?token= 으로 최초 1회 등록 후 MFA_SETUP_TOKEN을 반드시 비움. **주의**: 세션 쿠키 Secure라 HTTPS 없이는 로그인 불가 — cafe24 패널에서 HTTPS 강제가 선행 조건."
},
{
"file": "sql/schema_sht30.sql",
"line": 24,
"title": "17. 스키마 — 단일 파일, leak_ 네임스페이스",
"description": "**상황**: 신규 설치용 완전 스키마 — 테이블 7개(leak_sensor_log/status/metric, leak_sms_log, leak_ingest_nonce, leak_incident, leak_photo) 전부 이 파일 하나. **왜 중요**: v2607에서 구버전 마이그레이션 파일 폐기 — 이 파일이 유일한 DB 설치 경로. **주의**: 컬럼명은 접두사 없음(sensor_id, is_leak 등) — 테이블명만 leak_."
},
{
"file": "tools/make_deploy_bundles.py",
"line": 29,
"title": "18. 턴키 배포 번들 생성기",
"description": "**상황**: deploy/ 아래 서버 1 + Pi 3대 폴더를 생성 — API_KEY 자동 생성·4곳 동일 주입, 관리자 bcrypt, LF 정규화. **구조**: WEB_DIR/BASE_URL만 고치고 재실행하면 전체 재생성(.secrets/ 비밀 보존). **주의**: deploy/는 비밀 포함이라 gitignore — 절대 커밋 금지. 남는 수동 설정은 DB 계정·SMS 계정·TOTP 등록 3가지뿐."
},
{
"title": "다음 단계",
"description": "이제 센서 GPIO 인터럽트부터 SMS 발송까지 전체 경로를 파일 단위로 추적할 수 있다. 배포 절차는 docs/DEPLOYMENT_PLAN_RPI4x3.md, 기기별 설치는 deploy/*/SETUP.md, 보안 증적은 docs/SOURCE_SECURITY_EVIDENCE.md 참고. 신규 지점 추가 = Pi 폴더 복사 + config_local.py(SENSORS id) + 서버 LEAK_SENSOR_IDS 갱신 — 코드 수정 불필요."
}
]
}

View file

@ -0,0 +1,23 @@
# 서버실 통합 모니터링 시스템 (누수 + 온습도)
Raspberry Pi 4 센서 모듈 2대 — **1번: 누수 감지 + 카메라**, **2번: SHT30 온습도** — 와
선택적 **3번째 노드**(신규 지점, sensor_id 3)가 **동일한 cafe24 PHP 서버**로 HTTPS 보고
(raw-body 서명 `X-Signature = HMAC-SHA256(API_KEY, 요청본문)`)를 하고, 서버가 MySQL 저장,
임계/누수 판정, SMS 발송, 통합 대시보드(`dashboard.php`) 표시를 담당하는 단일 시스템입니다.
## 모듈 구성
| 폴더 | 역할 | sensor_id |
|---|---|---|
| `raspberry_pi4/` | 누수 감지(e-con 5C-08) + Pi Camera 촬영·사진 업로드 | 1 (신규 지점 노드는 3) |
| `raspberry_pi4_sht30/` | SHT30 온습도 측정(I2C) | 2 |
| `php/` | cafe24 서버 — API 수신, DB 저장, 판정/SMS, 대시보드 | — |
| `sql/` | DB 스키마(`schema_sht30.sql` = 신규 설치 완결) + 마이그레이션 | — |
| `firmware/` | STM32F407 마이그레이션 트랙(별도) — **RPi4 배포와 무관** | — |
## 문서
- [SYSTEM_OVERVIEW.md](./SYSTEM_OVERVIEW.md) — 전체 아키텍처·모듈 매핑·서명 스킴 (기준 문서)
- [docs/DEPLOYMENT_PLAN_RPI4x3.md](./docs/DEPLOYMENT_PLAN_RPI4x3.md) — RPi4 × 3 + cafe24 신규 설치 배포 계획
- [docs/INSTALL_PI_SERVER.md](./docs/INSTALL_PI_SERVER.md) — cafe24 서버 설치 절차 (STM32 펌웨어 중심)
- [docs/OPERATIONS_SECURITY_CHECKLIST.md](./docs/OPERATIONS_SECURITY_CHECKLIST.md) — 운영·보안 체크리스트

View file

@ -25,7 +25,7 @@
| 구분 | 폴더 | 보드 | 센서 | sensor_id | 서버 보고 |
|---|---|---|---|---|---|
| **1번 모듈** | `raspberry_pi4/` | Raspberry Pi 4 | e-con 5C-08 누수 + Pi Camera | 1 | `leak_alert`/`leak_recovery`/`periodic` + `photo_upload.php` |
| **2번 모듈** | `raspberry_pi4_sht30/` | Raspberry Pi 4 | SHT30 온습도 (I2C) | 2 | `startup`/`periodic` + 온도/습도 (`sensor_metric`) |
| **2번 모듈** | `raspberry_pi4_sht30/` | Raspberry Pi 4 | SHT30 온습도 (I2C) | 2 | `startup`/`periodic` + 온도/습도 (`leak_sensor_metric`) |
두 모듈 모두:
- 비밀값은 `config_local.py`(미커밋)에만 — `cp config_local.example.py config_local.py` 후 채움.
@ -36,8 +36,8 @@
## 서버 (cafe24 `php/`)
- 웹 루트(예): `public_html/raspi_leck_detecter/` — `docs/INSTALL_PI_SERVER.md` 참고.
- DB 스키마: `sql/schema_sht30.sql` (신규) 또는 `sql/migration_drop_leak.sql` (전환).
- 대시보드: `dashboard.php` — `sensor_status`/`sensor_metric`/`sms_log` 표시, 기본 차트 sensor 2.
- DB 스키마: `sql/schema_sht30.sql` — 신규 설치는 이 파일 하나로 완결(테이블 전체 `leak_` 접두사).
- 대시보드: `dashboard.php` — `leak_sensor_status`/`leak_sensor_metric`/`leak_sms_log` 표시, 기본 차트 sensor 2.
- 인증: `X-Signature = HMAC-SHA256(API_KEY, raw_body)`. 모든 모듈의 `API_KEY` = 서버 `config.local.php` 의 `API_KEY`.
---
@ -54,21 +54,20 @@
본문 JSON 을 고정 필드 순서 문자열로 만들어 그 바이트에 서명하고 그대로 POST 합니다.
(구버전의 `sha256(API_KEY + 정렬 JSON)` body-field 서명은 폐기 — 서버 기본 설정이 거부.)
사진 업로드 서명 canonical = `device_id|sensor_id|log_id|timestamp` (공백 없음).
- **스키마(가산·멱등):** `sensor_log.is_leak` / `sensor_status.is_leak` 컬럼, `event_type` 의
- **스키마(가산·멱등):** `leak_sensor_log.is_leak` / `leak_sensor_status.is_leak` 컬럼, `event_type` 의
`leak_alert`/`leak_recovery`, `leak_incident`(사고 추적) + `leak_photo`(사진 메타) 테이블을
다시 추가했습니다. `sensor_metric`(SHT30)은 그대로 보존합니다.
- **수신:** `api/sensor_data.php` 가 누수 이벤트를 받아 `sensor_log`/`sensor_status` 갱신,
다시 추가했습니다. `leak_sensor_metric`(SHT30)은 그대로 보존합니다.
- **수신:** `api/sensor_data.php` 가 누수 이벤트를 받아 `leak_sensor_log`/`leak_sensor_status` 갱신,
`leak_incident` 개시/종료, `leak_alert` 응답에 `log_id` 반환. SHT30 측정 경로는 100% 유지.
- **사진:** 새 `api/photo_upload.php` (서명·신선도·MIME(finfo)·크기 상한·서버 생성 무작위 파일명).
- **SMS:** `sms_send.php` 에 `send_leak_alert_sms`(쿨다운) / `send_leak_recovery_sms`(경보 후에만).
### 운영 적용 절차 (기존 SHT30 서버에 누수 통합)
### 운영 적용 절차 (신규 설치)
1. 기존 서버에서 **`sql/migration_add_leak.sql` 1회 실행**(가산·멱등 — 운영 데이터 보존, 재실행 안전).
신규(통합) 설치는 `sql/schema_sht30.sql` 하나만 실행하면 됩니다(이미 누수 요소 포함).
1. **`sql/schema_sht30.sql` 1회 실행** — 이 파일 하나로 완결(누수 요소 포함, 테이블 전체 `leak_` 접두사).
(구버전 무접두 테이블 대상 마이그레이션 파일들은 v2607 테이블 리네임과 함께 폐기되었습니다.)
2. 아래 서버 파일을 (재)배포:
`api/sensor_data.php`, `api/photo_upload.php`, `sms_send.php`, `dashboard.php`, `config.php`,
`sql/migration_add_leak.sql`.
`api/sensor_data.php`, `api/photo_upload.php`, `sms_send.php`, `dashboard.php`, `config.php`.
3. **쓰기 가능한 `php/var/photos/`** 디렉터리 확보(웹서버 사용자 쓰기 권한; 없으면 업로드 시 자동 생성 시도).
4. 1번 모듈 `raspberry_pi4/config_local.py` 의 `API_URL`/`PHOTO_UPLOAD_URL` 을 이 통합 서버로,
`API_KEY` 를 서버 `config.local.php` 의 `API_KEY` 와 동일하게 설정.

View file

@ -0,0 +1,279 @@
# 배포 계획: 누수+온습도 통합 모니터링 시스템 (cafe24 서버 1 + Raspberry Pi 4 × 3)
기준 저장소: `C:/Users/USER/Desktop/complete/leak_sms_v3`
확정 조건: RPi4 3대 모두 신품(OS 없음, SD 새로 플래시), 설치 현장 인터넷 가용(오프라인 vendor 번들은 선택이나 재현성 위해 권장), cafe24 서버 신규 설치.
---
## 1. 전체 로드맵
**순서 원칙: 서버 먼저.** 모든 Pi의 `test_connection.py` 검증과 API_KEY 발급이 서버에 의존하므로 서버가 선행되어야 한다 (`raspberry_pi4/config_local.example.py`, `raspberry_pi4_sht30/README.md` — API_KEY는 서버 `config.local.php`와 byte-단위 동일해야 함).
```
[0단계] cafe24 서버 신규 설치 ──────────────► API_KEY 확정, 엔드포인트 URL 확정
│
├─ (병렬 가능) SD 카드 3장 OS 플래시 + 공통 OS 프로비저닝
│ ※ 서버와 의존성 없음 — 서버 작업과 동시 진행 가능
│
▼
[1단계] Pi-A (모듈1: 누수+카메라, sensor_id=1) ┐
[2단계] Pi-B (모듈2: SHT30, sensor_id=2) ├─ 서로 독립, 병렬 설치 가능
[3단계] Pi-C (sensor_id=3, 신규 지점) ┘ (단, Pi-C는 센서 종류 결정 선행 — §6)
│
▼
[4단계] end-to-end 검증 (대시보드 / SMS / 사진 / 오프라인 감지) + 보안 체크리스트 (§7)
```
**병렬 가능 구간:**
- 서버 설치(0단계)와 Pi 3대의 OS 플래시·공통 프로비저닝(§3)은 완전 병렬.
- Pi-A / Pi-B / Pi-C의 모듈 설치는 서로 독립 — 서버의 API_KEY만 확정되면 동시 진행 가능.
- (선택) 오프라인 vendor 번들 생성(`vendor.sh`)은 인터넷 연결된 aarch64 Bookworm Pi 1대에서 선행 — 첫 번째로 부팅한 Pi에서 만들어 나머지에 복사하는 흐름 권장 (`raspberry_pi4/vendor.sh`, `raspberry_pi4_sht30/vendor.sh`의 .deb은 타깃 아키텍처에서만 받을 수 있음).
**Pi-C 블로커:** 센서 종류(누수 / SHT30 / 둘 다) 미결정 → §6의 작업량 비교로 먼저 결정 필요.
---
## 2. 0단계: cafe24 서버 신규 설치
### 2.1 DB 스키마
- [ ] cafe24 phpMyAdmin(또는 MySQL 콘솔)에서 **`sql/schema_sht30.sql` 1회 실행**. 신규 설치는 이 파일 하나로 완결 — leak_sensor_log, leak_sensor_status, leak_sensor_metric, leak_sms_log, leak_ingest_nonce, leak_incident, leak_photo 및 leak 컬럼/ENUM 전부 포함 (`sql/schema_sht30.sql:25-167`).
- [ ] 마이그레이션 파일 없음 — v2607 테이블 리네임(`leak_` 접두사)과 함께 폐기. 신규 설치는 스키마 파일 하나로 완결.
### 2.2 파일 업로드
- [ ] `php/` 전체를 웹루트 `public_html/<WEB_DIR>/`에 업로드 (`docs/INSTALL_PI_SERVER.md` §3, `php/.htaccess` 헤더).
- [ ] **주의: `docs/INSTALL_PI_SERVER.md:33-56`의 업로드 목록은 STALE** — `api/photo_upload.php`, `photo.php`, `var/.htaccess`, `var/index.html`, `var/photos/`가 누락되어 있음. 누수 사진 파이프라인에 필수이므로 **php/ 트리 전체**를 기준으로 업로드할 것.
- [ ] `php/var/` 를 PHP 쓰기 가능하게 설정 (heartbeat_last.txt, ingest_rejections.json, login_attempts.json, admin_audit.log 저장). 코드 기준 디렉터리 0750 / 파일 0640 (`php/api/photo_upload.php:303,321`) — cafe24 공유호스팅에서 755/775가 필요할 수 있음(갭, §8). `var/photos/`는 첫 업로드 시 자동 생성 + .htaccess 가드 자동 작성 (`php/api/photo_upload.php:139-153, 301-321`).
- [ ] 웹에서 `/var/` 접근이 403인지 확인 (`php/.htaccess:14` + `php/var/.htaccess`).
### 2.3 config.local.php / API_KEY
- [ ] `php/config.local.example.php` 복사 → `config.local.php` 작성:
- `DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PASS` — cafe24 MySQL 자격증명
- `API_KEY` — **긴 랜덤 비밀키 생성.** 기본값 `'change-this-api-key'`이면 ingest가 503으로 거부됨(fail-closed, `php/api/sensor_data.php:73-77`). 이 키가 Pi 3대 전부의 `config_local.py` API_KEY와 byte-단위 동일해야 함.
- `ALLOW_LEGACY_BODY_SIGNATURE` — **false 유지** (`php/config.php:60`, `php/ops_checks.php:101`)
- `SMS_USER_ID / SMS_SECURE / SMS_SENDER / SMS_RECIPIENTS` — cafe24 SMS 계정, **사전 등록된 발신번호**, 잔액 충전 필요 (게이트웨이 `php/config.php:66`)
- 임계값: `METRIC_TEMP_HIGH_C=27`, `METRIC_RH_HIGH=60`, `METRIC_RH_LOW=-1`(저습 비활성), `METRIC_DEWPOINT_MARGIN_C=0`(결로 비활성) — 예시값 기준 (`php/config.local.example.php:44-50`, commit c8b448c)
### 2.4 관리자 인증 (fail-closed — 미설정 시 로그인 자체가 불가)
- [ ] `php setup_hash.php "새-관리자-비밀번호"` 실행 → bcrypt 출력물을 `ADMIN_PASSWORD_HASH`에 (`docs/INSTALL_PI_SERVER.md:135-141`)
- [ ] MFA 등록: `MFA_SETUP_TOKEN`에 임시 랜덤값 설정 → `setup_mfa.php?token=<토큰>` 접속 → TOTP 앱 등록 → 표시된 `ADMIN_TOTP_SECRET`을 config.local.php에 → **`MFA_SETUP_TOKEN` 반드시 비우기** (`docs/INSTALL_PI_SERVER.md:100-106`, `php/ops_checks.php:100`)
- [ ] `ADMIN_TOTP_SECRET`은 서버에서 생성하기 전까지 **절대 자리표시자 값을 넣지 말 것** (`php/config.local.example.php:33-40`)
### 2.5 HTTPS / cron
- [ ] HTTPS 리다이렉트는 **cafe24 호스팅 관리 패널에서** 설정 — .htaccess 리다이렉트는 리버스프록시 뒤에서 무한루프 위험으로 의도적으로 비활성 (`php/.htaccess:6-8`, `docs/SOURCE_SECURITY_EVIDENCE.md` §2.1). 세션 쿠키가 Secure라 HTTPS 없이는 로그인 불가.
- [ ] cron 1분 주기: CLI형 `* * * * * php /home/<user>/public_html/<WEB_DIR>/cron_heartbeat.php`. HTTP형 사용 시 **`?key=<CRON_SECRET>` 필수** — `CRON_SECRET = hash_hmac('sha256','cron',API_KEY)` (`php/config.php:199`, `php/cron_heartbeat.php:163-169`). 이 값은 UI 어디에도 표시되지 않아 직접 계산해야 함(갭). `docs/INSTALL_PI_SERVER.md:165`의 URL 예시는 key 파라미터가 빠져 있어 그대로 쓰면 403.
- [ ] (선택) 보존기간 정리: `php retention_cleanup.php --execute` CLI 전용 (`php/retention_cleanup.php:11-16`) — cron 등록 안 하면 365일 보존이 실제로 집행되지 않음.
### 2.6 서버 검증
- [ ] `login.php` 로그인(비밀번호+TOTP) → `setup_wizard.php`에서 전체 점검: DB 연결, 필수 테이블, API_KEY 교체 여부, 관리자 해시/TOTP, MFA 토큰 제거, 레거시 서명 비활성, SMS 수신자, 임계값, var 쓰기권한, **테스트 SMS 발송 버튼** (`php/setup_wizard.php:29-45`, `php/ops_checks.php:44-111`)
- [ ] 주의: setup_wizard는 4개 코어 테이블만 검사 — `leak_incident`/`leak_photo`/`leak_ingest_nonce` 누락은 감지 못 하고 조용히 기능 저하됨 (`php/ops_checks.php:51`). 스키마 실행 후 phpMyAdmin에서 7개 테이블 존재를 직접 확인할 것.
- [ ] CORS 오리진이 `https://pores.kr`로 하드코딩되어 있음 (`php/api/sensor_data.php:49`, `php/api/photo_upload.php:29`) — 다른 도메인 배포 시 수정 필요(갭).
---
## 3. 공통: 새 Pi OS 프로비저닝 (3대 공통)
### 3.1 문서화된 요구사항 (repo에 근거 있음)
| 항목 | 값 | 근거 |
|---|---|---|
| OS | Raspberry Pi OS **Bookworm 64-bit (aarch64)** | `raspberry_pi4/install.sh:4`, `raspberry_pi4_sht30/install.sh:4`, 양쪽 README §2 |
| Python | 시스템 3.11 (Bookworm 기본; wheel이 cp311로 핀) | `vendor.sh` TARGET_PYVER=311 |
| 사용자 | `pi` (sudo 가능) — systemd 유닛 User=pi, 경로 /home/pi/* 하드코딩 | 양쪽 `systemd/*.service`, install.sh SERVICE_USER 기본값 |
| 네트워크 | 유선 Gigabit 또는 WiFi, 서버로 HTTPS egress | 양쪽 README §2 |
| 시각 동기 | NTP 필수 — 서버가 타임스탬프 ±600초 밖이면 거부 | `raspberry_pi4_sht30/README.md:61`, `php/config.php:45` |
| I2C (Pi-B) | install.sh가 자동 처리 (dtparam=i2c_arm=on 등) — raspi-config는 수동 폴백만 | `raspberry_pi4_sht30/install.sh:62-88` |
### 3.2 문서에 없어 현장에서 결정해야 하는 것 (전부 갭 — repo에 절차 없음)
- **SD 이미징 절차 없음**: OS 이미지 버전/체크섬/Imager 사용법 미문서화. → 결정: Raspberry Pi Imager 등으로 Bookworm 64-bit(Lite 권장 여부도 미명시)를 플래시하되, 사용한 이미지 버전을 기록해 3대 동일하게 맞출 것.
- **`pi` 사용자 생성 절차 없음**: Bookworm 이미지는 기본 `pi` 계정이 없음 — Imager 커스텀 설정 또는 userconf.txt로 `pi` 계정을 만들어야 systemd 유닛/경로가 무수정으로 동작. 비밀번호 정책 미문서화.
- **SSH/헤드리스 활성화 절차 없음**: Imager 설정 또는 boot 파티션 `ssh` 파일 방식 중 결정 필요.
- **호스트네임 규약 없음**: DEVICE_ID(rpi4-leak-01 등)는 애플리케이션 식별자일 뿐 OS 호스트네임 아님. → 결정 권장: 호스트네임을 DEVICE_ID와 일치시켜 운영 혼선 방지.
- **네트워크 설정(고정 IP vs DHCP, WiFi 프로비저닝) 없음.**
- **로케일/타임존, apt 보안 업데이트 정책, OS 하드닝 절차 없음** (§7·§8 참조).
### 3.3 (선택·권장) 오프라인 vendor 번들
현장에 인터넷이 있으므로 install.sh 온라인 경로로도 설치 가능하나, 재현성을 위해:
- [ ] 첫 부팅한 Pi(인터넷 연결, aarch64 Bookworm) 1대에서 각 모듈의 `./vendor.sh` 실행 → `wheelhouse/` + `debs/` 생성. **.deb은 반드시 타깃 아키텍처(aarch64 Bookworm)에서만 받을 수 있음** (`vendor.sh:7-10,55`) — x86 빌드머신 불가.
- [ ] 생성된 번들을 나머지 Pi의 모듈 폴더에 복사 → `install.sh`가 자동으로 오프라인 모드 진입(불완전 번들은 하드 에러, 온라인 폴백 없음).
- [ ] 갭: 번들 무결성 해시 검증 절차 없음 — sha256sum 목록을 직접 만들어 전달 권장.
---
## 4. Pi-A: 모듈 1 (누수 + Pi Camera, sensor_id=1)
소스: `raspberry_pi4/` — 근거는 `raspberry_pi4/README.md`, `install.sh`, `config.py`, `systemd/leak-sensor.service`.
### 4.1 배선 (README.md §2.1)
- [ ] e-con 5C-08 누수센서 (~1.5MΩ 고임피던스): 센서 입력 → **GPIO17(BCM) = 물리핀 11**, 3.3V → 핀 1, GND → 핀 6
- [ ] **외부 4.7MΩ 풀다운 필수**, 내부 풀 OFF 유지 (`config.py` USE_INTERNAL_PULL=False → PUD_OFF; 내부 50kΩ 풀다운이 1.5MΩ 센서 신호를 죽임 — README.md:49-53)
- [ ] 로직: ACTIVE_LOW=False — 건조=LOW, 누수=HIGH; 디바운스 500ms (`config.py:20-24`)
- [ ] Pi Camera: Pi 4의 **15핀 1.0mm CSI 커넥터** (Zero 2 W용 22핀 리본 비호환 — 15핀 리본 또는 어댑터 필요). 전원 USB-C 5V/3A(카메라 사용 시 여유 권장).
### 4.2 설치
- [ ] 폴더를 `/home/pi/raspberry_pi4`에 배치 (유닛 파일이 경로 하드코딩 — `systemd/leak-sensor.service:12-13,26-27`; 다르면 유닛 수정)
- [ ] `cp config_local.example.py config_local.py` 후 5개 값 기입:
- `API_URL` = `https://<서버>/<WEB_DIR>/api/sensor_data.php`
- `PHOTO_UPLOAD_URL` = `.../api/photo_upload.php`
- `API_KEY` = 서버 config.local.php와 byte-단위 동일
- `DEVICE_ID` = 예: `rpi4-leak-01`, `DEVICE_LOCATION` = 설치 장소
- 누락 시 fail-closed로 기동 거부 (`config.py:69-83`); 절대 커밋 금지 (`.gitignore`)
- [ ] `chmod +x install.sh && ./install.sh` — 온라인: apt로 `python3-rpi-lgpio python3-requests python3-venv rpicam-apps`(또는 libcamera-apps) 설치; 오프라인 번들 있으면 자동 오프라인. venv(--system-site-packages) + `requests==2.32.3`, 로그파일 `/var/log/leak_sensor.log` (640, pi 소유) 생성.
- [ ] **주의(모순 문서)**: `raspberry_pi4/README.md` §3(57-68행)의 "php/ 서버는 누수 이벤트 거부, 레거시 서버로 향하라"는 안내는 **STALE** — `config_local.example.py:8-16`과 머지된 PR #1 기준 **통합 서버가 정답**. README §3을 따르지 말 것.
### 4.3 systemd
- [ ] `sudo cp systemd/leak-sensor.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable leak-sensor && sudo systemctl start leak-sensor`
- [ ] 유닛 특성: User=pi, **SupplementaryGroups=video**(카메라 접근 필수), Restart=always/10s, PrivateTmp=true, network-online.target 이후 기동
### 4.4 End-to-end 검증
- [ ] `source venv/bin/activate && python3 test_connection.py` — 3단계: test.php POST / 무서명 POST(거부 확인) / HMAC 서명 POST(200 확인)
- [ ] 수동 실행 `python3 leak_sensor.py` → startup 이벤트가 서버 sensor_log에 기록되는지 대시보드 확인
- [ ] **카메라 사전 검증(갭 — 문서화된 절차 없음)**: 설치 절차에 카메라 감지 확인이 없어 첫 누수 때에야 실패가 드러남. `rpicam-still`로 수동 테스트샷을 찍어볼 것을 권장.
- [ ] 누수 모의 시험: 센서 단락 → 대시보드 누수 카드(sensor_id=1 전용, `php/dashboard.php:256-264`) 표시 + `[누수감지]` SMS + leak_incident 행 + `photo.php?id=N`으로 사진 열람(관리자 세션 필요)
- [ ] `systemctl status leak-sensor`, `journalctl -u leak-sensor -f` 정상 확인
---
## 5. Pi-B: 모듈 2 (SHT30 온습도, sensor_id=2)
소스: `raspberry_pi4_sht30/` — 근거는 해당 README.md, install.sh, config.py, systemd/sht30-sensor.service.
### 5.1 배선 (README.md §2.1)
- [ ] Sensirion SHT30 (한진데이타 P4422-3), I2C 버스 1: VCC → **3.3V 핀 1 (5V 금지)**, GND → 핀 6 등, SDA → GPIO2/핀 3, SCL → GPIO3/핀 5
- [ ] **ADDR → GND = 주소 0x44** (VDD면 0x45 — `config.py` I2C_ADDR와 일치시킬 것)
- [ ] SDA/SCL에 3.3V 풀업 4.7kΩ ×2 (모듈 온보드 풀업 있으면 생략)
### 5.2 설치
- [ ] 폴더를 `/home/pi/raspberry_pi4_sht30`에 배치 (유닛 경로 하드코딩)
- [ ] `cp config_local.example.py config_local.py` → `API_URL`(Pi-A와 동일 엔드포인트) / `API_KEY`(동일 키) / `DEVICE_ID`(예: rpi4-sht30-01) / `DEVICE_LOCATION` 기입 (fail-closed)
- [ ] `./install.sh` — [1/4] apt: `python3-requests python3-venv i2c-tools`; **[2/4] I2C 자동 활성화**: `/boot/firmware/config.txt`에 dtparam=i2c_arm=on, i2c-dev 모듈 로드+영속화, `usermod -aG i2c pi`; [3/4] venv + `requests==2.32.3, smbus2==0.5.0`; [4/4] `/var/log/sht30_sensor.log`
- [ ] install.sh가 리부트 안내를 출력하면 `sudo reboot` (I2C 신규 활성화 시 — README §4.2). **주의(갭)**: `usermod -aG i2c`도 재로그인/리부트 후 반영 — 리부트 없이 곧바로 test_sensor.py를 돌리면 /dev/i2c-1 권한 오류 가능.
### 5.3 systemd
- [ ] `sudo cp systemd/sht30-sensor.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable sht30-sensor && sudo systemctl start sht30-sensor`
- [ ] 유닛 특성: User=pi, **SupplementaryGroups=i2c**, Restart=always/10s, PrivateTmp=true. install.sh는 유닛을 설치하지 않으므로 이 수동 단계 누락 금지(갭).
### 5.4 End-to-end 검증
- [ ] `i2cdetect -y 1` → **0x44** 표시 확인 (README §5)
- [ ] `python3 test_sensor.py` (CRC/포맷 자가검사 + 1회 측정) — **주의: 하드웨어 없이도 시뮬레이션으로 통과하므로**, i2cdetect 확인을 반드시 선행. 시뮬레이션 폴백이 배선 오류를 가릴 수 있음(갭): 서비스 로그에 시뮬레이션 WARN이 없는지 `journalctl`로 확인할 것.
- [ ] `python3 test_connection.py` — 무서명 400/403, 서명 200; 403이면 API_KEY 불일치, 400이면 NTP/타임스탬프 의심
- [ ] 수동 실행 후 startup 이벤트 → 이후 300초 주기 보고. 대시보드에서 sensor_id=2 카드 + 차트(기본 sensor 2, `php/dashboard.php:125-129`) + leak_sensor_metric 5분 축적 확인
- [ ] 임계값 시험: 고온/고습 유발 → 태그된 SMS + 30분 쿨다운 + 회복 SMS 확인
---
## 6. Pi-C: sensor_id=3 신규 지점 (extension-pattern 기준)
근거: `docs/solutions/best-practices/sensor-telemetry-extension-pattern-2026-05-21.md` (단, 이 문서 자체가 legacy 선언 — 현행 코드 기준으로 재검증된 아래 내용을 따를 것).
### 6.1 그냥 되는 것 (서버 무변경)
- **등록 절차 없음**: sensor_id는 HMAC 서명된 본문으로 도착, 1..255 범위검사 후 `leak_sensor_status`에 UPSERT로 자동 등록 (`php/api/sensor_data.php:199-205, 316-340`). SQL 마이그레이션 불필요.
- 온습도 보고 시 metric 저장, **임계값 SMS/회복 SMS(센서별 쿨다운)**, 오프라인/온라인 SMS, 에스컬레이션, stuck 감지, 월간 리포트 GROUP BY — 전부 sensor_id 단위로 자동 동작 (`php/cron_heartbeat.php:22-83`, `php/sms_send.php:426-509`).
- 대시보드 **센서 상태 카드 목록**에는 sensor 3이 자동 표시 (`php/dashboard.php:87, 635-690`).
- 누수 이벤트도 서버 처리 자체는 됨: `[누수감지]` SMS + leak_incident 생성 + 사고 테이블 표시 (`php/sms_send.php:546-608`, `php/api/sensor_data.php:371-411`, incident 쿼리는 무필터).
### 6.2 코드/스키마 작업이 필요한 것
| 항목 | 내용 | 근거 |
|---|---|---|
| 차트 | sensor_id=2 하드코딩, 선택 UI 없음 — sensor 3 이력은 저장되지만 차트로 못 봄 | `php/dashboard.php:126-129` |
| 최근 온습도 카드 | 차트 센서(=2)의 최신값만 표시 | `php/dashboard.php:218-220, 596-601` |
| 30일 평균 | 전 센서 혼합 집계(GROUP BY 없음) — 지점이 다른 sensor 3이 들어오면 평균이 무의미해짐 | `php/dashboard.php:210-216` |
| 누수 상태 카드 | sensor_id===1 하드코딩 — Pi-C가 누수형이면 실시간 카드 미표시 | `php/dashboard.php:256-264, 693` |
| 임계값/쿨다운 | METRIC_* 전역 단일값 — 지점별 임계 필요 시 코드/스키마 변경 | `php/config.php:134-153` |
| 하트비트 타임아웃 | 전역 1200초 — **누수형 Pi-C(보고 주기 3600초)는 매 사이클 오프라인 오탐** → 전역 상향 시 5분 센서의 감지 지연 | `php/config.php:197`, `raspberry_pi4/config.py` REPORT_INTERVAL=3600 |
| SENSOR_ID 위치 (SHT30형) | git 추적되는 `config.py:16-17`을 직접 수정해야 함 — config_local.py에 넣어도 무시됨(import 목록 고정, `config.py:70-76`) | drift 위험 |
### 6.3 센서 종류별 작업량 비교
| | **SHT30형 (권장 최소)** | **누수형** | **둘 다 (한 Pi)** |
|---|---|---|---|
| Pi측 | `raspberry_pi4_sht30/` 복사, `config.py` SENSOR_ID=3/SENSOR_NAME 수정(추적 파일!), config_local.py 작성(DEVICE_ID 예: rpi4-sht30-02) | `raspberry_pi4/` 복사, `config.py` SENSORS에 `{"id":3,"gpio":22,...}` 추가(주석 예시 기존재, `config.py:17`), config_local.py + PHOTO_UPLOAD_URL | 두 모듈 폴더 모두 설치 — 핀 충돌 없음(PR_BODY.md §7). 단 두 모듈이 각각 sensor_id를 가지므로 id 3·4 두 개를 쓸지 결정 필요(갭 — 문서화된 패턴 없음) |
| 서버측 필수 | **없음** | **없음** (수신·SMS·incident 동작) | 없음 |
| 서버측 권장 수정 | 차트/최근값 카드가 sensor 2 고정 → 보고 싶으면 dashboard.php 수정; 30일 평균 혼합 문제 | + 누수 카드 sensor 1 고정 수정, **HEARTBEAT_TIMEOUT_SEC 충돌 해결(가장 큰 이슈)** | 양쪽 전부 |
| 결론 | **작업량 최소** — 대시보드 차트만 감수하면 무수정 운영 가능 | 서버 무수정 시 오프라인 오탐 + 대시보드 비가시 감수 필요 | 최대 작업량 |
공통 주의: sensor_status는 sensor_id 단독 키 — Pi-C가 실수로 id 1/2를 재사용하면 기존 지점 상태를 **조용히 덮어씀** (`php/api/sensor_data.php:316-340`). DEVICE_ID는 표시용일 뿐 인증 정체성이 아님.
---
## 7. 배포 시점 보안 체크리스트 (repo 근거 항목만)
**서버 (근거: `docs/OPERATIONS_SECURITY_CHECKLIST.md`, `docs/SOURCE_SECURITY_EVIDENCE.md`, `php/ops_checks.php`)**
- [ ] API_KEY 기본값 교체 확인 (기본값이면 ingest 503 — `php/api/sensor_data.php:73-77`)
- [ ] `ALLOW_LEGACY_BODY_SIGNATURE=false` (`php/ops_checks.php:101`)
- [ ] MFA_SETUP_TOKEN 비움 확인 (`php/ops_checks.php:100`)
- [ ] HTTPS를 cafe24 패널에서 강제 + 인증서/리다이렉트 화면 증적 캡처 (`docs/SOURCE_SECURITY_EVIDENCE.md` §2.1 — .htaccess는 의도적으로 리다이렉트 안 함, **조용히 누락되기 쉬운 수동 단계**)
- [ ] 위조 서명 403 / 정상 서명 200 테스트 증적 (`docs/SOURCE_SECURITY_EVIDENCE.md` §5)
- [ ] config.local.php 등 민감 파일 직접 접근 차단 확인 (`php/.htaccess` FilesMatch — `docs/SOURCE_SECURITY_EVIDENCE.md` §2.8)
- [ ] `php/var/admin_audit.log`에 로그인/로그아웃/MFA 이벤트 기록 확인
- [ ] `curl -I https://<도메인>/<WEB_DIR>/login.php` 응답 헤더 점검 (checklist §3)
- [ ] SMS 잔액/발신번호/수신자 최신성 확인; 백업 + 복원 테스트(`php scripts/backup_evidence.php`) — checklist §9
**Pi 3대 공통**
- [ ] `config_local.py` 커밋 금지(.gitignore 확인); 노출 시 서버·전 노드 동시 API_KEY 교체 (양쪽 README §7)
- [ ] 단일 공유 API_KEY 구조 인지: Pi 1대 탈취 = 전 지점 ingest 자격 노출, 기기별 폐기 불가 (extension-pattern 갭) — 지점 추가마다 노출면 확대
- [ ] 기기 인바운드 차단 증적: `ss -lntup`으로 불필요 리스닝 데몬 없음 확인 (`docs/SOURCE_SECURITY_EVIDENCE.md` §4 — 단 달성 방법은 미문서화, §8 갭)
- [ ] `systemctl status leak-sensor`(및 sht30-sensor) 화면 증적 (§5 최소 증적 세트)
- [ ] NTP 동기 확인 (±600초 초과 시 전 POST 거부)
**repo에 없어 자체 수립 필요 (문서화 갭 — §8):** Pi 계정/SSH 하드닝, 호스트 방화벽(ufw 등), 시크릿 파일 권한(chmod 600), OS 패치 정책.
---
## 8. 문서화 갭 / 결정 필요 사항 (통합·우선순위)
### P0 — 배포를 잘못되게 만들 수 있는 것
1. **STALE 문서 모순 — 모듈1 대상 서버**: `raspberry_pi4/README.md` §3·§7은 "php/ 서버가 누수 거부 / 레거시 sha256 서명"이라 함. 현행은 통합 서버 + raw-body HMAC (`config_local.example.py:8-16`, SYSTEM_OVERVIEW.md, PR #1). README만 보고 배포하면 오설정. → README §3/§7 갱신 필요.
2. **`docs/INSTALL_PI_SERVER.md` 업로드 목록 STALE**: photo_upload.php/photo.php/var 가드 누락 (33-56행) — 그대로 따르면 누수 사진 파이프라인 결손. 또한 이 문서 자체가 STM32(v2606) 중심이며 RPi4 신규 설치 통합 가이드는 부재 (root README.md는 0바이트).
3. **cron URL 오류**: INSTALL_PI_SERVER.md:165에 `?key=<CRON_SECRET>` 누락 → 403. CRON_SECRET 표시 UI 없음, 수동 계산 필요.
4. **신품 Pi 프로비저닝 절차 전무**: SD 이미징, `pi` 계정 생성(Bookworm 기본 미존재), SSH 활성화, 호스트네임, 네트워크 설정 — 전부 미문서화. **결정 필요**: 이미지 버전, 계정/비밀번호 정책, 고정IP vs DHCP, 호스트네임 규약.
5. **Pi-C 센서 종류 결정**: §6.3 비교 기준으로 결정. 누수형 선택 시 HEARTBEAT_TIMEOUT_SEC 충돌(전역 1200초 vs 보고 3600초)을 반드시 먼저 해결.
### P1 — 운영 중 사고로 이어질 수 있는 것
6. **Pi OS 하드닝 부재**: 계정/SSH/방화벽/시크릿 파일 권한(600)/패치 정책이 4개 보안 문서 어디에도 없음 — `docs/SOURCE_SECURITY_EVIDENCE.md`도 "코드로 증명 불가, 기관 자료 필요"로만 표기. → 자체 표준 수립 필요.
7. **단일 공유 API_KEY / 센서 레지스트리 부재**: 키 보유자는 임의 sensor_id 생성·기존 id 덮어쓰기 가능; 기기별 폐기 불가. 지점이 늘수록(Pi-C) 위험 증가 — device_id↔sensor_id 서버측 바인딩 검토 권장.
8. **시뮬레이션 폴백이 오류 은폐**: SHT30 모듈은 I2C 실패 시 그럴듯한 더미값을 운영 서버로 전송(WARN 로그만) — FORCE_REAL 옵션 없음. 배포 검증 시 journalctl 확인을 절차화할 것.
9. **NTP 의존 미절차화**: 서버가 ±600초 강제하는데 시각동기 확인 단계가 설치 절차에 없음 (현장 인터넷 가용이라 기본 NTP로 충족 예상되나 확인 단계 추가 권장).
10. **카메라 검증 단계 부재** (Pi-A): 첫 누수 때에야 실패 발견 — 설치 시 테스트샷 절차 추가 필요.
### P2 — 정리/개선
11. **설정 규약 이원화**: `docs/SHT30_SENSOR2_SETUP.md` §3과 `php/ops_checks.php:97`은 `/etc/sht30-sensor.env`를 참조하나 실제 모듈은 `config_local.py`만 읽음 — 문서 갱신 필요.
12. **SENSOR_ID가 추적 파일(config.py)에 위치** (SHT30형): config_local 오버라이드 허용하는 소규모 수정 권장 (Pi-C에서 실익).
13. **systemd 유닛 경로/User=pi 하드코딩** 2개 파일 — 비표준 배치 시 수동 수정 2곳.
14. **센서 폐기 UI 없음**: leak_sensor_status 행 수동 SQL 삭제 전까지 영구 오프라인 표시.
15. **임계값 문서 드리프트**: SOURCE_SECURITY_EVIDENCE.md §2.4(30℃/70%)와 현행 권장값(27℃/60%, commit c8b448c) 불일치.
16. **retention_cleanup cron 미문서화**; **오프라인 번들 해시 검증 부재**(--require-hashes 없음); **CORS pores.kr 잔재**; **로그 권한 실패 시 조용한 콘솔 폴백**(양 모듈); **var/ 권한 비트 미문서화**(cafe24에서 setup_wizard '상태 파일 폴더' 체크로 확인).
17. **firmware/(STM32) 트랙은 이번 배포와 무관** — RPi4 배포에 불필요 (`docs/stm32f407_migration_plan.md`는 별도 마이그레이션 트랙, 링크/실행 미검증 상태).
---
## 9. [확정] Pi-C = 누수 + 카메라 (sensor_id 3) — 결정 반영 부록
> ✅ **v2607 구현 완료:** §9.2 필수 코드 작업 3건 전부 반영됨 — REPORT_INTERVAL=600(하트비트 오탐 해소), 대시보드 LEAK_SENSOR_IDS 루프(누수 카드 다중 지점), config_local.py SENSORS 오버라이드(+형식 검증 fail-closed). 테이블은 전부 leak_ 접두사로 리네임됨. 배포는 tools/make_deploy_bundles.py 번들 사용.
사용자 결정(2026-07-02): Pi-C는 **누수 + 카메라** 구성.
### 9.1 누수형 선택의 이점 (SHT30형 대비)
- 대시보드 차트 sensor 2 하드코딩 문제(§6.2) **해당 없음** — Pi-C는 metric을 안 보냄.
- 30일 평균 혼합 오염(§6.2) **해당 없음** — 동일 이유.
- SENSOR_ID 추적파일(config.py) 문제는 누수 모듈도 동일하나, `raspberry_pi4/config.py:13` SENSORS 리스트에 id=3 항목 추가 형태라 구조는 명확.
### 9.2 필수 코드 작업 (검증 완료 — file:line 확인됨)
1. **하트비트 오탐 (P0, Pi-A에도 이미 존재하는 버그)**
- `php/cron_heartbeat.php:25-30` — leak_sensor_status 전체를 전역 `HEARTBEAT_TIMEOUT_SEC`(기본 1200s, `php/config.php:197`)로 검사. 필터 없음.
- `raspberry_pi4/config.py:35` — 누수 모듈 정상 주기 보고 3600s.
- 결과: 누수 노드(id 1, id 3)는 **매 사이클 오프라인 SMS 오탐 → 복귀 SMS 반복**.
- 권장 수정(택1):
- (a) **최소 변경**: 누수 모듈 `REPORT_INTERVAL` 3600 → 600 (Pi-A·Pi-C 공통). 주기 POST는 소형 JSON이라 부하 무시 가능. 서버 무수정.
- (b) **구조적**: sensor_status에 per-sensor timeout 컬럼 추가 + cron_heartbeat/dashboard 반영 (스키마+코드 변경).
2. **대시보드 누수 카드 (Pi-C 가시성)**
- `php/dashboard.php:260` — `(int)$s['sensor_id'] === 1` 하드코딩. Pi-C 누수 상태 카드 미표시.
- 수정: is_leak 값이 있는 모든 센서에 대해 카드 렌더 (사고 테이블은 이미 무필터 — 수정 불요).
3. **Pi-C 설정값**
- `raspberry_pi4/config.py` SENSORS에 `{"id": 3, ...}` (gpio 17 유지 — 별도 Pi이므로 핀 충돌 없음), DEVICE_ID 예: `rpi4-leak-02`, DEVICE_LOCATION = 신규 지점명.
### 9.3 Pi-C 추가 하드웨어 (구매 확인 필요)
- e-con 5C-08 누수센서 1개 + 4.7MΩ 풀다운 저항
- Pi Camera (15핀 CSI 리본) 1개
- SD카드·USB-C 5V/3A 전원 (3대 공통)
### 9.4 보안 유의 (extension-pattern 갭)
- 단일 공유 API_KEY로 지점 3개 — 노출면 확대. sensor_status는 sensor_id 단독 키이므로 Pi-C가 id 1/2 오설정 시 기존 지점 상태를 조용히 덮어씀 (`php/api/sensor_data.php:316-340`). 배포 전 config 교차 점검 필수.

View file

@ -281,13 +281,12 @@ return [
<div class="step">
<h3>스키마 실행</h3>
<p><b>신규 설치</b>는 <code>sql/schema_sht30.sql</code> 전체를 복사해 <span class="kbd">SQL</span> 탭에 붙여넣고 <span class="kbd">실행</span>.
생성 테이블: <code>sensor_log</code>, <code>sensor_status</code>, <code>sensor_metric</code>, <code>sms_log</code>
(+ 재전송 방어용 <code>ingest_nonce</code>).</p>
생성 테이블: <code>leak_sensor_log</code>, <code>leak_sensor_status</code>, <code>leak_sensor_metric</code>, <code>leak_sms_log</code>
(+ 재전송 방어용 <code>leak_ingest_nonce</code>).</p>
<table>
<thead><tr><th>SQL 파일</th><th>적용 상황</th></tr></thead>
<tbody>
<tr><td><code>sql/schema_sht30.sql</code></td><td>신규 설치(통합 스키마)</td></tr>
<tr><td><code>sql/migration_drop_leak.sql</code></td><td>기존 누수(v2605) 설치 → 온습도 전용 전환. <b>비가역 — 실행 전 DB 전체 백업</b></td></tr>
</tbody>
</table>
<div class="crit"><b>중복 실행 주의.</b> 이미 적용한 마이그레이션을 다시 실행하면 오류가 날 수 있습니다.
@ -301,7 +300,7 @@ return [
<div class="ph-title">캡처 위치 — 그림 3 <span class="fname">→ screenshots/03-db-phpmyadmin.png</span></div>
<div class="ph-what">phpMyAdmin <b>SQL 탭에 <code>schema_sht30.sql</code> 붙여넣고 [실행]한 결과</b>(“쿼리 실행 성공” + 좌측 4~5개 테이블 생성된 화면)</div>
</div>
<figcaption>그림 3. DB 스키마 실행 결과 (sensor_log/sensor_status/sensor_metric/sms_log/ingest_nonce)</figcaption>
<figcaption>그림 3. DB 스키마 실행 결과 (leak_sensor_log/leak_sensor_status/leak_sensor_metric/leak_sms_log/leak_ingest_nonce)</figcaption>
</figure>
</section>
@ -575,7 +574,7 @@ https://your-domain.example/raspi_leck_detecter/security_evidence.php</code></pr
<ul style="margin:6px 0 0;">
<li><code>setup_wizard.php</code> 필수 점검 항목 통과 + 테스트 SMS 수신</li>
<li><code>dashboard.php</code> 에 최신 온/습도와 센서 상태(<code>sensor_id=2</code>) 표시</li>
<li><code>sensor_metric</code> 에 주기 측정값(periodic) 저장 · STM32 콘솔에 서버 200 기록</li>
<li><code>leak_sensor_metric</code> 에 주기 측정값(periodic) 저장 · STM32 콘솔에 서버 200 기록</li>
<li>임계 초과 테스트 시 <code>metric_status</code> 경보 + SMS 수신 / 월간 보고서에 내역 조회</li>
</ul>
</div>
@ -599,8 +598,8 @@ https://your-domain.example/raspi_leck_detecter/security_evidence.php</code></pr
<tr><td>API 403</td><td>서버 <code>API_KEY</code> ↔ 펌웨어 <code>APP_API_KEY</code> 일치, raw-body 서명</td></tr>
<tr><td>API 500</td><td>Cafe24 PHP 오류 로그, DB 접속 정보, 테이블 생성 여부</td></tr>
<tr><td>로그인 불가</td><td><code>ADMIN_PASSWORD_HASH</code>, <code>ADMIN_TOTP_SECRET</code>, 인증 앱 시간 동기</td></tr>
<tr><td>SMS 미발송</td><td>Cafe24 SMS 계정·secure key·발신번호 등록·<b>잔액</b>, <code>sms_log</code>. 대시보드 ‘수집 거부/발송 실패’ 배너 확인</td></tr>
<tr><td>온습도 미표시</td><td>I²C 배선(PB6/PB7, 0x44), 4.7kΩ 풀업, <code>sensor_metric</code>, 펌웨어 측정 로그</td></tr>
<tr><td>SMS 미발송</td><td>Cafe24 SMS 계정·secure key·발신번호 등록·<b>잔액</b>, <code>leak_sms_log</code>. 대시보드 ‘수집 거부/발송 실패’ 배너 확인</td></tr>
<tr><td>온습도 미표시</td><td>I²C 배선(PB6/PB7, 0x44), 4.7kΩ 풀업, <code>leak_sensor_metric</code>, 펌웨어 측정 로그</td></tr>
<tr><td>TLS 실패</td><td><code>server_ca.c</code> 실제 CA 반영, <b>SNTP 시간 동기</b>(<code>APP_SNTP_SERVER</code>=LAN NTP)</td></tr>
<tr><td>데이터 전혀 안 들어옴</td><td>대시보드 <b>수집 거부 사유 카운터</b>(서명불일치→키/펌웨어, 시각오차→SNTP/RTC)로 원인 분별</td></tr>
<tr><td>오프라인 알림 과다/지연</td><td><code>HEARTBEAT_TIMEOUT_SEC</code>(1200초), cron 주기</td></tr>

View file

@ -281,13 +281,12 @@ return [
<div class="step">
<h3>스키마 실행</h3>
<p><b>신규 설치</b>는 <code>sql/schema_sht30.sql</code> 전체를 복사해 <span class="kbd">SQL</span> 탭에 붙여넣고 <span class="kbd">실행</span>.
생성 테이블: <code>sensor_log</code>, <code>sensor_status</code>, <code>sensor_metric</code>, <code>sms_log</code>
(+ 재전송 방어용 <code>ingest_nonce</code>).</p>
생성 테이블: <code>leak_sensor_log</code>, <code>leak_sensor_status</code>, <code>leak_sensor_metric</code>, <code>leak_sms_log</code>
(+ 재전송 방어용 <code>leak_ingest_nonce</code>).</p>
<table>
<thead><tr><th>SQL 파일</th><th>적용 상황</th></tr></thead>
<tbody>
<tr><td><code>sql/schema_sht30.sql</code></td><td>신규 설치(통합 스키마)</td></tr>
<tr><td><code>sql/migration_drop_leak.sql</code></td><td>기존 누수(v2605) 설치 → 온습도 전용 전환. <b>비가역 — 실행 전 DB 전체 백업</b></td></tr>
</tbody>
</table>
<div class="crit"><b>중복 실행 주의.</b> 이미 적용한 마이그레이션을 다시 실행하면 오류가 날 수 있습니다.
@ -301,7 +300,7 @@ return [
<div class="ph-title">캡처 위치 — 그림 3 <span class="fname">→ screenshots/03-db-phpmyadmin.png</span></div>
<div class="ph-what">phpMyAdmin <b>SQL 탭에 <code>schema_sht30.sql</code> 붙여넣고 [실행]한 결과</b>(“쿼리 실행 성공” + 좌측 4~5개 테이블 생성된 화면)</div>
</div>
<figcaption>그림 3. DB 스키마 실행 결과 (sensor_log/sensor_status/sensor_metric/sms_log/ingest_nonce)</figcaption>
<figcaption>그림 3. DB 스키마 실행 결과 (leak_sensor_log/leak_sensor_status/leak_sensor_metric/leak_sms_log/leak_ingest_nonce)</figcaption>
</figure>
</section>
@ -575,7 +574,7 @@ https://your-domain.example/raspi_leck_detecter/security_evidence.php</code></pr
<ul style="margin:6px 0 0;">
<li><code>setup_wizard.php</code> 필수 점검 항목 통과 + 테스트 SMS 수신</li>
<li><code>dashboard.php</code> 에 최신 온/습도와 센서 상태(<code>sensor_id=2</code>) 표시</li>
<li><code>sensor_metric</code> 에 주기 측정값(periodic) 저장 · STM32 콘솔에 서버 200 기록</li>
<li><code>leak_sensor_metric</code> 에 주기 측정값(periodic) 저장 · STM32 콘솔에 서버 200 기록</li>
<li>임계 초과 테스트 시 <code>metric_status</code> 경보 + SMS 수신 / 월간 보고서에 내역 조회</li>
</ul>
</div>
@ -599,8 +598,8 @@ https://your-domain.example/raspi_leck_detecter/security_evidence.php</code></pr
<tr><td>API 403</td><td>서버 <code>API_KEY</code> ↔ 펌웨어 <code>APP_API_KEY</code> 일치, raw-body 서명</td></tr>
<tr><td>API 500</td><td>Cafe24 PHP 오류 로그, DB 접속 정보, 테이블 생성 여부</td></tr>
<tr><td>로그인 불가</td><td><code>ADMIN_PASSWORD_HASH</code>, <code>ADMIN_TOTP_SECRET</code>, 인증 앱 시간 동기</td></tr>
<tr><td>SMS 미발송</td><td>Cafe24 SMS 계정·secure key·발신번호 등록·<b>잔액</b>, <code>sms_log</code>. 대시보드 ‘수집 거부/발송 실패’ 배너 확인</td></tr>
<tr><td>온습도 미표시</td><td>I²C 배선(PB6/PB7, 0x44), 4.7kΩ 풀업, <code>sensor_metric</code>, 펌웨어 측정 로그</td></tr>
<tr><td>SMS 미발송</td><td>Cafe24 SMS 계정·secure key·발신번호 등록·<b>잔액</b>, <code>leak_sms_log</code>. 대시보드 ‘수집 거부/발송 실패’ 배너 확인</td></tr>
<tr><td>온습도 미표시</td><td>I²C 배선(PB6/PB7, 0x44), 4.7kΩ 풀업, <code>leak_sensor_metric</code>, 펌웨어 측정 로그</td></tr>
<tr><td>TLS 실패</td><td><code>server_ca.c</code> 실제 CA 반영, <b>SNTP 시간 동기</b>(<code>APP_SNTP_SERVER</code>=LAN NTP)</td></tr>
<tr><td>데이터 전혀 안 들어옴</td><td>대시보드 <b>수집 거부 사유 카운터</b>(서명불일치→키/펌웨어, 시각오차→SNTP/RTC)로 원인 분별</td></tr>
<tr><td>오프라인 알림 과다/지연</td><td><code>HEARTBEAT_TIMEOUT_SEC</code>(1200초), cron 주기</td></tr>

View file

@ -2,6 +2,8 @@
이 문서는 서버실 온습도(SHT30) 모니터링 시스템 v2606을 실제 운영 환경에 배포할 때 STM32F407VGT6 펌웨어와 Cafe24 PHP/MySQL 서버에 각각 무엇을 설치하고 설정해야 하는지 정리한 설치용 문서입니다.
> **참고:** 이 문서는 **STM32 펌웨어 중심**의 설치 문서입니다. Raspberry Pi 4 모듈(누수+카메라 / SHT30) 배포자는 [docs/DEPLOYMENT_PLAN_RPI4x3.md](./DEPLOYMENT_PLAN_RPI4x3.md)와 [SYSTEM_OVERVIEW.md](../SYSTEM_OVERVIEW.md)를 먼저 참고하세요. 서버 설치 절차(§3~§6)는 공통입니다.
## 1. 설치 대상
| 구분 | 설치 위치 | 역할 |
@ -28,19 +30,20 @@
## 3. 서버에 설치할 파일
Cafe24 웹 루트 아래에 다음 구조로 PHP 파일을 업로드합니다.
Cafe24 웹 루트 아래에 **repo 의 `php/` 트리 전체를 그대로 업로드**합니다. 개별 파일을 선별해 올리면 누수 사진 파이프라인(`api/photo_upload.php`, `photo.php`, `var/photos/`)이 빠지기 쉽습니다.
```text
public_html/raspi_leck_detecter/
public_html/raspi_leck_detecter/ ← php/ 트리 전체 업로드
.htaccess
blocked.php
config.php
config.local.php
config.local.php ← 서버에서 example 복사 후 작성(저장소에 없음)
config.local.example.php
ops_checks.php
sms_send.php
cron_heartbeat.php
dashboard.php
photo.php ← 누수 사진 조회(관리자 인증 필요)
setup_mfa.php
admin_security.php
setup_wizard.php
@ -49,10 +52,17 @@ public_html/raspi_leck_detecter/
login.php
retention_cleanup.php
setup_hash.php
test_mobile.php
api/
sensor_data.php
photo_upload.php ← 누수 사진 업로드 API
var/
.gitkeep
.htaccess ← var/ 웹 직접 접근 차단(403)
index.html
photos/ ← 누수 사진 저장 폴더(.htaccess/index.html 포함,
.htaccess 없으면 첫 업로드 시 자동 생성 시도)
index.html
```
운영 서버에는 `config.local.php`가 반드시 필요합니다. `config.local.example.php`를 복사해서 만들고 실제 운영 값으로 교체합니다.
@ -113,16 +123,7 @@ https://your-domain.example/raspi_leck_detecter/setup_mfa.php?token=임시토큰
SOURCE /path/to/sql/schema_sht30.sql;
```
phpMyAdmin에서는 [schema_sht30.sql](../sql/schema_sht30.sql) 내용을 복사해서 SQL 실행 창에 붙여넣습니다. 생성 테이블은 `sensor_log`, `sensor_status`, `sensor_metric`, `sms_log`입니다.
기존 누수 설치를 온습도 전용으로 전환하는 경우에는 전환 마이그레이션을 적용합니다.
| SQL 파일 | 적용 상황 |
|---|---|
| [schema_sht30.sql](../sql/schema_sht30.sql) | 신규 설치(통합 스키마) |
| [migration_drop_leak.sql](../sql/migration_drop_leak.sql) | 기존 누수 설치 → 온습도 전용 전환(레거시 컬럼/테이블 정리, `sensor_metric` 보장) |
이미 적용된 마이그레이션을 중복 실행하면 DB 오류가 날 수 있으므로, 기존 설치에서는 `setup_wizard.php`의 점검 결과를 먼저 확인합니다.
phpMyAdmin에서는 [schema_sht30.sql](../sql/schema_sht30.sql) 내용을 복사해서 SQL 실행 창에 붙여넣습니다. 생성 테이블은 총 7개 — `leak_sensor_log`, `leak_sensor_status`, `leak_sensor_metric`, `leak_sms_log`, `leak_ingest_nonce`(재전송 방어), `leak_incident`, `leak_photo`(누수 사고/사진) — 이며, 신규 설치는 이 파일 하나로 완결됩니다(누수 컬럼/테이블 포함). 모든 테이블은 `leak_` 접두사를 사용해 같은 DB 의 다른 테이블과 격리됩니다.
## 5. 서버 권한 및 관리자 설정
@ -149,7 +150,7 @@ https://your-domain.example/raspi_leck_detecter/setup_wizard.php
점검 화면에서 확인할 항목은 다음과 같습니다.
- DB 연결
- 필수 테이블 존재 여부(`sensor_log`, `sensor_status`, `sensor_metric`, `sms_log`)
- 필수 테이블 존재 여부(`leak_sensor_log`, `leak_sensor_status`, `leak_sensor_metric`, `leak_sms_log`)
- API 키 기본값 교체 여부
- 관리자 해시 설정 여부
- 관리자 TOTP 설정 여부
@ -162,10 +163,12 @@ https://your-domain.example/raspi_leck_detecter/setup_wizard.php
장비 오프라인 감지를 위해 `cron_heartbeat.php`를 주기 실행합니다. Cafe24 cron 또는 외부 스케줄러에서 다음 URL을 1분 간격으로 호출합니다.
```text
https://your-domain.example/raspi_leck_detecter/cron_heartbeat.php
https://your-domain.example/raspi_leck_detecter/cron_heartbeat.php?key=<CRON_SECRET>
```
서버 CLI cron을 사용할 수 있다면 다음처럼 구성할 수 있습니다.
HTTP 호출에는 `?key=<CRON_SECRET>` 파라미터가 **필수**입니다(누락 시 403). `CRON_SECRET`은 `API_KEY`에서 파생되며 다음으로 계산합니다: `php -r "echo hash_hmac('sha256','cron','<API_KEY>');"`
서버 CLI cron을 사용할 수 있다면 다음처럼 구성할 수 있습니다(CLI 실행에는 key가 필요 없습니다).
```cron
* * * * * php /home/hosting_user/public_html/raspi_leck_detecter/cron_heartbeat.php
@ -223,7 +226,7 @@ https://your-domain.example/raspi_leck_detecter/security_evidence.php
- `dashboard.php`에 최신 온도/습도와 센서 상태(`sensor_id=2`)가 표시
- `security_evidence.php`에서 운영 점검 결과와 보안통제 매트릭스 조회 가능
- STM32 콘솔 로그에 서버 200 응답 기록 확인
- `sensor_metric`에 주기 측정값(periodic) 저장
- `leak_sensor_metric`에 주기 측정값(periodic) 저장
- 임계 초과 테스트 시 `metric_status` 경보 기록 및 SMS 수신
- 월간 보고서에서 임계 경보/복귀/오프라인 내역 조회 가능
@ -234,8 +237,8 @@ https://your-domain.example/raspi_leck_detecter/security_evidence.php
| API 403 | 서버 `API_KEY`와 펌웨어 `APP_API_KEY` 일치 여부, raw-body 서명 |
| API 500 | Cafe24 PHP 오류 로그, DB 접속 정보, 테이블 생성 여부 |
| 로그인 불가 | `ADMIN_PASSWORD_HASH`, `ADMIN_TOTP_SECRET`, 인증 앱 시간 동기화 |
| SMS 미발송 | Cafe24 SMS 계정, secure key, 발신번호 등록, 잔액, `sms_log` |
| 온습도 미표시 | I2C 배선(PB6/PB7, 0x44), 4.7kΩ 풀업, `sensor_metric` 테이블, 펌웨어 측정 로그 |
| SMS 미발송 | Cafe24 SMS 계정, secure key, 발신번호 등록, 잔액, `leak_sms_log` |
| 온습도 미표시 | I2C 배선(PB6/PB7, 0x44), 4.7kΩ 풀업, `leak_sensor_metric` 테이블, 펌웨어 측정 로그 |
| 임계 경보 미발송 | `config.php`/`config.local.php`의 `METRIC_*`, `METRIC_ALERT_COOLDOWN_SEC`(1800초) |
| TLS 실패 | `firmware/certs/server_ca.c` 실제 CA 반영, SNTP 시간 동기(인증서 유효기간 검증) |
| 오프라인 알림 과다/지연 | `HEARTBEAT_CHECK_INTERVAL_SEC`, `HEARTBEAT_TIMEOUT_SEC`(기본 1200초), cron 주기 |

View file

@ -10,7 +10,7 @@
| 주기 | 점검 목적 | 보관 증적 |
|---|---|---|
| 일일 | 온습도 임계 경보, 장비 오프라인, SMS 실패 여부 확인 | 대시보드 화면 또는 이상 없음 기록 |
| 주간 | STM32 보고 수신 상태, 최근 측정값, 측정 공백 여부 확인 | 대시보드 온습도 추이, `sensor_metric` 최근 기록 |
| 주간 | STM32 보고 수신 상태, 최근 측정값, 측정 공백 여부 확인 | 대시보드 온습도 추이, `leak_sensor_metric` 최근 기록 |
| 월간 | 보안통제 매트릭스 점검, 백업, 수신자 현행화 | 보안 증적 보고서, 월간 보고서, 백업 목록 |
| 월간 | 관리자 MFA, 감사로그, 보관기간 정리 확인 | MFA 등록 확인표, `php/var/admin_audit.log`, retention 실행 결과 |
| 변경 시 | API 키, 서버 경로, 수신자, 임계값, 네트워크 변경 검증 | 변경 승인 기록, 재점검 결과 |
@ -23,9 +23,9 @@
| 설치 점검 | `setup_wizard.php` 접속 | 중요 항목 0건 또는 조치계획 기록 | 설치 점검 캡처 |
| 보안 증적 | `security_evidence.php` 접속 | 운영 점검 결과와 보안통제 매트릭스 출력 | Markdown 다운로드 |
| 월간 보고 | `monthly_report.php` 조회 | 임계 경보, 복귀, 오프라인, SMS 현황 표시 | PDF 또는 CSV |
| 장비 상태 | `sensor_status`(`sensor_id=2`) | 최근 수신 시각이 `HEARTBEAT_TIMEOUT_SEC` 이내 | 대시보드 센서 카드 |
| 측정 수집 | `sensor_metric` | 5분 주기 측정값 누적 | 온습도 추이 캡처 |
| SMS | 최근 실패 | 반복 실패 없음 | `sms_log` 또는 대시보드 |
| 장비 상태 | `leak_sensor_status`(`sensor_id=2`) | 최근 수신 시각이 `HEARTBEAT_TIMEOUT_SEC` 이내 | 대시보드 센서 카드 |
| 측정 수집 | `leak_sensor_metric` | 5분 주기 측정값 누적 | 온습도 추이 캡처 |
| SMS | 최근 실패 | 반복 실패 없음 | `leak_sms_log` 또는 대시보드 |
| 임계 설정 | `METRIC_*` | 기관 기준과 일치 | `config.local.php` 발췌(값만) |
| 백업 | DB/설정 백업 | 정해진 주기대로 백업 존재 | 백업 파일 목록 |
| 관리자 MFA | `ADMIN_TOTP_SECRET` | 인증 앱 등록 완료 | MFA 등록 확인표 |
@ -65,7 +65,7 @@ USART3(PD8/PD9, 115200) 콘솔 로그로 다음을 확인합니다.
|---|---|---|
| 임계 초과 감지 | 측정값이 임계 초과 시 `metric_status` 경보가 기록되는가 | 감지 시각/항목 |
| SMS 발송 | 수신자가 임계 경보 SMS를 받는가 | 수신 시각 |
| 쿨다운 | 동일 종류 경보가 30분 내 중복 발송되지 않는가 | `sms_log` 간격 |
| 쿨다운 | 동일 종류 경보가 30분 내 중복 발송되지 않는가 | `leak_sms_log` 간격 |
| 정상복귀 | 히스테리시스 포함 정상 회복 시 복귀 SMS 1회 발송되는가 | 복귀 시각 |
| 보고서 반영 | 월간 보고서에 경보/복귀가 반영되는가 | 보고서 파일 |
@ -76,7 +76,7 @@ USART3(PD8/PD9, 115200) 콘솔 로그로 다음을 확인합니다.
| 임계 경보 발생 | 당일 원인 확인 후 조치 결과 기록(공조/환기/장비 점검) |
| SMS 반복 실패 | Cafe24 SMS 잔액, 발신번호, secure key, 수신자 확인 |
| 장비 오프라인 | 전원, LAN 연결, PHY 링크, 펌웨어 콘솔 로그 확인 |
| 온습도 미수집 | I2C 배선(PB6/PB7, 0x44), 4.7kΩ 풀업, `sensor_metric` 테이블 확인 |
| 온습도 미수집 | I2C 배선(PB6/PB7, 0x44), 4.7kΩ 풀업, `leak_sensor_metric` 테이블 확인 |
| HTTPS/TLS 미확인 | Cafe24 SSL 설정, 펌웨어 CA 반영, SNTP 시간 동기 확인 |
| 백업 미수행 | 백업 주기 재설정 및 다음 월 점검에 포함 |

View file

@ -51,8 +51,7 @@ php/
blocked.php # 차단 안내 화면
api/sensor_data.php # STM32 센서 데이터 수신
sql/
schema_sht30.sql # 신규 설치용 통합 스키마
migration_drop_leak.sql # 기존 누수 설치 → 온습도 전용 전환 마이그레이션
schema_sht30.sql # 신규 설치용 통합 스키마 (이 파일 하나로 완결)
_backup/
pre_v2605_20260519/ # 정리 전 원본 백업
```
@ -69,7 +68,7 @@ _backup/
SOURCE /path/to/sql/schema_sht30.sql;
```
phpMyAdmin에서 실행할 때는 `sql/schema_sht30.sql` 내용을 복사해 실행합니다. 생성 테이블은 `sensor_log`, `sensor_status`, `sensor_metric`, `sms_log`입니다.
phpMyAdmin에서 실행할 때는 `sql/schema_sht30.sql` 내용을 복사해 실행합니다. 생성 테이블은 `leak_sensor_log`, `leak_sensor_status`, `leak_sensor_metric`, `leak_sms_log`입니다.
3. PHP 파일을 업로드합니다.
@ -126,29 +125,21 @@ _backup/
* * * * * php /home/hosting_user/public_html/raspi_leck_detecter/cron_heartbeat.php
```
## 기존 누수 설치 → 온습도 전용 전환
## 설치 스키마
1. 현재 운영 파일과 DB를 백업합니다.
2. `php/config.local.example.php` 기준으로 `config.local.php`를 갱신합니다(METRIC_* 임계 확인).
3. DB에 전환 마이그레이션을 적용합니다.
```sql
SOURCE /path/to/sql/migration_drop_leak.sql;
```
이 마이그레이션은 레거시 누수 컬럼/테이블(`leak_photo`, `leak_incident`, `is_leak` 등)을 정리하고 `sensor_metric`을 보장합니다.
4. 신규 설치라면 위 마이그레이션 대신 `sql/schema_sht30.sql`만 사용합니다.
신규 설치는 `sql/schema_sht30.sql` 하나만 실행합니다(누수+온습도 통합, 테이블 전체 `leak_` 접두사).
구버전 무접두 테이블 대상 마이그레이션 파일들은 v2607 테이블 리네임과 함께 폐기되었습니다(git 이력 참고).
## 이벤트 정책
| 이벤트 | 조건 | DB | SMS |
|---|---|---|---|
| `startup` | 기기 시작 첫 측정 | `sensor_log` | X |
| `periodic` | 정상 주기 측정(5분) | `sensor_metric` | X |
| 고온/저온/고습/저습 경보 | 측정값이 임계 초과 | `sensor_metric.metric_status` | O (종류별 30분 쿨다운) |
| 정상복귀 | 경보 후 히스테리시스 포함 정상 회복 | `sensor_metric` | O (1회) |
| 장비 오프라인 | 마지막 수신 후 `HEARTBEAT_TIMEOUT_SEC`(기본 1200초=20분) 초과 | `sensor_status` | O |
| 장비 복구 | 오프라인 후 재수신 | `sensor_status` | O |
| `startup` | 기기 시작 첫 측정 | `leak_sensor_log` | X |
| `periodic` | 정상 주기 측정(5분) | `leak_sensor_metric` | X |
| 고온/저온/고습/저습 경보 | 측정값이 임계 초과 | `leak_sensor_metric.metric_status` | O (종류별 30분 쿨다운) |
| 정상복귀 | 경보 후 히스테리시스 포함 정상 회복 | `leak_sensor_metric` | O (1회) |
| 장비 오프라인 | 마지막 수신 후 `HEARTBEAT_TIMEOUT_SEC`(기본 1200초=20분) 초과 | `leak_sensor_status` | O |
| 장비 복구 | 오프라인 후 재수신 | `leak_sensor_status` | O |
## 운영 기준
@ -161,7 +152,7 @@ _backup/
- 로그인 성공/실패, 로그아웃, MFA 등록 검증은 `php/var/admin_audit.log`에 감사로그로 기록합니다.
- heartbeat 체크 실행 주기와 오프라인 판정 시간(`HEARTBEAT_TIMEOUT_SEC`, 기본 1200초)은 분리되어 있습니다.
- 대시보드는 위험 상태를 첫 화면에 우선 표시하고, 최신 온습도, 운영 자가진단, 30일 운영 요약을 함께 보여줍니다.
- 운영 자가진단은 API 키, 관리자 해시, SMS 수신자, 필수 테이블(`sensor_metric` 포함), 상태 파일 폴더 권한, 최근 SMS 실패를 확인합니다.
- 운영 자가진단은 API 키, 관리자 해시, SMS 수신자, 필수 테이블(`leak_sensor_metric` 포함), 상태 파일 폴더 권한, 최근 SMS 실패를 확인합니다.
- 월간 보고서는 온습도 임계 경보, 정상복귀, 오프라인/복구, SMS 현황을 조회하고 CSV/인쇄/PDF로 출력할 수 있습니다.
- 보관기간 정리는 `php/retention_cleanup.php --dry-run`으로 삭제 대상을 먼저 확인한 뒤, 운영 백업 완료 후 `--execute`로 수행합니다.
- 실행-검토 과정에서 나온 실수와 교훈은 `docs/solutions/`에 남겨 다음 개선 사이클에서 반복하지 않습니다.
@ -198,10 +189,10 @@ php scripts/backup_evidence.php --backup-dir /path/to/backups --restore-test "YY
| 증상 | 확인 지점 |
|---|---|
| API 403 | 펌웨어 `APP_API_KEY`와 서버 `API_KEY` 일치 여부, raw-body 서명 |
| API 500 | Cafe24 PHP 오류 로그, DB 접속 정보, `sensor_metric` 등 테이블 생성 여부 |
| API 500 | Cafe24 PHP 오류 로그, DB 접속 정보, `leak_sensor_metric` 등 테이블 생성 여부 |
| 로그인 불가 | `config.local.php`의 `ADMIN_PASSWORD_HASH`, `ADMIN_TOTP_SECRET`, 인증 앱 시간 동기화 |
| SMS 미발송 | Cafe24 SMS 인증값, 발신번호 등록, 잔액, `sms_log` |
| 온습도 미표시 | I2C 배선(PB6/PB7, 0x44), 4.7kΩ 풀업, `sensor_metric` 테이블, 펌웨어 측정 로그 |
| SMS 미발송 | Cafe24 SMS 인증값, 발신번호 등록, 잔액, `leak_sms_log` |
| 온습도 미표시 | I2C 배선(PB6/PB7, 0x44), 4.7kΩ 풀업, `leak_sensor_metric` 테이블, 펌웨어 측정 로그 |
| 임계 경보 미발송 | `config.php`/`config.local.php`의 `METRIC_*` 임계, `METRIC_ALERT_COOLDOWN_SEC`(30분) |
| 오프라인 알림 지연 | `HEARTBEAT_CHECK_INTERVAL_SEC`, `HEARTBEAT_TIMEOUT_SEC`(기본 1200초) |
| 대시보드 진단 경고 | `config.local.php`, DB 마이그레이션, `php/var` 권한 |

View file

@ -14,9 +14,9 @@
| raw-body API 서명 검증 (HMAC-SHA256) | `config.php` `verify_signature_raw()`=`hash_hmac('sha256', $raw_body, API_KEY)`, 펌웨어 `sig.c`/`secrets.h` (`X-Signature`) | 반영 (서명 방식 정정 후 일치, 아래 §2.1 참조) |
| 측정값 범위 검증 | `api/sensor_data.php` (온도 -40~125℃ / 습도 0~100%) | 반영 |
| 서버 임계 판정·쿨다운 | `config.php` `METRIC_*`(고온30/저온10·고습70/저습20, 30분 쿨다운, 히스테리시스), `api/sensor_data.php` | 반영 |
| 임계 경보/정상복귀 SMS | `api/sensor_data.php`, `sms_log` (`[고온경보]`/`[저온경보]`/`[고습경보]`/`[저습경보]`) | 반영 |
| 장비 오프라인/복구 SMS | `cron_heartbeat.php`, `sensor_status.offline_alerted`, `sms_log` | 반영 |
| 측정 이력 저장 | `sensor_metric` 테이블 | 반영 |
| 임계 경보/정상복귀 SMS | `api/sensor_data.php`, `leak_sms_log` (`[고온경보]`/`[저온경보]`/`[고습경보]`/`[저습경보]`) | 반영 |
| 장비 오프라인/복구 SMS | `cron_heartbeat.php`, `leak_sensor_status.offline_alerted`, `leak_sms_log` | 반영 |
| 측정 이력 저장 | `leak_sensor_metric` 테이블 | 반영 |
| 보관기간 정리 | `retention_cleanup.php --dry-run`, `--execute` (SMS/센서/측정/감사로그) | 반영 |
| 백업·복구 증빙 | `scripts/backup_evidence.php` | 반영 |
| 보안 증적 보고서 | `security_evidence.php?format=md` | 반영 |

View file

@ -16,7 +16,7 @@
| 5 | 네트워크 증적 | 방화벽 정책, 단말 인바운드 차단 확인, HTTPS 인증서 화면 |
| 6 | 물리 설치 증적 | STM32 보드, SHT30 센서 설치 사진과 장비 ID |
| 7 | 관리자 접근통제 증적 | MFA 등록 화면, 로그인 화면, 감사로그 존재 확인 |
| 8 | 측정/임계 경보 증적 | 측정값 저장(`sensor_metric`), 종류별 경보·정상복귀 SMS(`sms_log`) 화면 |
| 8 | 측정/임계 경보 증적 | 측정값 저장(`leak_sensor_metric`), 종류별 경보·정상복귀 SMS(`leak_sms_log`) 화면 |
| 9 | 운영 절차 증적 | 운영·보안 점검표, 보관기간 정리 dry-run, 백업 파일 목록, 수신자 현행화 확인 |
| 10 | 소스코드 기반 설명 | `SOURCE_SECURITY_EVIDENCE.md`의 기술통제 설명 |
@ -79,7 +79,7 @@ security-evidence-YYYYMMDD/
- [ ] `security_evidence.php` 점검 결과에 중요 실패 항목이 있으면 조치 결과 또는 예외 사유를 적는다.
- [ ] 단말 증적에는 환경변수 존재 여부만 있고 원문 값은 없다.
- [ ] MFA 증빙은 운영 비밀키가 아닌 증빙용 임시 키 또는 마스킹된 키를 사용한다.
- [ ] 측정/경보 증빙은 측정값 저장(`sensor_metric`)과 종류별 경보·정상복귀 SMS(`sms_log`)를 함께 보여준다.
- [ ] 측정/경보 증빙은 측정값 저장(`leak_sensor_metric`)과 종류별 경보·정상복귀 SMS(`leak_sms_log`)를 함께 보여준다.
- [ ] 실제 도메인은 `https://`로 접속되는 화면을 포함한다.
- [ ] 네트워크 증적에는 단말로 들어오는 불필요한 포트가 없다는 근거가 포함된다.
- [ ] 월간 보고서에는 생성 시각과 보고 범위가 보인다.

View file

@ -210,7 +210,7 @@ DB 백업 주기: 매일 / 매주 / 매월
| 관리자 인증 | 관리자 로그인 화면, MFA 등록 증빙, 감사로그 | 비밀번호 해시, TOTP MFA, 세션 보호, 실패 제한 코드 설명 |
| 측정/임계 | 임계 운영 기준(`METRIC_*`) | 측정값 범위 검증, 서버 임계 판정·쿨다운 설명 |
| 개인정보 | SMS 수신자 관리 기준 | SMS 설정 구조, 로그 테이블 설명 |
| 로그관리 | 로그 보관기간, 관리자 감사로그 | `sensor_log`, `sensor_metric`, `sms_log`, `php/var/admin_audit.log` 설명 |
| 로그관리 | 로그 보관기간, 관리자 감사로그 | `leak_sensor_log`, `leak_sensor_metric`, `leak_sms_log`, `php/var/admin_audit.log` 설명 |
| 백업 | DB/설정 백업 주기, 복구 테스트 결과 | `backup_evidence.php` 결과와 백업·복구 문구 |
| 패치 | 월간 패치 점검 담당자 | 점검표 양식 |
| 물리보안 | 설치 사진, 자산 정보 | 자산대장 양식 |

View file

@ -14,7 +14,7 @@
현재 HWP 보안대책서는 사업 목적, 추진체계, 관리적·물리적·기술적 보안대책의 큰 틀은 갖추고 있습니다. 다만 실제 구현된 시스템과 비교하면 본문이 과거 범위(화재·누수·현장 사진·사고 대응)를 기준으로 작성되어 있어, 현재 구현 범위인 **서버실 온습도(SHT30) 모니터링 + 임계 SMS 경보 + 장비 오프라인 알림**으로 정정해야 합니다.
- 사업 범위가 `화재/누수/장비 전원 이상`으로 넓게 쓰여 있으나, 현재 구현은 `서버실 온습도(SHT30) 측정 + 임계(고온/저온/고습/저습) SMS 경보 + 정상복귀 SMS + 장비 오프라인/복구 SMS + 대시보드`입니다. 누수센서·카메라·현장 사진 기능은 제거되었습니다.
- 구성도에는 큰 흐름이 있으나, 실제 구성요소인 STM32 보드, SHT30 온습도 센서, Cafe24 PHP API, MySQL, 측정 이력 테이블(`sensor_metric`), SMS 이력(`sms_log`), 월간 보고서가 명확히 표현되어 있지 않습니다.
- 구성도에는 큰 흐름이 있으나, 실제 구성요소인 STM32 보드, SHT30 온습도 센서, Cafe24 PHP API, MySQL, 측정 이력 테이블(`leak_sensor_metric`), SMS 이력(`leak_sms_log`), 월간 보고서가 명확히 표현되어 있지 않습니다.
- API 키, 서명 검증, `config.local.php`, `firmware/common/secrets.h`(`APP_API_KEY`), 서버 임계 판정(`config.php`의 `METRIC_*`), 관리자 TOTP MFA, 관리자 감사로그 같은 실제 보안 구현 항목이 본문에 구체적으로 반영되어야 합니다.
- 로그 보관기간, SMS 수신자 개인정보 관리, `retention_cleanup.php` 기반 정리 절차, 백업/복구 증빙, 장애 대응 절차가 선언 수준에 머물러 있어 운영 기준표가 필요합니다.
- '현장 사진/시설정보'는 더 이상 처리하지 않으므로 해당 절은 삭제하고, 통제 공백처럼 보이지 않도록 **온습도 임계 경보 + 오발송 방지(30분 쿨다운 / 복구 히스테리시스)**로 대체 서술합니다.
@ -35,7 +35,7 @@
| 권한 및 계정관리 (L106~L110) | 관리자 계정 최소 부여, 비밀번호 정책 | `ADMIN_PASSWORD_HASH`, `ADMIN_TOTP_SECRET`, `MFA_SETUP_TOKEN`, 세션 쿠키 옵션, 로그인 실패 잠금, 감사로그, 비밀번호/TOTP 교체 절차 추가 | 높음 |
| 단말기 보안대책 (L135~L143) | 기본 계정 변경, 하드코딩 금지 | 비밀값은 `firmware/common/secrets.h`(`APP_API_KEY`)에 두고 미커밋(.gitignore)으로 빌드 머신에서만 보관, API 키 서버 `config.php`의 `API_KEY`와 일치, 불필요 기능 비활성화 추가. (카메라 권한 관리 문구 불필요) | 높음 |
| 플랫폼/서비스 보안대책 (L150~L156) | 인증, 패치, 기본 서비스 제거 | API 서명 검증, 측정값 범위 검증(온도 -40~125℃ / 습도 0~100%), DB prepared statement, TOTP MFA, `config.local.php` 비공개 관리 추가. (사진 업로드/MIME/매직바이트 검증 문구 삭제) | 높음 |
| 로그관리 (L94~L95) | 로그 보관기간 정의, 위변조 방지 | `sensor_log`, `sensor_metric`, `sms_log`, 서버 `php/var/admin_audit.log`, `retention_cleanup.php` 기준으로 보관·점검 기준 추가. (`leak_incident`/`leak_photo`/Pi 로그 삭제) | 높음 |
| 로그관리 (L94~L95) | 로그 보관기간 정의, 위변조 방지 | `leak_sensor_log`, `leak_sensor_metric`, `leak_sms_log`, 서버 `php/var/admin_audit.log`, `retention_cleanup.php` 기준으로 보관·점검 기준 추가. (`leak_incident`/`leak_photo`/Pi 로그 삭제) | 높음 |
| 장비 설치·보호 (L114, L118~L120) | "화재·누수 감지센서, IoT 보드 및 **카메라**" | "SHT30 온습도 센서, STM32 보드"로 교체. 카메라 문구 삭제. 자산대장 등록은 유지 | 높음 |
## 3. HWP에 추가해야 할 장·절
@ -50,7 +50,7 @@
| SHT30 온습도 센서 | I2C 온습도 측정(5분 주기) | 서버실 고정 설치, 임의 탈거 방지, 자산대장 등록 |
| PHP API | `api/sensor_data.php` | API 키 서명 검증, HTTPS, 측정값 범위 검증, 서버 임계 판정 |
| 임계 판정 | `config.php`의 `METRIC_*` 상수 | 고온30/저온10℃·고습70/저습20% 임계, 복구 히스테리시스(1.0℃/3.0%), 30분 쿨다운 |
| MySQL DB | `sensor_log`, `sensor_status`, `sensor_metric`, `sms_log` | 최소 권한 DB 계정, 백업, 보관기간 관리 |
| MySQL DB | `leak_sensor_log`, `leak_sensor_status`, `leak_sensor_metric`, `leak_sms_log` | 최소 권한 DB 계정, 백업, 보관기간 관리 |
| 대시보드 | `dashboard.php` | 관리자 로그인, TOTP MFA, 세션 보호, 온습도/경보 현황 표시 |
| MFA 등록 | `setup_mfa.php` | 임시 등록 토큰, 인증 앱 등록 검증, 외부 QR/API 미사용 |
| 설치 점검 | `setup_wizard.php` | DB/권한/SMS/임계 설정 점검 |
@ -69,7 +69,7 @@
2. STM32 보드가 측정값을 수집하여 정상 보고(`periodic`/`startup`)를 구성한다.
3. 단말이 HTTPS로 PHP API에 측정값을 전송한다(인바운드 포트 미개방, 아웃바운드 POST만).
4. 서버가 API 서명, API 키 일치, 필수 필드, 측정값 범위(온도 -40~125℃ / 습도 0~100%)를 검증한다.
5. 서버가 운영 임계(`METRIC_*`)로 고온/저온/고습/저습을 재판정하여 `sensor_metric`에 저장하고, 정상 보고는 `sensor_status`를 갱신한다.
5. 서버가 운영 임계(`METRIC_*`)로 고온/저온/고습/저습을 재판정하여 `leak_sensor_metric`에 저장하고, 정상 보고는 `leak_sensor_status`를 갱신한다.
6. 임계 이탈 시 30분 쿨다운과 복구 히스테리시스를 적용하여 종류별 SMS를 발송하고, 임계 안쪽으로 회복하면 정상복귀 SMS를 발송한다.
7. heartbeat 타임아웃이 지나면 장비 오프라인 SMS를, 보고 재개 시 복구 SMS를 발송한다.
8. 대시보드와 월간 보고서에서 온습도·경보·SMS·오프라인 현황을 표시한다.
@ -85,12 +85,12 @@
| 단계 | 처리 내용 | 증적 |
|---|---|---|
| 측정 | SHT30 측정값 수신, `sensor_metric` 기록, 임계 판정 | `sensor_metric.metric_status` |
| 경보 | 임계 이탈 시 종류별 SMS 발송(쿨다운 적용) | `sms_log` (`[고온경보]`/`[저온경보]`/`[고습경보]`/`[저습경보]`) |
| 측정 | SHT30 측정값 수신, `leak_sensor_metric` 기록, 임계 판정 | `leak_sensor_metric.metric_status` |
| 경보 | 임계 이탈 시 종류별 SMS 발송(쿨다운 적용) | `leak_sms_log` (`[고온경보]`/`[저온경보]`/`[고습경보]`/`[저습경보]`) |
| 확인 | 담당자가 대시보드에서 온습도/경보 상태 확인 | 대시보드 캡처 |
| 조치 | 공조/환기 등 현장 점검 및 환경 정상화 | 운영 점검 기록 |
| 복구 | 임계 안쪽으로 회복 시 정상복귀 SMS | `sms_log` 복구 메시지 |
| 오프라인 | heartbeat 타임아웃 시 오프라인 SMS, 복구 시 복구 SMS | `cron_heartbeat.php` 실행 로그, `sms_log` |
| 복구 | 임계 안쪽으로 회복 시 정상복귀 SMS | `leak_sms_log` 복구 메시지 |
| 오프라인 | heartbeat 타임아웃 시 오프라인 SMS, 복구 시 복구 SMS | `cron_heartbeat.php` 실행 로그, `leak_sms_log` |
| 보고 | 월간 보고서에 경보·SMS·오프라인 현황 반영 | `monthly_report.php` |
### 3.4 설치·운영 점검 절차
@ -100,8 +100,8 @@
| 점검 항목 | 확인 방법 | 불합격 시 조치 |
|---|---|---|
| DB 연결 | 설치 점검 화면 | `config.local.php` DB 설정 수정 |
| 필수 테이블 | 설치 점검 화면 | `sql/schema_sht30.sql` 또는 `sql/migration_drop_leak.sql` 적용 |
| 측정 이력 테이블 | 설치 점검 화면 | `sensor_metric` 생성 확인 |
| 필수 테이블 | 설치 점검 화면 | `sql/schema_sht30.sql` 적용 |
| 측정 이력 테이블 | 설치 점검 화면 | `leak_sensor_metric` 생성 확인 |
| 임계 설정 | 설치 점검 화면 | `config.php`/`config.local.php`의 `METRIC_*` 값 확인 |
| API 키 | 설치 점검 화면 | 서버와 단말의 키 일치 확인 |
| 관리자 해시 | 설치 점검 화면 | `php setup_hash.php`로 해시 생성 |
@ -120,9 +120,9 @@ HWP의 "보관기간은 최소한으로 설정" 문구를 아래처럼 구체화
| 데이터 | 보관 위치 | 권장 보관기간 | 파기/정리 기준 |
|---|---|---:|---|
| 센서 이벤트 로그 | `sensor_log` | 365일 | 백업 후 기간 초과분 삭제(`retention_cleanup.php`) |
| 온습도 측정 이력 | `sensor_metric` | 365일 | 기간 초과분 삭제 |
| SMS 발송 로그 | `sms_log` | 365일 | 개인정보 최소화 후 기간 초과분 삭제 |
| 센서 이벤트 로그 | `leak_sensor_log` | 365일 | 백업 후 기간 초과분 삭제(`retention_cleanup.php`) |
| 온습도 측정 이력 | `leak_sensor_metric` | 365일 | 기간 초과분 삭제 |
| SMS 발송 로그 | `leak_sms_log` | 365일 | 개인정보 최소화 후 기간 초과분 삭제 |
| 관리자 감사로그 | `php/var/admin_audit.log` | 365일 | 기간 초과분 정리 |
| 설정 비밀값 | `config.local.php`, `firmware/common/secrets.h`(`APP_API_KEY`, 미커밋) | 운영 중 | 담당자 변경 또는 유출 의심 시 즉시 교체 |
@ -196,7 +196,7 @@ HWP의 "보관기간은 최소한으로 설정" 문구를 아래처럼 구체화
교체 문구:
> 본 시스템은 현장 사진 등 시설정보를 수집·저장하지 않으며, 처리하는 개인정보는 SMS 수신자 휴대전화번호로 한정한다. 수신자 번호는 알림 발송 목적에 한하여 최소 수집·이용하고, 담당자 변경 시 즉시 현행화한다. 온습도 임계 경보는 동일 종류 30분 쿨다운과 복구 히스테리시스(온도 1.0℃, 습도 3.0%)를 적용하여 경계 채터링과 SMS 오발송을 방지한다. SMS 발송 이력(`sms_log`)은 장애 대응·감사 목적 범위에서만 열람하고, 보관기간 경과 시 삭제 또는 비식별 처리한다.
> 본 시스템은 현장 사진 등 시설정보를 수집·저장하지 않으며, 처리하는 개인정보는 SMS 수신자 휴대전화번호로 한정한다. 수신자 번호는 알림 발송 목적에 한하여 최소 수집·이용하고, 담당자 변경 시 즉시 현행화한다. 온습도 임계 경보는 동일 종류 30분 쿨다운과 복구 히스테리시스(온도 1.0℃, 습도 3.0%)를 적용하여 경계 채터링과 SMS 오발송을 방지한다. SMS 발송 이력(`leak_sms_log`)은 장애 대응·감사 목적 범위에서만 열람하고, 보관기간 경과 시 삭제 또는 비식별 처리한다.
## 6. 추가 증적 목록

View file

@ -100,10 +100,10 @@ HWP 추가 문구:
| 전송구간 | 센서 데이터 탈취·변조 | HTTPS/TLS 사용, API 서명 검증 | `sensor_data.php`, 단말 펌웨어 | 실제 HTTPS 화면, `security_evidence.php?format=md` |
| API 인증 | 임의 측정값 주입 | 공유 API 키와 요청 서명 검증 | PHP API, `firmware/common/secrets.h`(`APP_API_KEY`) | 403 테스트 결과, 보안 증적 보고서 |
| 측정값 검증 | 비정상 측정값 저장 | 필수 필드·측정값 범위(온도 -40~125℃ / 습도 0~100%) 검증 | `sensor_data.php` | 비정상 요청 차단 테스트 결과 |
| 임계 경보 | 오발송·미발송 | 서버 임계 판정(`METRIC_*`), 30분 쿨다운, 복구 히스테리시스 | `config.php`, `sensor_data.php` | `sms_log`, `sensor_metric` 상태 |
| 임계 경보 | 오발송·미발송 | 서버 임계 판정(`METRIC_*`), 30분 쿨다운, 복구 히스테리시스 | `config.php`, `sensor_data.php` | `leak_sms_log`, `leak_sensor_metric` 상태 |
| 비밀값 | DB/SMS/API 키 유출 | 서버는 `config.local.php`, STM32 단말은 `firmware/common/secrets.h`(`APP_API_KEY`)로 분리, 저장소 미포함(.gitignore) | 서버/단말 설정 | STM32 콘솔 로그, .gitignore 설정, 저장소 검색 결과 |
| 관리자 인증 | 대시보드 무단 접근 | 비밀번호 해시, TOTP MFA, 세션 쿠키 보호, 로그인 실패 제한, 감사로그 | `login.php`, `setup_mfa.php`, `admin_security.php`, `dashboard.php`, `security_evidence.php` | 로그인 화면, MFA 등록 증빙, 감사로그, 보안 증적 보고서 |
| 개인정보 | SMS 수신자 번호 노출 | 최소 수집, 담당자 변경 시 현행화, 로그 접근 제한 | `config.local.php`, `sms_log` | 수신자 목록 관리대장 |
| 개인정보 | SMS 수신자 번호 노출 | 최소 수집, 담당자 변경 시 현행화, 로그 접근 제한 | `config.local.php`, `leak_sms_log` | 수신자 목록 관리대장 |
| 로그관리 | 추적 불가 | 센서/측정/SMS 로그와 관리자 감사로그 저장, 보관기간 정리 | MySQL, `php/var/admin_audit.log`, `retention_cleanup.php` | 월간 보고서, 단말 증적 보고서, 감사로그, retention dry-run 결과 |
| 백업 | 장애 시 복구 불가 | DB·설정 파일 백업, 복구 점검, 백업 증빙 생성 | 운영 절차, `scripts/backup_evidence.php` | 백업 이력, 복구 테스트 결과, backup-evidence 보고서 |
| 패치 | 취약한 OS/PHP 사용 | 단말 OS, PHP, DB 정기 업데이트 | 운영 절차 | `OPERATIONS_SECURITY_CHECKLIST.md`, 월간 점검표 |
@ -126,13 +126,13 @@ HWP에 아래 문구를 추가합니다.
### 5.2 측정값 검증 및 서버 임계 판정
> 센서 데이터 API는 POST JSON 요청과 필수 필드(장비 ID·센서 ID·이벤트 유형·타임스탬프)를 검증하고, 온도(-40~125℃)·습도(0~100%) 범위를 벗어난 값은 무효 처리한다. 임계 판정은 펌웨어가 아니라 서버 `config.php`의 운영 임계(`METRIC_*`: 고온30/저온10℃, 고습70/저습20%)로 재수행하여 `sensor_metric`에 상태를 기록한다. 동일 종류 경보는 30분 쿨다운, 복구는 히스테리시스(온도 1.0℃, 습도 3.0%)를 적용하여 SMS 오발송을 방지한다.
> 센서 데이터 API는 POST JSON 요청과 필수 필드(장비 ID·센서 ID·이벤트 유형·타임스탬프)를 검증하고, 온도(-40~125℃)·습도(0~100%) 범위를 벗어난 값은 무효 처리한다. 임계 판정은 펌웨어가 아니라 서버 `config.php`의 운영 임계(`METRIC_*`: 고온30/저온10℃, 고습70/저습20%)로 재수행하여 `leak_sensor_metric`에 상태를 기록한다. 동일 종류 경보는 30분 쿨다운, 복구는 히스테리시스(온도 1.0℃, 습도 3.0%)를 적용하여 SMS 오발송을 방지한다.
확인 증적:
- 정상 측정값 저장 결과(`sensor_metric`)
- 정상 측정값 저장 결과(`leak_sensor_metric`)
- 비정상 메소드/필드 누락/범위 초과 차단 결과
- 임계 초과 시 종류별 SMS, 회복 시 정상복귀 SMS 발송 이력(`sms_log`)
- 임계 초과 시 종류별 SMS, 회복 시 정상복귀 SMS 발송 이력(`leak_sms_log`)
### 5.3 관리자 페이지 접근통제
@ -164,7 +164,7 @@ STM32 단말 firmware/common/secrets.h: 저장소 미커밋(.gitignore), 빌드
확인 증적:
- `sensor_status` 마지막 보고 시각
- `leak_sensor_status` 마지막 보고 시각
- `cron_heartbeat.php` 실행 로그
- 오프라인 SMS 발송 이력
@ -213,7 +213,7 @@ HWP에 아래 책임표를 추가합니다.
|---|---|---:|---|
| SMS 수신자 번호 | 개인정보 | 담당자 재직/업무 담당 기간 | 담당자 변경 즉시 삭제·수정 |
| SMS 발송 로그 | 개인정보 포함 가능 | 1년 | DB 삭제 또는 비식별 처리 |
| 온습도 측정 이력 | 운영 데이터 | 1년 | 기간 초과 DB 삭제(`sensor_metric`) |
| 온습도 측정 이력 | 운영 데이터 | 1년 | 기간 초과 DB 삭제(`leak_sensor_metric`) |
| 센서 로그 | 운영 로그 | 1년 | 기간 초과 DB 삭제 |
| 관리자 감사로그 | 감사/운영 이력 | 1년 | 기간 초과분 정리 |
| API 키/SMS 인증키 | 인증정보 | 운영 중 | 교체 후 구 키 폐기 |
@ -266,9 +266,9 @@ HWP에 아래 책임표를 추가합니다.
| 정보 | 예시 | 목적 | 접근자 | 저장 위치 |
|---|---|---|---|---|
| 담당자 휴대전화번호 | SMS 수신자 번호 | 이상상태 알림 | 운영 관리자 | `config.local.php`, `sms_log` |
| 온습도 측정값 | 온도, 습도, 시각, 임계 상태 | 환경 모니터링 | 운영 관리자 | `sensor_metric`, MySQL |
| 센서 이벤트 | 센서 ID, 상태, 시각 | 장애 감지 | 운영 관리자 | `sensor_log`, MySQL |
| 담당자 휴대전화번호 | SMS 수신자 번호 | 이상상태 알림 | 운영 관리자 | `config.local.php`, `leak_sms_log` |
| 온습도 측정값 | 온도, 습도, 시각, 임계 상태 | 환경 모니터링 | 운영 관리자 | `leak_sensor_metric`, MySQL |
| 센서 이벤트 | 센서 ID, 상태, 시각 | 장애 감지 | 운영 관리자 | `leak_sensor_log`, MySQL |
| 관리자 계정 | 관리자 ID, 비밀번호 해시, TOTP 비밀키 | 대시보드 접근 | 운영 관리자 | `config.local.php`, `php/var/admin_audit.log` |
| 장비 식별자 | Device ID, 위치명 | 장비 관리 | 운영 관리자 | 단말 설정, DB |
@ -278,7 +278,7 @@ HWP에 아래 책임표를 추가합니다.
### 9.3 경보 오발송 방지 문구
> 본 시스템은 현장 사진 등 시설정보를 수집·저장하지 않는다. 온습도 임계 경보는 서버에서 운영 임계(`METRIC_*`)로 판정하며, 동일 종류 경보는 30분 쿨다운, 정상복귀 판정은 히스테리시스(온도 1.0℃, 습도 3.0%)를 적용하여 경계 채터링과 SMS 오발송을 방지한다. 임계 초과·정상복귀·장비 오프라인·복구 이력은 `sms_log`에 기록하며, 보관기간 경과 시 정리한다.
> 본 시스템은 현장 사진 등 시설정보를 수집·저장하지 않는다. 온습도 임계 경보는 서버에서 운영 임계(`METRIC_*`)로 판정하며, 동일 종류 경보는 30분 쿨다운, 정상복귀 판정은 히스테리시스(온도 1.0℃, 습도 3.0%)를 적용하여 경계 채터링과 SMS 오발송을 방지한다. 임계 초과·정상복귀·장비 오프라인·복구 이력은 `leak_sms_log`에 기록하며, 보관기간 경과 시 정리한다.
## 10. 보안성 검토 제출 관점의 필수 첨부물
@ -317,7 +317,7 @@ HWP 부록에 아래 체크리스트를 넣습니다.
| 11 | 장비 오프라인 감지가 동작하는가 | heartbeat 기준 알림 | | |
| 12 | DB 백업과 복구 점검을 수행했는가 | 복구 테스트 완료 | | |
| 13 | 단말이 부팅 후 자동 보고를 시작하는가 | 전원 인가 시 startup 보고·주기 보고 동작 | | |
| 14 | 온습도 측정·저장이 검증되었는가 | `sensor_metric` 측정 이력 저장 | | |
| 14 | 온습도 측정·저장이 검증되었는가 | `leak_sensor_metric` 측정 이력 저장 | | |
| 15 | 월간 보고서가 생성되는가 | 월별 조회/CSV 가능 | | |
| 16 | 관리자 감사로그가 남는가 | 로그인 성공/실패, 로그아웃 기록 | | |
| 17 | 보관기간 정리 절차가 있는가 | `retention_cleanup.php --dry-run` 결과 확인 | | |
@ -336,8 +336,8 @@ HWP 부록에 아래 체크리스트를 넣습니다.
| 월간 보고서 화면 캡처 | 경보 처리 및 SMS/온습도 현황 확인 |
| API 정상/비정상 테스트 로그 | 정상 요청 저장, 비정상 키 거부 확인 |
| 테스트 SMS 수신 화면 | 담당자 알림 경로 확인 |
| 측정값 저장 결과 | `sensor_metric` 기록 및 임계 상태 확인 |
| 임계 경보/복구 SMS 결과 | 종류별 경보·정상복귀 SMS 발송(`sms_log`) 확인 |
| 측정값 저장 결과 | `leak_sensor_metric` 기록 및 임계 상태 확인 |
| 임계 경보/복구 SMS 결과 | 종류별 경보·정상복귀 SMS 발송(`leak_sms_log`) 확인 |
| 관리자 감사로그 | 로그인 성공/실패, 로그아웃 기록 확인 |
| 보관기간 정리 dry-run | 측정/로그/감사로그 정리 대상 확인 |
| 백업·복구 증빙 보고서 | 백업 파일 목록, 설정/감사로그 존재, 복구 테스트 결과 |

View file

@ -10,12 +10,6 @@
SOURCE sql/schema_sht30.sql;
```
기존 누수 설치를 온습도 전용으로 전환하는 경우에는 전환 마이그레이션을 적용합니다.
```sql
SOURCE sql/migration_drop_leak.sql;
```
2. 서버 파일을 배포합니다.
```text
@ -26,7 +20,7 @@
sql/schema_sht30.sql
```
API는 `sht30` 측정을 수신하면 `sensor_status`를 갱신하고 측정값을 `sensor_metric`에 저장합니다. 서버가 `METRIC_*` 임계로 고온/저온/고습/저습을 판정하고, 초과 시 종류별 30분 쿨다운으로 SMS를 발송합니다.
API는 `sht30` 측정을 수신하면 `leak_sensor_status`를 갱신하고 측정값을 `leak_sensor_metric`에 저장합니다. 서버가 `METRIC_*` 임계로 고온/저온/고습/저습을 판정하고, 초과 시 종류별 30분 쿨다운으로 SMS를 발송합니다.
## 2. 기본 구성 — STM32F407VGT6 (`sht30_fw`)
@ -112,8 +106,8 @@ SHT30_I2C_ADDR=0x44
첫 정상 보고 후:
- `sensor_status`에 `sensor_id=2` 항목이 생성됩니다.
- `sensor_metric`에 온도/습도 이력이 누적됩니다.
- `leak_sensor_status`에 `sensor_id=2` 항목이 생성됩니다.
- `leak_sensor_metric`에 온도/습도 이력이 누적됩니다.
- `dashboard.php`가 센서 카드에 최신 온도/습도와 임계 상태를 표시합니다.
장비가 `HEARTBEAT_TIMEOUT_SEC`(기본 1200초=20분)을 초과해 보고를 멈추면 서버가 오프라인으로 판정하고 SMS를 발송합니다.

View file

@ -103,14 +103,14 @@ HWP 반영 문구:
사용자가 제공해야 하는 증적:
- 정상 측정값 저장 화면 또는 DB 기록(`sensor_metric`)
- 정상 측정값 저장 화면 또는 DB 기록(`leak_sensor_metric`)
- 비정상 메소드/필드 누락/범위 초과 요청 차단 테스트 결과
### 2.4 서버 임계 판정 및 SMS 오발송 방지
HWP 반영 문구:
> 온습도 임계 판정은 펌웨어가 아니라 서버에서 수행한다. 서버는 `config.php`의 운영 임계(`METRIC_*`: 고온30/저온10℃, 고습70/저습20%)로 고온·저온·고습·저습을 재판정하여 `sensor_metric.metric_status`에 기록하고, 임계 이탈 시 종류별 SMS를 발송한다. 동일 종류 경보는 30분 쿨다운을 적용하고, 정상복귀 판정은 히스테리시스(온도 1.0℃, 습도 3.0%)를 적용하여 경계 채터링과 SMS 오발송을 방지한다. 임계 안쪽으로 회복하면 정상복귀 SMS를 발송한다.
> 온습도 임계 판정은 펌웨어가 아니라 서버에서 수행한다. 서버는 `config.php`의 운영 임계(`METRIC_*`: 고온30/저온10℃, 고습70/저습20%)로 고온·저온·고습·저습을 재판정하여 `leak_sensor_metric.metric_status`에 기록하고, 임계 이탈 시 종류별 SMS를 발송한다. 동일 종류 경보는 30분 쿨다운을 적용하고, 정상복귀 판정은 히스테리시스(온도 1.0℃, 습도 3.0%)를 적용하여 경계 채터링과 SMS 오발송을 방지한다. 임계 안쪽으로 회복하면 정상복귀 SMS를 발송한다.
코드 근거:
@ -119,13 +119,13 @@ HWP 반영 문구:
- `METRIC_TEMP_HYSTERESIS_C`(1.0), `METRIC_RH_HYSTERESIS`(3.0)
- `METRIC_ALERT_COOLDOWN_SEC`(1800, 30분)
- `php/api/sensor_data.php`
- `evaluate_metric_thresholds()`로 서버 재판정 → `sensor_metric`
- `evaluate_metric_thresholds()`로 서버 재판정 → `leak_sensor_metric`
- 경보 종류(`high_temp`/`low_temp`/`high_humidity`/`low_humidity`)별 SMS, 쿨다운/복구 판정
- 임계 이탈 시 `send_metric_alert_sms()`, 회복 시 `send_metric_recovery_sms()`
사용자가 제공해야 하는 증적:
- 임계 초과 시 종류별 경보 SMS 발송 이력(`sms_log`: `[고온경보]`/`[저온경보]`/`[고습경보]`/`[저습경보]`)
- 임계 초과 시 종류별 경보 SMS 발송 이력(`leak_sms_log`: `[고온경보]`/`[저온경보]`/`[고습경보]`/`[저습경보]`)
- 정상복귀 SMS 발송 이력
- 쿨다운 적용으로 동일 경보가 30분 내 재발송되지 않는 결과
@ -140,15 +140,15 @@ HWP 반영 문구:
- `php/config.php`
- `HEARTBEAT_TIMEOUT_SEC`(1200), `HEARTBEAT_CHECK_INTERVAL_SEC`(300)
- `php/cron_heartbeat.php`
- 마지막 보고 시각 기준 오프라인 판정, `sensor_status.offline_alerted` 관리
- 마지막 보고 시각 기준 오프라인 판정, `leak_sensor_status.offline_alerted` 관리
- `php/api/sensor_data.php`
- 오프라인 상태였던 센서가 다시 보고하면 복구 SMS 발송
사용자가 제공해야 하는 증적:
- `sensor_status` 마지막 보고 시각
- `leak_sensor_status` 마지막 보고 시각
- `cron_heartbeat.php` 실행 로그
- 오프라인/복구 SMS 발송 이력(`sms_log`)
- 오프라인/복구 SMS 발송 이력(`leak_sms_log`)
### 2.6 비밀값 코드 분리
@ -270,6 +270,6 @@ curl -I https://example.com/sht30_monitor/login.php
2. `setup_wizard.php` 점검 화면
3. API 정상/403 테스트 결과
4. 단말 `systemctl status leak-sensor` 화면
5. 측정값 저장(`sensor_metric`) 및 임계 경보 SMS(`sms_log`) 화면
5. 측정값 저장(`leak_sensor_metric`) 및 임계 경보 SMS(`leak_sms_log`) 화면
이 5개가 있으면 소스코드 기반 보안대책과 운영 적용 증적을 어느 정도 연결할 수 있습니다.

View file

@ -117,7 +117,7 @@
<h2>운영 자가진단</h2>
<div class="diag-list">
<div class="diag"><i class="dot"></i><div><strong>API 키 · 정상</strong><span>RPi와 PHP API_KEY 일치</span></div></div>
<div class="diag"><i class="dot"></i><div><strong>SHT30 metric table · 정상</strong><span>sensor_metric 저장 가능</span></div></div>
<div class="diag"><i class="dot"></i><div><strong>SHT30 metric table · 정상</strong><span>leak_sensor_metric 저장 가능</span></div></div>
<div class="diag warn"><i class="dot"></i><div><strong>최근 SMS 실패 · 확인 필요</strong><span>최근 7일 실패 1건</span></div></div>
</div>
</aside>

View file

@ -316,7 +316,7 @@
<div class="grid">
<div class="tile"><strong>상단 상태바</strong><span><span class="state ok">정상</span>이면 운영 경고 없음. <span class="state warn">주의</span>나 <span class="state danger">위험</span>이면 바로 하단 세부 항목을 확인한다.</span></div>
<div class="tile"><strong>요약 카드</strong><span>등록 센서, 임계 경보, 오프라인, 30일 임계 SMS, SMS 실패를 빠르게 확인한다.</span></div>
<div class="tile"><strong>운영 자가진단</strong><span>API 키, DB 테이블, sensor_metric 저장, SMS 실패 등 운영 준비 상태를 보여준다.</span></div>
<div class="tile"><strong>운영 자가진단</strong><span>API 키, DB 테이블, leak_sensor_metric 저장, SMS 실패 등 운영 준비 상태를 보여준다.</span></div>
</div>
</section>
@ -401,7 +401,7 @@
<tr>
<td><span class="state warn">센서 온습도 미표시</span></td>
<td>SHT30 데이터가 저장되지 않았거나 측정이 실패했다.</td>
<td><code>sensor_metric</code> 테이블, <code>sht30-sensor</code> 서비스, I2C 주소 <code>0x44/0x45</code> 확인.</td>
<td><code>leak_sensor_metric</code> 테이블, <code>sht30-sensor</code> 서비스, I2C 주소 <code>0x44/0x45</code> 확인.</td>
</tr>
<tr>
<td><span class="state warn">SMS 실패</span></td>

View file

@ -317,7 +317,7 @@
<div class="hero-copy">
<div class="eyebrow">설치 목표</div>
<h2>SHT30 온도와 습도를 5분마다 서버로 보내고 대시보드에 최신값을 표시한다.</h2>
<p>두 장비 모두 SHT30 센서를 I2C로 읽어 `sensor_metric`에 온습도 이력을 남기며, 임계 초과 시 서버에서 경보 SMS를 발송한다.</p>
<p>두 장비 모두 SHT30 센서를 I2C로 읽어 `leak_sensor_metric`에 온습도 이력을 남기며, 임계 초과 시 서버에서 경보 SMS를 발송한다.</p>
<div class="footer-actions">
<button class="btn" type="button" onclick="window.print()">인쇄 또는 PDF 저장</button>
<a class="btn secondary" href="../SHT30_SENSOR2_SETUP.md">Markdown 설명서</a>
@ -521,7 +521,7 @@ journalctl -u sht30-sensor -f</code></pre>
</tr>
<tr>
<td><span class="status warn">metric_saved false</span></td>
<td>서버는 받았지만 `sensor_metric` 테이블이 없음.</td>
<td>서버는 받았지만 `leak_sensor_metric` 테이블이 없음.</td>
<td>`sql/migration_sensor_metric.sql` 실행 후 다시 `--once` 테스트.</td>
</tr>
<tr>

View file

@ -69,7 +69,7 @@ SHT30 모듈은 3.3V 단일 전원으로 동작합니다. SCL/SDA는 오픈드
정상 측정 범위(벗어나면 펌웨어가 `metric_status="out_of_range"` 로 표시): 온도 `[-40, 125]°C`, 상대습도 `[0, 100]%`.
> 참고: `out_of_range` 는 **펌웨어 전용 타당성(plausibility) 분류**입니다. 서버는 이 값을 무시하고 운영 임계(`METRIC_*`)로 재판정하므로, DB `sensor_metric.metric_status` 에는 `normal`/`high_temp`/`low_temp`/`high_humidity`/`low_humidity` 만 저장되며 `out_of_range` 는 저장되지 않습니다.
> 참고: `out_of_range` 는 **펌웨어 전용 타당성(plausibility) 분류**입니다. 서버는 이 값을 무시하고 운영 임계(`METRIC_*`)로 재판정하므로, DB `leak_sensor_metric.metric_status` 에는 `normal`/`high_temp`/`low_temp`/`high_humidity`/`low_humidity` 만 저장되며 `out_of_range` 는 저장되지 않습니다.
---

View file

@ -74,7 +74,7 @@
| 프로세스 관리 | systemd 재시작 | IWDG 워치독 + 재연결 | 변경(이식) |
| 로깅 | 로테이팅 파일 로그 | UART 콘솔 | 변경(축소) |
> **임계 판정의 위치:** 펌웨어는 `temperature_c`/`humidity_percent` 원값과 `metric_status`(센서 물리/타당성 범위 기준 `normal`/`out_of_range`)만 보고한다. 고온/저온/고습/저습 경보와 SMS 트리거는 **서버 `config.php` 의 `METRIC_*` 임계**가 담당한다. 폐쇄망에서 임계값을 조정할 때 펌웨어 재플래시가 불필요하도록 의도된 분리다. `out_of_range` 는 **펌웨어 전용 타당성 분류**이며 서버가 무시·재판정하므로, DB `sensor_metric.metric_status` 에는 `normal`/`high_temp`/`low_temp`/`high_humidity`/`low_humidity` 만 저장된다(`out_of_range` 는 DB에 저장되지 않음).
> **임계 판정의 위치:** 펌웨어는 `temperature_c`/`humidity_percent` 원값과 `metric_status`(센서 물리/타당성 범위 기준 `normal`/`out_of_range`)만 보고한다. 고온/저온/고습/저습 경보와 SMS 트리거는 **서버 `config.php` 의 `METRIC_*` 임계**가 담당한다. 폐쇄망에서 임계값을 조정할 때 펌웨어 재플래시가 불필요하도록 의도된 분리다. `out_of_range` 는 **펌웨어 전용 타당성 분류**이며 서버가 무시·재판정하므로, DB `leak_sensor_metric.metric_status` 에는 `normal`/`high_temp`/`low_temp`/`high_humidity`/`low_humidity` 만 저장된다(`out_of_range` 는 DB에 저장되지 않음).
자세한 함수 단위 매핑은 [`docs/PORTING_NOTES.md`](docs/PORTING_NOTES.md) 참고.

View file

@ -100,7 +100,7 @@ Sensirion SHT30 (한진데이터 P4422-3 모듈), I2C. RPi `sht30_monitor.py`
- 풀업: SCL/SDA 각각 **4.7kΩ → 3.3V** (모듈에 내장 풀업이 있으면 생략). I2C 속도 100kHz(Standard) 권장.
- plausibility 범위(벗어나면 펌웨어가 `metric_status="out_of_range"` 로 표시): T `[-40, 125]°C`, RH `[0, 100]%` ([`common/app_config.h`](../common/app_config.h)).
운영 경보 임계(고온/저온/고습/저습 → SMS)는 **서버 `php/config.php` 의 `METRIC_*` 상수**가 판정한다(펌웨어는 원값만 보고).
`out_of_range` 는 **펌웨어 전용 타당성 분류**이며 서버가 무시·재판정하므로, DB `sensor_metric.metric_status` 에는 `normal`/`high_temp`/`low_temp`/`high_humidity`/`low_humidity` 만 저장된다(`out_of_range` 는 DB에 남지 않음).
`out_of_range` 는 **펌웨어 전용 타당성 분류**이며 서버가 무시·재판정하므로, DB `leak_sensor_metric.metric_status` 에는 `normal`/`high_temp`/`low_temp`/`high_humidity`/`low_humidity` 만 저장된다(`out_of_range` 는 DB에 남지 않음).
```
SHT30 모듈 STM32

View file

@ -120,7 +120,7 @@ SHT30 측정에서 SMS 발송까지의 책임 분담:
센서 물리/타당성 범위(`APP_SHT30_TEMP/RH_LOW/HIGH`) 기준으로 `metric_status`(`normal`/`out_of_range`)만 부여해
`sensor_data.php` 로 POST 한다. 펌웨어는 **고온/저온/고습/저습 같은 운영 임계를 판정하지 않는다.**
여기서 `out_of_range` 는 **펌웨어 전용 타당성 분류**다. 서버는 이 값을 무시하고 운영 임계로 재판정하므로
DB `sensor_metric.metric_status` 에는 `normal`/`high_temp`/`low_temp`/`high_humidity`/`low_humidity` 만 저장되고 `out_of_range` 는 저장되지 않는다.
DB `leak_sensor_metric.metric_status` 에는 `normal`/`high_temp`/`low_temp`/`high_humidity`/`low_humidity` 만 저장되고 `out_of_range` 는 저장되지 않는다.
2. **서버(임계·SMS)**: `sensor_data.php` 가 본문을 DB 에 적재하고, `config.php` 의 `METRIC_*` 임계와 비교해
고온/저온/고습/저습 조건이면 **SMS 게이트웨이로 문자**를 발송한다.

View file

@ -59,13 +59,17 @@ e-con **5C-08 저항형 누수감지센서**를 Raspberry Pi 4 Model B 의 GPIO
이 모듈은 `leak_alert` / `leak_recovery` 이벤트와 `photo_upload.php` 를 **수신하는 서버**에
보고합니다.
> ⚠️ **현재 이 repo 의 `php/` 서버는 SHT30 온습도 전용으로 전환**되었습니다
> (`sql/migration_drop_leak.sql` 가 `leak_photo`/`leak_incident` 테이블과 `is_leak` 컬럼을
> 제거하고 `event_type` 을 `startup|periodic` 로 축소). 따라서 현재 repo 서버는 leak 이벤트와
> 사진 업로드를 **거부**합니다.
> ✅ **이 repo 의 `php/` 서버가 바로 그 서버입니다(단일 서버 통합 완료).**
> 1번(누수+카메라)·2번(SHT30 온습도) 두 모듈 모두 **동일한 cafe24 `php/` 서버**에
> raw-body 서명 `X-Signature = lowercase_hex(HMAC-SHA256(key=API_KEY, msg=요청본문 바이트))`
> 로 보고하며, 누수 상태·사진·온습도가 하나의 대시보드(`dashboard.php`)에 함께 표시됩니다.
>
> 이 Pi 4 모듈은 **leak 를 받는 기존 서버**(기존 `raspi_leck_detecter` 엔드포인트)로 보고하도록
> `config_local.py` 의 `API_URL` / `PHOTO_UPLOAD_URL` 을 설정해야 합니다.
> - **신규 설치:** `sql/schema_sht30.sql` 하나만 실행 — 누수 요소
> (`leak_sensor_log.is_leak` / `leak_sensor_status.is_leak` 컬럼, `leak_incident`, `leak_photo`,
> `leak_ingest_nonce` 테이블)가 이미 포함되어 있습니다.
>
> `config_local.py` 의 `API_URL` / `PHOTO_UPLOAD_URL` 을 이 통합 서버로 설정하고,
> `API_KEY` 는 서버 `config.local.php` 의 `API_KEY` 와 **바이트 단위로 동일**하게 맞춥니다.
---
@ -146,7 +150,11 @@ journalctl -u leak-sensor -f
- 비밀값(`API_KEY`)·서버 URL·기기 정보는 `config_local.py`(미커밋)에만 둡니다.
커밋되는 파일에는 placeholder 만 존재합니다.
- 보고 본문은 `API_KEY + 정렬된 JSON` 의 SHA-256 서명으로 위변조를 방지합니다
(`leak_sensor.make_signature` ↔ 서버 검증 로직과 동일 포맷).
- ⚠️ 이 서명은 **레거시 누수 서버와의 호환을 위해 의도적으로** `SHA-256(API_KEY + JSON)` 방식을 유지합니다. HMAC-SHA256 이 길이연장 공격에 면역이라 더 안전하지만, 전환하려면 **서버 검증부를 동시에 변경**해야 합니다.
- 보고 본문은 **raw-body HMAC 서명**으로 위변조를 방지합니다:
`X-Signature = lowercase_hex(HMAC-SHA256(key=API_KEY, msg=요청본문 바이트))`
(`leak_sensor.sign_raw` ↔ 서버 `config.php::verify_signature_raw`, 2번 모듈과 동일 스킴).
- 사진 업로드 서명의 canonical 문자열은 `device_id|sensor_id|log_id|timestamp`
(공백 없음)입니다 (`config_local.example.py` 참고).
- 구버전의 `sha256(API_KEY + 정렬 JSON)` body-field 서명은 **폐기**되었고, 서버 기본
설정이 거부합니다(`ALLOW_LEGACY_BODY_SIGNATURE=false`, `php/config.php`).
- `config_local.py` 노출 시 즉시 `API_KEY` 를 교체(서버 동시 변경)하세요.