docs: 다이어그램 4종 세트 완성, 범례 한글화

아키텍처 · 운영자 심사 워크플로 · 제출물 상태 전이를 신설하고, 기존 증거
데이터 흐름까지 4종의 범례를 한글화했다. 렌더러 기본 범례가 "User UI /
Agent logic / Context-trace" 나 "policy / PII" 처럼 타 도메인 문구였다 —
legend.entries 로 라벨만 바꿨고 종류와 개수는 그대로다.

  system-architecture      폐쇄망 경계와 그 밖의 외부 검색 API. 나가는 경로는
                           store_remote_fetch 하나뿐이고 앞에 호출 게이트가 있다
  operator-review-workflow 4레인(제출·자동·심사·차단). 비상 차단이 걸려도
                           커버리지 부족 표시로 심사가 이어지는 경로 포함
  submission-lifecycle     분석 실패와 보류가 종료가 아니라 분석 중으로
                           돌아오는 두 고리, 판정 후 보존 상태

작성 중 내린 고도 판단 두 개를 남긴다.

  아키텍처에서 analyzer/scorer 를 "분석 · 채점" 한 컴포넌트로 합쳤다. 이
  고도에서 그 분리는 엣지를 벌지 못하고, store 노드에 엣지 7개가 몰려
  통로가 부족해졌다. 채점 내부는 dataflow 다이어그램이 담당한다.

  라이프사이클에서 "재분석 요청"을 상태에서 뺐다. decisionStatus 값이 아니라
  운영자 액션이고, 보류가 같은 의미를 담아 긴 복귀 엣지를 하나로 줄인다.

검증: 4종 모두 showcase 통과(아티팩트 검사 9/9 · 합성 오류 0 · 경고 0),
visual-check 통과(1440x900 · 2048x1320 각 Light/Dark, 오버플로 0).
html sha256 — dataflow a62f0f5c / architecture f5a4b6e1 /
workflow 402914a3 / lifecycle 62e934a3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BStHFrjQj2VypecSG9ex1V
This commit is contained in:
changukyu 2026-09-19 00:34:51 +09:00
parent e4fffc40be
commit d45018c4d0
10 changed files with 997 additions and 4 deletions

View file

