report output format 변경 및 clinic info출력 추가
parent
c1f39aceff
commit
9b4e99abf9
|
|
@ -4,18 +4,33 @@ from fastapi import APIRouter, Depends, HTTPException, Response
|
|||
from common.db import fetchone
|
||||
from common.deps import verify_api_key
|
||||
from integrations.llm.schemas.plan import PlanOutput
|
||||
from models.plan import PlanApiResponse
|
||||
|
||||
router = APIRouter(prefix="/api/plans", tags=["plans"], dependencies=[Depends(verify_api_key)])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("/{run_id}", response_model=PlanOutput | None)
|
||||
@router.get("/{run_id}", response_model=PlanApiResponse)
|
||||
async def get_plan(run_id: str):
|
||||
logger.info("GET /api/plans/%s", run_id)
|
||||
row = await fetchone("SELECT plan_data FROM analysis_runs WHERE analysis_run_id = %s", (run_id,))
|
||||
row = await fetchone(
|
||||
"SELECT ar.plan_data, ar.created_at, h.hospital_name, h.hospital_name_en, h.url"
|
||||
" FROM analysis_runs ar"
|
||||
" JOIN hospital_baseinfo h ON ar.hospital_id = h.hospital_id"
|
||||
" WHERE ar.analysis_run_id = %s",
|
||||
(run_id,),
|
||||
)
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Run not found")
|
||||
if row["plan_data"] is None:
|
||||
return Response(status_code=204)
|
||||
data = json.loads(row["plan_data"]) if isinstance(row["plan_data"], str) else row["plan_data"]
|
||||
return PlanOutput(**data)
|
||||
plan = PlanOutput(**data)
|
||||
return PlanApiResponse(
|
||||
id=run_id,
|
||||
clinicName=row["hospital_name"],
|
||||
clinicNameEn=row["hospital_name_en"],
|
||||
createdAt=str(row["created_at"]),
|
||||
targetUrl=row["url"],
|
||||
**plan.model_dump(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,16 +4,20 @@ from fastapi import APIRouter, Depends, HTTPException, Response
|
|||
from common.db import fetchone
|
||||
from common.deps import verify_api_key
|
||||
from integrations.llm.schemas.report import ReportOutput
|
||||
from models.report import MarketingReportResponse
|
||||
|
||||
router = APIRouter(prefix="/api/reports", tags=["reports"], dependencies=[Depends(verify_api_key)])
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("/{run_id}", response_model=ReportOutput | None)
|
||||
@router.get("/{run_id}", response_model=MarketingReportResponse, response_model_by_alias=True)
|
||||
async def get_report(run_id: str):
|
||||
logger.info("GET /api/reports/%s", run_id)
|
||||
row = await fetchone(
|
||||
"SELECT report_data FROM analysis_runs WHERE analysis_run_id = %s",
|
||||
"SELECT ar.report_data, ar.created_at, h.hospital_name, h.hospital_name_en, h.url"
|
||||
" FROM analysis_runs ar"
|
||||
" JOIN hospital_baseinfo h ON ar.hospital_id = h.hospital_id"
|
||||
" WHERE ar.analysis_run_id = %s",
|
||||
(run_id,),
|
||||
)
|
||||
if row is None:
|
||||
|
|
@ -21,4 +25,11 @@ async def get_report(run_id: str):
|
|||
if row["report_data"] is None:
|
||||
return Response(status_code=204)
|
||||
data = json.loads(row["report_data"]) if isinstance(row["report_data"], str) else row["report_data"]
|
||||
return ReportOutput(**data)
|
||||
llm_output = ReportOutput(**data)
|
||||
response = MarketingReportResponse.model_validate(llm_output.model_dump())
|
||||
response.id = run_id
|
||||
response.clinic_name = row["hospital_name"]
|
||||
response.clinic_name_en = row["hospital_name_en"]
|
||||
response.created_at = str(row["created_at"])
|
||||
response.target_url = row["url"]
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import httpx
|
|||
REQUEST_TIMEOUT = 60
|
||||
|
||||
|
||||
|
||||
def get_env(key: str) -> str:
|
||||
v = os.environ.get(key, "")
|
||||
if not v:
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class LLMService:
|
|||
response = await self.client.chat.completions.create(
|
||||
model=prompt.model,
|
||||
messages=[{"role": "user", "content": prompt_text}],
|
||||
max_tokens=16000,
|
||||
response_format={
|
||||
"type": "json_schema",
|
||||
"json_schema": {"name": prompt.output_class.__name__, "schema": prompt.output_class.model_json_schema()},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,316 @@
|
|||
from __future__ import annotations
|
||||
from typing import Literal
|
||||
from pydantic import BaseModel
|
||||
|
||||
Severity = Literal["critical", "warning", "good", "excellent", "unknown"]
|
||||
ChannelStatus = Literal["active", "inactive", "unknown", "not_found"]
|
||||
DataSource = Literal["registry", "scrape"]
|
||||
Language = Literal["KR", "EN"]
|
||||
VideoType = Literal["Short", "Long"]
|
||||
AnnotationType = Literal["highlight", "arrow", "text"]
|
||||
|
||||
|
||||
class ScreenshotAnnotation(BaseModel):
|
||||
type: AnnotationType
|
||||
x: float
|
||||
y: float
|
||||
width: float | None = None
|
||||
height: float | None = None
|
||||
label: str | None = None
|
||||
color: str | None = None
|
||||
|
||||
|
||||
class ScreenshotEvidence(BaseModel):
|
||||
id: str
|
||||
url: str
|
||||
channel: str
|
||||
captured_at: str
|
||||
caption: str
|
||||
source_url: str | None = None
|
||||
annotations: list[ScreenshotAnnotation] | None = None
|
||||
|
||||
|
||||
class DiagnosisItem(BaseModel):
|
||||
category: str
|
||||
detail: str
|
||||
severity: Severity
|
||||
evidence_ids: list[str] | None = None
|
||||
|
||||
|
||||
# --- ClinicSnapshot ---
|
||||
|
||||
class LeadDoctor(BaseModel):
|
||||
name: str
|
||||
credentials: str
|
||||
rating: float
|
||||
review_count: int
|
||||
|
||||
|
||||
class PriceRange(BaseModel):
|
||||
min: str
|
||||
max: str
|
||||
currency: str
|
||||
|
||||
|
||||
class LogoImages(BaseModel):
|
||||
circle: str | None = None
|
||||
horizontal: str | None = None
|
||||
korean: str | None = None
|
||||
|
||||
|
||||
class BrandColors(BaseModel):
|
||||
primary: str
|
||||
accent: str
|
||||
text: str
|
||||
|
||||
|
||||
class RegistryData(BaseModel):
|
||||
district: str | None = None
|
||||
branches: str | None = None
|
||||
brand_group: str | None = None
|
||||
website_en: str | None = None
|
||||
naver_place_url: str | None = None
|
||||
gangnam_unni_url: str | None = None
|
||||
google_maps_url: str | None = None
|
||||
|
||||
|
||||
class ClinicSnapshot(BaseModel):
|
||||
name: str
|
||||
name_en: str
|
||||
established: str
|
||||
years_in_business: int
|
||||
staff_count: int
|
||||
lead_doctor: LeadDoctor
|
||||
overall_rating: float
|
||||
total_reviews: int
|
||||
price_range: PriceRange
|
||||
certifications: list[str]
|
||||
media_appearances: list[str]
|
||||
medical_tourism: list[str]
|
||||
location: str
|
||||
nearest_station: str
|
||||
phone: str
|
||||
domain: str
|
||||
logo_images: LogoImages | None = None
|
||||
brand_colors: BrandColors | None = None
|
||||
source: DataSource | None = None
|
||||
registry_data: RegistryData | None = None
|
||||
|
||||
|
||||
# --- ChannelScore ---
|
||||
|
||||
class ChannelScore(BaseModel):
|
||||
channel: str
|
||||
icon: str
|
||||
score: int
|
||||
max_score: int
|
||||
status: Severity
|
||||
headline: str
|
||||
|
||||
|
||||
# --- YouTube ---
|
||||
|
||||
class WeeklyViewGrowth(BaseModel):
|
||||
absolute: int
|
||||
percentage: float
|
||||
|
||||
|
||||
class EstimatedRevenue(BaseModel):
|
||||
min: int
|
||||
max: int
|
||||
|
||||
|
||||
class LinkedUrl(BaseModel):
|
||||
label: str
|
||||
url: str
|
||||
|
||||
|
||||
class TopVideo(BaseModel):
|
||||
title: str
|
||||
views: int
|
||||
uploaded_ago: str
|
||||
type: VideoType
|
||||
duration: str | None = None
|
||||
|
||||
|
||||
class YouTubeAudit(BaseModel):
|
||||
channel_name: str
|
||||
handle: str
|
||||
subscribers: int
|
||||
total_videos: int
|
||||
total_views: int
|
||||
weekly_view_growth: WeeklyViewGrowth
|
||||
estimated_monthly_revenue: EstimatedRevenue
|
||||
avg_video_length: str
|
||||
upload_frequency: str
|
||||
channel_created_date: str
|
||||
subscriber_rank: str
|
||||
channel_description: str
|
||||
linked_urls: list[LinkedUrl]
|
||||
playlists: list[str]
|
||||
top_videos: list[TopVideo]
|
||||
diagnosis: list[DiagnosisItem]
|
||||
|
||||
|
||||
# --- Instagram ---
|
||||
|
||||
class InstagramAccount(BaseModel):
|
||||
handle: str
|
||||
language: Language
|
||||
label: str
|
||||
posts: int
|
||||
followers: int
|
||||
following: int
|
||||
category: str
|
||||
profile_link: str
|
||||
highlights: list[str]
|
||||
reels_count: int
|
||||
content_format: str
|
||||
profile_photo: str
|
||||
bio: str
|
||||
|
||||
|
||||
class InstagramAudit(BaseModel):
|
||||
accounts: list[InstagramAccount]
|
||||
diagnosis: list[DiagnosisItem]
|
||||
|
||||
|
||||
# --- Facebook ---
|
||||
|
||||
class BrandInconsistencyValue(BaseModel):
|
||||
channel: str
|
||||
value: str
|
||||
is_correct: bool
|
||||
|
||||
|
||||
class BrandInconsistency(BaseModel):
|
||||
field: str
|
||||
values: list[BrandInconsistencyValue]
|
||||
impact: str
|
||||
recommendation: str
|
||||
|
||||
|
||||
class FacebookPage(BaseModel):
|
||||
url: str
|
||||
page_name: str
|
||||
language: Language
|
||||
label: str
|
||||
followers: int
|
||||
following: int
|
||||
category: str
|
||||
bio: str
|
||||
logo: str
|
||||
logo_description: str
|
||||
link: str
|
||||
linked_domain: str
|
||||
reviews: int
|
||||
recent_post_age: str
|
||||
has_whatsapp: bool
|
||||
post_frequency: str | None = None
|
||||
top_content_type: str | None = None
|
||||
engagement: str | None = None
|
||||
|
||||
|
||||
class FacebookAudit(BaseModel):
|
||||
pages: list[FacebookPage]
|
||||
diagnosis: list[DiagnosisItem]
|
||||
brand_inconsistencies: list[BrandInconsistency]
|
||||
consolidation_recommendation: str
|
||||
|
||||
|
||||
# --- 기타 채널 / 웹사이트 ---
|
||||
|
||||
class OtherChannel(BaseModel):
|
||||
name: str
|
||||
status: ChannelStatus
|
||||
details: str
|
||||
url: str | None = None
|
||||
|
||||
|
||||
class TrackingPixel(BaseModel):
|
||||
name: str
|
||||
installed: bool
|
||||
details: str | None = None
|
||||
|
||||
|
||||
class SnsLink(BaseModel):
|
||||
platform: str
|
||||
url: str
|
||||
location: str
|
||||
|
||||
|
||||
class AdditionalDomain(BaseModel):
|
||||
domain: str
|
||||
purpose: str
|
||||
|
||||
|
||||
class WebsiteAudit(BaseModel):
|
||||
primary_domain: str
|
||||
additional_domains: list[AdditionalDomain]
|
||||
sns_links_on_site: bool
|
||||
sns_links_detail: list[SnsLink] | None = None
|
||||
tracking_pixels: list[TrackingPixel]
|
||||
main_cta: str
|
||||
|
||||
|
||||
# --- Transformation ---
|
||||
|
||||
class AsIsToBeItem(BaseModel):
|
||||
area: str
|
||||
as_is: str
|
||||
to_be: str
|
||||
|
||||
|
||||
class StrategyDetail(BaseModel):
|
||||
strategy: str
|
||||
detail: str
|
||||
|
||||
|
||||
class PlatformStrategy(BaseModel):
|
||||
platform: str
|
||||
icon: str
|
||||
current_metric: str
|
||||
target_metric: str
|
||||
strategies: list[StrategyDetail]
|
||||
|
||||
|
||||
class NewChannelProposal(BaseModel):
|
||||
channel: str
|
||||
priority: str
|
||||
rationale: str
|
||||
|
||||
|
||||
class TransformationProposal(BaseModel):
|
||||
brand_identity: list[AsIsToBeItem]
|
||||
content_strategy: list[AsIsToBeItem]
|
||||
platform_strategies: list[PlatformStrategy]
|
||||
website_improvements: list[AsIsToBeItem]
|
||||
new_channel_proposals: list[NewChannelProposal]
|
||||
|
||||
|
||||
# --- Roadmap / KPI ---
|
||||
|
||||
class RoadmapTask(BaseModel):
|
||||
task: str
|
||||
completed: bool
|
||||
|
||||
|
||||
class RoadmapMonth(BaseModel):
|
||||
month: int
|
||||
title: str
|
||||
subtitle: str
|
||||
tasks: list[RoadmapTask]
|
||||
|
||||
|
||||
class KPIMetric(BaseModel):
|
||||
metric: str
|
||||
current: str
|
||||
target_3_month: str
|
||||
target_12_month: str
|
||||
|
||||
|
||||
# --- ReportInput (prompt 템플릿 변수) ---
|
||||
|
||||
# template.format(**model_dump()) 에 삽입될 변수들
|
||||
# 각 채널 raw_data를 호출부에서 json.dumps()로 직렬화해서 넘겨야 함
|
||||
class ReportInput(BaseModel):
|
||||
clinic_name: str | None = None
|
||||
clinic_name_en: str | None = None
|
||||
|
|
@ -18,26 +326,25 @@ class ReportInput(BaseModel):
|
|||
gangnam_unni: str | None = None
|
||||
|
||||
|
||||
class ChannelScore(BaseModel):
|
||||
score: int
|
||||
summary: str
|
||||
strengths: list[str]
|
||||
weaknesses: list[str]
|
||||
# --- MarketingReport ---
|
||||
|
||||
|
||||
class ConversionStrategy(BaseModel):
|
||||
summary: str
|
||||
actions: list[str]
|
||||
|
||||
|
||||
# response_format으로 OpenAI structured output에 전달 — dict 필드 사용 불가
|
||||
class ReportOutput(BaseModel):
|
||||
class MarketingReport(BaseModel):
|
||||
id: str
|
||||
created_at: str
|
||||
target_url: str
|
||||
overall_score: int
|
||||
instagram: ChannelScore | None = None
|
||||
facebook: ChannelScore | None = None
|
||||
naver_blog: ChannelScore | None = None
|
||||
youtube: ChannelScore | None = None
|
||||
gangnam_unni: ChannelScore | None = None
|
||||
conversion_strategy: ConversionStrategy
|
||||
roadmap: list[str]
|
||||
kpis: list[str]
|
||||
clinic_snapshot: ClinicSnapshot
|
||||
channel_scores: list[ChannelScore]
|
||||
youtube_audit: YouTubeAudit
|
||||
instagram_audit: InstagramAudit
|
||||
facebook_audit: FacebookAudit
|
||||
other_channels: list[OtherChannel]
|
||||
website_audit: WebsiteAudit
|
||||
problem_diagnosis: list[DiagnosisItem]
|
||||
transformation: TransformationProposal
|
||||
roadmap: list[RoadmapMonth]
|
||||
kpi_dashboard: list[KPIMetric]
|
||||
screenshots: list[ScreenshotEvidence] | None = None
|
||||
|
||||
|
||||
ReportOutput = MarketingReport
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
from pydantic import BaseModel
|
||||
from integrations.llm.schemas.plan import (
|
||||
BrandGuide, ChannelStrategyCard, ContentStrategyData,
|
||||
CalendarData, AssetCollectionData, RepurposingProposalItem,
|
||||
)
|
||||
|
||||
|
||||
class PlanCreate(BaseModel):
|
||||
report_id: str
|
||||
regenerate: bool = False
|
||||
|
||||
|
||||
class PlanResponse(BaseModel):
|
||||
class PlanApiResponse(BaseModel):
|
||||
id: str
|
||||
analysis_run_id: str
|
||||
brand_guide: dict
|
||||
channel_strategies: list
|
||||
content_strategy: dict
|
||||
calendar: list
|
||||
created_at: str
|
||||
clinicName: str | None = None
|
||||
clinicNameEn: str | None = None
|
||||
createdAt: str
|
||||
targetUrl: str
|
||||
brandGuide: BrandGuide
|
||||
channelStrategies: list[ChannelStrategyCard]
|
||||
contentStrategy: ContentStrategyData
|
||||
calendar: CalendarData
|
||||
assetCollection: AssetCollectionData
|
||||
repurposingProposals: list[RepurposingProposalItem] | None = None
|
||||
|
|
|
|||
|
|
@ -1,22 +1,318 @@
|
|||
from pydantic import BaseModel
|
||||
from __future__ import annotations
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from integrations.llm.schemas.report import (
|
||||
Severity, ChannelStatus, DataSource, Language, VideoType, AnnotationType,
|
||||
)
|
||||
|
||||
|
||||
class ClinicInfo(BaseModel):
|
||||
def _to_camel(s: str) -> str:
|
||||
head, *tail = s.split("_")
|
||||
return head + "".join(w.capitalize() for w in tail)
|
||||
|
||||
|
||||
class CamelModel(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True, alias_generator=_to_camel)
|
||||
|
||||
|
||||
class ScreenshotAnnotation(CamelModel):
|
||||
type: AnnotationType
|
||||
x: float
|
||||
y: float
|
||||
width: float | None = None
|
||||
height: float | None = None
|
||||
label: str | None = None
|
||||
color: str | None = None
|
||||
|
||||
|
||||
class ScreenshotEvidence(CamelModel):
|
||||
id: str
|
||||
url: str
|
||||
channel: str
|
||||
captured_at: str
|
||||
caption: str
|
||||
source_url: str | None = None
|
||||
annotations: list[ScreenshotAnnotation] | None = None
|
||||
|
||||
|
||||
class DiagnosisItem(CamelModel):
|
||||
category: str
|
||||
detail: str
|
||||
severity: Severity
|
||||
evidence_ids: list[str] | None = None
|
||||
|
||||
|
||||
class LeadDoctor(CamelModel):
|
||||
name: str
|
||||
credentials: str
|
||||
rating: float
|
||||
review_count: int
|
||||
|
||||
|
||||
class PriceRange(CamelModel):
|
||||
min: str
|
||||
max: str
|
||||
currency: str
|
||||
|
||||
|
||||
class LogoImages(CamelModel):
|
||||
circle: str | None = None
|
||||
horizontal: str | None = None
|
||||
korean: str | None = None
|
||||
|
||||
|
||||
class BrandColors(CamelModel):
|
||||
primary: str
|
||||
accent: str
|
||||
text: str
|
||||
|
||||
|
||||
class RegistryData(CamelModel):
|
||||
district: str | None = None
|
||||
branches: str | None = None
|
||||
brand_group: str | None = None
|
||||
website_en: str | None = None
|
||||
naver_place_url: str | None = None
|
||||
gangnam_unni_url: str | None = None
|
||||
google_maps_url: str | None = None
|
||||
|
||||
|
||||
class ClinicSnapshot(CamelModel):
|
||||
name: str
|
||||
name_en: str
|
||||
established: str
|
||||
years_in_business: int
|
||||
staff_count: int
|
||||
lead_doctor: LeadDoctor
|
||||
overall_rating: float
|
||||
total_reviews: int
|
||||
price_range: PriceRange
|
||||
certifications: list[str]
|
||||
media_appearances: list[str]
|
||||
medical_tourism: list[str]
|
||||
location: str
|
||||
nearest_station: str
|
||||
phone: str
|
||||
domain: str
|
||||
logo_images: LogoImages | None = None
|
||||
brand_colors: BrandColors | None = None
|
||||
source: DataSource | None = None
|
||||
registry_data: RegistryData | None = None
|
||||
|
||||
|
||||
class ChannelScore(CamelModel):
|
||||
channel: str
|
||||
icon: str
|
||||
score: int
|
||||
max_score: int
|
||||
status: Severity
|
||||
headline: str
|
||||
|
||||
|
||||
class WeeklyViewGrowth(CamelModel):
|
||||
absolute: int
|
||||
percentage: float
|
||||
|
||||
|
||||
class EstimatedRevenue(CamelModel):
|
||||
min: int
|
||||
max: int
|
||||
|
||||
|
||||
class LinkedUrl(CamelModel):
|
||||
label: str
|
||||
url: str
|
||||
|
||||
|
||||
class ReportResponse(BaseModel):
|
||||
class TopVideo(CamelModel):
|
||||
title: str
|
||||
views: int
|
||||
uploaded_ago: str
|
||||
type: VideoType
|
||||
duration: str | None = None
|
||||
|
||||
|
||||
class YouTubeAudit(CamelModel):
|
||||
channel_name: str
|
||||
handle: str
|
||||
subscribers: int
|
||||
total_videos: int
|
||||
total_views: int
|
||||
weekly_view_growth: WeeklyViewGrowth
|
||||
estimated_monthly_revenue: EstimatedRevenue
|
||||
avg_video_length: str
|
||||
upload_frequency: str
|
||||
channel_created_date: str
|
||||
subscriber_rank: str
|
||||
channel_description: str
|
||||
linked_urls: list[LinkedUrl]
|
||||
playlists: list[str]
|
||||
top_videos: list[TopVideo]
|
||||
diagnosis: list[DiagnosisItem]
|
||||
|
||||
|
||||
class InstagramAccount(CamelModel):
|
||||
handle: str
|
||||
language: Language
|
||||
label: str
|
||||
posts: int
|
||||
followers: int
|
||||
following: int
|
||||
category: str
|
||||
profile_link: str
|
||||
highlights: list[str]
|
||||
reels_count: int
|
||||
content_format: str
|
||||
profile_photo: str
|
||||
bio: str
|
||||
|
||||
|
||||
class InstagramAudit(CamelModel):
|
||||
accounts: list[InstagramAccount]
|
||||
diagnosis: list[DiagnosisItem]
|
||||
|
||||
|
||||
class BrandInconsistencyValue(CamelModel):
|
||||
channel: str
|
||||
value: str
|
||||
is_correct: bool
|
||||
|
||||
|
||||
class BrandInconsistency(CamelModel):
|
||||
field: str
|
||||
values: list[BrandInconsistencyValue]
|
||||
impact: str
|
||||
recommendation: str
|
||||
|
||||
|
||||
class FacebookPage(CamelModel):
|
||||
url: str
|
||||
page_name: str
|
||||
language: Language
|
||||
label: str
|
||||
followers: int
|
||||
following: int
|
||||
category: str
|
||||
bio: str
|
||||
logo: str
|
||||
logo_description: str
|
||||
link: str
|
||||
linked_domain: str
|
||||
reviews: int
|
||||
recent_post_age: str
|
||||
has_whatsapp: bool
|
||||
post_frequency: str | None = None
|
||||
top_content_type: str | None = None
|
||||
engagement: str | None = None
|
||||
|
||||
|
||||
class FacebookAudit(CamelModel):
|
||||
pages: list[FacebookPage]
|
||||
diagnosis: list[DiagnosisItem]
|
||||
brand_inconsistencies: list[BrandInconsistency]
|
||||
consolidation_recommendation: str
|
||||
|
||||
|
||||
class OtherChannel(CamelModel):
|
||||
name: str
|
||||
status: ChannelStatus
|
||||
details: str
|
||||
url: str | None = None
|
||||
|
||||
|
||||
class TrackingPixel(CamelModel):
|
||||
name: str
|
||||
installed: bool
|
||||
details: str | None = None
|
||||
|
||||
|
||||
class SnsLink(CamelModel):
|
||||
platform: str
|
||||
url: str
|
||||
location: str
|
||||
|
||||
|
||||
class AdditionalDomain(CamelModel):
|
||||
domain: str
|
||||
purpose: str
|
||||
|
||||
|
||||
class WebsiteAudit(CamelModel):
|
||||
primary_domain: str
|
||||
additional_domains: list[AdditionalDomain]
|
||||
sns_links_on_site: bool
|
||||
sns_links_detail: list[SnsLink] | None = None
|
||||
tracking_pixels: list[TrackingPixel]
|
||||
main_cta: str
|
||||
|
||||
|
||||
class AsIsToBeItem(CamelModel):
|
||||
area: str
|
||||
as_is: str
|
||||
to_be: str
|
||||
|
||||
|
||||
class StrategyDetail(CamelModel):
|
||||
strategy: str
|
||||
detail: str
|
||||
|
||||
|
||||
class PlatformStrategy(CamelModel):
|
||||
platform: str
|
||||
icon: str
|
||||
current_metric: str
|
||||
target_metric: str
|
||||
strategies: list[StrategyDetail]
|
||||
|
||||
|
||||
class NewChannelProposal(CamelModel):
|
||||
channel: str
|
||||
priority: str
|
||||
rationale: str
|
||||
|
||||
|
||||
class TransformationProposal(CamelModel):
|
||||
brand_identity: list[AsIsToBeItem]
|
||||
content_strategy: list[AsIsToBeItem]
|
||||
platform_strategies: list[PlatformStrategy]
|
||||
website_improvements: list[AsIsToBeItem]
|
||||
new_channel_proposals: list[NewChannelProposal]
|
||||
|
||||
|
||||
class RoadmapTask(CamelModel):
|
||||
task: str
|
||||
completed: bool
|
||||
|
||||
|
||||
class RoadmapMonth(CamelModel):
|
||||
month: int
|
||||
title: str
|
||||
subtitle: str
|
||||
tasks: list[RoadmapTask]
|
||||
|
||||
|
||||
class KPIMetric(CamelModel):
|
||||
metric: str
|
||||
current: str
|
||||
target_3_month: str
|
||||
target_12_month: str
|
||||
|
||||
|
||||
class MarketingReportResponse(CamelModel):
|
||||
id: str
|
||||
clinic: ClinicInfo
|
||||
clinic_name: str | None = None
|
||||
clinic_name_en: str | None = None
|
||||
created_at: str
|
||||
target_url: str
|
||||
overall_score: int
|
||||
youtube: dict
|
||||
instagram: dict
|
||||
facebook: dict
|
||||
naver_place: dict
|
||||
naver_blog: dict
|
||||
gangnam_unni: dict
|
||||
conversion_strategy: dict
|
||||
roadmap: list
|
||||
kpis: list
|
||||
generated_at: str
|
||||
clinic_snapshot: ClinicSnapshot
|
||||
channel_scores: list[ChannelScore]
|
||||
youtube_audit: YouTubeAudit
|
||||
instagram_audit: InstagramAudit
|
||||
facebook_audit: FacebookAudit
|
||||
other_channels: list[OtherChannel]
|
||||
website_audit: WebsiteAudit
|
||||
problem_diagnosis: list[DiagnosisItem]
|
||||
transformation: TransformationProposal
|
||||
roadmap: list[RoadmapMonth]
|
||||
kpi_dashboard: list[KPIMetric]
|
||||
screenshots: list[ScreenshotEvidence] | None = None
|
||||
|
|
|
|||
Loading…
Reference in New Issue