크롤링에 마케팅 분석 추가, 태그 추가, 부대 시설 추가 완료

insta
bluebamus 2025-12-22 16:01:39 +09:00
parent 79ec5daa0d
commit b5b8e88005
11 changed files with 237 additions and 11 deletions

View File

@ -1,4 +1,5 @@
import json import json
import re
from datetime import date from datetime import date
from pathlib import Path from pathlib import Path
@ -19,8 +20,10 @@ from app.home.schemas.home import (
GenerateResponse, GenerateResponse,
GenerateUploadResponse, GenerateUploadResponse,
GenerateUrlsRequest, GenerateUrlsRequest,
MarketingAnalysis,
ProcessedInfo, ProcessedInfo,
) )
from app.utils.chatgpt_prompt import ChatgptService
from app.home.worker.main_task import task_process from app.home.worker.main_task import task_process
from app.utils.nvMapScraper import NvMapScraper from app.utils.nvMapScraper import NvMapScraper
@ -68,6 +71,32 @@ def _extract_region_from_address(road_address: str | None) -> str:
return "" return ""
def _parse_marketing_analysis(raw_response: str) -> MarketingAnalysis:
"""ChatGPT 마케팅 분석 응답을 파싱하여 MarketingAnalysis 객체로 변환"""
tags: list[str] = []
facilities: list[str] = []
report = raw_response
# JSON 블록 추출 시도
json_match = re.search(r"```json\s*(\{.*?\})\s*```", raw_response, re.DOTALL)
if json_match:
try:
json_data = json.loads(json_match.group(1))
tags = json_data.get("tags", [])
facilities = json_data.get("facilities", [])
# JSON 블록을 제외한 리포트 부분 추출
report = raw_response[: json_match.start()].strip()
# --- 구분자 제거
if report.startswith("---"):
report = report[3:].strip()
if report.endswith("---"):
report = report[:-3].strip()
except json.JSONDecodeError:
pass
return MarketingAnalysis(report=report, tags=tags, facilities=facilities)
@router.post( @router.post(
"/crawling", "/crawling",
summary="네이버 지도 크롤링", summary="네이버 지도 크롤링",
@ -100,18 +129,34 @@ async def crawling(request_body: CrawlingRequest):
# 가공된 정보 생성 # 가공된 정보 생성
processed_info = None processed_info = None
marketing_analysis = None
if scraper.base_info: if scraper.base_info:
road_address = scraper.base_info.get("roadAddress", "") road_address = scraper.base_info.get("roadAddress", "")
customer_name = scraper.base_info.get("name", "")
region = _extract_region_from_address(road_address)
processed_info = ProcessedInfo( processed_info = ProcessedInfo(
customer_name=scraper.base_info.get("name", ""), customer_name=customer_name,
region=_extract_region_from_address(road_address), region=region,
detail_region_info=road_address or "", detail_region_info=road_address or "",
) )
# ChatGPT를 이용한 마케팅 분석
chatgpt_service = ChatgptService(
customer_name=customer_name,
region=region,
detail_region_info=road_address or "",
)
prompt = chatgpt_service.build_market_analysis_prompt()
raw_response = await chatgpt_service.generate(prompt)
marketing_analysis = _parse_marketing_analysis(raw_response)
return { return {
"image_list": scraper.image_link_list, "image_list": scraper.image_link_list,
"image_count": len(scraper.image_link_list) if scraper.image_link_list else 0, "image_count": len(scraper.image_link_list) if scraper.image_link_list else 0,
"processed_info": processed_info, "processed_info": processed_info,
"marketing_analysis": marketing_analysis,
} }

View File

@ -125,6 +125,14 @@ class ProcessedInfo(BaseModel):
detail_region_info: str = Field(..., description="상세 지역 정보 (roadAddress)") detail_region_info: str = Field(..., description="상세 지역 정보 (roadAddress)")
class MarketingAnalysis(BaseModel):
"""마케팅 분석 결과 스키마"""
report: str = Field(..., description="마케팅 분석 리포트")
tags: list[str] = Field(default_factory=list, description="추천 태그 목록")
facilities: list[str] = Field(default_factory=list, description="추천 부대시설 목록")
class CrawlingResponse(BaseModel): class CrawlingResponse(BaseModel):
"""크롤링 응답 스키마""" """크롤링 응답 스키마"""
@ -133,6 +141,9 @@ class CrawlingResponse(BaseModel):
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)"
) )
marketing_analysis: Optional[MarketingAnalysis] = Field(
None, description="마케팅 분석 결과 (report, tags, facilities)"
)
class ErrorResponse(BaseModel): class ErrorResponse(BaseModel):

