지역 정보(맛집·관광지·축제)는 행정구역 코드(local_contents.region_code) 단위로 캐시돼 "그 시군구에 있는 것"을 줬다. 양양군 업장 옆 5km 속초 관광지는 빠지고 같은 군 반대편 30km 맛집이 붙는 구조라, 캔버스의 지역 정보 섹션은 늘 "준비 중"이었다. 업장 좌표로 TourAPI 를 직접 물어 업장 단위(place_contents)에 담고, 캔버스는 스크린샷으로 받은 형식(도보 시간 필터 + 카드 캐러셀)으로 통일했다.
107 lines
6.3 KiB
Python
107 lines
6.3 KiB
Python
"""외부 장소 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
|
|
|
|
|
|
# ── 음식 중분류(TourAPI lclsSystm2) 추정 ─────────────────────────────────
|
|
# 주변 맛집에서 **같은 중분류(경쟁 업소)를 빼는** 기준이다(2026-09-08 결정).
|
|
# TourAPI 분류체계(lclsSystmCode2 실측) — FD01 한식 · FD02 외국식(중·일·서양·기타외국·퓨전)
|
|
# · FD03 간이음식(제과·피자/햄버거/샌드위치·치킨·김밥분식·이동음식) · FD04 주점 · FD05 카페/찻집.
|
|
#
|
|
# ★ 순서가 결과를 바꾼다. 카카오는 카페를 "음식점 > 카페 > …" 아래 두므로 "음식점"이 항상 붙어 있다 —
|
|
# 그래서 "음식점" 은 판정어로 쓰지 않고, 구체적인 업태(카페·주점·간이·외국식)를 한식보다 먼저 본다.
|
|
# ★ 한식은 마지막이고 판정어가 좁다("한식"·"한정식"·"백반"·"국밥"…). 고기·회 같은 재료명은 넣지 않는다 —
|
|
# "양식 > 스테이크" 를 고기라고 한식으로 넣으면 서양식 스테이크집이 한식이 된다.
|
|
_FOOD_CLASS_KEYWORDS: list[tuple[str, tuple[str, ...]]] = [
|
|
("FD05", ("카페", "커피", "찻집", "디저트", "음료", "주스", "빙수", "케이크", "브런치")),
|
|
("FD04", ("주점", "술집", "호프", "맥주", "이자카야", "포차", "와인바", "칵테일", "펍")),
|
|
("FD03", ("제과", "베이커리", "빵", "도넛", "피자", "햄버거", "샌드위치", "치킨", "분식", "김밥",
|
|
"떡볶이", "간식", "토스트", "패스트푸드")),
|
|
("FD02", ("중식", "중국", "일식", "일본", "초밥", "라멘", "양식", "서양", "이탈리", "파스타", "스테이크",
|
|
"프렌치", "프랑스", "멕시", "아시아", "베트남", "태국", "인도", "퓨전")),
|
|
("FD01", ("한식", "한정식", "백반", "국밥", "찌개", "국수", "냉면", "삼겹", "갈비", "곱창", "족발", "보쌈",
|
|
"해장국", "설렁탕", "감자탕", "칼국수", "횟집", "회")),
|
|
]
|
|
|
|
|
|
def guess_food_class(category_name: Optional[str]) -> Optional[str]:
|
|
"""외부 분류 문자열 → TourAPI 음식 중분류 코드(FD01~FD05). 모르면 None(제외 없음).
|
|
|
|
예 — 카카오 "음식점 > 카페 > 커피전문점" → FD05 · 네이버 "카페,디저트" → FD05 ·
|
|
카카오 "음식점 > 한식 > 육류,고기" → FD01 · "음식점 > 양식 > 스테이크,립" → FD02
|
|
"""
|
|
text = (category_name or "").replace(" ", "")
|
|
if not text:
|
|
return None
|
|
for code, words in _FOOD_CLASS_KEYWORDS:
|
|
if any(word in text for word in words):
|
|
return code
|
|
return None
|