Compare commits

...

3 Commits

Author SHA1 Message Date
jaehwang f1dd675ecb Merge branch 'main' into youtube-description 2026-02-12 05:10:32 +00:00
jaehwang ada5dfeeb4 가사 marketing Intel 삽입 2026-02-12 05:10:08 +00:00
jaehwang 54e66e4682 add marketing intelligence db 2026-02-11 05:21:00 +00:00
10 changed files with 274 additions and 15 deletions

View File

@ -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...")

View File

@ -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]}...")
@ -281,6 +287,15 @@ async def _crawling_logic(url:str):
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)
m_id = marketing_intelligence.id
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)"
@ -360,6 +375,7 @@ async def _crawling_logic(url:str):
"image_count": len(scraper.image_link_list) if scraper.image_link_list else 0,
"processed_info": processed_info,
"marketing_analysis": marketing_analysis,
"m_id" : m_id
}

View File

@ -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"<Image(id={self.id}, task_id='{task_id_str}', img_name='{img_name_str}')>"
)
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"<Image(id={self.id}, task_id='{task_id_str}', img_name='{img_name_str}')>"
)

View File

@ -329,7 +329,8 @@ class CrawlingResponse(BaseModel):
"힐링스테이",
"스테이머뭄"
]
}
},
"m_id" : 1
}
}
)
@ -346,6 +347,7 @@ class CrawlingResponse(BaseModel):
marketing_analysis: Optional[MarketingPromptOutput] = Field(
None, description="마케팅 분석 결과 . 실패 시 null"
)
m_id : int = Field(..., description="마케팅 분석 결과 ID")
class ErrorResponse(BaseModel):

View File

@ -30,7 +30,7 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_session
from app.home.models import Project
from app.home.models import Project, MarketingIntel
from app.user.dependencies.auth import get_current_user
from app.user.models import User
from app.lyric.models import Lyric
@ -48,6 +48,7 @@ from app.utils.pagination import PaginatedResponse, get_paginated
from app.utils.prompts.prompts import lyric_prompt
import traceback as tb
import json
# 로거 설정
logger = get_logger("lyric")
@ -278,11 +279,15 @@ async def generate_lyric(
Full verse flow, immersive mood
"""
}
marketing_intel_result = await session.execute(select(MarketingIntel).where(MarketingIntel.id == request_body.m_id))
marketing_intel = marketing_intel_result.scalar_one_or_none()
lyric_input_data = {
"customer_name" : request_body.customer_name,
"region" : request_body.region,
"detail_region_info" : request_body.detail_region_info or "",
"marketing_intelligence_summary" : None, # task_idx 변경 후 marketing intelligence summary DB에 저장하고 사용할 필요가 있음
"marketing_intelligence_summary" : json.dumps(marketing_intel.intel_result, ensure_ascii = False),
"language" : request_body.language,
"promotional_expression_example" : promotional_expressions[request_body.language],
"timing_rules" : timing_rules["60s"], # 아직은 선택지 하나
@ -314,6 +319,7 @@ async def generate_lyric(
detail_region_info=request_body.detail_region_info,
language=request_body.language,
user_uuid=current_user.user_uuid,
marketing_inteligence = request_body.m_id
)
session.add(project)
await session.commit()

View File

@ -41,7 +41,8 @@ class GenerateLyricRequest(BaseModel):
"customer_name": "스테이 머뭄",
"region": "군산",
"detail_region_info": "군산 신흥동 말랭이 마을",
"language": "Korean"
"language": "Korean",
"m_id" : 1
}
"""
@ -53,6 +54,7 @@ class GenerateLyricRequest(BaseModel):
"region": "군산",
"detail_region_info": "군산 신흥동 말랭이 마을",
"language": "Korean",
"m_id" : 1
}
}
)
@ -67,6 +69,7 @@ class GenerateLyricRequest(BaseModel):
default="Korean",
description="가사 출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)",
)
m_id : Optional[int] = Field(None, description="마케팅 인텔리전스 ID 값")
class GenerateLyricResponse(BaseModel):

View File

@ -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"]

View File

@ -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 최적화")

View File

@ -4,6 +4,7 @@ You are a content marketing expert, brand strategist, and creative songwriter
specializing in Korean pension / accommodation businesses.
You create lyrics strictly based on Brand & Marketing Intelligence analysis
and optimized for viral short-form video content.
Marketing Intelligence Report is background reference.
[INPUT]
Business Name: {customer_name}

View File

@ -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 brands 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 23 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 12 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: 4565 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 (34 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 35 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 (12)
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.