File size: 7,499 Bytes
131589b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
"""모두의 빛길 — 노드/엣지 데이터 스키마 정의.

이 모듈은 추천 그래프를 구성하는 모든 노드 타입을 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",
]