직접입력 기능

feature-BizInput
김성경 2026-06-02 10:20:43 +09:00
parent ac3732549f
commit e56aa35170
2 changed files with 148 additions and 79 deletions

View File

@ -25,6 +25,7 @@ from app.home.schemas.home_schema import (
ImageUploadResponse, ImageUploadResponse,
ImageUploadResultItem, ImageUploadResultItem,
ImageUrlItem, ImageUrlItem,
ManualMarketingRequest,
ProcessedInfo, ProcessedInfo,
# MarketingAnalysis, # MarketingAnalysis,
) )
@ -133,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에서 시/군 이름 추출 """주소에서 시/군 이름 추출 — 도로명 우선, 실패 시 지번으로 재시도"""
매칭 우선순위: def _parse(address: str) -> str:
1. KOREAN_CITIES 직접 매칭 (/ 접미사 포함) token_set = set(address.split())
2. KOREAN_CITIES 접미사 생략 매칭 for city in KOREAN_CITIES:
3. 주소 번째 토큰이 /군으로 끝나는 경우 (: "전북 군산시 ...") if city in token_set: # 완전 일치 (토큰 단위)
4. 주소 번째 토큰이 /동인 경우 번째 토큰으로 광역시 매핑 (: "서울 강남구 ...") return city
""" if city[:-1] in token_set: # 접미사 생략 일치 (토큰 단위)
if not road_address: 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 "" return ""
for city in KOREAN_CITIES: if road_address:
if city in road_address: result = _parse(road_address)
return city if result:
if city[:-1] in road_address: return result
return city
tokens = road_address.split() if jibun_address:
if len(tokens) >= 2: return _parse(jibun_address)
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 "" return ""
@ -290,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
@ -310,70 +313,29 @@ async def _crawling_logic(
logger.info("[crawling] Step 3: ChatGPT 마케팅 분석 시작...") logger.info("[crawling] Step 3: ChatGPT 마케팅 분석 시작...")
try: try:
# Step 3-1: ChatGPT 서비스 초기화 # Step 3-1: ChatGPT 서비스 초기화 및 입력 데이터 구성
step3_1_start = time.perf_counter()
chatgpt_service = ChatgptService() 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 = { input_marketing_data = {
"customer_name": customer_name, "customer_name": customer_name,
"region": region, "region": region,
"detail_region_info": road_address or "", "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 호출 # Step 3-2: GPT API 호출 → 구조화된 마케팅 분석 결과 반환
step3_3_start = time.perf_counter() marketing_analysis = await chatgpt_service.generate_structured_output(
structured_report = await chatgpt_service.generate_structured_output(
marketing_prompt, input_marketing_data marketing_prompt, input_marketing_data
) )
marketing_intelligence = MarketingIntel(
place_id = scraper.place_id, # Step 3-3: 분석 결과 DB 저장
intel_result = structured_report.model_dump() marketing_intel = MarketingIntel(
place_id=scraper.place_id,
intel_result=marketing_analysis.model_dump(),
) )
session.add(marketing_intelligence) session.add(marketing_intel)
await session.commit() await session.commit()
await session.refresh(marketing_intelligence) await session.refresh(marketing_intel)
m_id = marketing_intelligence.id m_id = marketing_intel.id
logger.debug(f"[MarketingPrompt] INSERT placeid {marketing_intelligence.place_id}") logger.debug(f"[MarketingPrompt] INSERT place_id={marketing_intel.place_id} id={marketing_intel.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 step3_elapsed = (time.perf_counter() - step3_start) * 1000
logger.info( 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): async def _autocomplete_logic(autocomplete_item:dict):
step1_start = time.perf_counter() step1_start = time.perf_counter()
try: try:

View File

@ -234,7 +234,7 @@ class CrawlingResponse(BaseModel):
description="처리 상태 (completed: 성공, failed: ChatGPT 분석 실패)" description="처리 상태 (completed: 성공, failed: ChatGPT 분석 실패)"
) )
image_list: Optional[list[str]] = Field(None, description="이미지 URL 목록") 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( processed_info: Optional[ProcessedInfo] = Field(
None, description="가공된 장소 정보 (customer_name, region, detail_region_info)" None, description="가공된 장소 정보 (customer_name, region, detail_region_info)"
) )
@ -244,6 +244,36 @@ class CrawlingResponse(BaseModel):
m_id : int = Field(..., description="마케팅 분석 결과 ID") 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): class ErrorResponse(BaseModel):
"""에러 응답 스키마""" """에러 응답 스키마"""