@ -1,5 +1,7 @@
# Project INDEX # Project INDEX
- 2026-09-19 · 다이어그램 4종으로 확장 — `system-architecture`(폐쇄망 경계·SSRF 가드 앞 호출 게이트), `operator-review-workflow`(4레인, 비상 차단 시 심사 계속 경로), `submission-lifecycle`(분석 실패·보류가 분석으로 복귀하는 두 고리)를 신설하고 기존 `evidence-dataflow` 포함 4종의 범례를 한글화했다. 렌더러 기본 범례가 "User UI / Agent logic / policy·PII" 같은 타 도메인 문구였다. 작성 중 내린 고도 판단: 아키텍처에서는 analyzer/scorer 를 한 컴포넌트로 합쳤고(세부는 dataflow 담당), 라이프사이클에서 "재분석 요청"은 decisionStatus 가 아닌 운영자 액션이라 상태에서 제외해 복귀 엣지를 하나로 줄였다. 4종 모두 archify showcase 통과(아티팩트 검사 9/9 · 합성 오류 0 · 경고 0) + visual-check 통과(1440×900 · 2048×1320 각 Light/Dark, 오버플로 0). 수신증 — dataflow html a62f0f5c / architecture f5a4b6e1 / workflow 402914a3 / lifecycle 62e934a3
- 2026-09-19 · `docs/diagrams/evidence-dataflow.html` 신설 — 증거 데이터 흐름(수집 → 경계 검사 → 정규화·채점 → 영속화 → 소비) 자체 완결 다이어그램. 9/18 리뷰 수정의 불변식을 그림으로 고정한다: **제공자별 풀·버킷**(RiskScorer v3), **라운드트립이 원 점수를 재현**(`_evidence_payload` ↔ `_domain_evidence_from_ui`), **host 는 저장 시 해석·복원 시 우선 반환**, 지식DB 지문 매칭이 풀링 밖 유일한 high 승격 경로. 프로덕션 LLM 경로가 영속화 payload 를 복원해 쓴다는 사실(`store_operations.py:487`, `EvidenceEnricher` 는 테스트 전용)도 반영. archify showcase 검증 통과(아티팩트 검사 9/9 · 합성 오류 0 · 경고 0), 브라우저 증거 통과(1440×900 · 2048×1320 각 Light/Dark 4컷, 오버플로 0, 최소 투영 텍스트 6.71px ≥ 6px). spec sha256 42c334cd · html sha256 268f3352 (822,655 B). CDN·원격 fetch 없음 — 폐쇄망 규칙 준수 - 2026-09-19 · `docs/diagrams/evidence-dataflow.html` 신설 — 증거 데이터 흐름(수집 → 경계 검사 → 정규화·채점 → 영속화 → 소비) 자체 완결 다이어그램. 9/18 리뷰 수정의 불변식을 그림으로 고정한다: **제공자별 풀·버킷**(RiskScorer v3), **라운드트립이 원 점수를 재현**(`_evidence_payload` ↔ `_domain_evidence_from_ui`), **host 는 저장 시 해석·복원 시 우선 반환**, 지식DB 지문 매칭이 풀링 밖 유일한 high 승격 경로. 프로덕션 LLM 경로가 영속화 payload 를 복원해 쓴다는 사실(`store_operations.py:487`, `EvidenceEnricher` 는 테스트 전용)도 반영. archify showcase 검증 통과(아티팩트 검사 9/9 · 합성 오류 0 · 경고 0), 브라우저 증거 통과(1440×900 · 2048×1320 각 Light/Dark 4컷, 오버플로 0, 최소 투영 텍스트 6.71px ≥ 6px). spec sha256 42c334cd · html sha256 268f3352 (822,655 B). CDN·원격 fetch 없음 — 폐쇄망 규칙 준수
- 2026-09-18 · `/code-review` 15건 전수 처리 (**헤드라인 1건은 반증**). 반증: "CSE 웹 증거가 라운드트립 후 0→45점" — 리뷰가 `match` 키 없는 합성 Evidence 로 재현한 것이고, 실제 CSE 웹 결과는 `google_custom_search.py:301` 에서 `match="page"` 를 달고 오며 `matchType` 이 충실히 왕복한다(신선/복구 모두 35점). 직렬화 불변식 위반 없음. 대신 그 조사에서 **리뷰가 놓친 라이브 버그**가 나왔다: `counted_web_buckets` 가 제공자를 안 가려 Vision 이 `match="page"` 를 먼저 보고하면 **CSE 웹 히트가 점수도 사유도 없이 통째로 버려졌다**. 실수정 — **점수 v2 → v3**(풀·버킷을 제공자별로: Vision 자기 상관 뷰 4개는 60/medium 유지, Vision full + CSE page 는 45+35=80/high 복원 · 도달 불가였던 `category` 탈출구 제거 + 미등록 유명인 미탐은 의도된 트레이드임을 명시, 승격 경로는 지식DB/FINGERPRINT), **저장·복구 5종**(`_result_host` 가 자기 host 를 안 읽어 재직렬화 시 API 이름으로 퇴행하던 잠재 버그 + 점 없는 토큰 호스트 거부 · 복구가 남긴 고아 `faceCrops` 참조를 재생성/정리 · `assetMissing` 해제 경로 신설로 두 번째 소실도 감사에 남김 · 플래그+감사를 한 트랜잭션으로 · 스코어러 마이그레이션 밴드 변경에 `scoredVersion` 스탬프 + 감사 1회), **콘솔 4종**(긴 CDN 호스트가 제목을 세로 컬럼으로 만든 flex 버그 — 실측 360px 51×95px→246×19px, **1280px 9×380px→205×19px 로 폰 전용이 아니었음** · 20ch 로 잘린 호스트를 툴팁에 · `bloggerLink` 폴백 누락 · 보존 케이스 설명 표시), **PROMPT_VERSION ko-v4-structured-host**(실모델 프로브 qwen2.5:7b — grounded 3/dropped 0/verified True, 반향 없음). 테스트 +14건. **ruff clean · pytest 586 passed**. 발견된 기존 결함 1건은 미수정 기록: 로컬 ollama 가 떠 있으면 `test_http_app.py` 3건이 실호출 타임아웃으로 실패한다(기본 생성 store 가 `127.0.0.1:11434` 기본값에 붙음 — clean HEAD 에서도 동일). 커밋 b0aa4c5 / 4ba83eb / e38dad8 - 2026-09-18 · `/code-review` 15건 전수 처리 (**헤드라인 1건은 반증**). 반증: "CSE 웹 증거가 라운드트립 후 0→45점" — 리뷰가 `match` 키 없는 합성 Evidence 로 재현한 것이고, 실제 CSE 웹 결과는 `google_custom_search.py:301` 에서 `match="page"` 를 달고 오며 `matchType` 이 충실히 왕복한다(신선/복구 모두 35점). 직렬화 불변식 위반 없음. 대신 그 조사에서 **리뷰가 놓친 라이브 버그**가 나왔다: `counted_web_buckets` 가 제공자를 안 가려 Vision 이 `match="page"` 를 먼저 보고하면 **CSE 웹 히트가 점수도 사유도 없이 통째로 버려졌다**. 실수정 — **점수 v2 → v3**(풀·버킷을 제공자별로: Vision 자기 상관 뷰 4개는 60/medium 유지, Vision full + CSE page 는 45+35=80/high 복원 · 도달 불가였던 `category` 탈출구 제거 + 미등록 유명인 미탐은 의도된 트레이드임을 명시, 승격 경로는 지식DB/FINGERPRINT), **저장·복구 5종**(`_result_host` 가 자기 host 를 안 읽어 재직렬화 시 API 이름으로 퇴행하던 잠재 버그 + 점 없는 토큰 호스트 거부 · 복구가 남긴 고아 `faceCrops` 참조를 재생성/정리 · `assetMissing` 해제 경로 신설로 두 번째 소실도 감사에 남김 · 플래그+감사를 한 트랜잭션으로 · 스코어러 마이그레이션 밴드 변경에 `scoredVersion` 스탬프 + 감사 1회), **콘솔 4종**(긴 CDN 호스트가 제목을 세로 컬럼으로 만든 flex 버그 — 실측 360px 51×95px→246×19px, **1280px 9×380px→205×19px 로 폰 전용이 아니었음** · 20ch 로 잘린 호스트를 툴팁에 · `bloggerLink` 폴백 누락 · 보존 케이스 설명 표시), **PROMPT_VERSION ko-v4-structured-host**(실모델 프로브 qwen2.5:7b — grounded 3/dropped 0/verified True, 반향 없음). 테스트 +14건. **ruff clean · pytest 586 passed**. 발견된 기존 결함 1건은 미수정 기록: 로컬 ollama 가 떠 있으면 `test_http_app.py` 3건이 실호출 타임아웃으로 실패한다(기본 생성 store 가 `127.0.0.1:11434` 기본값에 붙음 — clean HEAD 에서도 동일). 커밋 b0aa4c5 / 4ba83eb / e38dad8

