fix(report): ClinicSnapshot/YouTubeAudit/Instagram*/Facebook* Optional 완화

required로 두면 LLM 응답이나 수집 데이터 누락 시 pydantic ValidationError로
리포트 endpoint 전체가 500으로 죽음. 실제 테스트(청담오라클)에서 LLM이
weekly_view_growth, established 등 10개 필드를 null 반환하는 케이스 확인.

- ClinicSnapshot/YouTubeAudit: schemas + models 양쪽 모두 Optional (LLM 입력 검증
  + FastAPI 응답 검증 둘 다 통과 필요)
- InstagramAccount/InstagramAudit/FacebookPage/FacebookAudit: models만 (인스타·페북 빈
  계정/페이지 케이스 대응)
- list[T] 필드는 기본값 [] 부여

트레이드오프: 스키마 레벨 데이터 완결성 보장 약화. 운영하며 자주 비는 필드
패턴 보고 collection 단계 보강 필요.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
main
Mina Choi 2026-05-29 16:42:04 +09:00
parent 71b605eaa6
commit 5dbc7d7ffe
2 changed files with 94 additions and 84 deletions

View File

@ -68,16 +68,18 @@ class RegistryData(BaseModel):
class ClinicSnapshot(BaseModel): class ClinicSnapshot(BaseModel):
name: str # _build_clinic_snapshot은 source 데이터 있을 때만 채움 (`if x:` 가드).
name_en: str # required면 강남언니/홈페이지 누락 병원에서 ValidationError로 리포트 실패.
staff_count: int name: str | None = None
lead_doctor: LeadDoctor name_en: str | None = None
overall_rating: float staff_count: int | None = None
total_reviews: int lead_doctor: LeadDoctor | None = None
certifications: list[str] overall_rating: float | None = None
location: str total_reviews: int | None = None
phone: str certifications: list[str] = []
domain: str location: str | None = None
phone: str | None = None
domain: str | None = None
logo_images: LogoImages | None = None logo_images: LogoImages | None = None
brand_colors: BrandColors | None = None brand_colors: BrandColors | None = None
source: DataSource | None = None source: DataSource | None = None
@ -121,21 +123,23 @@ class TopVideo(BaseModel):
class YouTubeAudit(BaseModel): class YouTubeAudit(BaseModel):
channel_name: str # YouTube 미수집 병원에서 _build_youtube_audit가 채울 수 없는 필드 빔.
handle: str # required면 ValidationError로 리포트 실패 → Optional로 받아 부분 응답 허용.
subscribers: int channel_name: str | None = None
total_videos: int handle: str | None = None
total_views: int subscribers: int | None = None
weekly_view_growth: WeeklyViewGrowth total_videos: int | None = None
estimated_monthly_revenue: EstimatedRevenue total_views: int | None = None
avg_video_length: str weekly_view_growth: WeeklyViewGrowth | None = None
upload_frequency: str estimated_monthly_revenue: EstimatedRevenue | None = None
channel_created_date: str avg_video_length: str | None = None
channel_description: str upload_frequency: str | None = None
linked_urls: list[LinkedUrl] channel_created_date: str | None = None
playlists: list[str] channel_description: str | None = None
top_videos: list[TopVideo] linked_urls: list[LinkedUrl] = []
diagnosis: list[DiagnosisItem] playlists: list[str] = []
top_videos: list[TopVideo] = []
diagnosis: list[DiagnosisItem] = []
# --- Instagram --- # --- Instagram ---

View File

