ai-review/vector/app/services/restaurant_service.py
2025-06-16 07:08:09 +09:00

231 lines
9.4 KiB
Python

# app/services/restaurant_service.py
import aiohttp
import asyncio
import logging
from typing import List, Optional, Dict, Any
from ..config.settings import settings
from ..models.restaurant_models import RestaurantInfo
from ..utils.category_utils import extract_food_category
logger = logging.getLogger(__name__)
class RestaurantService:
"""음식점 API 연동 서비스"""
def __init__(self):
self.base_url = settings.get_restaurant_api_url()
self.timeout = aiohttp.ClientTimeout(total=settings.REQUEST_TIMEOUT)
async def find_store_by_name_and_region(self, region: str, store_name: str) -> Optional[RestaurantInfo]:
"""
지역과 가게명으로 가게를 찾습니다.
Args:
region: 지역 (시군구 + 읍면동)
store_name: 가게명
Returns:
찾은 가게 정보 (첫 번째 결과)
"""
try:
logger.info(f"가게 검색 시작: region={region}, store_name={store_name}")
async with aiohttp.ClientSession(timeout=self.timeout) as session:
# Restaurant API 호출
url = f"{self.base_url}/collect"
payload = {
"query": store_name,
"region": region,
"size": 15,
"pages": 3,
"save_to_file": False
}
logger.info(f"Restaurant API 호출: {url}")
async with session.post(url, json=payload) as response:
if response.status == 200:
data = await response.json()
restaurants = data.get('restaurants', [])
if restaurants:
# 첫 번째 결과 반환
restaurant_data = restaurants[0]
restaurant = RestaurantInfo(**restaurant_data)
logger.info(f"가게 찾기 성공: {restaurant.place_name}")
return restaurant
else:
logger.warning(f"가게를 찾을 수 없음: {store_name}")
return None
else:
logger.error(f"Restaurant API 호출 실패: HTTP {response.status}")
error_text = await response.text()
logger.error(f"Error response: {error_text}")
return None
except asyncio.TimeoutError:
logger.error("Restaurant API 호출 타임아웃")
return None
except Exception as e:
logger.error(f"가게 검색 중 오류: {str(e)}")
return None
async def find_similar_stores(self, region: str, food_category: str, max_count: int = 50) -> List[RestaurantInfo]:
"""
동종 업체를 찾습니다.
Args:
region: 지역
food_category: 음식 카테고리
max_count: 최대 검색 개수
Returns:
동종 업체 목록
"""
try:
logger.info(f"동종 업체 검색 시작: region={region}, food_category={food_category}")
# 검색 쿼리 생성 (음식 카테고리만 포함)
search_query = self._clean_food_category(food_category)
logger.info(f"음식점 수집 요청: query='{search_query}' region='{region}'")
similar_stores = []
async with aiohttp.ClientSession(timeout=self.timeout) as session:
# 페이지별로 검색 (최대 5페이지)
max_pages = min(5, (max_count // 15) + 1)
for page in range(1, max_pages + 1):
if len(similar_stores) >= max_count:
break
url = f"{self.base_url}/collect"
payload = {
"query": search_query, # 음식 카테고리만 포함
"region": region, # 지역 정보는 별도 파라미터
"size": 15,
"pages": 1, # 페이지별로 하나씩 호출
"save_to_file": False
}
logger.info(f"동종 업체 검색 ({page}/{max_pages}): {url}")
try:
async with session.post(url, json=payload) as response:
if response.status == 200:
data = await response.json()
restaurants = data.get('restaurants', [])
for restaurant_data in restaurants:
if len(similar_stores) >= max_count:
break
try:
restaurant = RestaurantInfo(**restaurant_data)
# 카테고리 유사성 검사
if self._is_similar_category(food_category, restaurant.category_name):
similar_stores.append(restaurant)
except Exception as e:
logger.warning(f"음식점 데이터 파싱 실패: {e}")
continue
logger.info(f"페이지 {page} 완료: {len(restaurants)}개 발견, 총 {len(similar_stores)}개 수집")
else:
logger.warning(f"페이지 {page} API 호출 실패: HTTP {response.status}")
break
except Exception as e:
logger.warning(f"페이지 {page} 처리 실패: {e}")
continue
# API 호출 간격 조절
await asyncio.sleep(settings.REQUEST_DELAY)
logger.info(f"동종 업체 검색 완료: {len(similar_stores)}개 발견")
return similar_stores
except Exception as e:
logger.error(f"동종 업체 검색 중 오류: {str(e)}")
return []
def _clean_food_category(self, food_category: str) -> str:
"""
음식 카테고리를 정리하여 검색 키워드로 변환합니다.
Args:
food_category: 원본 음식 카테고리 (예: "육류,고기")
Returns:
정리된 검색 키워드 (예: "육류 고기")
"""
if not food_category:
return "음식점"
# 콤마와 슬래시를 공백으로 변경
cleaned = food_category.replace(',', ' ').replace('/', ' ')
# 불필요한 단어 제거
stop_words = ['음식점', '요리', '전문점', '맛집']
keywords = []
for keyword in cleaned.split():
keyword = keyword.strip()
if keyword and keyword not in stop_words:
keywords.append(keyword)
# 키워드가 없으면 기본 검색어 사용
if not keywords:
return "음식점"
# 지역 정보는 포함하지 않고 음식 카테고리만 반환
return ' '.join(keywords)
def _is_similar_category(self, target_category: str, restaurant_category: str) -> bool:
"""
두 카테고리가 유사한지 판단합니다.
Args:
target_category: 대상 카테고리
restaurant_category: 음식점 카테고리
Returns:
유사성 여부
"""
if not target_category or not restaurant_category:
return False
# 정규화
target_lower = target_category.lower().strip()
restaurant_lower = restaurant_category.lower().strip()
# 완전 일치
if target_lower == restaurant_lower:
return True
# 키워드 기반 매칭
target_keywords = set(target_lower.replace(',', ' ').replace('/', ' ').split())
restaurant_keywords = set(restaurant_lower.replace(',', ' ').replace('/', ' ').split())
# 교집합이 있으면 유사한 것으로 판단
common_keywords = target_keywords.intersection(restaurant_keywords)
return len(common_keywords) > 0
async def health_check(self) -> bool:
"""
Restaurant API 상태를 확인합니다.
Returns:
API 상태 (True: 정상, False: 비정상)
"""
try:
async with aiohttp.ClientSession(timeout=self.timeout) as session:
url = f"{self.base_url}/health"
async with session.get(url) as response:
return response.status == 200
except Exception as e:
logger.error(f"Restaurant API 헬스체크 실패: {e}")
return False