분석 사이클 완료 후 자동 실행되는 출력 파이프라인: - event_generator: 분석결과 → 이벤트 자동 생성 (7개 룰, 카테고리별 dedup) - violation_classifier: 위반 유형 라벨링 (EEZ/DARK/MMSI/TRANSSHIP/GEAR/RISK) - kpi_writer: 실시간 KPI 6개 갱신 (오늘 기준 카운트) - stats_aggregator: hourly/daily/monthly 사전 집계 (UPSERT) - alert_dispatcher: CRITICAL/HIGH 이벤트 자동 알림 생성 scheduler.py에 출력 모듈 통합 (분석 8단계 완료 후 실행, non-fatal) DB 연동 테스트 통과 (alerts 8건 생성, KPI tracking_active=2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
110 lines
3.5 KiB
Python
110 lines
3.5 KiB
Python
"""
|
|
실시간 KPI 갱신 — prediction_kpi_realtime 테이블 업데이트.
|
|
|
|
매 분석 사이클마다 오늘 날짜 기준 카운트를 계산하여 6개 KPI 갱신.
|
|
"""
|
|
import logging
|
|
from datetime import date, datetime, timezone
|
|
|
|
from config import qualified_table
|
|
from db.kcgdb import get_conn
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
KPI_TABLE = qualified_table('prediction_kpi_realtime')
|
|
EVENTS_TABLE = qualified_table('prediction_events')
|
|
ENF_TABLE = qualified_table('enforcement_records')
|
|
VAR_TABLE = qualified_table('vessel_analysis_results')
|
|
|
|
|
|
def run_kpi_writer() -> dict:
|
|
"""
|
|
오늘 날짜 기준으로 6개 KPI를 재계산하여 갱신.
|
|
|
|
Returns:
|
|
{ kpi_key: value } 딕셔너리
|
|
"""
|
|
today = date.today()
|
|
today_start = datetime(today.year, today.month, today.day, tzinfo=timezone.utc)
|
|
now = datetime.now(timezone.utc)
|
|
results = {}
|
|
|
|
with get_conn() as conn:
|
|
cur = conn.cursor()
|
|
|
|
# 1. 실시간 탐지 (오늘 분석 결과 수)
|
|
cur.execute(
|
|
f"SELECT COUNT(DISTINCT mmsi) FROM {VAR_TABLE} WHERE analyzed_at >= %s",
|
|
(today_start,)
|
|
)
|
|
realtime = cur.fetchone()[0] or 0
|
|
results['realtime_detection'] = realtime
|
|
|
|
# 2. EEZ 침범 (오늘 EEZ 관련 이벤트)
|
|
cur.execute(
|
|
f"SELECT COUNT(*) FROM {EVENTS_TABLE} WHERE category = 'EEZ_INTRUSION' AND occurred_at >= %s",
|
|
(today_start,)
|
|
)
|
|
eez = cur.fetchone()[0] or 0
|
|
results['eez_violation'] = eez
|
|
|
|
# 3. 다크베셀 (현재 dark 상태인 선박)
|
|
cur.execute(
|
|
f"""SELECT COUNT(DISTINCT mmsi) FROM {VAR_TABLE}
|
|
WHERE is_dark = true AND analyzed_at >= %s""",
|
|
(today_start,)
|
|
)
|
|
dark = cur.fetchone()[0] or 0
|
|
results['dark_vessel'] = dark
|
|
|
|
# 4. 환적 의심 (오늘)
|
|
cur.execute(
|
|
f"""SELECT COUNT(*) FROM {EVENTS_TABLE}
|
|
WHERE category = 'ILLEGAL_TRANSSHIP' AND occurred_at >= %s""",
|
|
(today_start,)
|
|
)
|
|
transship = cur.fetchone()[0] or 0
|
|
results['illegal_transship'] = transship
|
|
|
|
# 5. 추적 중 (IN_PROGRESS 상태 이벤트)
|
|
cur.execute(
|
|
f"SELECT COUNT(*) FROM {EVENTS_TABLE} WHERE status = 'IN_PROGRESS'"
|
|
)
|
|
tracking = cur.fetchone()[0] or 0
|
|
results['tracking_active'] = tracking
|
|
|
|
# 6. 나포/검문 (오늘 단속)
|
|
cur.execute(
|
|
f"SELECT COUNT(*) FROM {ENF_TABLE} WHERE enforced_at >= %s",
|
|
(today_start,)
|
|
)
|
|
captured = cur.fetchone()[0] or 0
|
|
results['captured_inspected'] = captured
|
|
|
|
# KPI 테이블 업데이트 (이전 값과 비교하여 trend 계산)
|
|
for key, value in results.items():
|
|
cur.execute(
|
|
f"SELECT value FROM {KPI_TABLE} WHERE kpi_key = %s",
|
|
(key,)
|
|
)
|
|
row = cur.fetchone()
|
|
prev = row[0] if row else 0
|
|
if value > prev:
|
|
trend, delta = 'up', ((value - prev) / max(prev, 1)) * 100
|
|
elif value < prev:
|
|
trend, delta = 'down', ((value - prev) / max(prev, 1)) * 100
|
|
else:
|
|
trend, delta = 'flat', 0.0
|
|
|
|
cur.execute(
|
|
f"""UPDATE {KPI_TABLE}
|
|
SET value = %s, trend = %s, delta_pct = %s, updated_at = %s
|
|
WHERE kpi_key = %s""",
|
|
(value, trend, round(delta, 2), now, key)
|
|
)
|
|
|
|
conn.commit()
|
|
|
|
logger.info(f'kpi_writer: {results}')
|
|
return results
|