|
|
|
@ -26,7 +26,6 @@ from app.home.schemas.home_schema import (
|
|
|
|
ImageUploadResultItem,
|
|
|
|
ImageUploadResultItem,
|
|
|
|
ImageUrlItem,
|
|
|
|
ImageUrlItem,
|
|
|
|
ManualMarketingRequest,
|
|
|
|
ManualMarketingRequest,
|
|
|
|
MarketingAnalysisResponse,
|
|
|
|
|
|
|
|
ProcessedInfo,
|
|
|
|
ProcessedInfo,
|
|
|
|
# MarketingAnalysis,
|
|
|
|
# MarketingAnalysis,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
@ -135,31 +134,32 @@ METRO_CITY_MAP = {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_region_from_address(road_address: str | None) -> str:
|
|
|
|
def _extract_region_from_address(road_address: str | None, jibun_address: str | None = None) -> str:
|
|
|
|
"""roadAddress에서 시/군 이름 추출
|
|
|
|
"""주소에서 시/군 이름 추출 — 도로명 우선, 실패 시 지번으로 재시도"""
|
|
|
|
|
|
|
|
|
|
|
|
매칭 우선순위:
|
|
|
|
|
|
|
|
1. KOREAN_CITIES 직접 매칭 (시/군 접미사 포함)
|
|
|
|
|
|
|
|
2. KOREAN_CITIES 접미사 생략 매칭
|
|
|
|
|
|
|
|
3. 주소 두 번째 토큰이 시/군으로 끝나는 경우 (예: "전북 군산시 ...")
|
|
|
|
|
|
|
|
4. 주소 두 번째 토큰이 구/동인 경우 → 첫 번째 토큰으로 광역시 매핑 (예: "서울 강남구 ...")
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
if not road_address:
|
|
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse(address: str) -> str:
|
|
|
|
|
|
|
|
token_set = set(address.split())
|
|
|
|
for city in KOREAN_CITIES:
|
|
|
|
for city in KOREAN_CITIES:
|
|
|
|
if city in road_address:
|
|
|
|
if city in token_set: # 완전 일치 (토큰 단위)
|
|
|
|
return city
|
|
|
|
return city
|
|
|
|
if city[:-1] in road_address:
|
|
|
|
if city[:-1] in token_set: # 접미사 생략 일치 (토큰 단위)
|
|
|
|
return city
|
|
|
|
return city
|
|
|
|
|
|
|
|
tokens = address.split()
|
|
|
|
tokens = road_address.split()
|
|
|
|
|
|
|
|
if len(tokens) >= 2:
|
|
|
|
if len(tokens) >= 2:
|
|
|
|
second = tokens[1]
|
|
|
|
second = tokens[1]
|
|
|
|
if second.endswith("시") or second.endswith("군"):
|
|
|
|
if second.endswith("시") or second.endswith("군"):
|
|
|
|
return second
|
|
|
|
return second
|
|
|
|
if second.endswith("구") or second.endswith("동"):
|
|
|
|
if second.endswith("구") or second.endswith("동"):
|
|
|
|
return METRO_CITY_MAP.get(tokens[0], "")
|
|
|
|
return METRO_CITY_MAP.get(tokens[0], "")
|
|
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if road_address:
|
|
|
|
|
|
|
|
result = _parse(road_address)
|
|
|
|
|
|
|
|
if result:
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if jibun_address:
|
|
|
|
|
|
|
|
return _parse(jibun_address)
|
|
|
|
|
|
|
|
|
|
|
|
return ""
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
@ -292,14 +292,15 @@ async def _crawling_logic(
|
|
|
|
marketing_analysis = None
|
|
|
|
marketing_analysis = None
|
|
|
|
|
|
|
|
|
|
|
|
if scraper.base_info:
|
|
|
|
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", "")
|
|
|
|
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(
|
|
|
|
processed_info = ProcessedInfo(
|
|
|
|
customer_name=customer_name,
|
|
|
|
customer_name=customer_name,
|
|
|
|
region=region,
|
|
|
|
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
|
|
|
|
step2_elapsed = (time.perf_counter() - step2_start) * 1000
|
|
|
|
@ -312,13 +313,30 @@ async def _crawling_logic(
|
|
|
|
logger.info("[crawling] Step 3: ChatGPT 마케팅 분석 시작...")
|
|
|
|
logger.info("[crawling] Step 3: ChatGPT 마케팅 분석 시작...")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
marketing_analysis, m_id = await _run_marketing_analysis(
|
|
|
|
# Step 3-1: ChatGPT 서비스 초기화 및 입력 데이터 구성
|
|
|
|
customer_name=customer_name,
|
|
|
|
chatgpt_service = ChatgptService()
|
|
|
|
region=region,
|
|
|
|
input_marketing_data = {
|
|
|
|
detail_region_info=road_address or "",
|
|
|
|
"customer_name": customer_name,
|
|
|
|
place_id=scraper.place_id,
|
|
|
|
"region": region,
|
|
|
|
session=session,
|
|
|
|
"detail_region_info": road_address or "",
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Step 3-2: GPT API 호출 → 구조화된 마케팅 분석 결과 반환
|
|
|
|
|
|
|
|
marketing_analysis = await chatgpt_service.generate_structured_output(
|
|
|
|
|
|
|
|
marketing_prompt, input_marketing_data
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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_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
|
|
|
|
step3_elapsed = (time.perf_counter() - step3_start) * 1000
|
|
|
|
logger.info(
|
|
|
|
logger.info(
|
|
|
|
f"[crawling] Step 3 완료 - 마케팅 분석 성공 ({step3_elapsed:.1f}ms)"
|
|
|
|
f"[crawling] Step 3 완료 - 마케팅 분석 성공 ({step3_elapsed:.1f}ms)"
|
|
|
|
@ -367,33 +385,6 @@ 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(
|
|
|
|
@router.post(
|
|
|
|
"/marketing",
|
|
|
|
"/marketing",
|
|
|
|
@ -410,13 +401,14 @@ async def _run_marketing_analysis(
|
|
|
|
- **marketing_analysis**: ChatGPT 마케팅 분석 결과
|
|
|
|
- **marketing_analysis**: ChatGPT 마케팅 분석 결과
|
|
|
|
- **m_id**: 마케팅 분석 결과 ID (이후 영상생성 파이프라인에 사용)
|
|
|
|
- **m_id**: 마케팅 분석 결과 ID (이후 영상생성 파이프라인에 사용)
|
|
|
|
""",
|
|
|
|
""",
|
|
|
|
response_model=MarketingAnalysisResponse,
|
|
|
|
response_model=CrawlingResponse,
|
|
|
|
tags=["Marketing"],
|
|
|
|
tags=["Marketing"],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
async def manual_marketing(
|
|
|
|
async def manual_marketing(
|
|
|
|
request_body: ManualMarketingRequest,
|
|
|
|
request_body: ManualMarketingRequest,
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
session: AsyncSession = Depends(get_session),
|
|
|
|
):
|
|
|
|
):
|
|
|
|
|
|
|
|
# Step 1: 주소에서 지역명 추출 및 processed_info 구성
|
|
|
|
region = _extract_region_from_address(request_body.address)
|
|
|
|
region = _extract_region_from_address(request_body.address)
|
|
|
|
processed_info = ProcessedInfo(
|
|
|
|
processed_info = ProcessedInfo(
|
|
|
|
customer_name=request_body.store_name,
|
|
|
|
customer_name=request_body.store_name,
|
|
|
|
@ -424,13 +416,28 @@ async def manual_marketing(
|
|
|
|
detail_region_info=request_body.address,
|
|
|
|
detail_region_info=request_body.address,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
marketing_analysis, m_id = await _run_marketing_analysis(
|
|
|
|
# Step 2: GPT API 호출 → 마케팅 분석 결과 생성
|
|
|
|
customer_name=request_body.store_name,
|
|
|
|
# place_id 없이 업체명+주소만으로 분석 (크롤링 없이 직접 입력된 경우)
|
|
|
|
region=region,
|
|
|
|
chatgpt_service = ChatgptService()
|
|
|
|
detail_region_info=request_body.address,
|
|
|
|
input_marketing_data = {
|
|
|
|
place_id=None,
|
|
|
|
"customer_name": request_body.store_name,
|
|
|
|
session=session,
|
|
|
|
"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:
|
|
|
|
except ChatGPTResponseError as e:
|
|
|
|
logger.error(
|
|
|
|
logger.error(
|
|
|
|
f"[marketing] ChatGPT Error: status={e.status}, "
|
|
|
|
f"[marketing] ChatGPT Error: status={e.status}, "
|
|
|
|
@ -447,7 +454,7 @@ async def manual_marketing(
|
|
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
|
detail="마케팅 분석 중 오류가 발생했습니다.",
|
|
|
|
detail="마케팅 분석 중 오류가 발생했습니다.",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return MarketingAnalysisResponse(
|
|
|
|
return CrawlingResponse(
|
|
|
|
status="completed",
|
|
|
|
status="completed",
|
|
|
|
processed_info=processed_info,
|
|
|
|
processed_info=processed_info,
|
|
|
|
marketing_analysis=marketing_analysis,
|
|
|
|
marketing_analysis=marketing_analysis,
|
|
|
|
|