View File

View File

@ -57,7 +57,7 @@ async def lyric_task(
lyric_id = await _save_lyric(task_id, project_id, lyric_prompt) lyric_id = await _save_lyric(task_id, project_id, lyric_prompt)
# GPT 호출 # GPT 호출
result = await service.generate_lyrics(prompt=lyric_prompt) result = await service.generate(prompt=lyric_prompt)
print(f"GPT Response:\n{result}") print(f"GPT Response:\n{result}")

View File

@ -332,7 +332,7 @@ async def make_song_result(request: Request, conn: Connection):
print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n") print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n")
# 7. 모델에게 요청 # 7. 모델에게 요청
generated_lyrics = await chatgpt_api.generate_lyrics(prompt=updated_prompt) generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt)
# 글자 수 계산 # 글자 수 계산
total_chars_with_space = len(generated_lyrics) total_chars_with_space = len(generated_lyrics)
@ -733,7 +733,7 @@ async def make_automation(request: Request, conn: Connection):
print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n") print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n")
# 7. 모델에게 요청 # 7. 모델에게 요청
generated_lyrics = await chatgpt_api.generate_lyrics(prompt=updated_prompt) generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt)
# 글자 수 계산 # 글자 수 계산
total_chars_with_space = len(generated_lyrics) total_chars_with_space = len(generated_lyrics)

View File

@ -332,7 +332,7 @@ async def make_song_result(request: Request, conn: Connection):
print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n") print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n")
# 7. 모델에게 요청 # 7. 모델에게 요청
generated_lyrics = await chatgpt_api.generate_lyrics(prompt=updated_prompt) generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt)
# 글자 수 계산 # 글자 수 계산
total_chars_with_space = len(generated_lyrics) total_chars_with_space = len(generated_lyrics)
@ -733,7 +733,7 @@ async def make_automation(request: Request, conn: Connection):
print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n") print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n")
# 7. 모델에게 요청 # 7. 모델에게 요청
generated_lyrics = await chatgpt_api.generate_lyrics(prompt=updated_prompt) generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt)
# 글자 수 계산 # 글자 수 계산
total_chars_with_space = len(generated_lyrics) total_chars_with_space = len(generated_lyrics)

View File

@ -81,6 +81,99 @@ ERROR: [Brief reason for failure in English]
""".strip() """.strip()
# fmt: on # fmt: on
MARKETING_ANALYSIS_PROMPT_TEMPLATE = """
[ROLE]
Content marketing expert specializing in pension/accommodation services in Korea
[INPUT]
- Business Name: {customer_name}
- Region: {region}
- Region Details: {detail_region_info}
[ANALYSIS REQUIREMENTS]
Provide comprehensive marketing analysis including:
1. Target Customer Segments
- Primary and secondary target personas
- Age groups, travel preferences, booking patterns
2. Unique Selling Propositions (USPs)
- Key differentiators based on location and region details
- Competitive advantages
3. Regional Characteristics
- Nearby attractions and famous places (within 10 min access)
- Local food, activities, and experiences
- Transportation accessibility
4. Seasonal Appeal Points
- Best seasons to visit
- Seasonal activities and events
- Peak/off-peak marketing opportunities
5. Marketing Keywords
- Recommended hashtags and search keywords
- Trending terms relevant to the property
[ADDITIONAL REQUIREMENTS]
1. Recommended Tags
- Generate 5 recommended hashtags/tags based on the business characteristics
- Tags should be trendy, searchable, and relevant to accommodation marketing
- Return as JSON with key "tags"
- **MUST be written in Korean (한국어)**
2. Facilities
- Based on the business name and region details, identify 5 likely facilities/amenities
- Consider typical facilities for accommodations in the given region
- Examples: 바베큐장, 수영장, 주차장, 와이파이, 주방, 테라스, 정원, etc.
- Return as JSON with key "facilities"
- **MUST be written in Korean (한국어)**
[CRITICAL LANGUAGE REQUIREMENT - ABSOLUTE RULE]
ALL OUTPUT MUST BE WRITTEN IN KOREAN (한국어)
- Analysis sections: Korean only
- Tags: Korean only
- Facilities: Korean only
- This is a NON-NEGOTIABLE requirement
- Any output in English or other languages is considered a FAILURE
- Violation of this rule invalidates the entire response
[OUTPUT RULES - STRICTLY ENFORCED]
- Output analysis ONLY
- ALL content MUST be written in Korean (한국어) - NO EXCEPTIONS
- NO greetings or closing remarks
- NO additional commentary before or after analysis
- Follow the exact format below
[OUTPUT FORMAT - SUCCESS]
---
## 타겟 고객 분석
[한국어로 작성된 타겟 고객 분석]
## 핵심 차별점 (USP)
[한국어로 작성된 USP 분석]
## 지역 특성
[한국어로 작성된 지역 특성 분석]
## 시즌별 매력 포인트
[한국어로 작성된 시즌별 분석]
## 마케팅 키워드
[한국어로 작성된 마케팅 키워드]
## JSON Data
```json
{{
"tags": ["태그1", "태그2", "태그3", "태그4", "태그5"],
"facilities": ["부대시설1", "부대시설2", "부대시설3", "부대시설4", "부대시설5"]
}}
```
---
[OUTPUT FORMAT - FAILURE]
If you cannot generate analysis due to insufficient information, invalid input, or any other reason:
---
ERROR: [Brief reason for failure in English]
---
""".strip()
# fmt: on
class ChatgptService: class ChatgptService:
def __init__( def __init__(
@ -105,7 +198,15 @@ class ChatgptService:
detail_region_info=self.detail_region_info, detail_region_info=self.detail_region_info,
) )
async def generate_lyrics(self, prompt: str | None = None) -> str: def build_market_analysis_prompt(self) -> str:
"""MARKETING_ANALYSIS_PROMPT_TEMPLATE에 고객 정보를 대입하여 완성된 프롬프트 반환"""
return MARKETING_ANALYSIS_PROMPT_TEMPLATE.format(
customer_name=self.customer_name,
region=self.region,
detail_region_info=self.detail_region_info,
)
async def generate(self, prompt: str | None = None) -> str:
"""GPT에게 프롬프트를 전달하여 결과를 반환""" """GPT에게 프롬프트를 전달하여 결과를 반환"""
if prompt is None: if prompt is None:
prompt = self.build_lyrics_prompt() prompt = self.build_lyrics_prompt()

