kcg-ai-monitoring/prediction/scripts/diagnostic-snapshot.sh
htlee 2ceeb966d8 feat(prediction): Phase 1-2 detection model registry + snapshot 관찰 보강
- models_core 패키지 신설 — BaseDetectionModel / ModelContext / ModelResult
  + Registry (ACTIVE 버전 인스턴스화, DAG 순환 검출, topo 플랜)
  + DAGExecutor (PRIMARY→ctx.shared 주입, SHADOW persist-only 오염 차단)
  + params_loader (5분 TTL 캐시), feature_flag (PREDICTION_USE_MODEL_REGISTRY)
- V034 스키마 정합성 사전 검증 + silent error 3건 선제 방어
  · model_id VARCHAR(64) 초과 시 __init__ 에서 즉시 ValueError
  · metric_key VARCHAR(64) 초과는 경고 후 drop (다른 metric 는 저장)
  · persist 가 ctx.conn 재사용 (pool maxconn=5 고갈 방지)
- scheduler.py — 10단계 feature flag 분기 (기본 0, 구 경로 보존)
- partition_manager — detection_model_run_outputs 월별 파티션 자동 생성/DROP
- 유닛테스트 15 케이스 전체 통과 (DAG 순환, SHADOW 오염 차단, 길이 검증)
- snapshot 스크립트 (hourly/diagnostic) 개선
  · spoofing gt0/gt03/gt05/gt07 세분화 — 'silent fault' vs 'no signal' 구분
  · V030 gear_identity_collisions 원시 섹션 (CRITICAL 51건 OPEN 포착)
  · V034 detection_model_* 모니터링 섹션 (Phase 2 대비)
  · stage timing 집계 + stats_hourly vs events category drift 감시

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 08:07:29 +09:00

