#!/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 #=================================================================== # 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 쌍끌이 공조 탐지 ---" $PSQL_TABLE << 'SQL' SELECT mmsi, zone_code, vessel_type, risk_score, (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-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-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 7: 사이클 로그 + 에러 #=================================================================== echo "" echo "=================================================================" echo "PART 7: 사이클 로그 (최근 6분)" echo "=================================================================" 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|ERROR|Traceback' | \ tail -20 #=================================================================== # 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