Merge branch 'main' into prompt
commit
2e27eb5742
|
|
@ -46,3 +46,7 @@ logs/
|
||||||
._*
|
._*
|
||||||
.Spotlight-V100
|
.Spotlight-V100
|
||||||
.Trashes
|
.Trashes
|
||||||
|
|
||||||
|
*.yml
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|
@ -16,6 +16,8 @@ from app.user.dependencies.auth import get_current_user
|
||||||
from app.user.models import User
|
from app.user.models import User
|
||||||
from app.home.schemas.home_schema import (
|
from app.home.schemas.home_schema import (
|
||||||
AutoCompleteRequest,
|
AutoCompleteRequest,
|
||||||
|
AccommodationSearchItem,
|
||||||
|
AccommodationSearchResponse,
|
||||||
CrawlingRequest,
|
CrawlingRequest,
|
||||||
CrawlingResponse,
|
CrawlingResponse,
|
||||||
ErrorResponse,
|
ErrorResponse,
|
||||||
|
|
@ -25,6 +27,7 @@ from app.home.schemas.home_schema import (
|
||||||
MarketingAnalysis,
|
MarketingAnalysis,
|
||||||
ProcessedInfo,
|
ProcessedInfo,
|
||||||
)
|
)
|
||||||
|
from app.home.services.naver_search import naver_search_client
|
||||||
from app.utils.upload_blob_as_request import AzureBlobUploader
|
from app.utils.upload_blob_as_request import AzureBlobUploader
|
||||||
from app.utils.chatgpt_prompt import ChatgptService, ChatGPTResponseError
|
from app.utils.chatgpt_prompt import ChatgptService, ChatGPTResponseError
|
||||||
from app.utils.common import generate_task_id
|
from app.utils.common import generate_task_id
|
||||||
|
|
@ -70,6 +73,47 @@ KOREAN_CITIES = [
|
||||||
router = APIRouter()
|
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:
|
def _extract_region_from_address(road_address: str | None) -> str:
|
||||||
"""roadAddress에서 시 이름 추출"""
|
"""roadAddress에서 시 이름 추출"""
|
||||||
if not road_address:
|
if not road_address:
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,45 @@ class AutoCompleteRequest(BaseModel):
|
||||||
address: str = Field(..., description="네이버 검색 place API 지번주소")
|
address: str = Field(..., description="네이버 검색 place API 지번주소")
|
||||||
roadAddress: Optional[str] = Field(None, 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": "<b>스테이</b>,<b>머뭄</b>",
|
||||||
|
"address": "전북특별자치도 군산시 신흥동 63-18",
|
||||||
|
"roadAddress": "전북특별자치도 군산시 절골길 18",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "머뭄<b>스테이</b>",
|
||||||
|
"address": "전북특별자치도 군산시 비응도동 123",
|
||||||
|
"roadAddress": "전북특별자치도 군산시 비응로 456",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
query: str = Field(..., description="검색어")
|
||||||
|
count: int = Field(..., description="검색 결과 수")
|
||||||
|
items: list[AccommodationSearchItem] = Field(
|
||||||
|
default_factory=list, description="검색 결과 목록"
|
||||||
|
)
|
||||||
|
|
||||||
class ProcessedInfo(BaseModel):
|
class ProcessedInfo(BaseModel):
|
||||||
"""가공된 장소 정보 스키마"""
|
"""가공된 장소 정보 스키마"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
14
config.py
14
config.py
|
|
@ -105,6 +105,19 @@ class CrawlerSettings(BaseSettings):
|
||||||
model_config = _base_config
|
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):
|
class AzureBlobSettings(BaseSettings):
|
||||||
"""Azure Blob Storage 설정"""
|
"""Azure Blob Storage 설정"""
|
||||||
|
|
||||||
|
|
@ -440,6 +453,7 @@ apikey_settings = APIKeySettings()
|
||||||
db_settings = DatabaseSettings()
|
db_settings = DatabaseSettings()
|
||||||
cors_settings = CORSSettings()
|
cors_settings = CORSSettings()
|
||||||
crawler_settings = CrawlerSettings()
|
crawler_settings = CrawlerSettings()
|
||||||
|
naver_api_settings = NaverAPISettings()
|
||||||
azure_blob_settings = AzureBlobSettings()
|
azure_blob_settings = AzureBlobSettings()
|
||||||
creatomate_settings = CreatomateSettings()
|
creatomate_settings = CreatomateSettings()
|
||||||
prompt_settings = PromptSettings()
|
prompt_settings = PromptSettings()
|
||||||
|
|
|
||||||
28
main.py
28
main.py
|
|
@ -51,6 +51,33 @@ tags_metadata = [
|
||||||
# "name": "Home",
|
# "name": "Home",
|
||||||
# "description": "홈 화면 및 프로젝트 관리 API",
|
# "description": "홈 화면 및 프로젝트 관리 API",
|
||||||
# },
|
# },
|
||||||
|
{
|
||||||
|
"name": "Search",
|
||||||
|
"description": """숙박/펜션 검색 API - 네이버 지역 검색 기반 자동완성
|
||||||
|
|
||||||
|
**인증: 불필요** (공개 API)
|
||||||
|
|
||||||
|
## 사용법
|
||||||
|
|
||||||
|
`GET /search/accommodation?query=스테이머뭄`
|
||||||
|
|
||||||
|
## 응답 예시
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "스테이머뭄",
|
||||||
|
"count": 1,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "<b>스테이</b>,<b>머뭄</b>",
|
||||||
|
"address": "전북특별자치도 군산시 신흥동 63-18",
|
||||||
|
"roadAddress": "전북특별자치도 군산시 절골길 18"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
""",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Crawling",
|
"name": "Crawling",
|
||||||
"description": """네이버 지도 크롤링 API - 장소 정보 및 이미지 수집
|
"description": """네이버 지도 크롤링 API - 장소 정보 및 이미지 수집
|
||||||
|
|
@ -190,6 +217,7 @@ def custom_openapi():
|
||||||
"/auth/test/", # 테스트 엔드포인트
|
"/auth/test/", # 테스트 엔드포인트
|
||||||
"/crawling",
|
"/crawling",
|
||||||
"/autocomplete",
|
"/autocomplete",
|
||||||
|
"/search", # 숙박 검색 자동완성
|
||||||
]
|
]
|
||||||
|
|
||||||
# 보안이 필요한 엔드포인트에 security 적용
|
# 보안이 필요한 엔드포인트에 security 적용
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue