"""선단 구성 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)