diff --git a/app/home/api/routers/v1/home.py b/app/home/api/routers/v1/home.py index f1e731d..b325f85 100644 --- a/app/home/api/routers/v1/home.py +++ b/app/home/api/routers/v1/home.py @@ -1,4 +1,5 @@ import json +import re from datetime import date from pathlib import Path @@ -19,8 +20,10 @@ from app.home.schemas.home import ( GenerateResponse, GenerateUploadResponse, GenerateUrlsRequest, + MarketingAnalysis, ProcessedInfo, ) +from app.utils.chatgpt_prompt import ChatgptService from app.home.worker.main_task import task_process from app.utils.nvMapScraper import NvMapScraper @@ -68,6 +71,32 @@ def _extract_region_from_address(road_address: str | None) -> str: 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( "/crawling", summary="네이버 지도 크롤링", @@ -100,18 +129,34 @@ async def crawling(request_body: CrawlingRequest): # 가공된 정보 생성 processed_info = None + marketing_analysis = None + if scraper.base_info: road_address = scraper.base_info.get("roadAddress", "") + customer_name = scraper.base_info.get("name", "") + region = _extract_region_from_address(road_address) + processed_info = ProcessedInfo( - customer_name=scraper.base_info.get("name", ""), - region=_extract_region_from_address(road_address), + customer_name=customer_name, + region=region, 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 { "image_list": scraper.image_link_list, "image_count": len(scraper.image_link_list) if scraper.image_link_list else 0, "processed_info": processed_info, + "marketing_analysis": marketing_analysis, } diff --git a/app/home/schemas/home.py b/app/home/schemas/home.py index 2e5af52..9a4a997 100644 --- a/app/home/schemas/home.py +++ b/app/home/schemas/home.py @@ -125,6 +125,14 @@ class ProcessedInfo(BaseModel): 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): """크롤링 응답 스키마""" @@ -133,6 +141,9 @@ class CrawlingResponse(BaseModel): processed_info: Optional[ProcessedInfo] = Field( None, description="가공된 장소 정보 (customer_name, region, detail_region_info)" ) + marketing_analysis: Optional[MarketingAnalysis] = Field( + None, description="마케팅 분석 결과 (report, tags, facilities)" + ) class ErrorResponse(BaseModel): diff --git a/app/home/services/market_analysis.py b/app/home/services/market_analysis.py new file mode 100644 index 0000000..e69de29 diff --git a/app/home/worker/main_task.py b/app/home/worker/main_task.py index 817d102..7300b5f 100644 --- a/app/home/worker/main_task.py +++ b/app/home/worker/main_task.py @@ -57,7 +57,7 @@ async def lyric_task( lyric_id = await _save_lyric(task_id, project_id, lyric_prompt) # GPT 호출 - result = await service.generate_lyrics(prompt=lyric_prompt) + result = await service.generate(prompt=lyric_prompt) print(f"GPT Response:\n{result}") diff --git a/app/lyric/services/lyrics.py b/app/lyric/services/lyrics.py index 2778ed4..0007e07 100644 --- a/app/lyric/services/lyrics.py +++ b/app/lyric/services/lyrics.py @@ -332,7 +332,7 @@ async def make_song_result(request: Request, conn: Connection): print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n") # 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) @@ -733,7 +733,7 @@ async def make_automation(request: Request, conn: Connection): print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n") # 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) diff --git a/app/song/services/song.py b/app/song/services/song.py index 550b4fc..8dbdc3a 100644 --- a/app/song/services/song.py +++ b/app/song/services/song.py @@ -332,7 +332,7 @@ async def make_song_result(request: Request, conn: Connection): print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n") # 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) @@ -733,7 +733,7 @@ async def make_automation(request: Request, conn: Connection): print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n") # 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) diff --git a/app/utils/chatgpt_prompt.py b/app/utils/chatgpt_prompt.py index c2586df..326684e 100644 --- a/app/utils/chatgpt_prompt.py +++ b/app/utils/chatgpt_prompt.py @@ -81,6 +81,99 @@ ERROR: [Brief reason for failure in English] """.strip() # 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: def __init__( @@ -105,7 +198,15 @@ class ChatgptService: 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에게 프롬프트를 전달하여 결과를 반환""" if prompt is None: prompt = self.build_lyrics_prompt() diff --git a/app/utils/nvMapScraper.py b/app/utils/nvMapScraper.py index db164d8..196ecbe 100644 --- a/app/utils/nvMapScraper.py +++ b/app/utils/nvMapScraper.py @@ -104,6 +104,8 @@ query getAccommodation($id: String!, $deviceType: String) { # 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×tamp=202512191123&fromPanelNum=2&locale=ko&searchText=%EC%8A%A4%ED%85%8C%EC%9D%B4%EB%A8%B8%EB%AD%84&svcName=map_pcv5×tamp=202512191007&from=map&entry=bmp&filterType=%EC%97%85%EC%B2%B4&businessCategory=pension" # scraper = NvMapScraper(url) # asyncio.run(scraper.scrap()) diff --git a/app/utils/upload_blob_as_request.py b/app/utils/upload_blob_as_request.py new file mode 100644 index 0000000..789c05e --- /dev/null +++ b/app/utils/upload_blob_as_request.py @@ -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() \ No newline at end of file diff --git a/app/video/services/video.py b/app/video/services/video.py index 550b4fc..8dbdc3a 100644 --- a/app/video/services/video.py +++ b/app/video/services/video.py @@ -332,7 +332,7 @@ async def make_song_result(request: Request, conn: Connection): print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n") # 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) @@ -733,7 +733,7 @@ async def make_automation(request: Request, conn: Connection): print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n") # 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) diff --git a/o2o_castad_backend.egg-info/SOURCES.txt b/o2o_castad_backend.egg-info/SOURCES.txt index aff0bfe..67d869a 100644 --- a/o2o_castad_backend.egg-info/SOURCES.txt +++ b/o2o_castad_backend.egg-info/SOURCES.txt @@ -37,6 +37,7 @@ app/home/tests/home/__init__.py app/home/tests/home/conftest.py app/home/tests/home/test_db.py app/home/worker/__init__.py +app/home/worker/main_task.py app/lyric/__init__.py app/lyric/dependencies.py app/lyric/models.py @@ -44,8 +45,9 @@ app/lyric/api/__init__.py app/lyric/api/lyrics_admin.py app/lyric/api/routers/__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/lyric.py app/lyric/schemas/lyrics_schema.py app/lyric/services/__init__.py app/lyric/services/base.py @@ -77,6 +79,7 @@ app/utils/__init__.py app/utils/chatgpt_prompt.py app/utils/cors.py app/utils/nvMapScraper.py +app/utils/upload_blob_as_request.py app/video/__init__.py app/video/dependencies.py app/video/models.py