- 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>
177 lines
5.6 KiB
Python
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)
|