두 유형은 타입 정의(edge/events.py)·서버 검증(EventValidator)·콘솔 필터·
통보 라벨까지 전 구간에 있었으나 생성하는 코드가 어디에도 없었다. 실제로
값을 넣는 것은 seed_demo.php 뿐이었고, 소개서가 내세운 "17종"은 15종이었다.
앞서 고친 EventValidator 누락과 정확히 반대 방향의 같은 병이다. 그때는
감지기가 있는데 서버가 막았고, 이번엔 서버·UI가 열려 있는데 감지기가 없다.
어느 쪽이든 화면은 멀쩡해 보인다.
엣지가 낼 수 없는 유형이라 서버에 둔다 — 전원·회선이 끊긴 노드는 자기가
죽었다고 신고할 수 없다. 하트비트의 부재로만 관측된다.
App\Alert\DeviceHealthWatch:
- dispatch_alerts.php 1분 크론의 1단계. 에스컬레이션·통보보다 앞에 두어
같은 실행 안에서 발행된 장치 이벤트가 곧바로 통보 대상이 되게 했다.
- 멱등은 EscalationWatch와 같은 방식(event_uuid UNIQUE 충돌)이되, 장치
건전성은 연속 상태라 uuid 앵커를 '장애 구간의 시작'으로 잡았다.
device_fault는 last_heartbeat_at(장애 내내 불변) → 장애당 정확히 1건.
monitoring_degraded는 창 안의 마지막 정상 하트비트 ts. 창 전체가 저하면
시작을 볼 수 없으므로 창 버킷으로 폴백해 시간당 1회 재고지한다 —
의도된 리마인더이고, 매 틱 재발행하는 앵커 드리프트가 아니다.
- 성능 저하는 지속을 요구(기본 5분). 한 번 튄 값에 울리면 운영자가 이
유형을 무시하게 되고, 그러면 감시가 멈춘 날에도 아무도 보지 않는다.
엣지 감지기들이 쓰는 규율과 같다.
- 하트비트를 한 번도 보낸 적 없는 장치는 장애가 아니다(미설치 노드).
새 현장 첫날이 빨간 벽이 되면 그 사이트는 이 유형을 영영 신뢰하지 않는다.
- 죽은 노드에 degraded를 겹쳐 내지 않는다 — 한 고장에 두 알람은 소음이다.
- fps NULL을 0으로 강압하지 않는다(측정 실패 ≠ 정지). PdoDeviceMetric
Repository가 NULL을 지키는 이유와 같다.
- 억제(정비 모드)는 여기서 보지 않는다. 기록은 남기고 통보 여부는
AlertDispatcher가 정한다 — 정비 창은 전화를 끄는 것이지 노드가 죽었던
기록을 지우는 것이 아니다.
camera_id는 NOT NULL인데 장치 단위 이벤트에는 카메라가 없다. '__device__'
센티넬을 쓰고, 콘솔은 "장치 전체"로 표시, 구역 편집기 카메라 목록에서는
제외한다(카메라 목록은 보고된 이벤트에서 파생되므로, 두면 존재하지 않는
카메라에 구역을 그리려는 일이 생긴다).
설정 4종 추가(device_stale_seconds 180 · degraded_sustain_seconds 300 ·
degraded_fps_ratio 0.6 · target_fps 12) — 두 config 템플릿에 한국어 설명과
함께. 낮은 프레임으로 운용하는 현장이 target_fps를 안 맞추면 전 노드가
영구 저하로 보고되므로 그 경고를 템플릿에 적었다.
검증: pytest 308 passed(3 skip: 모델 미동봉·shellcheck 미설치·AF_UNIX,
기존과 동일) · phpunit 240 passed(신규 16). dist 재생성으로 패리티 회복.
기존 소개서의 문제는 문장이 아니라 구조와 최신성이었다.
- 기대효과 섹션이 아예 없었다. 기능 목록만 있고 "그래서 뭐가 달라지나"가 없었다.
- 왜 만들었나가 4문장이었다. 이미 CCTV 가 있는 현장이 왜 또 사는지에 답하지 못했다.
- 내용이 0.9.0 시점이었다. 1.0.0 의 핵심인 통보·에스컬레이션이 로드맵에 "P3 예정"으로
적혀 있었고 테스트 320건·감사 43항목도 옛 수치였다 — 완성된 기능을 미완성으로 소개하는
문서가 고객에게 나갈 뻔했다.
재작성
- 기능 나열을 버리고 감지 → 대응 → 증거의 사슬로 재구성. 어느 한 칸이 비면 나머지가
무의미해진다는 것이 이 제품의 논지이므로 문서 구조도 그래야 한다.
- 왜 만들었나를 "CCTV 는 이미 있는데 왜 사고를 못 막나" 세 가지(아무도 안 본다 / 발견이
늦다 / 증명할 자료가 없다)로 세우고, 각각에 대응하는 답을 표로 연결.
- 기대효과 신설. 지어낸 ROI 대신 **제품이 스스로 계측해 보여주는 지표**(TTA, 니어미스:사고,
조치 전후 밀도, 오탐율)로 썼다 — 공급자 주장이 아니라 고객 화면의 숫자다.
'약속하지 않는 것' 절을 함께 넣었다. 사고를 없애지 못하고, 감지율 100%는 없고, 오탐은
초기에 반드시 난다. 과장은 도입 후에 반드시 되돌아온다.
- 설계 제약(감시하지 않는다 / 인터넷 없이 동작한다 / 클라우드를 안 쓴다)을 기능보다 앞에
두었다. 이 제품의 형태는 거기서 나왔다.
수치는 전부 코드와 대조해 교정
- 테스트 535건(pytest 311 + PHPUnit 224), 감사 55건, 감지 17종 + 서버 발행 1종
- "3초 안에 찾아낸다"는 과장이었다. 실제로는 쓰러진 자세가 3초 지속되면 확정이므로
"3초면 알아채고"로 고치고, 근거가 되는 3초를 감지 표에 함께 적었다.
이미지 브리프 신설 (docs/소개서-이미지-브리프.md)
- 현장 사진 4장 + 다이어그램 3장의 발주 명세. 각 항목에 "이 이미지가 없으면 전달 안 되는
것", 담을 것, 피할 것, 사양, 캡션안, 우선순위.
- 제품 화면 캡처 8장은 이미 확보되어 있어 발주 대상에서 제외.
- 개인정보 제약을 최상단에 명시. 얼굴이 식별되는 작업자 사진이 한 장이라도 들어가면
"감시하지 않는다"는 이 제품의 핵심 주장이 무너진다.
- 검토했으나 뺀 이미지(쓰러진 순간 프레임, 블러 전/후 비교, 스켈레톤 오버레이)와 그 이유도
기록. 나중에 "왜 없지"가 반복되지 않도록.
HTML 은 md-to-html-report 로 재생성(목차·표·콜아웃·인쇄 스타일). 실렌더 확인.