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