From e56aa35170780b389e52eb6da7c5a61b389bf224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=EA=B2=BD?= Date: Tue, 2 Jun 2026 10:20:43 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A7=81=EC=A0=91=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/home/api/routers/v1/home.py | 195 +++++++++++++++++++------------- app/home/schemas/home_schema.py | 32 +++++- 2 files changed, 148 insertions(+), 79 deletions(-) diff --git a/app/home/api/routers/v1/home.py b/app/home/api/routers/v1/home.py index baa6bf9..03fac5d 100644 --- a/app/home/api/routers/v1/home.py +++ b/app/home/api/routers/v1/home.py @@ -25,6 +25,7 @@ from app.home.schemas.home_schema import ( ImageUploadResponse, ImageUploadResultItem, ImageUrlItem, + ManualMarketingRequest, ProcessedInfo, # MarketingAnalysis, ) @@ -133,31 +134,32 @@ METRO_CITY_MAP = { } -def _extract_region_from_address(road_address: str | None) -> str: - """roadAddress에서 시/군 이름 추출 +def _extract_region_from_address(road_address: str | None, jibun_address: str | None = None) -> str: + """주소에서 시/군 이름 추출 — 도로명 우선, 실패 시 지번으로 재시도""" - 매칭 우선순위: - 1. KOREAN_CITIES 직접 매칭 (시/군 접미사 포함) - 2. KOREAN_CITIES 접미사 생략 매칭 - 3. 주소 두 번째 토큰이 시/군으로 끝나는 경우 (예: "전북 군산시 ...") - 4. 주소 두 번째 토큰이 구/동인 경우 → 첫 번째 토큰으로 광역시 매핑 (예: "서울 강남구 ...") - """ - if not road_address: + def _parse(address: str) -> str: + token_set = set(address.split()) + for city in KOREAN_CITIES: + if city in token_set: # 완전 일치 (토큰 단위) + return city + if city[:-1] in token_set: # 접미사 생략 일치 (토큰 단위) + return city + tokens = address.split() + if len(tokens) >= 2: + second = tokens[1] + if second.endswith("시") or second.endswith("군"): + return second + if second.endswith("구") or second.endswith("동"): + return METRO_CITY_MAP.get(tokens[0], "") return "" - for city in KOREAN_CITIES: - if city in road_address: - return city - if city[:-1] in road_address: - return city + if road_address: + result = _parse(road_address) + if result: + return result - tokens = road_address.split() - if len(tokens) >= 2: - second = tokens[1] - if second.endswith("시") or second.endswith("군"): - return second - if second.endswith("구") or second.endswith("동"): - return METRO_CITY_MAP.get(tokens[0], "") + if jibun_address: + return _parse(jibun_address) return "" @@ -290,14 +292,15 @@ async def _crawling_logic( marketing_analysis = None if scraper.base_info: - road_address = scraper.base_info.get("roadAddress", "") or scraper.base_info.get("address", "") + road_address = scraper.base_info.get("roadAddress", "") + jibun_address = scraper.base_info.get("address", "") customer_name = scraper.base_info.get("name", "") - region = _extract_region_from_address(road_address) + region = _extract_region_from_address(road_address, jibun_address) processed_info = ProcessedInfo( customer_name=customer_name, region=region, - detail_region_info=road_address or "", + detail_region_info=road_address or jibun_address or "", ) step2_elapsed = (time.perf_counter() - step2_start) * 1000 @@ -310,70 +313,29 @@ async def _crawling_logic( logger.info("[crawling] Step 3: ChatGPT 마케팅 분석 시작...") try: - # Step 3-1: ChatGPT 서비스 초기화 - step3_1_start = time.perf_counter() + # Step 3-1: ChatGPT 서비스 초기화 및 입력 데이터 구성 chatgpt_service = ChatgptService() - step3_1_elapsed = (time.perf_counter() - step3_1_start) * 1000 - logger.debug( - f"[crawling] Step 3-1: 서비스 초기화 완료 ({step3_1_elapsed:.1f}ms)" - ) - - # Step 3-2: 프롬프트 생성 - # step3_2_start = time.perf_counter() input_marketing_data = { "customer_name": customer_name, "region": region, "detail_region_info": road_address or "", } - # prompt = chatgpt_service.build_market_analysis_prompt() - # prompt1 = marketing_prompt.build_prompt(input_marketing_data) - # step3_2_elapsed = (time.perf_counter() - step3_2_start) * 1000 - # Step 3-3: GPT API 호출 - step3_3_start = time.perf_counter() - structured_report = await chatgpt_service.generate_structured_output( + # Step 3-2: GPT API 호출 → 구조화된 마케팅 분석 결과 반환 + marketing_analysis = await chatgpt_service.generate_structured_output( marketing_prompt, input_marketing_data - ) - marketing_intelligence = MarketingIntel( - place_id = scraper.place_id, - intel_result = structured_report.model_dump() ) - session.add(marketing_intelligence) + + # Step 3-3: 분석 결과 DB 저장 + marketing_intel = MarketingIntel( + place_id=scraper.place_id, + intel_result=marketing_analysis.model_dump(), + ) + session.add(marketing_intel) await session.commit() - await session.refresh(marketing_intelligence) - m_id = marketing_intelligence.id - logger.debug(f"[MarketingPrompt] INSERT placeid {marketing_intelligence.place_id}") - step3_3_elapsed = (time.perf_counter() - step3_3_start) * 1000 - logger.info( - f"[crawling] Step 3-3: GPT API 호출 완료 - ({step3_3_elapsed:.1f}ms)" - ) - logger.debug( - f"[crawling] Step 3-3: GPT API 호출 완료 - ({step3_3_elapsed:.1f}ms)" - ) - - # Step 3-4: 응답 파싱 (크롤링에서 가져온 facility_info 전달) - step3_4_start = time.perf_counter() - logger.debug( - f"[crawling] Step 3-4: 응답 파싱 시작 - facility_info: {scraper.facility_info}" - ) - - # 요약 Deprecated / 20250115 / Selling points를 첫 prompt에서 추출 중 - # parsed = await chatgpt_service.parse_marketing_analysis( - # structured_report, facility_info=scraper.facility_info - # ) - - # marketing_analysis = MarketingAnalysis(**parsed) - - logger.debug( - f"structured_report = {structured_report.model_dump()}" - ) - - marketing_analysis = structured_report - - step3_4_elapsed = (time.perf_counter() - step3_4_start) * 1000 - logger.debug( - f"[crawling] Step 3-4: 응답 파싱 완료 ({step3_4_elapsed:.1f}ms)" - ) + await session.refresh(marketing_intel) + m_id = marketing_intel.id + logger.debug(f"[MarketingPrompt] INSERT place_id={marketing_intel.place_id} id={marketing_intel.id}") step3_elapsed = (time.perf_counter() - step3_start) * 1000 logger.info( @@ -426,6 +388,83 @@ async def _crawling_logic( } + +@router.post( + "/marketing", + summary="업체명+주소 직접 입력 마케팅 분석", + description=""" +네이버 크롤링 없이 업체명과 주소를 직접 입력받아 마케팅 분석을 수행합니다. + +## 요청 필드 +- **customer_name**: 업체명 / 브랜드명 (필수) +- **address**: 도로명 또는 지번 주소 (필수) + +## 반환 정보 +- **processed_info**: 가공된 장소 정보 (customer_name, region, detail_region_info) +- **marketing_analysis**: ChatGPT 마케팅 분석 결과 +- **m_id**: 마케팅 분석 결과 ID (이후 영상생성 파이프라인에 사용) + """, + response_model=CrawlingResponse, + tags=["Marketing"], +) +async def manual_marketing( + request_body: ManualMarketingRequest, + session: AsyncSession = Depends(get_session), +): + # Step 1: 주소에서 지역명 추출 및 processed_info 구성 + region = _extract_region_from_address(request_body.address) + processed_info = ProcessedInfo( + customer_name=request_body.store_name, + region=region, + detail_region_info=request_body.address, + ) + try: + # Step 2: GPT API 호출 → 마케팅 분석 결과 생성 + # place_id 없이 업체명+주소만으로 분석 (크롤링 없이 직접 입력된 경우) + chatgpt_service = ChatgptService() + input_marketing_data = { + "customer_name": request_body.store_name, + "region": region, + "detail_region_info": request_body.address, + } + marketing_analysis = await chatgpt_service.generate_structured_output( + marketing_prompt, input_marketing_data + ) + + # Step 3: 분석 결과 DB 저장 (place_id=None — 네이버 장소와 연결되지 않음) + marketing_intel = MarketingIntel( + place_id=None, + intel_result=marketing_analysis.model_dump(), + ) + session.add(marketing_intel) + await session.commit() + await session.refresh(marketing_intel) + m_id = marketing_intel.id + logger.debug(f"[MarketingPrompt] INSERT id={marketing_intel.id}") + except ChatGPTResponseError as e: + logger.error( + f"[marketing] ChatGPT Error: status={e.status}, " + f"code={e.error_code}, message={e.error_message}" + ) + raise HTTPException( + status_code=status.HTTP_502_BAD_GATEWAY, + detail=f"마케팅 분석 중 ChatGPT 오류가 발생했습니다: {e.error_message}", + ) + except Exception as e: + logger.error(f"[marketing] 마케팅 분석 중 오류: {e}") + logger.exception("[marketing] 상세 오류:") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="마케팅 분석 중 오류가 발생했습니다.", + ) + return CrawlingResponse( + status="completed", + processed_info=processed_info, + marketing_analysis=marketing_analysis, + m_id=m_id, + ) + + async def _autocomplete_logic(autocomplete_item:dict): step1_start = time.perf_counter() try: diff --git a/app/home/schemas/home_schema.py b/app/home/schemas/home_schema.py index e227da2..3c5d283 100644 --- a/app/home/schemas/home_schema.py +++ b/app/home/schemas/home_schema.py @@ -234,7 +234,7 @@ class CrawlingResponse(BaseModel): description="처리 상태 (completed: 성공, failed: ChatGPT 분석 실패)" ) image_list: Optional[list[str]] = Field(None, description="이미지 URL 목록") - image_count: int = Field(..., description="이미지 개수") + image_count: Optional[int] = Field(None, description="이미지 개수") processed_info: Optional[ProcessedInfo] = Field( None, description="가공된 장소 정보 (customer_name, region, detail_region_info)" ) @@ -244,6 +244,36 @@ class CrawlingResponse(BaseModel): m_id : int = Field(..., description="마케팅 분석 결과 ID") +class ManualMarketingRequest(BaseModel): + """업체명+주소 직접 입력 마케팅 분석 요청""" + + model_config = ConfigDict( + json_schema_extra={ + "example": { + "store_name": "스테이 머뭄", + "address": "전북특별자치도 군산시 절골길 18", + } + } + ) + + store_name: str = Field(..., description="업체명 / 브랜드명") + address: str = Field(..., description="도로명 또는 지번 주소") + + +class MarketingAnalysisResponse(BaseModel): + """업체명+주소 직접 입력 마케팅 분석 응답""" + + status: str = Field( + default="completed", + description="처리 상태 (completed: 성공, failed: ChatGPT 분석 실패)", + ) + processed_info: ProcessedInfo = Field(..., description="가공된 장소 정보") + marketing_analysis: Optional[MarketingPromptOutput] = Field( + None, description="마케팅 분석 결과. 실패 시 null" + ) + m_id: int = Field(..., description="마케팅 분석 결과 ID") + + class ErrorResponse(BaseModel): """에러 응답 스키마"""