move_detect/docs/architecture.archify.json
유창욱 34506e150d docs: 설계 도해 3종 (구성 · 낙상 시퀀스 · 낙상 상태기계)
archify 소스 JSON 만 커밋한다. HTML 은 생성물이라 사용설명서와 같은 이유로
gitignore — 소스가 바뀐 뒤에도 낡은 사본이 최신처럼 보이기 때문.

시퀀스를 그리면서 드러난 것: 경광등은 확정 즉시(3초)지만 메일·웹훅은
크론 주기를 기다려 최대 63초다. 문서가 둘을 "통보"로 묶어 쓰던 표현이
실제로는 20배 차이나는 두 경로였다.

상태기계는 확정 창 3초와 추락 연결 창 3초가 겹쳐 있는 구조를 한 장에
담았다 — 4차 감사에서 잡은 CRITICAL(추락 승격 미발화)의 원인이고,
시간축을 그렸다면 코드를 읽기 전에 보였을 충돌이다.

검증: deliver 9/9 · 오류 0 · 경고 0, visual-check 1440x900 / 1600x1000 /
1920x1080 / 2048x1320 양 테마 containment·가독성 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBjuR7DW6PNNMows4KnbYU
2026-09-18 22:59:17 +09:00

343 lines
6.8 KiB
JSON

{
"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 로 재전송 공격 차단",
"죽은 노드는 자기가 죽었다고 신고할 수 없다 — 크론이 하트비트의 부재로 판정",
"통보는 링크만 — 현장 이미지는 메일로 나가지 않는다"
]
}
]
}