{ "schema_version": 1, "diagram_type": "architecture", "meta": { "title": "작업장 안전 AI CCTV — 시스템 구성", "output": "move-detect-architecture.html", "quality_profile": "showcase", "views": [ { "id": "detection-chain", "label": "감지 → 대응", "focus": [ "cameras", "edge", "beacon" ], "note": "카메라에서 경광등까지. 서버도 인터넷도 거치지 않는 즉시 대응 경로." }, { "id": "privacy-path", "label": "프라이버시 경로", "focus": [ "edge", "blur", "ingest", "db" ], "note": "영상은 나가지 않는다. 얼굴 블러 후 스냅샷 3장만, 7일 뒤 삭제." }, { "id": "server-issued", "label": "서버가 발행하는 사건", "focus": [ "cron", "db", "notify" ], "note": "죽은 노드는 자기가 죽었다고 신고할 수 없다 — 하트비트의 부재로 판정한다." } ] }, "components": [ { "id": "cameras", "type": "external", "label": "카메라", "sublabel": "CSI · USB", "tag": "노드당 2~4대", "pos": [ 40, 340 ], "size": [ 130, 64 ] }, { "id": "edge", "type": "backend", "label": "엣지 노드", "sublabel": "Pi 5 + Hailo-8", "tag": "판정 16종", "pos": [ 230, 332 ], "size": [ 165, 80 ] }, { "id": "beacon", "type": "external", "label": "현장 경광등", "sublabel": "GPIO 직결", "tag": "네트워크 불필요", "pos": [ 230, 180 ], "size": [ 165, 64 ] }, { "id": "queue", "type": "messagebus", "label": "오프라인 큐", "sublabel": "단절 시 보관", "pos": [ 230, 500 ], "size": [ 165, 64 ] }, { "id": "blur", "type": "security", "label": "얼굴 블러", "sublabel": "전송 전", "tag": "영상 녹화 없음", "pos": [ 465, 336 ], "size": [ 160, 72 ] }, { "id": "ingest", "type": "security", "label": "수집 API", "sublabel": "HMAC + nonce", "tag": "재전송 차단", "pos": [ 715, 332 ], "size": [ 155, 80 ] }, { "id": "app", "type": "backend", "label": "대시보드 서버", "sublabel": "PHP 8", "pos": [ 960, 332 ], "size": [ 160, 80 ] }, { "id": "db", "type": "database", "label": "MySQL", "sublabel": "move_* · 7일", "pos": [ 1150, 340 ], "size": [ 160, 64 ] }, { "id": "cron", "type": "backend", "label": "1분 크론", "sublabel": "이벤트 발행", "tag": "서버 발행 4종", "pos": [ 1150, 480 ], "size": [ 160, 76 ] }, { "id": "console", "type": "frontend", "label": "운영 콘솔", "sublabel": "바닐라 JS", "tag": "라이브뷰 없음", "pos": [ 960, 180 ], "size": [ 160, 76 ] }, { "id": "notify", "type": "external", "label": "메일 · 사내 웹훅", "sublabel": "링크만 전송", "pos": [ 1150, 630 ], "size": [ 160, 72 ] } ], "boundaries": [ { "kind": "region", "label": "현장 (폐쇄망 가능)", "wraps": [ "cameras", "edge", "beacon", "queue", "blur" ] }, { "kind": "region", "label": "Cafe24 공유호스팅", "wraps": [ "ingest", "app", "db", "cron" ] } ], "connections": [ { "id": "capture", "from": "cameras", "to": "edge", "label": "프레임" }, { "id": "local-beacon", "from": "edge", "to": "beacon", "label": "즉시 점등", "variant": "emphasis", "fromSide": "top", "toSide": "bottom" }, { "id": "evidence", "from": "edge", "to": "blur", "label": "이벤트 시" }, { "id": "spool", "from": "edge", "to": "queue", "label": "회선 단절 시", "variant": "dashed", "fromSide": "bottom", "toSide": "top", "labelDy": 30 }, { "id": "upload", "from": "blur", "to": "ingest", "label": "HMAC POST", "variant": "emphasis" }, { "id": "resend", "from": "queue", "to": "ingest", "label": "복구 후 재전송", "variant": "dashed", "fromSide": "right", "toSide": "bottom" }, { "id": "accept", "from": "ingest", "to": "app", "label": "검증 통과" }, { "id": "persist", "from": "app", "to": "db", "label": "SQL" }, { "id": "cron-db", "from": "cron", "to": "db", "label": "하트비트 부재로 판정", "fromSide": "top", "toSide": "bottom" }, { "id": "dispatch", "from": "cron", "to": "notify", "label": "1분 주기 통보", "fromSide": "bottom", "toSide": "top", "labelDy": 30 }, { "id": "serve", "from": "app", "to": "console", "label": "HTTPS · 역할 기반 접근", "fromSide": "top", "toSide": "bottom" } ], "cards": [ { "dot": "cyan", "title": "현장에서 끝나는 대응", "items": [ "판정은 엣지에서 완결 — 서버 왕복 없음", "경광등은 GPIO 직결이라 회선·서버가 죽어도 울린다", "끊긴 동안의 이벤트는 큐에 쌓였다가 복구 시 재전송" ] }, { "dot": "rose", "title": "영상을 남기지 않는다", "items": [ "링버퍼는 RAM 7.5초 — 디스크에 쓰지 않는다", "이벤트 순간에만 3프레임, 얼굴 블러는 전송 전 노드에서", "스냅샷 7일 후 자동 삭제 · 라이브뷰 화면 자체가 없음" ] }, { "dot": "emerald", "title": "서버가 보는 것", "items": [ "HMAC + nonce 로 재전송 공격 차단", "죽은 노드는 자기가 죽었다고 신고할 수 없다 — 크론이 하트비트의 부재로 판정", "통보는 링크만 — 현장 이미지는 메일로 나가지 않는다" ] } ] }