Compare commits
No commits in common. "f1dd675ecb7ae21d3bfd30b8074f235c769e04c1" and "18635d7995eb6fe1363a5d55641ac4eb3088eddb" have entirely different histories.
f1dd675ecb
...
18635d7995
|
|
@ -73,7 +73,7 @@ async def create_db_tables():
|
||||||
|
|
||||||
# 모델 import (테이블 메타데이터 등록용)
|
# 모델 import (테이블 메타데이터 등록용)
|
||||||
from app.user.models import User, RefreshToken, SocialAccount # noqa: F401
|
from app.user.models import User, RefreshToken, SocialAccount # noqa: F401
|
||||||
from app.home.models import Image, Project, MarketingIntel # noqa: F401
|
from app.home.models import Image, Project # noqa: F401
|
||||||
from app.lyric.models import Lyric # noqa: F401
|
from app.lyric.models import Lyric # noqa: F401
|
||||||
from app.song.models import Song, SongTimestamp # noqa: F401
|
from app.song.models import Song, SongTimestamp # noqa: F401
|
||||||
from app.video.models import Video # noqa: F401
|
from app.video.models import Video # noqa: F401
|
||||||
|
|
@ -93,7 +93,6 @@ async def create_db_tables():
|
||||||
Video.__table__,
|
Video.__table__,
|
||||||
SNSUploadTask.__table__,
|
SNSUploadTask.__table__,
|
||||||
SocialUpload.__table__,
|
SocialUpload.__table__,
|
||||||
MarketingIntel.__table__,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
logger.info("Creating database tables...")
|
logger.info("Creating database tables...")
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from sqlalchemy.exc import SQLAlchemyError
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.database.session import get_session, AsyncSessionLocal
|
from app.database.session import get_session, AsyncSessionLocal
|
||||||
from app.home.models import Image, MarketingIntel
|
from app.home.models import Image
|
||||||
from app.user.dependencies.auth import get_current_user
|
from app.user.dependencies.auth import get_current_user
|
||||||
from app.user.models import User
|
from app.user.models import User
|
||||||
from app.home.schemas.home_schema import (
|
from app.home.schemas.home_schema import (
|
||||||
|
|
@ -153,10 +153,8 @@ def _extract_region_from_address(road_address: str | None) -> str:
|
||||||
},
|
},
|
||||||
tags=["Crawling"],
|
tags=["Crawling"],
|
||||||
)
|
)
|
||||||
async def crawling(
|
async def crawling(request_body: CrawlingRequest):
|
||||||
request_body: CrawlingRequest,
|
return await _crawling_logic(request_body.url)
|
||||||
session: AsyncSession = Depends(get_session)):
|
|
||||||
return await _crawling_logic(request_body.url, session)
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/autocomplete",
|
"/autocomplete",
|
||||||
|
|
@ -189,15 +187,11 @@ async def crawling(
|
||||||
},
|
},
|
||||||
tags=["Crawling"],
|
tags=["Crawling"],
|
||||||
)
|
)
|
||||||
async def autocomplete_crawling(
|
async def autocomplete_crawling(request_body: AutoCompleteRequest):
|
||||||
request_body: AutoCompleteRequest,
|
url = await _autocomplete_logic(request_body.dict())
|
||||||
session: AsyncSession = Depends(get_session)):
|
return await _crawling_logic(url)
|
||||||
url = await _autocomplete_logic(request_body.model_dump())
|
|
||||||
return await _crawling_logic(url, session)
|
|
||||||
|
|
||||||
async def _crawling_logic(
|
async def _crawling_logic(url:str):
|
||||||
url:str,
|
|
||||||
session: AsyncSession):
|
|
||||||
request_start = time.perf_counter()
|
request_start = time.perf_counter()
|
||||||
logger.info("[crawling] ========== START ==========")
|
logger.info("[crawling] ========== START ==========")
|
||||||
logger.info(f"[crawling] URL: {url[:80]}...")
|
logger.info(f"[crawling] URL: {url[:80]}...")
|
||||||
|
|
@ -286,16 +280,7 @@ async def _crawling_logic(
|
||||||
step3_3_start = time.perf_counter()
|
step3_3_start = time.perf_counter()
|
||||||
structured_report = 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,
|
|
||||||
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
|
step3_3_elapsed = (time.perf_counter() - step3_3_start) * 1000
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[crawling] Step 3-3: GPT API 호출 완료 - ({step3_3_elapsed:.1f}ms)"
|
f"[crawling] Step 3-3: GPT API 호출 완료 - ({step3_3_elapsed:.1f}ms)"
|
||||||
|
|
@ -375,7 +360,6 @@ async def _crawling_logic(
|
||||||
"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,
|
"marketing_analysis": marketing_analysis,
|
||||||
"m_id" : m_id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ Home 모듈 SQLAlchemy 모델 정의
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, List, Optional, Any
|
from typing import TYPE_CHECKING, List, Optional
|
||||||
|
|
||||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text, JSON, func
|
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text, func
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from app.database.session import Base
|
from app.database.session import Base
|
||||||
|
|
@ -107,12 +107,6 @@ class Project(Base):
|
||||||
comment="상세 지역 정보",
|
comment="상세 지역 정보",
|
||||||
)
|
)
|
||||||
|
|
||||||
marketing_inteligence: Mapped[Optional[str]] = mapped_column(
|
|
||||||
Integer,
|
|
||||||
nullable=True,
|
|
||||||
comment="마케팅 인텔리전스 결과 정보 저장",
|
|
||||||
)
|
|
||||||
|
|
||||||
language: Mapped[str] = mapped_column(
|
language: Mapped[str] = mapped_column(
|
||||||
String(50),
|
String(50),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
|
|
@ -255,66 +249,3 @@ class Image(Base):
|
||||||
return (
|
return (
|
||||||
f"<Image(id={self.id}, task_id='{task_id_str}', img_name='{img_name_str}')>"
|
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}')>"
|
|
||||||
)
|
|
||||||
|
|
@ -329,8 +329,7 @@ class CrawlingResponse(BaseModel):
|
||||||
"힐링스테이",
|
"힐링스테이",
|
||||||
"스테이머뭄"
|
"스테이머뭄"
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"m_id" : 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -347,7 +346,6 @@ class CrawlingResponse(BaseModel):
|
||||||
marketing_analysis: Optional[MarketingPromptOutput] = Field(
|
marketing_analysis: Optional[MarketingPromptOutput] = Field(
|
||||||
None, description="마케팅 분석 결과 . 실패 시 null"
|
None, description="마케팅 분석 결과 . 실패 시 null"
|
||||||
)
|
)
|
||||||
m_id : int = Field(..., description="마케팅 분석 결과 ID")
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorResponse(BaseModel):
|
class ErrorResponse(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.database.session import get_session
|
from app.database.session import get_session
|
||||||
from app.home.models import Project, MarketingIntel
|
from app.home.models import Project
|
||||||
from app.user.dependencies.auth import get_current_user
|
from app.user.dependencies.auth import get_current_user
|
||||||
from app.user.models import User
|
from app.user.models import User
|
||||||
from app.lyric.models import Lyric
|
from app.lyric.models import Lyric
|
||||||
|
|
@ -48,7 +48,6 @@ from app.utils.pagination import PaginatedResponse, get_paginated
|
||||||
|
|
||||||
from app.utils.prompts.prompts import lyric_prompt
|
from app.utils.prompts.prompts import lyric_prompt
|
||||||
import traceback as tb
|
import traceback as tb
|
||||||
import json
|
|
||||||
# 로거 설정
|
# 로거 설정
|
||||||
logger = get_logger("lyric")
|
logger = get_logger("lyric")
|
||||||
|
|
||||||
|
|
@ -279,15 +278,11 @@ async def generate_lyric(
|
||||||
Full verse flow, immersive mood
|
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 = {
|
lyric_input_data = {
|
||||||
"customer_name" : request_body.customer_name,
|
"customer_name" : request_body.customer_name,
|
||||||
"region" : request_body.region,
|
"region" : request_body.region,
|
||||||
"detail_region_info" : request_body.detail_region_info or "",
|
"detail_region_info" : request_body.detail_region_info or "",
|
||||||
"marketing_intelligence_summary" : json.dumps(marketing_intel.intel_result, ensure_ascii = False),
|
"marketing_intelligence_summary" : None, # task_idx 변경 후 marketing intelligence summary DB에 저장하고 사용할 필요가 있음
|
||||||
"language" : request_body.language,
|
"language" : request_body.language,
|
||||||
"promotional_expression_example" : promotional_expressions[request_body.language],
|
"promotional_expression_example" : promotional_expressions[request_body.language],
|
||||||
"timing_rules" : timing_rules["60s"], # 아직은 선택지 하나
|
"timing_rules" : timing_rules["60s"], # 아직은 선택지 하나
|
||||||
|
|
@ -319,7 +314,6 @@ async def generate_lyric(
|
||||||
detail_region_info=request_body.detail_region_info,
|
detail_region_info=request_body.detail_region_info,
|
||||||
language=request_body.language,
|
language=request_body.language,
|
||||||
user_uuid=current_user.user_uuid,
|
user_uuid=current_user.user_uuid,
|
||||||
marketing_inteligence = request_body.m_id
|
|
||||||
)
|
)
|
||||||
session.add(project)
|
session.add(project)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,7 @@ class GenerateLyricRequest(BaseModel):
|
||||||
"customer_name": "스테이 머뭄",
|
"customer_name": "스테이 머뭄",
|
||||||
"region": "군산",
|
"region": "군산",
|
||||||
"detail_region_info": "군산 신흥동 말랭이 마을",
|
"detail_region_info": "군산 신흥동 말랭이 마을",
|
||||||
"language": "Korean",
|
"language": "Korean"
|
||||||
"m_id" : 1
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -54,7 +53,6 @@ class GenerateLyricRequest(BaseModel):
|
||||||
"region": "군산",
|
"region": "군산",
|
||||||
"detail_region_info": "군산 신흥동 말랭이 마을",
|
"detail_region_info": "군산 신흥동 말랭이 마을",
|
||||||
"language": "Korean",
|
"language": "Korean",
|
||||||
"m_id" : 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -69,7 +67,6 @@ class GenerateLyricRequest(BaseModel):
|
||||||
default="Korean",
|
default="Korean",
|
||||||
description="가사 출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)",
|
description="가사 출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)",
|
||||||
)
|
)
|
||||||
m_id : Optional[int] = Field(None, description="마케팅 인텔리전스 ID 값")
|
|
||||||
|
|
||||||
|
|
||||||
class GenerateLyricResponse(BaseModel):
|
class GenerateLyricResponse(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class NvMapScraper:
|
||||||
|
|
||||||
GRAPHQL_URL: str = "https://pcmap-api.place.naver.com/graphql"
|
GRAPHQL_URL: str = "https://pcmap-api.place.naver.com/graphql"
|
||||||
REQUEST_TIMEOUT = 120 # 초
|
REQUEST_TIMEOUT = 120 # 초
|
||||||
data_source_identifier = "nv"
|
|
||||||
OVERVIEW_QUERY: str = """
|
OVERVIEW_QUERY: str = """
|
||||||
query getAccommodation($id: String!, $deviceType: String) {
|
query getAccommodation($id: String!, $deviceType: String) {
|
||||||
business: placeDetail(input: {id: $id, isNx: true, deviceType: $deviceType}) {
|
business: placeDetail(input: {id: $id, isNx: true, deviceType: $deviceType}) {
|
||||||
|
|
@ -99,8 +99,6 @@ query getAccommodation($id: String!, $deviceType: String) {
|
||||||
data = await self._call_get_accommodation(place_id)
|
data = await self._call_get_accommodation(place_id)
|
||||||
self.rawdata = data
|
self.rawdata = data
|
||||||
fac_data = await self._get_facility_string(place_id)
|
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.rawdata["facilities"] = fac_data
|
||||||
self.image_link_list = [
|
self.image_link_list = [
|
||||||
nv_image["origin"]
|
nv_image["origin"]
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
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 최적화")
|
|
||||||
|
|
||||||
|
|
@ -4,7 +4,6 @@ You are a content marketing expert, brand strategist, and creative songwriter
|
||||||
specializing in Korean pension / accommodation businesses.
|
specializing in Korean pension / accommodation businesses.
|
||||||
You create lyrics strictly based on Brand & Marketing Intelligence analysis
|
You create lyrics strictly based on Brand & Marketing Intelligence analysis
|
||||||
and optimized for viral short-form video content.
|
and optimized for viral short-form video content.
|
||||||
Marketing Intelligence Report is background reference.
|
|
||||||
|
|
||||||
[INPUT]
|
[INPUT]
|
||||||
Business Name: {customer_name}
|
Business Name: {customer_name}
|
||||||
|
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
[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.
|
|
||||||
Loading…
Reference in New Issue