|
12 | 12 | from services.exception_handler import * |
13 | 13 | from django.utils import timezone |
14 | 14 | from services.tour_api_service import TourAPIService, area_codes |
| 15 | +import difflib |
| 16 | +from services.utils import haversine |
15 | 17 |
|
16 | 18 | logger = logging.getLogger(APP_LOGGER) |
17 | 19 |
|
@@ -202,42 +204,73 @@ def _handle_custom_places(self, travel_id: int, custom_places: List[Dict]) -> No |
202 | 204 | def _get_or_create_place(self, place_data: Dict) -> Place: |
203 | 205 | """근처 장소 검색 또는 새로운 장소 생성""" |
204 | 206 | mapX, mapY = float(place_data['mapX']), float(place_data['mapY']) |
| 207 | + name = place_data.get('name') |
| 208 | + if name is None: NoRequiredParameterException() |
| 209 | + |
| 210 | + # 이름이 일치하고, 그 일치하는 장소들 중에서 1km 내에 있는 장소 정보를 가져옵니다. |
| 211 | + place = self.__get_place_by_name_equal(name, mapX, mapY) |
| 212 | + if place is not None: return place |
205 | 213 |
|
| 214 | + # 이름 불일치 시 |
206 | 215 | # 10m 내 기존 장소 검색 |
207 | | - existing_place = self._find_nearest_place(mapX, mapY) |
| 216 | + existing_place = self._find_nearest_place(name, mapX, mapY) |
208 | 217 | if existing_place: |
209 | 218 | return existing_place |
210 | 219 |
|
211 | 220 | # 새 장소 생성 |
212 | 221 | return self._create_new_place(place_data, mapX, mapY) |
213 | 222 |
|
214 | | - def _find_nearest_place(self, x: float, y: float) -> Optional[Place]: |
215 | | - """10m 내에 있는 가장 가까운 장소 찾기""" |
| 223 | + def _find_nearest_place(self, original_place_name:str, x: float, y: float) -> Optional[Place]: |
| 224 | + """50m 내에 있는 가장 가까운 장소 찾기""" |
216 | 225 | from django.db.models import FloatField |
217 | 226 | from django.db.models.functions import Cast |
218 | | - from services.utils import haversine |
219 | 227 |
|
220 | 228 | near_places = Place.objects.annotate( |
221 | 229 | mapX_float=Cast('mapX', FloatField()), |
222 | 230 | mapY_float=Cast('mapY', FloatField()) |
223 | 231 | ).filter( |
224 | | - mapX_float__gte=(x - self.LON_DIF_PER_10M), |
225 | | - mapX_float__lte=(x + self.LON_DIF_PER_10M), |
226 | | - mapY_float__gte=(y - self.LAT_DIF_PER_10M), |
227 | | - mapY_float__lte=(y + self.LAT_DIF_PER_10M) |
| 232 | + mapX_float__gte=(x - (self.LON_DIF_PER_10M * 10)), |
| 233 | + mapX_float__lte=(x + (self.LON_DIF_PER_10M * 10)), |
| 234 | + mapY_float__gte=(y - (self.LAT_DIF_PER_10M * 10)), |
| 235 | + mapY_float__lte=(y + (self.LAT_DIF_PER_10M * 10)) |
228 | 236 | ) |
229 | 237 |
|
| 238 | + near_places_after_similarity = [] |
| 239 | + for place in near_places: |
| 240 | + # 이름 유사도 검사 |
| 241 | + if self._check_name_similarity(original_place_name, place.name): |
| 242 | + near_places_after_similarity.append(place) |
| 243 | + |
| 244 | + if len(near_places_after_similarity) == 1: return near_places_after_similarity.pop() # 하나면 그냥 반환 |
| 245 | + |
230 | 246 | closest_place = None |
231 | 247 | min_distance = None |
232 | 248 |
|
233 | | - for place in near_places: |
| 249 | + for place in near_places_after_similarity: |
234 | 250 | distance = haversine(x, y, float(place.mapX), float(place.mapY)) |
235 | 251 | if min_distance is None or distance < min_distance: |
236 | 252 | closest_place = place |
237 | 253 | min_distance = distance |
238 | 254 |
|
| 255 | + |
239 | 256 | return closest_place |
240 | 257 |
|
| 258 | + def _check_name_similarity(self, name1, name2): |
| 259 | + # 장소 이름 유사도 검사를 실시합니다. |
| 260 | + CUTLINE = 0.8 # 유사도 80% 이상 시 통과 |
| 261 | + answer_bytes = bytes(name1, 'utf-8') |
| 262 | + input_bytes = bytes(name2, 'utf-8') |
| 263 | + answer_bytes_list = list(answer_bytes) |
| 264 | + input_bytes_list = list(input_bytes) |
| 265 | + |
| 266 | + sm = difflib.SequenceMatcher(None, answer_bytes_list, input_bytes_list) |
| 267 | + similar = sm.ratio() |
| 268 | + if similar >= CUTLINE: |
| 269 | + return True |
| 270 | + else: |
| 271 | + return False |
| 272 | + |
| 273 | + |
241 | 274 | def _create_new_place(self, place_data: Dict, mapX: float, mapY: float) -> Place: |
242 | 275 | """새로운 장소 생성""" |
243 | 276 | name = place_data.get('name') |
@@ -279,6 +312,15 @@ def _update_place_address(self, place: Place) -> None: |
279 | 312 | serializer.is_valid(raise_exception=True) |
280 | 313 | serializer.save() |
281 | 314 |
|
| 315 | + @staticmethod |
| 316 | + def __get_place_by_name_equal(name, mapX, mapY): |
| 317 | + db_places = Place.objects.filter(name=name) |
| 318 | + for db_place in db_places: |
| 319 | + if haversine(db_place.mapX, db_place.mapY, mapX, mapY) > 1: # 1km보다 크다면 |
| 320 | + continue |
| 321 | + return db_place |
| 322 | + return None |
| 323 | + |
282 | 324 |
|
283 | 325 | class TravelUpdateService: |
284 | 326 | """여행 업데이트와 관련된 로직을 담당하는 서비스""" |
|
0 commit comments