diff --git a/backend/src/main/java/gc/mda/kcg/domain/fleet/FleetCompanyController.java b/backend/src/main/java/gc/mda/kcg/domain/fleet/FleetCompanyController.java index 2f48b5c..0078379 100644 --- a/backend/src/main/java/gc/mda/kcg/domain/fleet/FleetCompanyController.java +++ b/backend/src/main/java/gc/mda/kcg/domain/fleet/FleetCompanyController.java @@ -1,6 +1,7 @@ package gc.mda.kcg.domain.fleet; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.GetMapping; @@ -17,10 +18,14 @@ public class FleetCompanyController { private final JdbcTemplate jdbcTemplate; + @Value("${DB_SCHEMA:kcg}") + private String dbSchema; + @GetMapping public ResponseEntity>> getFleetCompanies() { List> results = jdbcTemplate.queryForList( - "SELECT id, name_cn AS \"nameCn\", name_en AS \"nameEn\" FROM kcg.fleet_companies ORDER BY id" + "SELECT id, name_cn AS \"nameCn\", name_en AS \"nameEn\" FROM " + + dbSchema + ".fleet_companies ORDER BY id" ); return ResponseEntity.ok(results); } diff --git a/prediction/algorithms/polygon_builder.py b/prediction/algorithms/polygon_builder.py index db6e6ff..1133e95 100644 --- a/prediction/algorithms/polygon_builder.py +++ b/prediction/algorithms/polygon_builder.py @@ -15,6 +15,8 @@ from zoneinfo import ZoneInfo import pandas as pd +from algorithms.gear_name_rules import is_trackable_parent_name + try: from shapely.geometry import MultiPoint, Point from shapely import wkt as shapely_wkt @@ -197,6 +199,8 @@ def detect_gear_groups( continue parent_raw = (m.group(1) or name).strip() + if not is_trackable_parent_name(parent_raw): + continue parent_key = _normalize_parent(parent_raw) # 대표 이름: 공백 없는 버전 우선 (더 정규화된 형태) if parent_key not in parent_display or ' ' not in parent_raw: diff --git a/prediction/chat/domain_knowledge.py b/prediction/chat/domain_knowledge.py index f7eb7fc..991ffa8 100644 --- a/prediction/chat/domain_knowledge.py +++ b/prediction/chat/domain_knowledge.py @@ -9,6 +9,8 @@ - MarineTraffic AIS/GNSS 스푸핑 가이드 """ +from config import settings + # ── 역할 정의 ── ROLE_DEFINITION = """당신은 대한민국 해양경찰청의 **해양상황 분석 AI 어시스턴트**입니다. Python AI 분석 파이프라인(7단계 + 8개 알고리즘)의 실시간 결과를 기반으로, @@ -406,6 +408,8 @@ snpdb (AIS 원본 항적) → vessel_store (인메모리 24h) → 7단계 파이 - 집계 데이터(몇 척인지)는 이미 시스템 프롬프트에 있으므로 도구 불필요 - 대부분의 질문은 kcgdb로 충분 — snpdb 직접 조회는 특수한 항적 분석에만 사용""" +DB_SCHEMA_AND_TOOLS = DB_SCHEMA_AND_TOOLS.replace('kcg.', f'{settings.KCGDB_SCHEMA}.') + # ── 지식 섹션 레지스트리 (키워드 → 상세 텍스트) ── KNOWLEDGE_SECTIONS: dict[str, str] = { diff --git a/prediction/chat/tools.py b/prediction/chat/tools.py index f863ed4..dc05fb7 100644 --- a/prediction/chat/tools.py +++ b/prediction/chat/tools.py @@ -5,7 +5,14 @@ import logging import re from typing import Optional +from config import qualified_table + logger = logging.getLogger(__name__) +VESSEL_ANALYSIS_RESULTS = qualified_table('vessel_analysis_results') +FLEET_VESSELS = qualified_table('fleet_vessels') +GROUP_POLYGON_SNAPSHOTS = qualified_table('group_polygon_snapshots') +GEAR_CORRELATION_SCORES = qualified_table('gear_correlation_scores') +CORRELATION_PARAM_MODELS = qualified_table('correlation_param_models') # ── 사전 쿼리 패턴 (키워드 기반, 1회 왕복으로 해결) ── @@ -117,8 +124,8 @@ def execute_prequery(params: dict) -> str: v.cluster_id, v.cluster_size, v.dist_to_baseline_nm, v.is_transship_suspect, v.transship_pair_mmsi, fv.permit_no, fv.name_cn, fv.gear_code - FROM kcg.vessel_analysis_results v - LEFT JOIN kcg.fleet_vessels fv ON v.mmsi = fv.mmsi + FROM {VESSEL_ANALYSIS_RESULTS} v + LEFT JOIN {FLEET_VESSELS} fv ON v.mmsi = fv.mmsi WHERE {where} ORDER BY v.risk_score DESC LIMIT 30 @@ -217,7 +224,7 @@ def _query_fleet_group(params: dict) -> str: try: from db import kcgdb - conditions = ["snapshot_time = (SELECT MAX(snapshot_time) FROM kcg.group_polygon_snapshots)"] + conditions = [f"snapshot_time = (SELECT MAX(snapshot_time) FROM {GROUP_POLYGON_SNAPSHOTS})"] bind_params: list = [] if 'group_type' in params: @@ -230,7 +237,7 @@ def _query_fleet_group(params: dict) -> str: where = ' AND '.join(conditions) query = f""" SELECT group_type, group_key, group_label, member_count, zone_name, members - FROM kcg.group_polygon_snapshots + FROM {GROUP_POLYGON_SNAPSHOTS} WHERE {where} ORDER BY member_count DESC LIMIT 20 @@ -376,8 +383,8 @@ def _query_gear_correlation(params: dict) -> str: 'SELECT target_name, target_mmsi, target_type, current_score, ' 'streak_count, observation_count, proximity_ratio, visit_score, ' 'heading_coherence, freeze_state ' - 'FROM kcg.gear_correlation_scores s ' - 'JOIN kcg.correlation_param_models m ON s.model_id = m.id ' + f'FROM {GEAR_CORRELATION_SCORES} s ' + f'JOIN {CORRELATION_PARAM_MODELS} m ON s.model_id = m.id ' 'WHERE s.group_key = %s AND m.is_default = TRUE AND s.current_score >= 0.3 ' 'ORDER BY s.current_score DESC LIMIT %s', (group_key, limit), diff --git a/prediction/db/partition_manager.py b/prediction/db/partition_manager.py index eb5dec8..9941229 100644 --- a/prediction/db/partition_manager.py +++ b/prediction/db/partition_manager.py @@ -10,15 +10,21 @@ APScheduler 일별 작업으로 실행: import logging from datetime import date, datetime, timedelta +from config import qualified_table, settings + logger = logging.getLogger(__name__) +SYSTEM_CONFIG = qualified_table('system_config') +GEAR_CORRELATION_RAW_METRICS = qualified_table('gear_correlation_raw_metrics') +GEAR_CORRELATION_SCORES = qualified_table('gear_correlation_scores') + def _get_config_int(conn, key: str, default: int) -> int: """system_config에서 설정값 조회. 없으면 default.""" cur = conn.cursor() try: cur.execute( - "SELECT value::text FROM kcg.system_config WHERE key = %s", + f"SELECT value::text FROM {SYSTEM_CONFIG} WHERE key = %s", (key,), ) row = cur.fetchone() @@ -40,18 +46,18 @@ def _create_future_partitions(conn, days_ahead: int) -> int: cur.execute( "SELECT 1 FROM pg_class c " "JOIN pg_namespace n ON n.oid = c.relnamespace " - "WHERE c.relname = %s AND n.nspname = 'kcg'", - (partition_name,), + "WHERE c.relname = %s AND n.nspname = %s", + (partition_name, settings.KCGDB_SCHEMA), ) if cur.fetchone() is None: next_d = d + timedelta(days=1) cur.execute( - f"CREATE TABLE IF NOT EXISTS kcg.{partition_name} " - f"PARTITION OF kcg.gear_correlation_raw_metrics " + f"CREATE TABLE IF NOT EXISTS {qualified_table(partition_name)} " + f"PARTITION OF {GEAR_CORRELATION_RAW_METRICS} " f"FOR VALUES FROM ('{d.isoformat()}') TO ('{next_d.isoformat()}')" ) created += 1 - logger.info('created partition: kcg.%s', partition_name) + logger.info('created partition: %s.%s', settings.KCGDB_SCHEMA, partition_name) conn.commit() except Exception as e: conn.rollback() @@ -71,7 +77,8 @@ def _drop_expired_partitions(conn, retention_days: int) -> int: "SELECT c.relname FROM pg_class c " "JOIN pg_namespace n ON n.oid = c.relnamespace " "WHERE c.relname LIKE 'gear_correlation_raw_metrics_%%' " - "AND n.nspname = 'kcg' AND c.relkind = 'r'" + "AND n.nspname = %s AND c.relkind = 'r'", + (settings.KCGDB_SCHEMA,), ) for (name,) in cur.fetchall(): date_str = name.rsplit('_', 1)[-1] @@ -80,9 +87,9 @@ def _drop_expired_partitions(conn, retention_days: int) -> int: except ValueError: continue if partition_date < cutoff: - cur.execute(f'DROP TABLE IF EXISTS kcg.{name}') + cur.execute(f'DROP TABLE IF EXISTS {qualified_table(name)}') dropped += 1 - logger.info('dropped expired partition: kcg.%s', name) + logger.info('dropped expired partition: %s.%s', settings.KCGDB_SCHEMA, name) conn.commit() except Exception as e: conn.rollback() @@ -97,7 +104,7 @@ def _cleanup_stale_scores(conn, cleanup_days: int) -> int: cur = conn.cursor() try: cur.execute( - "DELETE FROM kcg.gear_correlation_scores " + f"DELETE FROM {GEAR_CORRELATION_SCORES} " "WHERE last_observed_at < NOW() - make_interval(days => %s)", (cleanup_days,), )