View File

@ -104,6 +104,8 @@ query getAccommodation($id: String!, $deviceType: String) {
# if __name__ == "__main__": # if __name__ == "__main__":
# import asyncio
# url = "https://map.naver.com/p/search/%EC%8A%A4%ED%85%8C%EC%9D%B4%EB%A8%B8%EB%AD%84/place/1133638931?c=14.70,0,0,0,dh&placePath=/photo?businessCategory=pension&fromPanelNum=2&locale=ko&searchText=%EC%8A%A4%ED%85%8C%EC%9D%B4%EB%A8%B8%EB%AD%84&svcName=map_pcv5&timestamp=202512191123&fromPanelNum=2&locale=ko&searchText=%EC%8A%A4%ED%85%8C%EC%9D%B4%EB%A8%B8%EB%AD%84&svcName=map_pcv5&timestamp=202512191007&from=map&entry=bmp&filterType=%EC%97%85%EC%B2%B4&businessCategory=pension" # url = "https://map.naver.com/p/search/%EC%8A%A4%ED%85%8C%EC%9D%B4%EB%A8%B8%EB%AD%84/place/1133638931?c=14.70,0,0,0,dh&placePath=/photo?businessCategory=pension&fromPanelNum=2&locale=ko&searchText=%EC%8A%A4%ED%85%8C%EC%9D%B4%EB%A8%B8%EB%AD%84&svcName=map_pcv5&timestamp=202512191123&fromPanelNum=2&locale=ko&searchText=%EC%8A%A4%ED%85%8C%EC%9D%B4%EB%A8%B8%EB%AD%84&svcName=map_pcv5&timestamp=202512191007&from=map&entry=bmp&filterType=%EC%97%85%EC%B2%B4&businessCategory=pension"
# scraper = NvMapScraper(url) # scraper = NvMapScraper(url)
# asyncio.run(scraper.scrap()) # asyncio.run(scraper.scrap())

View File

@ -0,0 +1,64 @@
import requests
from pathlib import Path
SAS_TOKEN = "sp=racwdl&st=2025-12-01T00:13:29Z&se=2026-07-31T08:28:29Z&spr=https&sv=2024-11-04&sr=c&sig=7fE2ozVBPu3Gq43%2FZDxEYdEcPLDXyNVfTf16IBasmVQ%3D"
def upload_music_to_azure_blob(file_path = "스테이 머뭄_1.mp3", url = "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/test_my_mp3_should_now_exist.mp3"):
access_url = f"{url}?{SAS_TOKEN}"
headers = {
"Content-Type": "audio/mpeg",
"x-ms-blob-type": "BlockBlob"
}
with open(file_path, "rb") as file:
response = requests.put(access_url, data=file, headers=headers)
if response.status_code in [200, 201]:
print(f"Success Status Code: {response.status_code}")
else:
print(f"Failed Status Code: {response.status_code}")
print(f"Response: {response.text}")
def upload_video_to_azure_blob(file_path = "스테이 머뭄.mp4", url = "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/test_my_mp3_should_now_exist.mp4"):
access_url = f"{url}?{SAS_TOKEN}"
headers = {
"Content-Type": "video/mp4",
"x-ms-blob-type": "BlockBlob"
}
with open(file_path, "rb") as file:
response = requests.put(access_url, data=file, headers=headers)
if response.status_code in [200, 201]:
print(f"Success Status Code: {response.status_code}")
else:
print(f"Failed Status Code: {response.status_code}")
print(f"Response: {response.text}")
def upload_image_to_azure_blob(file_path = "스테이 머뭄.png", url = "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/dev-user-idx/dev-task-idx/test_my_mp3_should_now_exist.png"):
access_url = f"{url}?{SAS_TOKEN}"
extension = Path(file_path).suffix.lower()
content_types = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
".bmp": "image/bmp"
}
content_type = content_types.get(extension, "image/jpeg")
headers = {
"Content-Type": content_type,
"x-ms-blob-type": "BlockBlob"
}
with open(file_path, "rb") as file:
response = requests.put(access_url, data=file, headers=headers)
if response.status_code in [200, 201]:
print(f"Success Status Code: {response.status_code}")
else:
print(f"Failed Status Code: {response.status_code}")
print(f"Response: {response.text}")
upload_video_to_azure_blob()
upload_image_to_azure_blob()

