From f7e3142ca4cd28de65b222dde67095efbde9585b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=B1=EA=B2=BD?= Date: Thu, 28 May 2026 14:23:07 +0900 Subject: [PATCH] =?UTF-8?q?=EC=97=85=EC=B2=B4=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EA=B8=B0=EB=8A=A5=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- app/home/api/routers/v1/home.py | 163 +++++++++++------- app/home/models.py | 6 +- app/home/schemas/home_schema.py | 30 ++++ ...2026_05_28_marketing_place_id_nullable.sql | 8 + 5 files changed, 139 insertions(+), 71 deletions(-) create mode 100644 docs/database-schema/migration_2026_05_28_marketing_place_id_nullable.sql diff --git a/.gitignore b/.gitignore index 45387fd..cdd7623 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,5 @@ Dockerfile .dockerignore zzz/ -credentials/service_account.json \ No newline at end of file +credentials/service_account.json +o2o-castad-scheduler/ \ No newline at end of file diff --git a/app/home/api/routers/v1/home.py b/app/home/api/routers/v1/home.py index baa6bf9..3a68676 100644 --- a/app/home/api/routers/v1/home.py +++ b/app/home/api/routers/v1/home.py @@ -25,6 +25,8 @@ from app.home.schemas.home_schema import ( ImageUploadResponse, ImageUploadResultItem, ImageUrlItem, + ManualMarketingRequest, + MarketingAnalysisResponse, ProcessedInfo, # MarketingAnalysis, ) @@ -310,71 +312,13 @@ async def _crawling_logic( logger.info("[crawling] Step 3: ChatGPT 마케팅 분석 시작...") try: - # Step 3-1: ChatGPT 서비스 초기화 - step3_1_start = time.perf_counter() - 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)" + marketing_analysis, m_id = await _run_marketing_analysis( + customer_name=customer_name, + region=region, + detail_region_info=road_address or "", + place_id=scraper.place_id, + session=session, ) - - # 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( - marketing_prompt, input_marketing_data - ) - marketing_intelligence = MarketingIntel( - place_id = scraper.place_id, - intel_result = structured_report.model_dump() - ) - session.add(marketing_intelligence) - 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)" - ) - step3_elapsed = (time.perf_counter() - step3_start) * 1000 logger.info( f"[crawling] Step 3 완료 - 마케팅 분석 성공 ({step3_elapsed:.1f}ms)" @@ -395,7 +339,6 @@ async def _crawling_logic( f"[crawling] Step 3 FAILED - GPT 마케팅 분석 중 오류: {e} ({step3_elapsed:.1f}ms)" ) logger.exception("[crawling] Step 3 상세 오류:") - # GPT 실패 시에도 크롤링 결과는 반환 marketing_analysis = None gpt_status = "failed" else: @@ -413,8 +356,6 @@ async def _crawling_logic( logger.info(f"[crawling] - Step 2 (정보가공): {step2_elapsed:.1f}ms") if "step3_elapsed" in locals(): logger.info(f"[crawling] - Step 3 (GPT 분석): {step3_elapsed:.1f}ms") - if "step3_3_elapsed" in locals(): - logger.info(f"[crawling] - GPT API 호출: {step3_3_elapsed:.1f}ms") return { "status": gpt_status if 'gpt_status' in locals() else "completed", @@ -426,6 +367,94 @@ async def _crawling_logic( } +async def _run_marketing_analysis( + customer_name: str, + region: str, + detail_region_info: str, + place_id: Optional[str], + session: AsyncSession, +): + """ChatGPT 마케팅 분석 실행 → MarketingIntel 저장 → (MarketingPromptOutput, m_id) 반환""" + chatgpt_service = ChatgptService() + input_marketing_data = { + "customer_name": customer_name, + "region": region, + "detail_region_info": detail_region_info, + } + structured_report = await chatgpt_service.generate_structured_output( + marketing_prompt, input_marketing_data + ) + marketing_intel = MarketingIntel( + place_id=place_id, + intel_result=structured_report.model_dump(), + ) + session.add(marketing_intel) + await session.commit() + await session.refresh(marketing_intel) + logger.debug(f"[MarketingPrompt] INSERT place_id={marketing_intel.place_id} id={marketing_intel.id}") + return structured_report, marketing_intel.id + + +@router.post( + "/marketing", + summary="업체명+주소 직접 입력 마케팅 분석", + description=""" +네이버 크롤링 없이 업체명과 주소를 직접 입력받아 마케팅 분석을 수행합니다. + +## 요청 필드 +- **customer_name**: 업체명 / 브랜드명 (필수) +- **address**: 도로명 또는 지번 주소 (필수) + +## 반환 정보 +- **processed_info**: 가공된 장소 정보 (customer_name, region, detail_region_info) +- **marketing_analysis**: ChatGPT 마케팅 분석 결과 +- **m_id**: 마케팅 분석 결과 ID (이후 영상생성 파이프라인에 사용) + """, + response_model=MarketingAnalysisResponse, + tags=["Marketing"], +) +async def manual_marketing( + request_body: ManualMarketingRequest, + session: AsyncSession = Depends(get_session), +): + 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: + marketing_analysis, m_id = await _run_marketing_analysis( + customer_name=request_body.store_name, + region=region, + detail_region_info=request_body.address, + place_id=None, + session=session, + ) + 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 MarketingAnalysisResponse( + 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/models.py b/app/home/models.py index c372219..a7e55d5 100644 --- a/app/home/models.py +++ b/app/home/models.py @@ -289,10 +289,10 @@ class MarketingIntel(Base): comment="고유 식별자", ) - place_id: Mapped[str] = mapped_column( + place_id: Mapped[Optional[str]] = mapped_column( String(36), - nullable=False, - comment="매장 소스별 고유 식별자", + nullable=True, + comment="매장 소스별 고유 식별자 (네이버 크롤링 시 'nv{id}' 형식; 직접 입력 시 NULL)", ) intel_result : Mapped[dict[str, Any]] = mapped_column( diff --git a/app/home/schemas/home_schema.py b/app/home/schemas/home_schema.py index e227da2..62337fa 100644 --- a/app/home/schemas/home_schema.py +++ b/app/home/schemas/home_schema.py @@ -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): """에러 응답 스키마""" diff --git a/docs/database-schema/migration_2026_05_28_marketing_place_id_nullable.sql b/docs/database-schema/migration_2026_05_28_marketing_place_id_nullable.sql new file mode 100644 index 0000000..5e926fa --- /dev/null +++ b/docs/database-schema/migration_2026_05_28_marketing_place_id_nullable.sql @@ -0,0 +1,8 @@ +-- ============================================================ +-- Migration: marketing.place_id NULL 허용 +-- Date: 2026-05-28 +-- Description: 업체명+주소 직접 입력으로 마케팅 분석 생성 시 +-- 네이버 place_id가 없으므로 NULL 허용으로 변경 +-- ============================================================ + +ALTER TABLE marketing MODIFY place_id VARCHAR(36) NULL COMMENT '매장 소스별 고유 식별자 (네이버 크롤링 시 nv{id} 형식; 직접 입력 시 NULL)';