from __future__ import annotations from pydantic import BaseModel from models.status import Severity, ChannelStatus, DataSource, Language, VideoType, AnnotationType 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): # 코드 후처리(_build_overrides 또는 prompt 가드)가 채우는 자리는 Optional — # mock JSON에서 비워둬도 viewclinic 분기에서 raw_data가 덮어쓴다. name: str | None = None name_en: str | None = None established: str | None = None years_in_business: int | None = None staff_count: int | None = None lead_doctor: LeadDoctor | None = None overall_rating: float | None = None total_reviews: int | None = None price_range: PriceRange | None = None certifications: list[str] = [] media_appearances: list[str] = [] medical_tourism: list[str] = [] location: str | None = None nearest_station: str | None = None phone: str | None = None domain: str | None = None 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): # _build_overrides가 채우는 자리는 Optional channel_name: str | None = None handle: str | None = None subscribers: int | None = None total_videos: int | None = None total_views: int | None = None weekly_view_growth: WeeklyViewGrowth | None = None estimated_monthly_revenue: EstimatedRevenue | None = None avg_video_length: str | None = None upload_frequency: str | None = None channel_created_date: str | None = None subscriber_rank: str | None = None channel_description: str | None = None 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 템플릿 변수) --- class ReportInput(BaseModel): clinic_name: str | None = None clinic_name_en: str | None = None address: str | None = None phone: str | None = None slogan: str | None = None services: str | None = None doctors: str | None = None instagram: str | None = None facebook: str | None = None naver_blog: str | None = None youtube: str | None = None gangnam_unni: str | None = None market_competitors: str | None = None market_keywords: str | None = None market_trend: str | None = None market_target_audience: str | None = None branding: str | None = None brand_assets: str | None = None tiktok: str | None = None instagram_en: str | None = None facebook_en: str | None = None kakao_talk: str | None = None naver_cafe: str | None = None channel_logos: str | None = None # --- MarketingReport --- class MarketingReport(BaseModel): id: str created_at: str target_url: str overall_score: int 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