View file

@ -169,9 +169,16 @@ WAL 적용됨. DB 기본 경로는 `data/copyrighter.sqlite3`(gitignore).
## 다이어그램 ## 다이어그램
| 파일 | 내용 | 소스 JSON 과 산출물 HTML 이 같은 폴더에 쌍으로 있다. **그림을 고치려면 JSON 을 고쳐
|---|---| `archify validate` → `deliver` → `visual-check` 로 재생성한다** (HTML 직접 수정 금지).
| `docs/diagrams/evidence-dataflow.html` | 증거 데이터 흐름 5단(수집·경계 검사·정규화/채점·영속화·소비). 소스는 같은 폴더의 `evidence-dataflow.dataflow.json` (archify dataflow 스키마 v1) — 그림을 고치려면 JSON 을 고쳐 `archify deliver` 로 재생성한다. 자체 완결 HTML(외부 요청 없음) | 모두 자체 완결 HTML 이라 외부 요청이 없다.
| 파일 | 내용 | 타입 |
|---|---|---|
| `evidence-dataflow.html` | 증거 데이터 흐름 5단(수집·경계 검사·정규화/채점·영속화·소비). 라운드트립 불변식이 중심 | dataflow |
| `system-architecture.html` | 시스템 구성. 폐쇄망 경계와 그 밖의 외부 검색 API, SSRF 가드 앞의 호출 게이트 | architecture |
| `operator-review-workflow.html` | 운영자 심사 4레인(제출·자동·심사·차단). 비상 차단 시 심사가 이어지는 경로 포함 | workflow v2 |
| `submission-lifecycle.html` | 제출물 상태 전이. 분석 실패·보류가 종료가 아니라 분석으로 복귀하는 두 고리 | lifecycle |
## 알려진 부채 ## 알려진 부채

