랜딩 첫 화면이 상호명을 받으려면 검색이 로그인 앞에 있어야 하는데, 후보 조회는
place_id 와 토큰을 둘 다 요구했다. 로그인 관문을 에디터 진입 하나로 되돌려 놓고도
(b94daa9) API 는 그대로였다.
업종은 AI 를 한 번 더 부를 필요가 없다 — 카카오·네이버 검색 응답에 분류가 이미
들어 있고(category_group_code / category_name), 지금까지 받아 놓고 안 썼다.
- place.py: GET /v1/place/search 신설(인증 없음). ★ /{place_id} 앞에 둬야 한다 —
뒤에 두면 "search" 가 place_id 로 잡혀 422 다
- place_category: AD5·CE7·FD6 우선, 없으면 분류 문자열. 못 정하면 None —
억지로 고르면 틀린 스키마로 시작한다. HP8 은 피부과·성형외과일 때만
- kakao: KakaoPlace 에 category_group_code. 한글 분류는 바뀌어도 코드는 안 바뀐다
- rate_limit: 인증 없이 유료 API 를 부르는 경로라 IP 당 분당 20회(프로세스 메모리)
- 확정 경로(verify/candidates)는 인증 유지 — 남의 place_id 존재 여부를 열지 않는다
전체 562 passed
71 lines
3.7 KiB
Python
71 lines
3.7 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
|