diff --git a/app/home/api/routers/v1/home.py b/app/home/api/routers/v1/home.py index 9ee7867..3c8e175 100644 --- a/app/home/api/routers/v1/home.py +++ b/app/home/api/routers/v1/home.py @@ -16,6 +16,8 @@ from app.user.dependencies.auth import get_current_user from app.user.models import User from app.home.schemas.home_schema import ( AutoCompleteRequest, + AccommodationSearchItem, + AccommodationSearchResponse, CrawlingRequest, CrawlingResponse, ErrorResponse, @@ -25,6 +27,7 @@ from app.home.schemas.home_schema import ( MarketingAnalysis, ProcessedInfo, ) +from app.home.services.naver_search import naver_search_client from app.utils.upload_blob_as_request import AzureBlobUploader from app.utils.chatgpt_prompt import ChatgptService, ChatGPTResponseError from app.utils.common import generate_task_id @@ -70,6 +73,47 @@ KOREAN_CITIES = [ router = APIRouter() +@router.get( + "/search/accommodation", + summary="숙박/펜션 자동완성 검색", + description=""" +네이버 지역 검색 API를 이용한 숙박/펜션 자동완성 검색입니다. + +## 요청 파라미터 +- **query**: 검색어 (필수) + +## 반환 정보 +- **query**: 검색어 +- **count**: 검색 결과 수 (최대 10개) +- **items**: 검색 결과 목록 + - **title**: 숙소명 (HTML 태그 포함 가능) + - **address**: 지번 주소 + - **roadAddress**: 도로명 주소 + """, + response_model=AccommodationSearchResponse, + responses={ + 200: {"description": "검색 성공", "model": AccommodationSearchResponse}, + }, + tags=["Search"], +) +async def search_accommodation( + query: str, +) -> AccommodationSearchResponse: + """숙박/펜션 자동완성 검색""" + results = await naver_search_client.search_accommodation( + query=query, + display=10, + ) + + items = [AccommodationSearchItem(**item) for item in results] + + return AccommodationSearchResponse( + query=query, + count=len(items), + items=items, + ) + + def _extract_region_from_address(road_address: str | None) -> str: """roadAddress에서 시 이름 추출""" if not road_address: diff --git a/app/home/schemas/home_schema.py b/app/home/schemas/home_schema.py index 79f73da..1ea6285 100644 --- a/app/home/schemas/home_schema.py +++ b/app/home/schemas/home_schema.py @@ -139,6 +139,45 @@ class AutoCompleteRequest(BaseModel): address: str = Field(..., description="네이버 검색 place API 지번주소") roadAddress: Optional[str] = Field(None, description="네이버 검색 place API 도로명주소") + +class AccommodationSearchItem(BaseModel): + """숙박 검색 결과 아이템""" + + title: str = Field(..., description="숙소명 (HTML 태그 포함 가능)") + address: str = Field(..., description="지번 주소") + roadAddress: str = Field(default="", description="도로명 주소") + + +class AccommodationSearchResponse(BaseModel): + """숙박 자동완성 검색 응답""" + + model_config = ConfigDict( + json_schema_extra={ + "example": { + "query": "스테이 머뭄", + "count": 2, + "items": [ + { + "title": "스테이,머뭄", + "address": "전북특별자치도 군산시 신흥동 63-18", + "roadAddress": "전북특별자치도 군산시 절골길 18", + }, + { + "title": "머뭄스테이", + "address": "전북특별자치도 군산시 비응도동 123", + "roadAddress": "전북특별자치도 군산시 비응로 456", + }, + ], + } + } + ) + + query: str = Field(..., description="검색어") + count: int = Field(..., description="검색 결과 수") + items: list[AccommodationSearchItem] = Field( + default_factory=list, description="검색 결과 목록" + ) + class ProcessedInfo(BaseModel): """가공된 장소 정보 스키마""" diff --git a/app/home/services/naver_search.py b/app/home/services/naver_search.py new file mode 100644 index 0000000..343b1c5 --- /dev/null +++ b/app/home/services/naver_search.py @@ -0,0 +1,99 @@ +""" +네이버 지역 검색 API 클라이언트 + +숙박/펜션 자동완성 검색 기능을 제공합니다. +""" + +import logging +from typing import List + +import aiohttp + +from config import naver_api_settings + +logger = logging.getLogger(__name__) + + +class NaverSearchClient: + """ + 네이버 지역 검색 API 클라이언트 + + 숙박/펜션 카테고리 검색을 위한 클라이언트입니다. + """ + + def __init__(self) -> None: + self.client_id = naver_api_settings.NAVER_CLIENT_ID + self.client_secret = naver_api_settings.NAVER_CLIENT_SECRET + self.api_url = naver_api_settings.NAVER_LOCAL_API_URL + + async def search_accommodation( + self, + query: str, + display: int = 5, + ) -> List[dict]: + """ + 숙박/펜션 검색 + + Args: + query: 검색어 + display: 결과 개수 (기본 5개) + + Returns: + 검색 결과 리스트 (address, roadAddress, title) + """ + # 숙박/펜션 카테고리 검색을 위해 쿼리에 키워드 추가 + search_query = f"{query} 숙박" + + headers = { + "X-Naver-Client-Id": self.client_id, + "X-Naver-Client-Secret": self.client_secret, + } + + params = { + "query": search_query, + "display": display, + "sort": "random", # 정확도순 + } + + logger.info(f"[NAVER] 지역 검색 요청 - query: {search_query}") + + try: + async with aiohttp.ClientSession() as session: + async with session.get( + self.api_url, + headers=headers, + params=params, + ) as response: + if response.status != 200: + error_text = await response.text() + logger.error( + f"[NAVER] API 오류 - status: {response.status}, error: {error_text}" + ) + return [] + + data = await response.json() + items = data.get("items", []) + + # 필요한 필드만 추출 + results = [ + { + "address": item.get("address", ""), + "roadAddress": item.get("roadAddress", ""), + "title": item.get("title", ""), + } + for item in items + ] + + logger.info(f"[NAVER] 검색 완료 - 결과 수: {len(results)}") + return results + + except aiohttp.ClientError as e: + logger.error(f"[NAVER] 네트워크 오류 - {str(e)}") + return [] + except Exception as e: + logger.error(f"[NAVER] 예기치 않은 오류 - {str(e)}") + return [] + + +# 싱글톤 인스턴스 +naver_search_client = NaverSearchClient() diff --git a/config.py b/config.py index 40489c5..bba448f 100644 --- a/config.py +++ b/config.py @@ -105,6 +105,19 @@ class CrawlerSettings(BaseSettings): model_config = _base_config +class NaverAPISettings(BaseSettings): + """네이버 API 설정""" + + NAVER_CLIENT_ID: str = Field(default="", description="네이버 API 클라이언트 ID") + NAVER_CLIENT_SECRET: str = Field(default="", description="네이버 API 클라이언트 시크릿") + NAVER_LOCAL_API_URL: str = Field( + default="https://openapi.naver.com/v1/search/local.json", + description="네이버 지역 검색 API URL", + ) + + model_config = _base_config + + class AzureBlobSettings(BaseSettings): """Azure Blob Storage 설정""" @@ -437,6 +450,7 @@ apikey_settings = APIKeySettings() db_settings = DatabaseSettings() cors_settings = CORSSettings() crawler_settings = CrawlerSettings() +naver_api_settings = NaverAPISettings() azure_blob_settings = AzureBlobSettings() creatomate_settings = CreatomateSettings() prompt_settings = PromptSettings() diff --git a/main.py b/main.py index 516cada..43763a7 100644 --- a/main.py +++ b/main.py @@ -51,6 +51,33 @@ tags_metadata = [ # "name": "Home", # "description": "홈 화면 및 프로젝트 관리 API", # }, + { + "name": "Search", + "description": """숙박/펜션 검색 API - 네이버 지역 검색 기반 자동완성 + +**인증: 불필요** (공개 API) + +## 사용법 + +`GET /search/accommodation?query=스테이머뭄` + +## 응답 예시 + +```json +{ + "query": "스테이머뭄", + "count": 1, + "items": [ + { + "title": "스테이,머뭄", + "address": "전북특별자치도 군산시 신흥동 63-18", + "roadAddress": "전북특별자치도 군산시 절골길 18" + } + ] +} +``` +""", + }, { "name": "Crawling", "description": """네이버 지도 크롤링 API - 장소 정보 및 이미지 수집 @@ -190,6 +217,7 @@ def custom_openapi(): "/auth/test/", # 테스트 엔드포인트 "/crawling", "/autocomplete", + "/search", # 숙박 검색 자동완성 ] # 보안이 필요한 엔드포인트에 security 적용