View File

@ -332,7 +332,7 @@ async def make_song_result(request: Request, conn: Connection):
print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n") print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n")
# 7. 모델에게 요청 # 7. 모델에게 요청
generated_lyrics = await chatgpt_api.generate_lyrics(prompt=updated_prompt) generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt)
# 글자 수 계산 # 글자 수 계산
total_chars_with_space = len(generated_lyrics) total_chars_with_space = len(generated_lyrics)
@ -733,7 +733,7 @@ async def make_automation(request: Request, conn: Connection):
print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n") print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n")
# 7. 모델에게 요청 # 7. 모델에게 요청
generated_lyrics = await chatgpt_api.generate_lyrics(prompt=updated_prompt) generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt)
# 글자 수 계산 # 글자 수 계산
total_chars_with_space = len(generated_lyrics) total_chars_with_space = len(generated_lyrics)

View File

@ -37,6 +37,7 @@ app/home/tests/home/__init__.py
app/home/tests/home/conftest.py app/home/tests/home/conftest.py
app/home/tests/home/test_db.py app/home/tests/home/test_db.py
app/home/worker/__init__.py app/home/worker/__init__.py
app/home/worker/main_task.py
app/lyric/__init__.py app/lyric/__init__.py
app/lyric/dependencies.py app/lyric/dependencies.py
app/lyric/models.py app/lyric/models.py
@ -44,8 +45,9 @@ app/lyric/api/__init__.py
app/lyric/api/lyrics_admin.py app/lyric/api/lyrics_admin.py
app/lyric/api/routers/__init__.py app/lyric/api/routers/__init__.py
app/lyric/api/routers/v1/__init__.py app/lyric/api/routers/v1/__init__.py
app/lyric/api/routers/v1/router.py app/lyric/api/routers/v1/lyric.py
app/lyric/schemas/__init__.py app/lyric/schemas/__init__.py
app/lyric/schemas/lyric.py
app/lyric/schemas/lyrics_schema.py app/lyric/schemas/lyrics_schema.py
app/lyric/services/__init__.py app/lyric/services/__init__.py
app/lyric/services/base.py app/lyric/services/base.py
@ -77,6 +79,7 @@ app/utils/__init__.py
app/utils/chatgpt_prompt.py app/utils/chatgpt_prompt.py
app/utils/cors.py app/utils/cors.py
app/utils/nvMapScraper.py app/utils/nvMapScraper.py
app/utils/upload_blob_as_request.py
app/video/__init__.py app/video/__init__.py
app/video/dependencies.py app/video/dependencies.py
app/video/models.py app/video/models.py