256 lines
8.1 KiB
Python
256 lines
8.1 KiB
Python
"""
|
|
통합 레퍼런스 데이터 관리자
|
|
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()
|