왜 필요한가

기존 AI 위협 분석은 SITREP을 "생성"하는 데서 끝났다. 실제 C4I 운용에서는 AI가 CRITICAL/HIGH로 판단했다고 그대로 자동 조치되지 않는다 — 담당자가 확인하고 승인/반려해야 하며, 그 결정 자체가 감사 기록으로 남아야 한다.

이 기능은 "AI 판단 → 사람 승인 → 결정 기록"이라는 루프를 완성한다.

동작 방식

ThreatAnalysisService.analyze(event)
        ↓
   doAnalyze() -- 기존 RAG 분석(규칙 등급 + pgvector 검색 + Gemini SITREP)
        ↓
ThreatApprovalService.createIfNeeded(...)
        ├─ threatLevel이 HIGH/CRITICAL이 아니면 아무 일도 안 함
        ├─ 같은 targetId로 이미 PENDING 승인 요청이 있으면 중복 생성 안 함
        └─ 신규 생성 시 DB 저장 + /topic/approvals로 WebSocket 브로드캐스트

트리거 지점은 ThreatAnalysisService.analyze() 하나뿐이다. REST(수동 버튼)와 Kafka(analyzeAsync, 자동 분석) 둘 다 내부적으로 analyze()를 거치므로 별도 분기 없이 두 경로 모두 커버된다.

GEMINI_API_KEY 미설정 상태(AI 비활성화)에서도 규칙 기반 등급이 HIGH/CRITICAL이면 승인 요청이 생성된다 — AI 없이도 핵심 워크플로(사람이 고위험 표적을 검토)는 살아있어야 한다는 기존 Graceful Degradation 원칙과 동일.

데이터 모델

ThreatApproval(domain/approval/entity)

<table header-row="true"><tr><td>필드</td><td>설명</td></tr><tr><td>targetId, targetType, threatLevel, sitrep</td><td>승인 요청 시점의 스냅샷</td></tr><tr><td>status</td><td>PENDING → APPROVED / REJECTED</td></tr><tr><td>requestedAt</td><td>생성 시각</td></tr><tr><td>decidedAt, decidedBy, decisionReason</td><td>결정 시각/결정자/사유 — 감사 로그 역할</td></tr></table>

API

승인 대기 목록 조회

GET /api/v1/threat-approvals              # status=PENDING 기본값
GET /api/v1/threat-approvals?status=ALL   # 전체 이력

승인/반려 결정

POST /api/v1/threat-approvals/{id}/decide
Content-Type: application/json

{
  "decision": "APPROVED",
  "decidedBy": "operator1",
  "reason": "자산 방호 태세 전환 승인"
}

decision은 APPROVED/REJECTED만 허용되며, 그 외 값은 400으로 거부된다. 존재하지 않는 id는 404.

WebSocket

/topic/approvals — 승인 요청 생성 시점과 결정 시점 양쪽에 동일한 페이로드를 브로드캐스트한다. 기존 /topic/targets와 동일한 STOMP 브로커·엔드포인트(/ws)를 공유하며, 토픽만 분리된다.

프론트엔드

c4i-dashboard-frontend의 ApprovalPanel.tsx — 대시보드 상단 "🛡️ 승인 대기" 버튼으로 토글되는 좌측 패널.