From 5f75a2606f5304b8717aff674efed2e31ff4e885 Mon Sep 17 00:00:00 2001 From: multiful <129200984+multiful@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:30:13 +0900 Subject: [PATCH 1/2] update py --- ...\251_TourAPI\353\263\221\355\225\251.xlsx" | Bin src/api/router.py | 79 +++++++++++------ src/api/schemas.py | 3 + src/api/static/index.html | 18 +++- src/data/models.py | 2 + src/explain/pipeline.py | 10 ++- src/scoring/bonus_engine.py | 18 +++- tests/test_bonus_engine.py | 43 ++++++++- tests/test_pipeline_orchestrator.py | 27 ++++++ tests/test_resolve_poi.py | 82 ++++++++++++++++++ 10 files changed, 250 insertions(+), 32 deletions(-) rename "\354\240\234\354\243\274\355\212\271\353\263\204\354\236\220\354\271\230\353\217\204_\355\206\265\355\225\251_TourAPI\353\263\221\355\225\251.xlsx" => "data/\354\240\234\354\243\274\355\212\271\353\263\204\354\236\220\354\271\230\353\217\204_\355\206\265\355\225\251_TourAPI\353\263\221\355\225\251.xlsx" (100%) diff --git "a/\354\240\234\354\243\274\355\212\271\353\263\204\354\236\220\354\271\230\353\217\204_\355\206\265\355\225\251_TourAPI\353\263\221\355\225\251.xlsx" "b/data/\354\240\234\354\243\274\355\212\271\353\263\204\354\236\220\354\271\230\353\217\204_\355\206\265\355\225\251_TourAPI\353\263\221\355\225\251.xlsx" similarity index 100% rename from "\354\240\234\354\243\274\355\212\271\353\263\204\354\236\220\354\271\230\353\217\204_\355\206\265\355\225\251_TourAPI\353\263\221\355\225\251.xlsx" rename to "data/\354\240\234\354\243\274\355\212\271\353\263\204\354\236\220\354\271\230\353\217\204_\355\206\265\355\225\251_TourAPI\353\263\221\355\225\251.xlsx" diff --git a/src/api/router.py b/src/api/router.py index d457953..d9596e9 100644 --- a/src/api/router.py +++ b/src/api/router.py @@ -7,6 +7,7 @@ import re from pathlib import Path +import openpyxl from fastapi import APIRouter, File, HTTPException, UploadFile from src.api.schemas import ( @@ -290,35 +291,50 @@ def _parse_jeju_hours(raw: str) -> tuple[str, str] | None: def _build_jeju_place_index() -> dict[str, dict]: - """data/jeju_places.csv — 실측 좌표·영업시간을 가진 제주 전역 25,377건 통합 DB.""" + """data/제주특별자치도_통합_TourAPI병합.xlsx — 실측 좌표를 가진 제주 전역 26,679건 통합 DB. + 기존 jeju_places.csv(25,377건)에 TourAPI 실측 영업시간·contentid를 매칭 병합한 최신본 + (2,153건 매칭, 1,302건 신규 추가). 영업시간은 TourAPI 실측값을 원문(영업시간)보다 우선한다.""" index: dict[str, dict] = {} - path = _DATA_DIR / "jeju_places.csv" + path = _DATA_DIR / "제주특별자치도_통합_TourAPI병합.xlsx" if not path.exists(): return index - with open(path, encoding="utf-8-sig") as f: - for row in csv.DictReader(f): - name = (row.get("상호명") or "").strip() - if not name: - continue - key = _normalize(name) - if key in index: - continue - try: - lat, lng = float(row["위도"]), float(row["경도"]) - except (ValueError, TypeError, KeyError): - continue - entry = { - "name": name, "lat": lat, "lng": lng, "region": "제주", - "cat": _JEJU_CAT_MAP.get(row.get("대분류코드", ""), "12"), - "cat_name": row.get("대분류명", "관광지"), - "addr": (row.get("도로명주소") or "").strip(), - "has_coords": True, - "source": "jeju_csv", - } - hrs = _parse_jeju_hours(row.get("영업시간", "")) - if hrs: - entry["open_start"], entry["open_end"] = hrs - index[key] = entry + wb = openpyxl.load_workbook(path, read_only=True, data_only=True) + ws = wb.active + rows = ws.iter_rows(values_only=True) + header = [str(h or "").strip() for h in next(rows)] + for values in rows: + row = dict(zip(header, values, strict=True)) + name = str(row.get("상호명") or "").strip() + if not name: + continue + key = _normalize(name) + if key in index: + continue + try: + lat, lng = float(row["위도"]), float(row["경도"]) + except (ValueError, TypeError, KeyError): + continue + entry = { + "name": name, "lat": lat, "lng": lng, "region": "제주", + "cat": _JEJU_CAT_MAP.get(str(row.get("대분류코드") or ""), "12"), + "cat_name": row.get("대분류명") or "관광지", + "addr": str(row.get("도로명주소") or "").strip(), + "has_coords": True, + "source": "jeju_csv", + } + contentid = row.get("TourAPI_contentid") + if contentid not in (None, ""): + entry["contentid"] = str(int(contentid)) + hrs = ( + _parse_jeju_hours(str(row.get("TourAPI_영업시간") or "")) + or _parse_jeju_hours(str(row.get("영업시간") or "")) + ) + if hrs: + entry["open_start"], entry["open_end"] = hrs + if str(row.get("반려동물동반") or "").strip().upper() == "Y": + entry["pet_friendly"] = True + index[key] = entry + wb.close() return index @@ -660,6 +676,7 @@ def _resolve_poi(name: str, idx: int, address: str = "") -> tuple[POI, POIInfo]: open_start, open_end = hours.open_, hours.close_ hours_estimated = True dwell = _guess_dwell(name) + pet_friendly = bool(place.get("pet_friendly")) if place else False # 지식그래프 근거 — 지역·도보권 대안. 그래프 미설정/미매칭 시 조용히 생략. graph_region, graph_nearby = "", [] @@ -675,6 +692,7 @@ def _resolve_poi(name: str, idx: int, address: str = "") -> tuple[POI, POIInfo]: open_start=open_start, open_end=open_end, duration_min=dwell, category=category, hours_estimated=hours_estimated, + pet_friendly=pet_friendly, ) info = POIInfo( name=name, found=(confidence != "Low"), source=source, @@ -685,6 +703,7 @@ def _resolve_poi(name: str, idx: int, address: str = "") -> tuple[POI, POIInfo]: hours_estimated=hours_estimated, graph_region=graph_region, graph_nearby=graph_nearby, + pet_friendly=pet_friendly, ) return poi, info @@ -888,8 +907,16 @@ async def validate_plan(req: ValidateRequest) -> ValidateResponse: plan=plan, per_day_pois=per_day_pois, matrix={}, + pet_friendly_enabled=req.pet_friendly, ) + if result.wellness_matched: + wellness_names = set(result.wellness_matched) + poi_info_list = [ + info.model_copy(update={"wellness": True}) if info.name in wellness_names else info + for info in poi_info_list + ] + data_reliability_score = _calc_reliability_score(poi_info_list) # ── 월별 혼잡도 경고 생성 ────────────────────────────────────── diff --git a/src/api/schemas.py b/src/api/schemas.py index be39c04..3e5f765 100644 --- a/src/api/schemas.py +++ b/src/api/schemas.py @@ -30,6 +30,8 @@ class POIInfo(BaseModel): graph_nearby: list[str] = [] category: str = "" day_index: int = 0 + wellness: bool = False + pet_friendly: bool = False class PlaceItem(BaseModel): @@ -56,6 +58,7 @@ class ValidateRequest(BaseModel): travel_type: Optional[Literal["cultural", "nature", "shopping", "food", "adventure"]] = None date: str = "2026-05-10" start_time: str = "09:00" + pet_friendly: bool = False class ParseTextRequest(BaseModel): diff --git a/src/api/static/index.html b/src/api/static/index.html index 93f617c..8041df9 100644 --- a/src/api/static/index.html +++ b/src/api/static/index.html @@ -1404,6 +1404,12 @@

내 여행 일정,
실제로 가능한지 검증해드립니다

+
+ 반려동물과 함께 가세요? + +
여행 스타일 (선택)
@@ -1681,7 +1687,7 @@

데이터 라이선스