@ -66,16 +66,18 @@ class RegistryData(CamelModel):
class ClinicSnapshot(CamelModel): class ClinicSnapshot(CamelModel):
name: str # _build_clinic_snapshot은 source 데이터 있을 때만 필드 추가 (`if x:` 가드).
name_en: str # 강남언니/홈페이지 수집 누락된 병원에서 required면 ValidationError로 리포트 전체 실패.
staff_count: int name: str | None = None
lead_doctor: LeadDoctor name_en: str | None = None
overall_rating: float staff_count: int | None = None
total_reviews: int lead_doctor: LeadDoctor | None = None
certifications: list[str] overall_rating: float | None = None
location: str total_reviews: int | None = None
phone: str certifications: list[str] = []
domain: str location: str | None = None
phone: str | None = None
domain: str | None = None
logo_images: LogoImages | None = None logo_images: LogoImages | None = None
brand_colors: BrandColors | None = None brand_colors: BrandColors | None = None
source: DataSource | None = None source: DataSource | None = None
@ -115,42 +117,45 @@ class TopVideo(CamelModel):
class YouTubeAudit(CamelModel): class YouTubeAudit(CamelModel):
channel_name: str # YouTube 채널 없는 병원이면 _build_youtube_audit가 채울 수 없는 필드들 (channel_name 등)이 빔.
handle: str # required면 ValidationError로 리포트 실패 → Optional로 받아 부분 응답 허용.
subscribers: int channel_name: str | None = None
total_videos: int handle: str | None = None
total_views: int subscribers: int | None = None
weekly_view_growth: WeeklyViewGrowth total_videos: int | None = None
estimated_monthly_revenue: EstimatedRevenue total_views: int | None = None
avg_video_length: str weekly_view_growth: WeeklyViewGrowth | None = None
upload_frequency: str estimated_monthly_revenue: EstimatedRevenue | None = None
channel_created_date: str avg_video_length: str | None = None
channel_description: str upload_frequency: str | None = None
linked_urls: list[LinkedUrl] channel_created_date: str | None = None
playlists: list[str] channel_description: str | None = None
top_videos: list[TopVideo] linked_urls: list[LinkedUrl] = []
diagnosis: list[DiagnosisItem] playlists: list[str] = []
top_videos: list[TopVideo] = []
diagnosis: list[DiagnosisItem] = []
class InstagramAccount(CamelModel): class InstagramAccount(CamelModel):
handle: str # 인스타 계정(KR/EN) 미수집 시 빈 필드 가능 — Optional.
language: Language handle: str | None = None
label: str language: Language | None = None
posts: int label: str | None = None
followers: int posts: int | None = None
following: int followers: int | None = None
category: str following: int | None = None
profile_link: str category: str | None = None
highlights: list[str] profile_link: str | None = None
reels_count: int highlights: list[str] = []
content_format: str reels_count: int | None = None
profile_photo: str content_format: str | None = None
bio: str profile_photo: str | None = None
bio: str | None = None
class InstagramAudit(CamelModel): class InstagramAudit(CamelModel):
accounts: list[InstagramAccount] accounts: list[InstagramAccount] = []
diagnosis: list[DiagnosisItem] diagnosis: list[DiagnosisItem] = []
class BrandInconsistencyValue(CamelModel): class BrandInconsistencyValue(CamelModel):
@ -167,31 +172,32 @@ class BrandInconsistency(CamelModel):
class FacebookPage(CamelModel): class FacebookPage(CamelModel):
url: str # 페북 페이지(KR/EN) 미수집 시 빈 필드 가능 — Optional.
page_name: str url: str | None = None
language: Language page_name: str | None = None
label: str language: Language | None = None
followers: int label: str | None = None
following: int followers: int | None = None
category: str following: int | None = None
bio: str category: str | None = None
logo: str bio: str | None = None
logo_description: str logo: str | None = None
link: str logo_description: str | None = None
linked_domain: str link: str | None = None
reviews: int linked_domain: str | None = None
recent_post_age: str reviews: int | None = None
has_whatsapp: bool recent_post_age: str | None = None
has_whatsapp: bool | None = None
post_frequency: str | None = None post_frequency: str | None = None
top_content_type: str | None = None top_content_type: str | None = None
engagement: str | None = None engagement: str | None = None
class FacebookAudit(CamelModel): class FacebookAudit(CamelModel):
pages: list[FacebookPage] pages: list[FacebookPage] = []
diagnosis: list[DiagnosisItem] diagnosis: list[DiagnosisItem] = []
brand_inconsistencies: list[BrandInconsistency] brand_inconsistencies: list[BrandInconsistency] = []
consolidation_recommendation: str consolidation_recommendation: str | None = None
class OtherChannel(CamelModel): class OtherChannel(CamelModel):