여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다. - ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제 - 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다 - eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다 파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이). site·frontend·admin tsc, site vitest 105 passed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
76 lines
3.5 KiB
Python
76 lines
3.5 KiB
Python
"""외부 장소 DB 의 분류 → 우리 업종(PlaceCategory)."""
|
|
|
|
from typing import Optional
|
|
|
|
from common.enums import PlaceCategory
|
|
|
|
# 카카오 category_group_code.
|
|
_BY_GROUP_CODE = {
|
|
"AD5": PlaceCategory.LODGING,
|
|
"CE7": PlaceCategory.CAFE,
|
|
"FD6": PlaceCategory.RESTAURANT,
|
|
}
|
|
_HOSPITAL_GROUP_CODE = "HP8"
|
|
|
|
# 분류 문자열 키워드.
|
|
_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]:
|
|
"""분류 문자열(+ 카카오 그룹코드)로 업종을 추정한다."""
|
|
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) 추정 ───────────────────────────────── 주변 맛집에서 **같은 중분류(경쟁 업소)를 빼는** 기준이다.
|
|
_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)."""
|
|
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
|