""" 통합 레퍼런스 데이터 관리자 SQLite 로컬 캐시 + Supabase 동기화 준비 """ import os import sqlite3 from typing import Dict, Optional, List from dataclasses import dataclass from enum import Enum from pathlib import Path from app.core.reference_data_defaults import ( DEFAULT_STATIONS, DEFAULT_ERROR_CODES, REFERENCE_DATA_VERSION ) class DoorDirection(Enum): DW = "DW" # Door West (Left) DE = "DE" # Door East (Right) @dataclass class StationMeta: code: int name: str pos: float door_dir: DoorDirection station_no: int track_line: str # 'TC1' or 'TC2' class ReferenceDataManager: """ 통합 레퍼런스 데이터 관리자 - SQLite 로컬 캐싱 - 메모리 캐시로 빠른 조회 - Supabase 동기화 준비 """ _instance = None def __new__(cls): if cls._instance is None: cls._instance = super(ReferenceDataManager, cls).__new__(cls) cls._instance._initialized = False return cls._instance def __init__(self): if self._initialized: return self._initialized = True # DB 경로 설정 self.db_path = Path(__file__).parent / "ref_cache.db" # 메모리 캐시 self.tc1_stations: Dict[int, StationMeta] = {} self.tc2_stations: Dict[int, StationMeta] = {} self.station_name_map: Dict[str, int] = {} self.error_code_map: Dict[str, str] = {} self._is_loaded = False # 초기화 self._init_db() self._load_to_memory() def _init_db(self): """DB 초기화 (첫 실행 시 기본 데이터로 생성)""" db_exists = self.db_path.exists() conn = sqlite3.connect(self.db_path) cursor = conn.cursor() # 테이블 생성 cursor.execute(""" CREATE TABLE IF NOT EXISTS stations ( code INTEGER PRIMARY KEY, name TEXT NOT NULL, pos REAL NOT NULL, door_dir TEXT NOT NULL, station_no INTEGER NOT NULL, track_line TEXT NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) cursor.execute(""" CREATE TABLE IF NOT EXISTS metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) cursor.execute(""" CREATE TABLE IF NOT EXISTS error_codes ( code TEXT PRIMARY KEY, description TEXT NOT NULL ) """) # 인덱스 생성 cursor.execute("CREATE INDEX IF NOT EXISTS idx_station_name ON stations(name)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_track_line ON stations(track_line)") # 첫 실행 시 기본 데이터 삽입 if not db_exists: print("[RefData] Initializing database with default data...") # 역 정보 삽입 for station in DEFAULT_STATIONS: cursor.execute(""" INSERT INTO stations (code, name, pos, door_dir, station_no, track_line) VALUES (?, ?, ?, ?, ?, ?) """, ( station["code"], station["name"], station["pos"], station["door_dir"], station["station_no"], station["track_line"] )) # 에러 코드 삽입 for code, desc in DEFAULT_ERROR_CODES.items(): cursor.execute(""" INSERT INTO error_codes (code, description) VALUES (?, ?) """, (code, desc)) # 버전 정보 삽입 cursor.execute(""" INSERT INTO metadata (key, value) VALUES ('version', ?) """, (REFERENCE_DATA_VERSION,)) conn.commit() print(f"[RefData] Database initialized with version {REFERENCE_DATA_VERSION}") conn.close() def _load_to_memory(self): """SQLite에서 메모리로 로드 (빠른 조회를 위한 캐싱)""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() # 역 정보 로드 cursor.execute("SELECT code, name, pos, door_dir, station_no, track_line FROM stations") rows = cursor.fetchall() for row in rows: code, name, pos, door_dir, station_no, track_line = row station = StationMeta( code=code, name=name, pos=pos, door_dir=DoorDirection(door_dir), station_no=station_no, track_line=track_line ) if track_line == "TC1": self.tc1_stations[code] = station elif track_line == "TC2": self.tc2_stations[code] = station # 이름 매핑 (TC1 우선) if track_line == "TC1": self.station_name_map[name] = code # TC2 이름 매핑 (TC1에 없는 경우만) for code, station in self.tc2_stations.items(): if station.name not in self.station_name_map: self.station_name_map[station.name] = code # 에러 코드 로드 cursor.execute("SELECT code, description FROM error_codes") for code, desc in cursor.fetchall(): self.error_code_map[code] = desc conn.close() self._is_loaded = True print(f"[RefData] Loaded from cache. TC1: {len(self.tc1_stations)}, TC2: {len(self.tc2_stations)}") # ========== 조회 메서드 (기존 API 호환) ========== def get_station_name(self, code: int) -> str: """역 코드로 역 이름 조회""" if code in self.tc1_stations: return self.tc1_stations[code].name if code in self.tc2_stations: return self.tc2_stations[code].name return "" def get_station_info(self, code: int) -> Optional[StationMeta]: """역 코드로 역 정보 조회""" if code in self.tc1_stations: return self.tc1_stations[code] if code in self.tc2_stations: return self.tc2_stations[code] return None def get_station_code(self, name: str) -> int: """역 이름으로 역 코드 조회""" return self.station_name_map.get(name, 0) def get_station_distance(self, station_name: str, is_tc1: bool) -> float: """역 이름으로 거리 조회""" target_list = self.tc1_stations if is_tc1 else self.tc2_stations for station in target_list.values(): if station.name == station_name: return station.pos return 0.0 def get_door_direction(self, station_name_or_code, is_tc1: bool) -> DoorDirection: """역 이름 또는 코드로 도어 방향 조회""" target_list = self.tc1_stations if is_tc1 else self.tc2_stations for station in target_list.values(): if isinstance(station_name_or_code, str): if station.name == station_name_or_code: return station.door_dir else: if station.code == station_name_or_code: return station.door_dir return DoorDirection.DW # ========== Supabase 동기화 (추후 구현) ========== async def sync_from_supabase(self): """ Supabase에서 최신 데이터 동기화 TODO: 추후 구현 """ pass def get_version(self) -> str: """현재 데이터 버전 조회""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute("SELECT value FROM metadata WHERE key = 'version'") result = cursor.fetchone() conn.close() return result[0] if result else "unknown" # 전역 인스턴스 ref_data = ReferenceDataManager()