"""모두의 빛길 — 노드/엣지 데이터 스키마 정의. 이 모듈은 추천 그래프를 구성하는 모든 노드 타입을 dataclass로 정의한다. PyG HeteroData에 그대로 매핑할 수 있도록 단순한 평면 구조로 유지한다. 노드 타입 --------- - VenueNode : 문화시설 (박물관/미술관/공연장/도서관 등) - EventNode : 공연·전시 이벤트 - TransitNode : 지하철역·버스정류장 - AmenityNode : 화장실/쉼터/벤치/CCTV - HazardNode : 사고다발지역/공사구간/저조도 엣지 타입 --------- - (VENUE)-hosts->(EVENT) - (VENUE)<->(TRANSIT) near - (VENUE)<->(AMENITY) has_amenity - (TRANSIT)<->(TRANSIT) walkable / connects - (TRANSIT)->(HAZARD) passes_hazard """ from __future__ import annotations from dataclasses import dataclass, field from typing import List, Optional # ============================================================ # 노드 dataclass # ============================================================ @dataclass class VenueNode: """문화시설 노드.""" id: int name: str lat: float lng: float venue_type: str # 박물관/미술관/공연장/도서관/카페/문화센터 accessibility_score: float = 0.0 # 0~1, 무장애 종합점수 has_elevator: bool = False has_disabled_toilet: bool = False has_ramp: bool = False free: bool = False indoor: bool = True age_friendly: bool = False # 어르신 무료/할인 등 booking_url: str = "" phone: str = "" def feature_vec(self) -> List[float]: """GNN 입력 feature 벡터.""" return [ self.accessibility_score, float(self.has_elevator), float(self.has_disabled_toilet), float(self.has_ramp), float(self.free), float(self.indoor), float(self.age_friendly), # venue_type one-hot (8종) *self._type_onehot(), ] def _type_onehot(self) -> List[float]: types = ["박물관", "미술관", "공연장", "도서관", "카페", "문화센터", "체험관", "기타"] v = [0.0] * len(types) if self.venue_type in types: v[types.index(self.venue_type)] = 1.0 else: v[-1] = 1.0 return v @dataclass class EventNode: """공연·전시 이벤트 노드.""" id: int venue_id: int title: str start_time: str # "HH:MM" end_time: str age_limit: Optional[int] = None price: int = 0 booking_url: str = "" event_type: str = "공연" # 공연/전시/체험/강좌 has_subtitle: bool = False # 자막 has_sign_lang: bool = False # 수어 통역 has_audio_guide: bool = False # 음성해설/점자 def feature_vec(self) -> List[float]: sh, sm = map(int, self.start_time.split(":")) eh, em = map(int, self.end_time.split(":")) return [ sh + sm / 60.0, eh + em / 60.0, float(self.age_limit or 0), float(self.price), float(self.has_subtitle), float(self.has_sign_lang), float(self.has_audio_guide), float(self.event_type == "공연"), float(self.event_type == "전시"), float(self.event_type == "체험"), float(self.event_type == "강좌"), ] @dataclass class TransitNode: """지하철역·버스정류장 노드.""" id: int name: str lat: float lng: float transit_type: str = "지하철" # 지하철/버스/저상버스 has_elevator: bool = False has_toilet: bool = False line: str = "" def feature_vec(self) -> List[float]: return [ float(self.has_elevator), float(self.has_toilet), float(self.transit_type == "지하철"), float(self.transit_type == "저상버스"), float(self.transit_type == "버스"), ] @dataclass class AmenityNode: """편의시설 노드 — 화장실/쉼터/벤치/CCTV.""" id: int name: str lat: float lng: float amenity_type: str # 공공화장실/장애인화장실/무더위쉼터/벤치/CCTV/가로등 accessible: bool = False open_24h: bool = False def feature_vec(self) -> List[float]: return [ float(self.accessible), float(self.open_24h), float(self.amenity_type == "공공화장실"), float(self.amenity_type == "장애인화장실"), float(self.amenity_type == "무더위쉼터"), float(self.amenity_type == "벤치"), float(self.amenity_type == "CCTV"), float(self.amenity_type == "가로등"), ] @dataclass class HazardNode: """위험구간 노드 — 사고다발지역/공사구간/저조도.""" id: int lat: float lng: float hazard_type: str # 보행자사고다발/노인사고다발/공사/저조도/계단/턱 severity: float = 0.5 # 0~1 night_only: bool = False # 야간에만 위험 def feature_vec(self) -> List[float]: return [ self.severity, float(self.night_only), float(self.hazard_type == "보행자사고다발"), float(self.hazard_type == "노인사고다발"), float(self.hazard_type == "공사"), float(self.hazard_type == "저조도"), float(self.hazard_type == "계단"), float(self.hazard_type == "턱"), ] # ============================================================ # 컬렉션 컨테이너 # ============================================================ @dataclass class GraphData: """GraphBuilder에 입력될 컬렉션.""" venues: List[VenueNode] = field(default_factory=list) events: List[EventNode] = field(default_factory=list) transits: List[TransitNode] = field(default_factory=list) amenities: List[AmenityNode] = field(default_factory=list) hazards: List[HazardNode] = field(default_factory=list) def stats(self) -> dict: return { "VENUE": len(self.venues), "EVENT": len(self.events), "TRANSIT": len(self.transits), "AMENITY": len(self.amenities), "HAZARD": len(self.hazards), } # ============================================================ # 슬롯 정의 (slot_extractor와 공유) # ============================================================ SLOT_NAMES = [ "USER_TYPE", # 고령자/휠체어사용자/임산부/시각장애/청각장애/어린이동반/보행보조기 "COMPANION", # 가족/보호자/활동지원사/단체 "ORIGIN", # 병원/복지관/지하철역/자택 "WALK_LIMIT", # 짧음/거리지정/환승최소 "AVOID", # 위험회피/계단회피/경사회피/혼잡회피/사고다발회피/야외회피 "CULTURE_PREF", # 전시/공연/박물관/미술관/도서관/카페/체험/문화일반/미디어아트/전통문화 "WEATHER", # 폭염/한파/강우/미세먼지 "TIME_WINDOW", # 오전/낮/오후/저녁(야간)/시간한정 "BUDGET", # 무료/할인/저예산 "SPECIAL_NEEDS", # 화장실/수유실/안내견/점자/수어자막/엘리베이터/장애인화장실 ] __all__ = [ "VenueNode", "EventNode", "TransitNode", "AmenityNode", "HazardNode", "GraphData", "SLOT_NAMES", ]