From 54e66e468213f44a73d3feb79c16c8eba4aaf943 Mon Sep 17 00:00:00 2001 From: jaehwang Date: Wed, 11 Feb 2026 05:21:00 +0000 Subject: [PATCH] add marketing intelligence db --- app/database/session.py | 3 +- app/home/api/routers/v1/home.py | 28 +++- app/home/models.py | 73 ++++++++- app/utils/nvMapScraper.py | 4 +- app/utils/prompts/schemas/youtube_desc.py | 16 ++ .../prompts/templates/yt_upload_prompt.txt | 143 ++++++++++++++++++ 6 files changed, 256 insertions(+), 11 deletions(-) create mode 100644 app/utils/prompts/schemas/youtube_desc.py create mode 100644 app/utils/prompts/templates/yt_upload_prompt.txt diff --git a/app/database/session.py b/app/database/session.py index 38abe39..1412667 100644 --- a/app/database/session.py +++ b/app/database/session.py @@ -73,7 +73,7 @@ async def create_db_tables(): # 모델 import (테이블 메타데이터 등록용) from app.user.models import User, RefreshToken, SocialAccount # noqa: F401 - from app.home.models import Image, Project # noqa: F401 + from app.home.models import Image, Project, MarketingIntel # noqa: F401 from app.lyric.models import Lyric # noqa: F401 from app.song.models import Song, SongTimestamp # noqa: F401 from app.video.models import Video # noqa: F401 @@ -93,6 +93,7 @@ async def create_db_tables(): Video.__table__, SNSUploadTask.__table__, SocialUpload.__table__, + MarketingIntel.__table__, ] logger.info("Creating database tables...") diff --git a/app/home/api/routers/v1/home.py b/app/home/api/routers/v1/home.py index ceafe89..c648332 100644 --- a/app/home/api/routers/v1/home.py +++ b/app/home/api/routers/v1/home.py @@ -11,7 +11,7 @@ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from app.database.session import get_session, AsyncSessionLocal -from app.home.models import Image +from app.home.models import Image, MarketingIntel from app.user.dependencies.auth import get_current_user from app.user.models import User from app.home.schemas.home_schema import ( @@ -153,8 +153,10 @@ def _extract_region_from_address(road_address: str | None) -> str: }, tags=["Crawling"], ) -async def crawling(request_body: CrawlingRequest): - return await _crawling_logic(request_body.url) +async def crawling( + request_body: CrawlingRequest, + session: AsyncSession = Depends(get_session)): + return await _crawling_logic(request_body.url, session) @router.post( "/autocomplete", @@ -187,11 +189,15 @@ async def crawling(request_body: CrawlingRequest): }, tags=["Crawling"], ) -async def autocomplete_crawling(request_body: AutoCompleteRequest): - url = await _autocomplete_logic(request_body.dict()) - return await _crawling_logic(url) +async def autocomplete_crawling( + request_body: AutoCompleteRequest, + session: AsyncSession = Depends(get_session)): + url = await _autocomplete_logic(request_body.model_dump()) + return await _crawling_logic(url, session) -async def _crawling_logic(url:str): +async def _crawling_logic( + url:str, + session: AsyncSession): request_start = time.perf_counter() logger.info("[crawling] ========== START ==========") logger.info(f"[crawling] URL: {url[:80]}...") @@ -280,7 +286,15 @@ async def _crawling_logic(url:str): 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) + 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)" diff --git a/app/home/models.py b/app/home/models.py index af5619f..9a991a3 100644 --- a/app/home/models.py +++ b/app/home/models.py @@ -7,9 +7,9 @@ Home 모듈 SQLAlchemy 모델 정의 """ from datetime import datetime -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, List, Optional, Any -from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text, func +from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text, JSON, func from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database.session import Base @@ -107,6 +107,12 @@ class Project(Base): comment="상세 지역 정보", ) + marketing_inteligence: Mapped[Optional[str]] = mapped_column( + Integer, + nullable=True, + comment="마케팅 인텔리전스 결과 정보 저장", + ) + language: Mapped[str] = mapped_column( String(50), nullable=False, @@ -249,3 +255,66 @@ class Image(Base): return ( f"" ) + + +class MarketingIntel(Base): + """ + 마케팅 인텔리전스 결과물 테이블 + + 마케팅 분석 결과물 저장합니다. + + Attributes: + id: 고유 식별자 (자동 증가) + place_id : 데이터 소스별 식별자 + intel_result : 마케팅 분석 결과물 json + created_at: 생성 일시 (자동 설정) + """ + + __tablename__ = "marketing" + __table_args__ = ( + Index("idx_place_id", "place_id"), + { + "mysql_engine": "InnoDB", + "mysql_charset": "utf8mb4", + "mysql_collate": "utf8mb4_unicode_ci", + } + ) + + id: Mapped[int] = mapped_column( + Integer, + primary_key=True, + nullable=False, + autoincrement=True, + comment="고유 식별자", + ) + + place_id: Mapped[str] = mapped_column( + String(36), + nullable=False, + comment="매장 소스별 고유 식별자", + ) + + intel_result : Mapped[dict[str, Any]] = mapped_column( + JSON, + nullable=False, + comment="마케팅 인텔리전스 결과물", + ) + + created_at: Mapped[datetime] = mapped_column( + DateTime, + nullable=False, + server_default=func.now(), + comment="생성 일시", + ) + + def __repr__(self) -> str: + task_id_str = ( + (self.task_id[:10] + "...") if len(self.task_id) > 10 else self.task_id + ) + img_name_str = ( + (self.img_name[:10] + "...") if len(self.img_name) > 10 else self.img_name + ) + + return ( + f"" + ) \ No newline at end of file diff --git a/app/utils/nvMapScraper.py b/app/utils/nvMapScraper.py index d16f8f1..3ece3a6 100644 --- a/app/utils/nvMapScraper.py +++ b/app/utils/nvMapScraper.py @@ -30,7 +30,7 @@ class NvMapScraper: GRAPHQL_URL: str = "https://pcmap-api.place.naver.com/graphql" REQUEST_TIMEOUT = 120 # 초 - + data_source_identifier = "nv" OVERVIEW_QUERY: str = """ query getAccommodation($id: String!, $deviceType: String) { business: placeDetail(input: {id: $id, isNx: true, deviceType: $deviceType}) { @@ -99,6 +99,8 @@ query getAccommodation($id: String!, $deviceType: String) { data = await self._call_get_accommodation(place_id) self.rawdata = data fac_data = await self._get_facility_string(place_id) + # Naver 기준임, 구글 등 다른 데이터 소스의 경우 고유 Identifier 사용할 것. + self.place_id = self.data_source_identifier + place_id self.rawdata["facilities"] = fac_data self.image_link_list = [ nv_image["origin"] diff --git a/app/utils/prompts/schemas/youtube_desc.py b/app/utils/prompts/schemas/youtube_desc.py new file mode 100644 index 0000000..d81b97d --- /dev/null +++ b/app/utils/prompts/schemas/youtube_desc.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel, Field +from typing import List, Optional + +# Input 정의 +class YTUploadPromptInput(BaseModel): + customer_name : str = Field(..., description = "마케팅 대상 사업체 이름") + detail_region_info : str = Field(..., description = "마케팅 대상 지역 상세") + marketing_intelligence_summary : Optional[str] = Field(None, description = "마케팅 분석 정보 보고서") + language : str= Field(..., description = "영상 언어") + target_keywords: List[str] = Field(..., description="태그 키워드 리스트") + +# Output 정의 +class YTUploadPromptOutput(BaseModel): + title:str = Field(..., description="유튜브 영상 제목 - SEO/AEO 최적화") + description: str = Field(..., description = "유튜브 영상 설명 - SEO/AEO 최적화") + diff --git a/app/utils/prompts/templates/yt_upload_prompt.txt b/app/utils/prompts/templates/yt_upload_prompt.txt new file mode 100644 index 0000000..cc6c10a --- /dev/null +++ b/app/utils/prompts/templates/yt_upload_prompt.txt @@ -0,0 +1,143 @@ +[ROLE] +You are a YouTube SEO/AEO content strategist specialized in local stay, pension, and accommodation brands in Korea. +You create search-optimized, emotionally appealing, and action-driving titles and descriptions based on Brand & Marketing Intelligence. + +Your goal is to: + +Increase search visibility +Improve click-through rate +Reflect the brand’s positioning +Trigger emotional interest +Encourage booking or inquiry actions through subtle CTA + + +[INPUT] +Business Name: {customer_name} +Region Details: {detail_region_info} +Brand & Marketing Intelligence Report: {marketing_intelligence_summary} +Target Keywords: {target_keywords} +Output Language: {language} + + + +[INTERNAL ANALYSIS – DO NOT OUTPUT] +Analyze the following from the marketing intelligence: + +Core brand concept +Main emotional promise +Primary target persona +Top 2–3 USP signals +Stay context (date, healing, local trip, etc.) +Search intent behind the target keywords +Main booking trigger +Emotional moment that would make the viewer want to stay +Use these to guide: + +Title tone +Opening CTA line +Emotional hook in the first sentences + + +[TITLE GENERATION RULES] + +The title must: + +Include the business name or region when natural +Always wrap the business name in quotation marks +Example: “스테이 머뭄” +Include 1–2 high-intent keywords +Reflect emotional positioning +Suggest a desirable stay moment +Sound like a natural YouTube title, not an advertisement +Length rules: + +Hard limit: 100 characters +Target range: 45–65 characters +Place primary keyword in the first half +Avoid: + +ALL CAPS +Excessive symbols +Price or promotion language +Hard-sell expressions + + +[DESCRIPTION GENERATION RULES] + +Character rules: + +Maximum length: 1,000 characters +Critical information must appear within the first 150 characters +Language style rules (mandatory): + +Use polite Korean honorific style +Replace “있나요?” with “있으신가요?” +Do not start sentences with “이곳은” +Replace “선택이 됩니다” with “추천 드립니다” +Always wrap the business name in quotation marks +Example: “스테이 머뭄” +Avoid vague location words like “근대거리” alone +Use specific phrasing such as: +“군산 근대역사문화거리 일대” +Structure: + +Opening CTA (first line) +Must be a question or gentle suggestion +Must use honorific tone +Example: +“조용히 쉴 수 있는 군산숙소를 찾고 있으신가요?” +Core Stay Introduction (within first 150 characters total) +Mention business name with quotation marks +Mention region +Include main keyword +Briefly describe the stay experience +Brand Experience +Core value and emotional promise +Based on marketing intelligence positioning +Key Highlights (3–4 short lines) +Derived from USP signals +Natural sentences +Focus on booking-trigger moments +Local Context +Mention nearby experiences +Use specific local references +Example: +“군산 근대역사문화거리 일대 산책이나 로컬 카페 투어” +Soft Closing Line +One gentle, non-salesy closing sentence +Must end with a recommendation tone +Example: +“군산에서 조용한 시간을 보내고 싶다면 ‘스테이 머뭄’을 추천 드립니다.” + + +[SEO & AEO RULES] + +Naturally integrate 3–5 keywords from {target_keywords} +Avoid keyword stuffing +Use conversational, search-like phrasing +Optimize for: +YouTube search +Google video results +AI answer summaries +Keywords should appear in: + +Title (1–2) +First 150 characters of description +Highlight or context sections + + +[LANGUAGE RULE] + +All output must be written entirely in {language}. +No mixed languages. + + + +[OUTPUT FORMAT – STRICT] + +title: +description: + +No explanations. +No headings. +No extra text. \ No newline at end of file