530 lines
20 KiB
Bash
Raw Blame 히스토리

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# prediction 알고리즘 진단 스냅샷 수집기 (5분 주기, 수동 종료까지 연속 실행)
#
# 용도: DAR-03 G코드 + 쌍끌이 + 어구 위반 포함 알고리즘 동작 검증
# 실행: nohup bash /home/apps/kcg-ai-prediction/scripts/diagnostic-snapshot.sh &
# 종료: kill $(cat /home/apps/kcg-ai-prediction/data/diag/diag.pid)
# 출력: /home/apps/kcg-ai-prediction/data/diag/YYYYMMDD-HHMM.txt
set -u
OUTDIR=/home/apps/kcg-ai-prediction/data/diag
mkdir -p "$OUTDIR"
echo $$ > "$OUTDIR/diag.pid"
export PGPASSWORD=Kcg2026ai
PSQL="psql -U kcg-app -d kcgaidb -h 211.208.115.83 -P pager=off -x"
PSQL_TABLE="psql -U kcg-app -d kcgaidb -h 211.208.115.83 -P pager=off"
INTERVAL_SEC=300 # 5분
while true; do
STAMP=$(date '+%Y%m%d-%H%M')
OUT="$OUTDIR/$STAMP.txt"
{
echo "###################################################################"
echo "# PREDICTION DIAGNOSTIC SNAPSHOT (DAR-03 enhanced)"
echo "# generated: $(date '+%Y-%m-%d %H:%M:%S %Z')"
echo "# host: $(hostname)"
echo "# interval: ${INTERVAL_SEC}s"
echo "###################################################################"
#===================================================================
# PART 1: 종합 지표
#===================================================================
echo ""
echo "================================================================="
echo "PART 1: 종합 지표 (last 5min)"
echo "================================================================="
$PSQL_TABLE << 'SQL'
SELECT count(*) total,
count(*) FILTER (WHERE vessel_type != 'UNKNOWN') pipeline,
count(*) FILTER (WHERE vessel_type = 'UNKNOWN') lightweight,
count(*) FILTER (WHERE is_dark) dark,
count(*) FILTER (WHERE transship_suspect) transship,
count(*) FILTER (WHERE gear_judgment IS NOT NULL AND gear_judgment != '') gear_violation,
count(*) FILTER (WHERE risk_level='CRITICAL') crit,
count(*) FILTER (WHERE risk_level='HIGH') high,
round(avg(risk_score)::numeric, 1) avg_risk,
max(risk_score) max_risk,
round(count(*) FILTER (WHERE is_dark)::numeric / NULLIF(count(*), 0) * 100, 1) AS dark_pct
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes';
SQL
echo ""
echo "--- 1b. SPOOFING signal health (silent-vs-fault 구분) ---"
# gt0 > 0 인데 gt0.5 = 0 이면 "알고리즘 동작 + threshold 미돌파" (정상),
# gt0 = 0 이면 "알고리즘 자체가 계산을 못 하고 있음" (silent fault) → 로그 추적 필요.
$PSQL_TABLE << 'SQL'
SELECT count(*) total,
count(*) FILTER (WHERE spoofing_score > 0) gt0,
count(*) FILTER (WHERE spoofing_score > 0.3) gt03,
count(*) FILTER (WHERE spoofing_score > 0.5) gt05,
count(*) FILTER (WHERE spoofing_score > 0.7) gt07,
round(avg(spoofing_score)::numeric, 4) avg_score,
round(max(spoofing_score)::numeric, 4) max_score
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes';
SQL
#===================================================================
# PART 2: 다크베셀 심층 진단
#===================================================================
echo ""
echo "================================================================="
echo "PART 2: DARK VESSEL 심층 진단"
echo "================================================================="
echo ""
echo "--- 2-1. dark_suspicion_score 히스토그램 ---"
$PSQL_TABLE << 'SQL'
SELECT CASE
WHEN (features->>'dark_suspicion_score')::int >= 90 THEN 'a_90-100 (CRITICAL_HIGH)'
WHEN (features->>'dark_suspicion_score')::int >= 70 THEN 'b_70-89 (CRITICAL)'
WHEN (features->>'dark_suspicion_score')::int >= 50 THEN 'c_50-69 (HIGH)'
WHEN (features->>'dark_suspicion_score')::int >= 30 THEN 'd_30-49 (WATCH)'
WHEN (features->>'dark_suspicion_score')::int >= 1 THEN 'e_1-29 (NONE_SCORED)'
ELSE 'f_0 (NOT_DARK)'
END bucket,
count(*) cnt,
round(avg(gap_duration_min)::numeric, 0) avg_gap_min,
round(avg(risk_score)::numeric, 1) avg_risk
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes'
AND is_dark = true
GROUP BY bucket ORDER BY bucket;
SQL
echo ""
echo "--- 2-2. dark_patterns 발동 빈도 ---"
$PSQL_TABLE << 'SQL'
SELECT pattern,
count(*) cnt,
round(count(*)::numeric / NULLIF((SELECT count(*) FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes' AND is_dark), 0) * 100, 1) AS pct
FROM kcg.vessel_analysis_results,
LATERAL jsonb_array_elements_text(features->'dark_patterns') AS pattern
WHERE analyzed_at > now() - interval '5 minutes'
AND is_dark = true
GROUP BY pattern ORDER BY cnt DESC;
SQL
echo ""
echo "--- 2-3. P9/P10/P11 + coverage 요약 ---"
$PSQL_TABLE << 'SQL'
WITH dark AS (
SELECT features FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes' AND is_dark = true
)
SELECT count(*) total_dark,
count(*) FILTER (WHERE features->>'dark_patterns' LIKE '%fishing_vessel_dark%'
OR features->>'dark_patterns' LIKE '%cargo_natural_gap%') p9,
count(*) FILTER (WHERE features->>'dark_patterns' LIKE '%underway_deliberate_off%'
OR features->>'dark_patterns' LIKE '%anchored_natural_gap%') p10,
count(*) FILTER (WHERE features->>'dark_patterns' LIKE '%heading_cog_mismatch%') p11,
count(*) FILTER (WHERE features->>'dark_patterns' LIKE '%out_of_coverage%') coverage
FROM dark;
SQL
echo ""
echo "--- 2-4. CRITICAL dark 상위 10건 ---"
$PSQL_TABLE << 'SQL'
SELECT mmsi, gap_duration_min, zone_code, activity_state,
(features->>'dark_suspicion_score')::int AS score,
features->>'dark_tier' AS tier,
features->>'dark_patterns' AS patterns,
risk_score, risk_level
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes'
AND features->>'dark_tier' = 'CRITICAL'
ORDER BY (features->>'dark_suspicion_score')::int DESC
LIMIT 10;
SQL
#===================================================================
# PART 3: 환적 탐지
#===================================================================
echo ""
echo "================================================================="
echo "PART 3: TRANSSHIPMENT 진단"
echo "================================================================="
$PSQL_TABLE << 'SQL'
SELECT count(*) suspects,
count(*) FILTER (WHERE (features->>'transship_score')::numeric >= 70) critical,
count(*) FILTER (WHERE (features->>'transship_score')::numeric >= 50
AND (features->>'transship_score')::numeric < 70) high,
round(avg((features->>'transship_score')::numeric)::numeric, 1) avg_score,
round(avg(transship_duration_min)::numeric, 0) avg_dur
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes' AND transship_suspect = true;
SQL
echo ""
echo "--- 3-1. 환적 의심 개별 건 ---"
$PSQL_TABLE << 'SQL'
SELECT mmsi, transship_pair_mmsi pair, transship_duration_min dur,
(features->>'transship_score')::numeric score,
features->>'transship_tier' tier, zone_code
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes' AND transship_suspect = true
ORDER BY (features->>'transship_score')::numeric DESC;
SQL
#===================================================================
# PART 4: G코드 어구 위반 진단 (DAR-03 신규)
#===================================================================
echo ""
echo "================================================================="
echo "PART 4: G코드 어구 위반 진단 (DAR-03)"
echo "================================================================="
echo ""
echo "--- 4-1. gear_judgment 분포 ---"
$PSQL_TABLE << 'SQL'
SELECT coalesce(NULLIF(gear_judgment, ''), '(none)') AS judgment,
count(*) cnt,
round(avg(risk_score)::numeric, 1) avg_risk
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes'
GROUP BY judgment ORDER BY cnt DESC;
SQL
echo ""
echo "--- 4-2. G코드별 발동 빈도 ---"
$PSQL_TABLE << 'SQL'
SELECT gcode,
count(*) cnt,
round(avg(risk_score)::numeric, 1) avg_risk
FROM kcg.vessel_analysis_results,
LATERAL jsonb_array_elements_text(features->'g_codes') AS gcode
WHERE analyzed_at > now() - interval '5 minutes'
GROUP BY gcode ORDER BY cnt DESC;
SQL
echo ""
echo "--- 4-3. G-01 수역-어구 위반 상세 (상위 20건) ---"
$PSQL_TABLE << 'SQL'
SELECT mmsi, zone_code, vessel_type, risk_score, gear_judgment,
(features->>'gear_violation_score')::int AS gv_score,
(features->'gear_violation_evidence'->'G-01'->>'allowed')::text AS allowed
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes'
AND features->>'g_codes' LIKE '%G-01%'
ORDER BY risk_score DESC LIMIT 20;
SQL
echo ""
echo "--- 4-4. G-06 쌍끌이 공조 탐지 (tier 포함) ---"
$PSQL_TABLE << 'SQL'
SELECT mmsi, zone_code, vessel_type, risk_score,
features->>'pair_tier' tier,
(features->'gear_violation_evidence'->'G-06'->>'sync_duration_min') sync_min,
(features->'gear_violation_evidence'->'G-06'->>'mean_separation_nm') sep_nm,
(features->'gear_violation_evidence'->'G-06'->>'pair_mmsi') pair_mmsi,
features->>'pair_trawl_detected' pt
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes'
AND (features->>'pair_trawl_detected' = 'true' OR features->>'g_codes' LIKE '%G-06%')
ORDER BY risk_score DESC LIMIT 20;
SQL
echo ""
echo "--- 4-4.1 pair_trawl tier 분포 (DAR-03 신호 강도별) ---"
$PSQL_TABLE << 'SQL'
SELECT coalesce(features->>'pair_tier', '(none)') tier,
count(*) cnt,
round(avg((features->>'similarity')::numeric)::numeric, 3) avg_sim,
round(avg((features->'gear_violation_evidence'->'G-06'->>'sync_duration_min')::numeric)::numeric, 1) avg_sync_min
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes'
AND features->>'pair_trawl_detected' = 'true'
GROUP BY tier ORDER BY cnt DESC;
SQL
echo ""
echo "--- 4-4.2 pair detection reject 사유 (최근 5분 로그) ---"
ssh redis-211 "sudo journalctl -u kcg-ai-prediction --no-pager --since '5 minutes ago' | grep -oE 'pair detection:[^$]+reject=\{[^}]+\}' | tail -5" 2>/dev/null || true
echo ""
echo "--- 4-4.3 G-02 금어기 조업 탐지 ---"
$PSQL_TABLE << 'SQL'
SELECT mmsi, zone_code, vessel_type, risk_score,
features->>'g_codes' g_codes,
(features->'gear_violation_evidence'->'G-02'->>'observed_at') observed_at,
features->>'registered_fishery_code' fishery_code
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes'
AND features->>'g_codes' LIKE '%G-02%'
ORDER BY risk_score DESC LIMIT 15;
SQL
echo ""
echo "--- 4-4.4 G-03 미등록/허가외 어구 탐지 ---"
$PSQL_TABLE << 'SQL'
SELECT mmsi, zone_code, vessel_type, risk_score,
features->>'g_codes' g_codes,
(features->'gear_violation_evidence'->'G-03'->>'detected_gear') detected,
(features->'gear_violation_evidence'->'G-03'->>'registered_fishery_code') registered,
(features->'gear_violation_evidence'->'G-03'->>'allowed_gears') allowed
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes'
AND features->>'g_codes' LIKE '%G-03%'
ORDER BY risk_score DESC LIMIT 15;
SQL
echo ""
echo "--- 4-5. G-04 MMSI 조작 + G-05 어구 이동 ---"
$PSQL_TABLE << 'SQL'
SELECT mmsi, zone_code, vessel_type, risk_score,
features->>'g_codes' g_codes,
(features->'gear_violation_evidence'->'G-04'->>'cycling_count') g04_cycle,
(features->'gear_violation_evidence'->'G-05'->>'drift_nm') g05_drift
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes'
AND (features->>'g_codes' LIKE '%G-04%' OR features->>'g_codes' LIKE '%G-05%')
ORDER BY risk_score DESC LIMIT 10;
SQL
echo ""
echo "--- 4-5.5 pair_type 분포 (DAR-03 base-target 탐색, 5min) ---"
$PSQL_TABLE << 'SQL'
SELECT coalesce(features->>'pair_type', '(none)') pair_type,
count(*) cnt,
round(avg((features->>'similarity')::numeric)::numeric, 3) avg_sim
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes'
AND features->>'pair_trawl_detected' = 'true'
GROUP BY pair_type ORDER BY cnt DESC;
SQL
echo ""
echo "--- 4-6. GEAR_ILLEGAL 이벤트 ---"
$PSQL_TABLE << 'SQL'
SELECT category, level, title, count(*) cnt
FROM kcg.prediction_events
WHERE created_at > now() - interval '5 minutes'
AND category IN ('GEAR_ILLEGAL', 'MMSI_TAMPERING')
GROUP BY category, level, title ORDER BY cnt DESC;
SQL
echo ""
echo "--- 4-7. violation_categories ILLEGAL_GEAR ---"
$PSQL_TABLE << 'SQL'
SELECT count(*) total,
count(*) FILTER (WHERE gear_judgment = 'ZONE_VIOLATION') zone_viol,
count(*) FILTER (WHERE gear_judgment = 'PAIR_TRAWL') pair_trawl,
count(*) FILTER (WHERE gear_judgment = 'GEAR_MISMATCH') mismatch
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes'
AND 'ILLEGAL_GEAR' = ANY(violation_categories);
SQL
#===================================================================
# PART 5: 수역 × 어구 타입 교차 (G-01 검증 핵심)
#===================================================================
echo ""
echo "================================================================="
echo "PART 5: 수역 x 어구 타입 교차 (G-01 검증)"
echo "================================================================="
$PSQL_TABLE << 'SQL'
SELECT zone_code, vessel_type, count(*) total,
count(*) FILTER (WHERE features->>'g_codes' LIKE '%G-01%') g01
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes'
AND vessel_type != 'UNKNOWN' AND zone_code LIKE 'ZONE_%'
GROUP BY zone_code, vessel_type ORDER BY zone_code, vessel_type;
SQL
#===================================================================
# PART 6: 이벤트 + KPI
#===================================================================
echo ""
echo "================================================================="
echo "PART 6: 이벤트 + KPI"
echo "================================================================="
$PSQL_TABLE << 'SQL'
SELECT category, level, count(*) cnt
FROM kcg.prediction_events
WHERE created_at > now() - interval '5 minutes'
GROUP BY category, level ORDER BY cnt DESC;
SQL
$PSQL_TABLE << 'SQL'
SELECT kpi_key, value, trend, delta_pct, updated_at
FROM kcg.prediction_kpi_realtime ORDER BY kpi_key;
SQL
#===================================================================
# PART 6.5: V030 + V034 관찰 (원시 테이블)
#===================================================================
echo ""
echo "================================================================="
echo "PART 6.5: V030 gear_identity_collisions + V034 detection_model_*"
echo "================================================================="
echo ""
echo "--- 6.5-1. gear_identity_collisions severity x status (1h) ---"
$PSQL_TABLE << 'SQL'
SELECT severity, status, count(*) cnt, max(last_seen_at) last_seen
FROM kcg.gear_identity_collisions
WHERE last_seen_at > now() - interval '1 hour'
GROUP BY severity, status ORDER BY cnt DESC;
SQL
echo ""
echo "--- 6.5-2. detection_models + 버전 상태 (Phase 1-2 이후 활성) ---"
$PSQL_TABLE << 'SQL'
SELECT count(*) AS catalog,
count(*) FILTER (WHERE is_enabled) AS enabled
FROM kcg.detection_models;
SQL
$PSQL_TABLE << 'SQL'
SELECT status, coalesce(role,'(null)') role, count(*) cnt
FROM kcg.detection_model_versions
GROUP BY status, role ORDER BY status, role;
SQL
echo ""
echo "--- 6.5-3. detection_model_run_outputs 5분 적재 (feature flag ON 시 증가) ---"
$PSQL_TABLE << 'SQL'
SELECT model_id, role, count(*) rows
FROM kcg.detection_model_run_outputs
WHERE cycle_started_at > now() - interval '5 minutes'
GROUP BY model_id, role ORDER BY rows DESC LIMIT 10;
SQL
#===================================================================
# PART 7: 사이클 로그 + 에러 + stage timing
#===================================================================
echo ""
echo "================================================================="
echo "PART 7: 사이클 로그 (최근 6분)"
echo "================================================================="
# stage_runner (Phase 0-1) + DAGExecutor (Phase 1-2) 로그 추가
journalctl -u kcg-ai-prediction --since '6 minutes ago' --no-pager 2>/dev/null | \
grep -E 'analysis cycle:|lightweight|pipeline dark:|event_generator:|pair_trawl|gear_violation|GEAR_ILLEGAL|stage [a-z_]+ (ok|failed)|DAGExecutor done|detection model registry|ERROR|Traceback' | \
tail -40
echo ""
echo "--- 7-1. STAGE TIMING (소요시간 상위 + 실패) ---"
journalctl -u kcg-ai-prediction --since '6 minutes ago' --no-pager 2>/dev/null | \
grep -oE 'stage [a-z_@.[:blank:][:digit:].-]+ (ok in [0-9.]+s|failed)' | \
awk '/failed/ {print "FAIL " $0; next}
/ok in/ {n=split($0,a," "); sec=a[n]; sub(/s$/,"",sec); printf "%8.2fs %s\n", sec, $0}' | \
sort -rn | awk 'NR<=8 || /^FAIL/' | head -20
#===================================================================
# PART 7.5: 한중어업협정 레지스트리 매칭 (V029)
#===================================================================
echo ""
echo "================================================================="
echo "PART 7.5: FISHERY PERMIT CN REGISTRY 매칭 현황"
echo "================================================================="
echo ""
echo "--- 7.5-1. fleet_vessels 매칭 현황 (현재 연도) ---"
$PSQL_TABLE << 'SQL'
SELECT permit_year,
count(*) total,
count(mmsi) with_mmsi,
round(count(mmsi)::numeric / NULLIF(count(*),0) * 100, 1) match_pct
FROM kcg.fleet_vessels
WHERE permit_year IS NOT NULL
GROUP BY permit_year ORDER BY permit_year DESC;
SQL
echo ""
echo "--- 7.5-2. fishery_code 별 매칭률 (현재 연도) ---"
$PSQL_TABLE << 'SQL'
SELECT fishery_code, count(*) total, count(mmsi) matched,
round(count(mmsi)::numeric / NULLIF(count(*),0) * 100, 1) pct
FROM kcg.fleet_vessels
WHERE permit_year = EXTRACT(YEAR FROM now())::int
GROUP BY fishery_code ORDER BY total DESC;
SQL
echo ""
echo "--- 7.5-2b. match_method 분포 (NAME_EXACT vs NAME_FUZZY) ---"
$PSQL_TABLE << 'SQL'
SELECT coalesce(match_method, '(unmatched)') method,
count(*) cnt,
round(avg(match_confidence)::numeric, 3) avg_conf
FROM kcg.fleet_vessels
WHERE permit_year = EXTRACT(YEAR FROM now())::int
GROUP BY method ORDER BY cnt DESC;
SQL
echo ""
echo "--- 7.5-2c. fishery_code × match_method 교차 ---"
$PSQL_TABLE << 'SQL'
SELECT fishery_code,
count(*) FILTER (WHERE match_method = 'NAME_EXACT') exact,
count(*) FILTER (WHERE match_method = 'NAME_FUZZY') fuzzy,
count(*) FILTER (WHERE mmsi IS NULL) unmatched,
count(*) total
FROM kcg.fleet_vessels
WHERE permit_year = EXTRACT(YEAR FROM now())::int
GROUP BY fishery_code ORDER BY total DESC;
SQL
echo ""
echo "--- 7.5-3. vessel_analysis_results.gear_code 분포 (last 5min) ---"
$PSQL_TABLE << 'SQL'
SELECT coalesce(gear_code, '(null)') gear_code,
count(*) cnt,
round(avg(risk_score)::numeric, 1) avg_risk
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes'
GROUP BY gear_code ORDER BY cnt DESC LIMIT 15;
SQL
echo ""
echo "--- 7.5-4. 최근 매칭된 선박 (top 10 by last_seen_at) ---"
$PSQL_TABLE << 'SQL'
SELECT permit_no, fishery_code, name_cn, mmsi, match_method,
match_confidence, last_seen_at
FROM kcg.fleet_vessels
WHERE permit_year = EXTRACT(YEAR FROM now())::int AND mmsi IS NOT NULL
ORDER BY last_seen_at DESC NULLS LAST LIMIT 10;
SQL
#===================================================================
# PART 8: 해역별 종합 교차
#===================================================================
echo ""
echo "================================================================="
echo "PART 8: 해역별 종합 교차표"
echo "================================================================="
$PSQL_TABLE << 'SQL'
SELECT zone_code, count(*) total,
count(*) FILTER (WHERE is_dark) dark,
count(*) FILTER (WHERE transship_suspect) transship,
count(*) FILTER (WHERE gear_judgment IS NOT NULL AND gear_judgment != '') gear_viol,
count(*) FILTER (WHERE risk_level='CRITICAL') crit,
count(*) FILTER (WHERE risk_level='HIGH') high,
round(avg(risk_score)::numeric, 1) avg_risk
FROM kcg.vessel_analysis_results
WHERE analyzed_at > now() - interval '5 minutes'
GROUP BY zone_code ORDER BY total DESC;
SQL
echo ""
echo "================================================================="
echo "END OF SNAPSHOT $STAMP"
echo "================================================================="
} > "$OUT" 2>&1
echo "[diag] $(date '+%H:%M:%S') saved: $OUT ($(wc -l < "$OUT") lines)"
sleep $INTERVAL_SEC
done