POSA_LEAKSMS/docs/DEPLOYMENT_PLAN_RPI4x3.md
cw2007 fb3630d868 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
2026-07-02 18:52:15 +09:00

279 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 배포 계획: 누수+온습도 통합 모니터링 시스템 (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 교차 점검 필수.