kcg-monitoring/prediction/scripts/load_fleet_registry.py
htlee bb99387168 feat: 선단 등록 DB + 어망/어구 정체성 추적 시스템
- DB 007: fleet_companies, fleet_vessels, gear_identity_log, fleet_tracking_snapshot
- 906척 선단 구성 데이터 적재 (497개 회사, 279쌍 PT)
- FleetTracker: 등록 선단 ↔ AIS 매칭(NAME_EXACT) + 어구 정체성 추적
- track_similarity.py: DTW 기반 궤적 유사도 (TRACK_SIMILAR 플래그)
- scheduler: fleet_tracker 통합 (기존 assign_fleet_roles 대체)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 18:07:15 +09:00

177 lines
5.6 KiB
Python

"""선단 구성 JSX → kcgdb fleet_companies + fleet_vessels 적재.
Usage: python3 prediction/scripts/load_fleet_registry.py
"""
import json
import re
import sys
from pathlib import Path
import psycopg2
import psycopg2.extras
# JSX 파일에서 D 배열 추출
JSX_PATH = Path(__file__).parent.parent.parent.parent / 'gc-wing-dev' / 'legacy' / '선단구성_906척_어업수역 (1).jsx'
# kcgdb 접속 — prediction/.env 또는 환경변수
DB_HOST = '211.208.115.83'
DB_PORT = 5432
DB_NAME = 'kcgdb'
DB_USER = 'kcg_app'
DB_SCHEMA = 'kcg'
def parse_jsx(path: Path) -> list[list]:
"""JSX 파일에서 D=[ ... ] 배열을 파싱."""
text = path.read_text(encoding='utf-8')
# const D=[ 부터 ]; 까지 추출
m = re.search(r'const\s+D\s*=\s*\[', text)
if not m:
raise ValueError('D 배열을 찾을 수 없습니다')
start = m.end() - 1 # [ 위치
# 중첩 배열을 추적하여 닫는 ] 찾기
depth = 0
end = start
for i in range(start, len(text)):
if text[i] == '[':
depth += 1
elif text[i] == ']':
depth -= 1
if depth == 0:
end = i + 1
break
raw = text[start:end]
# JavaScript → JSON 변환 (trailing comma 제거)
raw = re.sub(r',\s*]', ']', raw)
raw = re.sub(r',\s*}', '}', raw)
return json.loads(raw)
def load_to_db(data: list[list], db_password: str):
"""파싱된 데이터를 DB에 적재."""
conn = psycopg2.connect(
host=DB_HOST, port=DB_PORT, dbname=DB_NAME,
user=DB_USER, password=db_password,
options=f'-c search_path={DB_SCHEMA}',
)
conn.autocommit = False
cur = conn.cursor()
try:
# 기존 데이터 초기화
cur.execute('DELETE FROM fleet_vessels')
cur.execute('DELETE FROM fleet_companies')
company_count = 0
vessel_count = 0
pair_links = [] # (vessel_id, pair_vessel_id) 후처리
for row in data:
if len(row) < 7:
continue
name_cn = row[0]
name_en = row[1]
# 회사 INSERT
cur.execute(
'INSERT INTO fleet_companies (name_cn, name_en) VALUES (%s, %s) RETURNING id',
(name_cn, name_en),
)
company_id = cur.fetchone()[0]
company_count += 1
# 인덱스: 0=own, 1=ownEn, 2=pairs, 3=gn, 4=ot, 5=ps, 6=fc, 7=upt, 8=upts
pairs = row[2] if len(row) > 2 and isinstance(row[2], list) else []
gn = row[3] if len(row) > 3 and isinstance(row[3], list) else []
ot = row[4] if len(row) > 4 and isinstance(row[4], list) else []
ps = row[5] if len(row) > 5 and isinstance(row[5], list) else []
fc = row[6] if len(row) > 6 and isinstance(row[6], list) else []
upt = row[7] if len(row) > 7 and isinstance(row[7], list) else []
upts = row[8] if len(row) > 8 and isinstance(row[8], list) else []
def insert_vessel(v, gear_code, role):
nonlocal vessel_count
if not isinstance(v, list) or len(v) < 4:
return None
cur.execute(
'''INSERT INTO fleet_vessels
(company_id, permit_no, name_cn, name_en, tonnage, gear_code, fleet_role)
VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id''',
(company_id, v[0], v[1], v[2], v[3], gear_code, role),
)
vessel_count += 1
return cur.fetchone()[0]
# PT 본선쌍 (pairs)
for pair in pairs:
if not isinstance(pair, list) or len(pair) < 2:
continue
main_id = insert_vessel(pair[0], 'C21', 'MAIN')
sub_id = insert_vessel(pair[1], 'C21', 'SUB')
if main_id and sub_id:
pair_links.append((main_id, sub_id))
# GN 유자망
for v in gn:
insert_vessel(v, 'C25', 'GN')
# OT 기타
for v in ot:
insert_vessel(v, 'C22', 'OT')
# PS 선망
for v in ps:
insert_vessel(v, 'C23', 'PS')
# FC 운반선
for v in fc:
insert_vessel(v, 'C40', 'FC')
# UPT 단독 본선
for v in upt:
insert_vessel(v, 'C21', 'MAIN_SOLO')
# UPTS 단독 부속선
for v in upts:
insert_vessel(v, 'C21', 'SUB_SOLO')
# PT 쌍 상호 참조 설정
for main_id, sub_id in pair_links:
cur.execute('UPDATE fleet_vessels SET pair_vessel_id = %s WHERE id = %s', (sub_id, main_id))
cur.execute('UPDATE fleet_vessels SET pair_vessel_id = %s WHERE id = %s', (main_id, sub_id))
conn.commit()
print(f'적재 완료: {company_count}개 회사, {vessel_count}척 선박, {len(pair_links)}쌍 PT')
except Exception as e:
conn.rollback()
print(f'적재 실패: {e}', file=sys.stderr)
raise
finally:
cur.close()
conn.close()
if __name__ == '__main__':
if not JSX_PATH.exists():
print(f'파일을 찾을 수 없습니다: {JSX_PATH}', file=sys.stderr)
sys.exit(1)
# DB 비밀번호 — 환경변수 또는 직접 입력
import os
password = os.environ.get('KCGDB_PASSWORD', 'Kcg2026monitor')
print(f'JSX 파싱: {JSX_PATH}')
data = parse_jsx(JSX_PATH)
print(f'파싱 완료: {len(data)}개 회사')
print('DB 적재 시작...')
load_to_db(data, password)