o2o-castad-backend/app/dashboard/schemas/dashboard_schema.py

284 lines
8.3 KiB
Python

"""
Dashboard API Schemas
대시보드 API의 요청/응답 Pydantic 스키마를 정의합니다.
YouTube Analytics API 데이터를 프론트엔드에 전달하기 위한 모델입니다.
사용 예시:
from app.dashboard.schemas import DashboardResponse, ContentMetric
# 라우터에서 response_model로 사용
@router.get("/dashboard/stats", response_model=DashboardResponse)
async def get_dashboard_stats():
...
"""
from datetime import datetime
from typing import Any, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field
def to_camel(string: str) -> str:
"""snake_case를 camelCase로 변환
Args:
string: snake_case 문자열 (예: "content_metrics")
Returns:
camelCase 문자열 (예: "contentMetrics")
Example:
>>> to_camel("content_metrics")
"contentMetrics"
>>> to_camel("this_year")
"thisYear"
"""
components = string.split("_")
return components[0] + "".join(x.capitalize() for x in components[1:])
class ContentMetric(BaseModel):
"""KPI 지표 카드
대시보드 상단에 표시되는 핵심 성과 지표(KPI) 카드입니다.
Attributes:
id: 지표 고유 ID (예: "total-views", "total-watch-time", "new-subscribers")
label: 한글 라벨 (예: "조회수")
value: 원시 숫자값 (단위: unit 참조, 포맷팅은 프론트에서 처리)
unit: 값의 단위 — "count" | "hours" | "minutes"
- count: 조회수, 구독자, 좋아요, 댓글, 공유, 업로드 영상
- hours: 시청시간 (estimatedMinutesWatched / 60)
- minutes: 평균 시청시간 (averageViewDuration / 60)
trend: 이전 기간 대비 증감량 (unit과 동일한 단위)
trend_direction: 증감 방향 ("up": 증가, "down": 감소, "-": 변동 없음)
Example:
>>> metric = ContentMetric(
... id="total-views",
... label="조회수",
... value=1200000.0,
... unit="count",
... trend=3800.0,
... trend_direction="up"
... )
"""
id: str
label: str
value: float
unit: str = "count"
trend: float
trend_direction: Literal["up", "down", "-"] = Field(alias="trendDirection")
model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True,
)
class MonthlyData(BaseModel):
"""월별 추이 데이터
전년 대비 월별 조회수 비교 데이터입니다.
Attributes:
month: 월 표시 (예: "1월", "2월")
this_year: 올해 해당 월 조회수
last_year: 작년 해당 월 조회수
Example:
>>> data = MonthlyData(
... month="1월",
... this_year=150000,
... last_year=120000
... )
"""
month: str
this_year: int = Field(alias="thisYear")
last_year: int = Field(alias="lastYear")
model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True,
)
class DailyData(BaseModel):
"""일별 추이 데이터 (mode=day 전용)
최근 30일과 이전 30일의 일별 조회수 비교 데이터입니다.
Attributes:
date: 날짜 표시 (예: "1/18", "1/19")
this_period: 최근 30일 조회수
last_period: 이전 30일 동일 요일 조회수
"""
date: str
this_period: int = Field(alias="thisPeriod")
last_period: int = Field(alias="lastPeriod")
model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True,
)
class TopContent(BaseModel):
"""인기 영상
조회수 기준 상위 인기 영상 정보입니다.
Attributes:
id: YouTube 영상 ID
title: 영상 제목
thumbnail: 썸네일 이미지 URL
platform: 플랫폼 ("youtube" 또는 "instagram")
views: 원시 조회수 정수 (포맷팅은 프론트에서 처리, 예: 125400)
engagement: 참여율 (예: "8.2%")
date: 업로드 날짜 (예: "2026.01.15")
Example:
>>> content = TopContent(
... id="video-id-1",
... title="힐링 영상",
... thumbnail="https://i.ytimg.com/...",
... platform="youtube",
... views=125400,
... engagement="8.2%",
... date="2026.01.15"
... )
"""
id: str
title: str
thumbnail: str
platform: Literal["youtube", "instagram"]
views: int
engagement: str
date: str
model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True,
)
class AudienceData(BaseModel):
"""시청자 분석 데이터
시청자의 연령대, 성별, 지역 분포 데이터입니다.
Attributes:
age_groups: 연령대별 시청자 비율 리스트
[{"label": "18-24", "percentage": 35}, ...]
gender: 성별 시청자 비율 (YouTube viewerPercentage 누적값)
{"male": 45, "female": 55}
top_regions: 상위 국가 리스트 (최대 5개)
[{"region": "대한민국", "percentage": 42}, ...]
Example:
>>> data = AudienceData(
... age_groups=[{"label": "18-24", "percentage": 35}],
... gender={"male": 45, "female": 55},
... top_regions=[{"region": "대한민국", "percentage": 42}]
... )
"""
age_groups: list[dict[str, Any]] = Field(alias="ageGroups")
gender: dict[str, int]
top_regions: list[dict[str, Any]] = Field(alias="topRegions")
model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True,
)
class DashboardResponse(BaseModel):
"""대시보드 전체 응답
GET /dashboard/stats 엔드포인트의 전체 응답 스키마입니다.
Attributes:
content_metrics: KPI 지표 카드 리스트 (8개)
monthly_data: 월별 추이 데이터 (mode=month 시 채움, 최근 12개월 vs 이전 12개월)
daily_data: 일별 추이 데이터 (mode=day 시 채움, 최근 30일 vs 이전 30일)
top_content: 조회수 기준 인기 영상 TOP 4
audience_data: 시청자 분석 데이터 (연령/성별/지역)
has_uploads: 업로드 영상 존재 여부 (False 시 모든 지표가 0, 빈 상태 UI 표시용)
Example:
>>> response = DashboardResponse(
... content_metrics=[...],
... monthly_data=[...],
... top_content=[...],
... audience_data=AudienceData(...),
... )
>>> json_str = response.model_dump_json() # JSON 직렬화
"""
content_metrics: list[ContentMetric] = Field(alias="contentMetrics")
monthly_data: list[MonthlyData] = Field(default=[], alias="monthlyData")
daily_data: list[DailyData] = Field(default=[], alias="dailyData")
top_content: list[TopContent] = Field(alias="topContent")
audience_data: AudienceData = Field(alias="audienceData")
has_uploads: bool = Field(default=True, alias="hasUploads")
model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True,
)
class ConnectedAccount(BaseModel):
"""연결된 소셜 계정 정보
Attributes:
id: SocialAccount 테이블 PK
platform: 플랫폼 (예: "youtube")
platform_username: 플랫폼 사용자명 (예: "@channelname")
platform_user_id: 플랫폼 채널 고유 ID — 재연동해도 불변.
/dashboard/stats?platform_user_id=<값> 으로 계정 선택에 사용
channel_title: YouTube 채널 제목 (SocialAccount.platform_data JSON에서 추출)
connected_at: 연동 일시
is_active: 활성화 상태
"""
id: int
platform: str
platform_user_id: str
platform_username: Optional[str] = None
channel_title: Optional[str] = None
connected_at: datetime
is_active: bool
model_config = ConfigDict(
alias_generator=to_camel,
populate_by_name=True,
)
class ConnectedAccountsResponse(BaseModel):
"""연결된 소셜 계정 목록 응답
Attributes:
accounts: 연결된 계정 목록
"""
accounts: list[ConnectedAccount]
class CacheDeleteResponse(BaseModel):
"""캐시 삭제 응답
Attributes:
deleted_count: 삭제된 캐시 키 개수
message: 처리 결과 메시지
"""
deleted_count: int
message: str