View file

@ -43,7 +43,24 @@
"viewBox": [ "viewBox": [
1080, 1080,
500 500
] ],
"legend": {
"mode": "auto",
"entries": {
"emphasis": {
"label": "주 경로"
},
"security": {
"label": "등급 통제"
},
"database": {
"label": "저장소"
},
"default": {
"label": "데이터 흐름"
}
}
}
}, },
"stages": [ "stages": [
{ {

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,349 @@
{
"schema_version": 2,
"diagram_type": "workflow",
"meta": {
"title": "운영자 심사 워크플로",
"output": "docs/diagrams/operator-review-workflow.html",
"quality_profile": "showcase",
"views": [
{
"id": "happy-path",
"label": "정상 경로",
"focus": [
"folder",
"seed",
"analyze",
"search",
"score",
"evidence",
"decide"
],
"note": "폴더 투입에서 판정까지 가는 주 경로."
},
{
"id": "stop-path",
"label": "비상 차단",
"focus": [
"search",
"stop",
"skipped",
"evidence"
],
"note": "외부 호출이 멈췄을 때 심사가 어떻게 이어지는지."
},
{
"id": "assist",
"label": "판독 보조",
"focus": [
"search",
"llm",
"evidence"
],
"note": "LLM 이 증거를 읽고 초안을 내는 보조 경로."
}
],
"legend": {
"mode": "auto",
"entries": {
"external": {
"label": "외부 입력"
},
"backend": {
"label": "서버 처리"
},
"frontend": {
"label": "운영자 화면"
},
"security": {
"label": "차단 · 정책"
},
"database": {
"label": "저장소"
}
}
}
},
"lanes": [
{
"id": "intake",
"label": "제출 · 시드"
},
{
"id": "auto",
"label": "자동 분석 · 검색"
},
{
"id": "review",
"label": "운영자 심사"
},
{
"id": "exception",
"label": "차단 · 보존",
"variant": "exception"
}
],
"phases": [
{
"id": "p-intake",
"label": "제출 · 시드",
"fromCol": 0,
"toCol": 1
},
{
"id": "p-auto",
"label": "자동 분석 · 검색",
"fromCol": 2,
"toCol": 3,
"variant": "emphasis"
},
{
"id": "p-judge",
"label": "채점 · 심사 · 판정",
"fromCol": 4,
"toCol": 5
}
],
"groups": [
{
"id": "g-exception",
"label": "외부 호출이 멈춘 경우",
"lane": "exception",
"fromCol": 3,
"toCol": 4,
"variant": "security"
},
{
"id": "g-assist",
"label": "판독 보조",
"lane": "review",
"fromCol": 3,
"toCol": 3,
"variant": "dashed"
}
],
"mainPath": [
"folder",
"seed",
"analyze",
"search",
"score",
"evidence",
"decide"
],
"nodes": [
{
"id": "folder",
"lane": "intake",
"col": 0,
"type": "external",
"label": "큐 폴더 · 업로드",
"sublabel": "이미지 · manifest",
"width": 132
},
{
"id": "seed",
"lane": "intake",
"col": 1,
"type": "backend",
"label": "시드 · 리로드",
"sublabel": "신규 레코드만",
"tag": "판정 보존",
"width": 132
},
{
"id": "analyze",
"lane": "auto",
"col": 2,
"type": "backend",
"label": "내부 분석",
"sublabel": "지문 · 얼굴 · 출처",
"width": 132
},
{
"id": "search",
"lane": "auto",
"col": 3,
"type": "backend",
"label": "자동 검색",
"sublabel": "쿼리 생성 · 승격",
"width": 132
},
{
"id": "score",
"lane": "auto",
"col": 4,
"type": "backend",
"label": "위험도 산출",
"sublabel": "점수 · 밴드 · 사유",
"tag": "v3",
"width": 132
},
{
"id": "llm",
"lane": "review",
"col": 3,
"type": "backend",
"label": "LLM 증거 판독",
"sublabel": "인용 검증 · 12행",
"width": 132
},
{
"id": "evidence",
"lane": "review",
"col": 4,
"type": "frontend",
"label": "증거 검토",
"sublabel": "호스트 · 썸네일 · 링크",
"width": 132
},
{
"id": "decide",
"lane": "review",
"col": 5,
"type": "backend",
"label": "판정",
"sublabel": "승인 · 보류 · 반려",
"tag": "감사 이벤트",
"width": 132
},
{
"id": "stop",
"lane": "exception",
"col": 3,
"type": "security",
"label": "비상 차단 스위치",
"sublabel": "쿼터 · provider 토글",
"width": 132
},
{
"id": "skipped",
"lane": "exception",
"col": 4,
"type": "security",
"label": "검색 건너뜀",
"sublabel": "0점 · 사유만 기록",
"width": 132
},
{
"id": "retained",
"lane": "exception",
"col": 5,
"type": "database",
"label": "보존된 케이스",
"sublabel": "원본 파일 없음",
"tag": "assetMissing",
"width": 132
}
],
"edges": [
{
"id": "e-intake",
"from": "folder",
"to": "seed",
"variant": "default"
},
{
"id": "e-seed-analyze",
"from": "seed",
"to": "analyze",
"variant": "emphasis"
},
{
"id": "e-analyze-search",
"from": "analyze",
"to": "search",
"variant": "default"
},
{
"id": "e-search-score",
"from": "search",
"to": "score",
"variant": "emphasis"
},
{
"id": "e-score-evidence",
"from": "score",
"to": "evidence",
"variant": "emphasis"
},
{
"id": "e-evidence-decide",
"from": "evidence",
"to": "decide",
"label": "근거 확인 후",
"variant": "emphasis"
},
{
"id": "e-search-stop",
"from": "search",
"to": "stop",
"label": "외부 호출 요청",
"variant": "security"
},
{
"id": "e-stop-skipped",
"from": "stop",
"to": "skipped",
"label": "차단됨",
"variant": "security",
"role": "error"
},
{
"id": "e-skipped-evidence",
"from": "skipped",
"to": "evidence",
"label": "커버리지 부족 표시",
"variant": "default",
"role": "branch"
},
{
"id": "e-search-llm",
"from": "search",
"to": "llm",
"label": "판독 요청",
"variant": "dashed",
"role": "async"
},
{
"id": "e-llm-evidence",
"from": "llm",
"to": "evidence",
"label": "인용된 초안",
"variant": "dashed"
},
{
"id": "e-decide-retained",
"from": "decide",
"to": "retained",
"label": "파일 소실 시",
"variant": "default",
"role": "branch"
}
],
"cards": [
{
"dot": "emerald",
"title": "주 경로",
"items": [
"리로드는 신규 레코드만 분석한다 — 기존 행은 판정과 함께 보존된다",
"자동 검색은 쿼리를 생성하고 유사도가 맞는 결과만 증거로 승격한다"
]
},
{
"dot": "rose",
"title": "멈췄을 때",
"items": [
"비상 차단은 시드 · 재분석 · 얼굴 크롭 경로까지 모두 막는다",
"건너뛴 검색은 0점이며 위험 사유가 아니라 운영 상태로만 기록된다"
]
},
{
"dot": "cyan",
"title": "판독 보조",
"items": [
"LLM 은 요약기가 아니라 증거 판독기다 — 인용 없는 claim 은 폐기된다",
"프롬프트 입력은 상위 12행으로 제한된다"
]
}
]
}

Binary file not shown.

View file

@ -0,0 +1,280 @@
{
"schema_version": 1,
"diagram_type": "lifecycle",
"meta": {
"title": "제출물 상태 전이",
"output": "docs/diagrams/submission-lifecycle.html",
"quality_profile": "showcase",
"viewBox": [
1080,
630
],
"views": [
{
"id": "main",
"label": "주 단계",
"focus": [
"intake",
"analyzing",
"pending",
"decision",
"approved"
],
"note": "접수에서 승인까지 이어지는 다섯 단계."
},
{
"id": "recovery",
"label": "복구 경로",
"focus": [
"analyzing",
"failed",
"held"
],
"note": "끝나지 않고 분석으로 되돌아오는 두 고리."
},
{
"id": "terminal",
"label": "종료 상태",
"focus": [
"decision",
"approved",
"rejected",
"retained"
],
"note": "되돌아오지 않는 상태와, 판정을 남긴 채 파일만 사라진 상태."
}
],
"legend": {
"mode": "auto",
"entries": {
"start": {
"label": "시작"
},
"active": {
"label": "진행 중"
},
"waiting": {
"label": "대기"
},
"decision": {
"label": "판정"
},
"success": {
"label": "정상 종료"
},
"failure": {
"label": "실패 · 종료"
}
}
}
},
"lanes": [
{
"id": "main",
"label": "제출물 단계"
},
{
"id": "waiting",
"label": "대기 · 보강"
},
{
"id": "exceptions",
"label": "복구 경로"
},
{
"id": "terminal",
"label": "종료 상태"
}
],
"states": [
{
"id": "intake",
"type": "start",
"label": "접수",
"sublabel": "폴더 · 업로드",
"lane": "main",
"col": 0,
"step": "01",
"tag": "신규"
},
{
"id": "analyzing",
"type": "active",
"label": "분석 중",
"sublabel": "지문 · 얼굴 · 검색",
"lane": "main",
"col": 1,
"step": "02"
},
{
"id": "pending",
"type": "active",
"label": "심사 대기",
"sublabel": "점수 · 밴드 산출됨",
"lane": "main",
"col": 2,
"step": "03",
"tag": "unreviewed"
},
{
"id": "decision",
"type": "decision",
"label": "판정",
"sublabel": "운영자 결정",
"lane": "main",
"col": 3,
"step": "04"
},
{
"id": "approved",
"type": "success",
"label": "승인",
"sublabel": "사용 가능",
"lane": "main",
"col": 4,
"step": "05",
"tag": "approved"
},
{
"id": "held",
"type": "waiting",
"label": "보류",
"sublabel": "추가 확인 필요",
"lane": "waiting",
"col": 0,
"tag": "held"
},
{
"id": "failed",
"type": "failure",
"label": "분석 실패",
"sublabel": "provider 실패",
"lane": "exceptions",
"col": 0,
"tag": "복구 가능",
"yOffset": 78
},
{
"id": "rejected",
"type": "failure",
"label": "반려",
"sublabel": "사용 불가",
"lane": "terminal",
"col": 1,
"tag": "rejected"
},
{
"id": "retained",
"type": "failure",
"label": "보존",
"sublabel": "원본 파일 없음",
"lane": "terminal",
"col": 2,
"tag": "assetMissing"
}
],
"transitions": [
{
"id": "t-intake",
"from": "intake",
"to": "analyzing",
"variant": "emphasis"
},
{
"id": "t-analyzed",
"from": "analyzing",
"to": "pending",
"variant": "emphasis",
"route": "straight"
},
{
"id": "t-review",
"from": "pending",
"to": "decision",
"variant": "emphasis"
},
{
"id": "t-approve",
"from": "decision",
"to": "approved",
"label": "승인",
"variant": "emphasis"
},
{
"id": "t-hold",
"from": "pending",
"to": "held",
"label": "추가 확인 필요",
"labelDy": 24
},
{
"id": "t-fail",
"from": "analyzing",
"to": "failed",
"label": "provider 실패",
"variant": "security",
"labelDy": -24
},
{
"id": "t-retry",
"from": "failed",
"to": "analyzing",
"label": "재시도",
"variant": "emphasis",
"labelDy": 6,
"channelX": 240,
"toSide": "bottom"
},
{
"id": "t-reject",
"from": "decision",
"to": "rejected",
"label": "반려",
"variant": "security",
"labelDy": 24
},
{
"id": "t-retain",
"from": "rejected",
"to": "retained",
"label": "원본 파일 소실",
"variant": "dashed",
"labelDy": 60
},
{
"id": "t-rerun",
"from": "held",
"to": "analyzing",
"label": "재분석 요청",
"variant": "emphasis",
"channelX": 170,
"toSide": "left"
}
],
"cards": [
{
"dot": "emerald",
"title": "주 단계",
"items": [
"접수된 제출물은 분석을 거쳐 심사 대기 상태에서 점수와 밴드를 갖는다",
"판정은 운영자만 내린다 — 자동 분석은 밴드까지만 만든다"
]
},
{
"dot": "rose",
"title": "되돌아오는 상태",
"items": [
"분석 실패와 재분석 요청은 종료가 아니라 분석 중으로 돌아온다",
"보류는 운영자가 증거를 보강하려는 상태다 — 재분석은 기존 증거를 지우지 않는다"
]
},
{
"dot": "amber",
"title": "보존",
"items": [
"판정을 남긴 케이스는 원본 파일이 사라져도 지워지지 않는다",
"파일이 돌아오면 보존 표시는 해제되고 감사에 남는다"
]
}
]
}

View file

@ -0,0 +1,338 @@
{
"schema_version": 1,
"diagram_type": "architecture",
"meta": {
"title": "POSA Copyrighter 시스템 구성",
"output": "docs/diagrams/system-architecture.html",
"quality_profile": "showcase",
"viewBox": [
1080,
500
],
"views": [
{
"id": "request-path",
"label": "요청 경로",
"focus": [
"console",
"http",
"store",
"sqlite"
],
"note": "정적 콘솔에서 스토어와 SQLite 까지 가는 주 경로."
},
{
"id": "analysis",
"label": "분석 · 채점",
"focus": [
"store",
"images",
"analysis"
],
"note": "제출 이미지에서 증거와 점수가 만들어지는 고리."
},
{
"id": "egress",
"label": "외부 경계",
"focus": [
"store",
"gate",
"fetch",
"providers",
"ollama"
],
"note": "폐쇄망을 넘는 유일한 경로와 그 앞의 두 관문."
}
]
},
"components": [
{
"id": "console",
"type": "frontend",
"label": "운영자 콘솔",
"sublabel": "정적 SPA · 빌드 없음",
"tag": "app-*.js 15",
"pos": [
36,
170
],
"size": [
124,
62
]
},
{
"id": "throttle",
"type": "security",
"label": "인증 스로틀",
"sublabel": "10회 / 60초",
"tag": "주소별",
"pos": [
250,
60
],
"size": [
124,
62
]
},
{
"id": "http",
"type": "backend",
"label": "http_app",
"sublabel": "라우팅 · 인증 · CSP",
"tag": "바디 10MB",
"pos": [
250,
170
],
"size": [
124,
62
]
},
{
"id": "images",
"type": "database",
"label": "이미지 저장소",
"sublabel": "제출물 · manifest",
"pos": [
464,
60
],
"size": [
124,
62
]
},
{
"id": "store",
"type": "backend",
"label": "CopyrighterStore",
"sublabel": "믹스인 8개 조립",
"tag": "판정 · 감사",
"pos": [
464,
170
],
"size": [
124,
62
]
},
{
"id": "sqlite",
"type": "database",
"label": "SQLite",
"sublabel": "payload JSON · WAL",
"tag": "SCHEMA v2",
"pos": [
464,
280
],
"size": [
124,
62
]
},
{
"id": "analysis",
"type": "backend",
"label": "분석 · 채점",
"sublabel": "지문 · 얼굴 · 출처",
"tag": "RiskScorer v3",
"pos": [
678,
60
],
"size": [
124,
62
]
},
{
"id": "gate",
"type": "security",
"label": "외부 호출 게이트",
"sublabel": "쿼터 · 비상 차단",
"pos": [
678,
280
],
"size": [
124,
62
]
},
{
"id": "ollama",
"type": "external",
"label": "로컬 LLM",
"sublabel": "ollama · qwen2.5:7b",
"tag": "선택",
"pos": [
892,
170
],
"size": [
124,
62
]
},
{
"id": "fetch",
"type": "security",
"label": "store_remote_fetch",
"sublabel": "SSRF 가드",
"tag": "DNS 1회 해석",
"pos": [
892,
280
],
"size": [
124,
62
]
},
{
"id": "providers",
"type": "external",
"label": "외부 검색 API",
"sublabel": "Vision · CSE · 네이버",
"pos": [
678,
420
],
"size": [
124,
62
]
}
],
"boundaries": [
{
"kind": "region",
"label": "폐쇄망 (air-gapped) — 런타임 서드파티는 이미지 처리 3종",
"wraps": [
"console",
"throttle",
"http",
"images",
"store",
"sqlite",
"analysis",
"gate",
"ollama",
"fetch"
]
},
{
"kind": "security-group",
"label": "네트워크 경계",
"wraps": [
"gate",
"fetch"
]
}
],
"connections": [
{
"id": "console-api",
"from": "console",
"to": "http",
"label": "REST",
"variant": "emphasis"
},
{
"id": "auth-throttle",
"from": "throttle",
"to": "http",
"label": "인증 시도 제한",
"variant": "security",
"labelDy": 24
},
{
"id": "http-store",
"from": "http",
"to": "store",
"variant": "emphasis"
},
{
"id": "store-sqlite",
"from": "store",
"to": "sqlite",
"label": "payload JSON",
"variant": "emphasis",
"labelDy": 24
},
{
"id": "store-images",
"from": "store",
"to": "images"
},
{
"id": "analysis-store",
"from": "analysis",
"to": "store",
"label": "증거 · 점수 · 밴드",
"labelDy": 24
},
{
"id": "store-gate",
"from": "store",
"to": "gate",
"label": "외부 호출 요청"
},
{
"id": "gate-fetch",
"from": "gate",
"to": "fetch",
"label": "허용 시 통과",
"variant": "security"
},
{
"id": "fetch-providers",
"from": "fetch",
"to": "providers",
"label": "HTTPS",
"variant": "security"
},
{
"id": "store-ollama",
"from": "store",
"to": "ollama",
"label": "판독 요청",
"variant": "dashed"
}
],
"cards": [
{
"dot": "rose",
"title": "경계",
"items": [
"설치·런타임에 인터넷 없음을 전제한다 — CDN · 원격 fetch · 자동업데이트 금지",
"외부로 나가는 경로는 store_remote_fetch 하나뿐이고 SSRF 가드가 앞에 있다",
"게이트는 쿼터와 비상 차단을 본다 — 시드 · 재분석 · 얼굴 크롭 경로 모두 통과한다"
]
},
{
"dot": "emerald",
"title": "서버",
"items": [
"http_app 이 라우팅 · Bearer/세션 인증 · CSP · CSRF · 10MB 바디 상한을 맡는다",
"CopyrighterStore 는 믹스인 8개로 조립되고 SQLite payload JSON 에 쓴다",
"BatchAnalyzer 를 스토어가 구동하고, 증거·점수·밴드가 스토어로 돌아온다"
]
},
{
"dot": "cyan",
"title": "콘솔 · 의존성",
"items": [
"콘솔은 빌드 스텝이 없는 클래식 스크립트 15개로 전역 스코프를 공유한다 — 로드 순서가 계약",
"얼굴 검출은 YuNet(ONNX)이고 가중치는 gitignore 된 models/ 에 있다",
"런타임 서드파티는 numpy · opencv-python-headless · pillow 3종",
"LLM 은 로컬 ollama 선택 사항이며 없으면 연결이 빠르게 실패한다"
]
}
]
}

Binary file not shown.