o2o-site-AEO/solution/backend/services/stay_guide.py
김성경 1f26bc7065 [feat] solution: 날씨 조건 세분화,
공식채널 단일화, 한일옥 거리 반영
날씨 조건을 7종으로 세분화,
축제 종료 여부와 무관하게 상시 노출,
'지역 읽기'갈래 축소, 야놀자(NOL) 브랜드명 제거.
2026-09-17 17:02:48 +09:00

81 lines
4.2 KiB
Python

"""확정된 NOL 링크의 수집 원문에서 공개 안내 섹션만 전달한다."""
import re
from urllib.parse import urlsplit
def nol_stay_guide(url: str, raw) -> dict:
try:
parsed = urlsplit(url)
except ValueError:
return {}
if parsed.scheme != "https" or parsed.hostname != "nol.yanolja.com":
return {}
if not re.fullmatch(r"/stay/domestic/\d+/?", parsed.path):
return {}
text = raw.get("text") if isinstance(raw, dict) else None
if not isinstance(text, str):
return {}
labels = {"시설/서비스": "service", "이용 안내": "policy", "예약 공지": "reservation"}
# 수집기가 붙인 경계로만 나눈다. 규정 안의 괄호나 문장은 분리하지 않는다.
sections = re.split(r"(?m)^\[(숙소 소개|시설/서비스|이용 안내|예약 공지)\]\s*\n", text)
guide = {}
for label, body in zip(sections[1::2], sections[2::2]):
if label not in labels:
continue
lines = body.strip().splitlines()
if lines and lines[0].strip() == label:
lines.pop(0)
# 제목 중복과 UI 버튼만 제외한다. 요약·추론 없이 수집 문장을 보존한다.
body = "\n".join(line for line in lines if line.strip() != "전체보기").strip()
# NOL 은 원 플랫폼 브랜드다 — 사장님 사이트에 그대로 실으면 남의 고객센터·정책을
# 우리 것처럼 안내하게 된다. 문장 자체는 그대로 두고 그 단어만 지운다.
body = re.sub(r"NOL\s*", "", body)
if body:
guide[labels[label]] = body
if guide:
guide["fields"] = structured_fields(guide)
return guide
def structured_fields(guide: dict) -> list[dict]:
"""확실한 표기만 구조화한다. 매칭되지 않은 내용은 안내 원문에 남는다."""
policy = guide.get("policy", "")
service = guide.get("service", "")
reservation = guide.get("reservation", "")
lines = {line.strip().lstrip("- ") for line in (policy + "\n" + reservation).splitlines()}
facilities = {line.strip() for line in service.splitlines()}
fields = []
def add(key, label, value, group="rules", note=None):
fields.append(dict(key=key, label=label, value=value, group=group,
**({"note": note} if note else {})))
for token, key, label in (("체크인", "check_in_time", "체크인 시간"),
("체크아웃", "check_out_time", "체크아웃 시간")):
matches = set(re.findall(rf"{token}\s+([0-2]\d:[0-5]\d)(?!\d)", policy))
if len(matches) == 1:
value = matches.pop()
if int(value[:2]) < 24:
add(key, label, value)
fees = [re.fullmatch(r"전 연령 동일 1인당\s*([\d,]+)\s*(만)?원", line) for line in lines]
amounts = {int(m[1].replace(",", "")) * (10000 if m[2] else 1) for m in fees if m}
if len(amounts) == 1:
add("extra_person_fee", "인원 추가 요금", f"{amounts.pop():,}", note="1인당 · 전 연령 동일")
if "반려동물 입실금지" in lines and not any("반려동물 입실가능" == line for line in lines):
add("pet_allowed", "반려동물 동반", "불가")
if "전 구역 금연" in lines:
add("smoking", "흡연", "불가", "facilities", "전 구역 금연")
for token, key, label in (("주차가능", "parking", "주차"), ("와이파이", "wifi", "와이파이"),
("취사가능", "cooking_allowed", "취사")):
if token in facilities:
restrictions = [line.strip().lstrip("- ") for line in reservation.splitlines()
if "조리금지" in line or "조리 금지" in line]
add(key, label, "가능", "facilities",
"\n".join(restrictions) if key == "cooking_allowed" and restrictions else None)
known = [name for name in ("욕조", "개별 화장실", "주방", "테라스/발코니", "OTT (스트리밍 서비스)",
"다이닝룸", "벽난로", "어메니티", "카페형룸") if name in facilities]
if known:
add("facilities", "부대시설", ", ".join(known), "facilities")
return fields