"""외부 장소 DB 의 분류 → 우리 업종(PlaceCategory). ★ 업종 판별에 LLM 을 부르지 않는다. 상호명으로 카카오·네이버를 부르는 건 어차피 하는 일이고, 그 응답에 분류가 함께 온다(`category_name`, 카카오는 `category_group_code` 까지). 지금까지 받아 놓고 쓰지 않았을 뿐이다 — 여기서 그 값을 업종으로 옮긴다. ★ 못 정하면 None 이다. 억지로 하나를 고르지 않는다. 업종은 수집 스키마와 JSON-LD 타입을 통째로 정하는 값이라, 틀린 업종으로 시작하면 되돌리는 비용이 크다. None 이면 화면이 사장님에게 직접 묻는다. """ from typing import Optional from common.enums import PlaceCategory # 카카오 category_group_code. 한글 분류 문자열은 카카오가 언제든 바꾸지만 이 코드는 안 바뀐다. # ★ HP8(병원)은 여기 없다 — 병원 전체가 아니라 피부과·성형외과만 열려 있어서, # 코드만으로는 우리 업종인지 알 수 없다. 아래 키워드로 한 번 더 좁힌다. _BY_GROUP_CODE = { "AD5": PlaceCategory.LODGING, "CE7": PlaceCategory.CAFE, "FD6": PlaceCategory.RESTAURANT, } _HOSPITAL_GROUP_CODE = "HP8" # 분류 문자열 키워드. 네이버는 group_code 를 주지 않아 이 경로만 탄다. # ★ 순서가 결과를 바꾼다. 카카오·네이버 모두 카페를 "음식점 > 카페" 아래 두기 때문에 # CAFE 를 RESTAURANT 보다 먼저 봐야 한다. 뒤집으면 카페가 전부 음식점이 된다. _KEYWORDS: list[tuple[PlaceCategory, tuple[str, ...]]] = [ # ★ "피부"·"성형" 이 아니라 "피부과"·"성형외과" 다. 앞의 둘로 보면 피부관리실(에스테틱)이 # 병원으로 걸린다 — 의료 광고 규제가 걸리는 업종이라 잘못 붙이면 가장 비싸다. (PlaceCategory.CLINIC, ("피부과", "성형외과")), (PlaceCategory.LODGING, ("숙박", "펜션", "호텔", "모텔", "리조트", "게스트하우스", "민박")), (PlaceCategory.CAFE, ("카페", "커피", "베이커리", "제과", "디저트")), (PlaceCategory.RESTAURANT, ("음식점", "한식", "일식", "중식", "양식", "분식", "주점", "술집", "치킨", "횟집", "뷔페", "패스트푸드")), ] def guess_category( category_name: Optional[str], group_code: Optional[str] = None ) -> Optional[PlaceCategory]: """분류 문자열(+ 카카오 그룹코드)로 업종을 추정한다. 모르면 None. category_name 예 — 카카오 "가정,생활 > 숙박 > 펜션" · 네이버 "숙박>펜션" """ text = (category_name or "").replace(" ", "") code = (group_code or "").strip().upper() if code == _HOSPITAL_GROUP_CODE: # 병원이라는 것까지만 안다. 진료과가 문자열에 없으면 우리 업종인지 알 수 없다. return PlaceCategory.CLINIC if _match(text) is PlaceCategory.CLINIC else None if code in _BY_GROUP_CODE: by_code = _BY_GROUP_CODE[code] # ★ 그룹코드를 문자열로 덮는 경우가 하나 있다: 카카오가 베이커리·브런치 가게를 # FD6(음식점)으로 주면서 분류 문자열엔 "카페"를 다는 일이 있다. 어느 쪽이 맞는지는 # 실측하지 않았다 — 사장님이 바꿀 수 있으니 이름이 더 구체적인 쪽을 기본값으로 둔다. if by_code is PlaceCategory.RESTAURANT and _match(text) is PlaceCategory.CAFE: return PlaceCategory.CAFE return by_code return _match(text) def _match(text: str) -> Optional[PlaceCategory]: for category, words in _KEYWORDS: if any(word in text for word in words): return category return None