사용하지 않는 코드 정리 및 전송 데이터 포멧팅 변경
parent
a0c352f567
commit
cb267192d2
|
|
@ -14,16 +14,17 @@ from sqlalchemy import func, select
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dashboard.exceptions import (
|
||||
NoVideosFoundError,
|
||||
YouTubeAccountNotConnectedError,
|
||||
YouTubeAccountNotFoundError,
|
||||
YouTubeAccountSelectionRequiredError,
|
||||
YouTubeTokenExpiredError,
|
||||
)
|
||||
from app.dashboard.schemas import (
|
||||
AudienceData,
|
||||
CacheDeleteResponse,
|
||||
ConnectedAccount,
|
||||
ConnectedAccountsResponse,
|
||||
ContentMetric,
|
||||
DashboardResponse,
|
||||
TopContent,
|
||||
)
|
||||
|
|
@ -53,7 +54,7 @@ router = APIRouter(prefix="/dashboard", tags=["Dashboard"])
|
|||
description="""
|
||||
연결된 소셜 계정 목록을 반환합니다.
|
||||
|
||||
여러 계정이 연결된 경우, 반환된 `id` 값을 `/dashboard/stats?social_account_id=<id>`에 전달하여 계정을 선택합니다.
|
||||
여러 계정이 연결된 경우, 반환된 `platformUserId` 값을 `/dashboard/stats?platform_user_id=<값>`에 전달하여 계정을 선택합니다.
|
||||
""",
|
||||
)
|
||||
async def get_connected_accounts(
|
||||
|
|
@ -107,18 +108,17 @@ YouTube Analytics API를 활용한 대시보드 통계를 조회합니다.
|
|||
|
||||
## 주요 기능
|
||||
- 최근 30개 업로드 영상 기준 통계 제공
|
||||
- KPI 지표: 업로드 영상 수, 총 조회수, 시청 시간, 평균 시청 시간, 신규 구독자, 좋아요, 댓글, 공유
|
||||
- KPI 지표: 조회수, 시청시간, 평균 시청시간, 신규 구독자, 좋아요, 댓글, 공유, 업로드 영상
|
||||
- 월별 추이: 최근 12개월 vs 이전 12개월 비교
|
||||
- 인기 영상 TOP 4
|
||||
- 시청자 분석: 연령/성별/지역 분포
|
||||
|
||||
## 성능 최적화
|
||||
- 8개 YouTube Analytics API를 병렬로 호출
|
||||
- 7개 YouTube Analytics API를 병렬로 호출
|
||||
- Redis 캐싱 적용 (TTL: 12시간)
|
||||
|
||||
## 사전 조건
|
||||
- YouTube 계정이 연동되어 있어야 합니다
|
||||
- 최소 1개 이상의 영상이 업로드되어 있어야 합니다
|
||||
|
||||
## 조회 모드
|
||||
- `day`: 최근 30일 통계 (현재 날짜 -2일 기준)
|
||||
|
|
@ -158,7 +158,6 @@ async def get_dashboard_stats(
|
|||
YouTubeAccountNotConnectedError: YouTube 계정이 연동되어 있지 않음
|
||||
YouTubeAccountSelectionRequiredError: 여러 계정이 연결되어 있으나 계정 미선택
|
||||
YouTubeAccountNotFoundError: 지정한 계정을 찾을 수 없음
|
||||
NoVideosFoundError: 업로드된 영상이 없음
|
||||
YouTubeTokenExpiredError: YouTube 토큰 만료 (재연동 필요)
|
||||
YouTubeAPIError: YouTube Analytics API 호출 실패
|
||||
"""
|
||||
|
|
@ -302,7 +301,7 @@ async def get_dashboard_stats(
|
|||
response = DashboardResponse.model_validate(payload["response"])
|
||||
for metric in response.content_metrics:
|
||||
if metric.id == "uploaded-videos":
|
||||
metric.value = str(period_video_count)
|
||||
metric.value = float(period_video_count)
|
||||
video_trend = float(period_video_count - prev_period_video_count)
|
||||
metric.trend = video_trend
|
||||
metric.trend_direction = "up" if video_trend > 0 else ("down" if video_trend < 0 else "-")
|
||||
|
|
@ -332,14 +331,7 @@ async def get_dashboard_stats(
|
|||
.limit(30)
|
||||
)
|
||||
rows = result.all()
|
||||
# 테스트를 위해 주석처리
|
||||
# if not rows:
|
||||
# logger.warning(
|
||||
# f"[NO VIDEOS] 업로드된 영상 없음 - " f"user_uuid={current_user.user_uuid}"
|
||||
# )
|
||||
# raise NoVideosFoundError()
|
||||
|
||||
# logger.debug(f"[5] 영상 조회 완료 - count={len(rows)}")
|
||||
logger.debug(f"[5] 영상 조회 완료 - count={len(rows)}")
|
||||
|
||||
# 6. video_ids + 메타데이터 조회용 dict 구성
|
||||
video_ids = []
|
||||
|
|
@ -354,6 +346,30 @@ async def get_dashboard_stats(
|
|||
f"[6] 영상 메타데이터 구성 완료 - count={len(video_ids)}, sample={video_ids[:3]}"
|
||||
)
|
||||
|
||||
# 6.1 업로드 영상 없음 → YouTube API 호출 없이 빈 응답 반환
|
||||
if not video_ids:
|
||||
logger.info(
|
||||
f"[DASHBOARD] 업로드 영상 없음, 빈 응답 반환 - "
|
||||
f"user_uuid={current_user.user_uuid}"
|
||||
)
|
||||
return DashboardResponse(
|
||||
content_metrics=[
|
||||
ContentMetric(id="total-views", label="조회수", value=0.0, unit="count", trend=0.0, trend_direction="-"),
|
||||
ContentMetric(id="total-watch-time", label="시청시간", value=0.0, unit="hours", trend=0.0, trend_direction="-"),
|
||||
ContentMetric(id="avg-view-duration", label="평균 시청시간", value=0.0, unit="minutes", trend=0.0, trend_direction="-"),
|
||||
ContentMetric(id="new-subscribers", label="신규 구독자", value=0.0, unit="count", trend=0.0, trend_direction="-"),
|
||||
ContentMetric(id="likes", label="좋아요", value=0.0, unit="count", trend=0.0, trend_direction="-"),
|
||||
ContentMetric(id="comments", label="댓글", value=0.0, unit="count", trend=0.0, trend_direction="-"),
|
||||
ContentMetric(id="shares", label="공유", value=0.0, unit="count", trend=0.0, trend_direction="-"),
|
||||
ContentMetric(id="uploaded-videos", label="업로드 영상", value=0.0, unit="count", trend=0.0, trend_direction="-"),
|
||||
],
|
||||
monthly_data=[],
|
||||
daily_data=[],
|
||||
top_content=[],
|
||||
audience_data=AudienceData(age_groups=[], gender={"male": 0, "female": 0}, top_regions=[]),
|
||||
has_uploads=False,
|
||||
)
|
||||
|
||||
# 7. 토큰 유효성 확인 및 자동 갱신 (만료 10분 전 갱신)
|
||||
try:
|
||||
access_token = await SocialAccountService().ensure_valid_token(
|
||||
|
|
@ -367,7 +383,7 @@ async def get_dashboard_stats(
|
|||
|
||||
logger.debug("[7] 토큰 유효성 확인 완료")
|
||||
|
||||
# 8. YouTube Analytics API 호출 (8개 병렬)
|
||||
# 8. YouTube Analytics API 호출 (7개 병렬)
|
||||
youtube_service = YouTubeAnalyticsService()
|
||||
raw_data = await youtube_service.fetch_all_metrics(
|
||||
video_ids=video_ids,
|
||||
|
|
@ -399,7 +415,7 @@ async def get_dashboard_stats(
|
|||
title=title,
|
||||
thumbnail=f"https://i.ytimg.com/vi/{video_id}/mqdefault.jpg",
|
||||
platform="youtube",
|
||||
views=DataProcessor._format_number(views),
|
||||
views=int(views),
|
||||
engagement=f"{engagement_rate:.1f}%",
|
||||
date=uploaded_at.strftime("%Y.%m.%d"),
|
||||
)
|
||||
|
|
@ -464,7 +480,7 @@ async def get_dashboard_stats(
|
|||
- 데이터 이상 발생 시 캐시 강제 갱신
|
||||
|
||||
## 캐시 키 구조
|
||||
`dashboard:{user_uuid}:{mode}` (mode: day 또는 month)
|
||||
`dashboard:{user_uuid}:{platform_user_id}:{mode}` (mode: day 또는 month)
|
||||
|
||||
## 파라미터
|
||||
- `user_uuid`: 특정 사용자 캐시만 삭제. 미입력 시 전체 삭제
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ from app.dashboard.schemas.dashboard_schema import (
|
|||
DailyData,
|
||||
DashboardResponse,
|
||||
MonthlyData,
|
||||
PlatformData,
|
||||
PlatformMetric,
|
||||
TopContent,
|
||||
)
|
||||
|
||||
|
|
@ -26,8 +24,6 @@ __all__ = [
|
|||
"MonthlyData",
|
||||
"TopContent",
|
||||
"AudienceData",
|
||||
"PlatformMetric",
|
||||
"PlatformData",
|
||||
"DashboardResponse",
|
||||
"CacheDeleteResponse",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ def to_camel(string: str) -> str:
|
|||
"""snake_case를 camelCase로 변환
|
||||
|
||||
Args:
|
||||
string: snake_case 문자열 (예: "label_en")
|
||||
string: snake_case 문자열 (예: "content_metrics")
|
||||
|
||||
Returns:
|
||||
camelCase 문자열 (예: "labelEn")
|
||||
camelCase 문자열 (예: "contentMetrics")
|
||||
|
||||
Example:
|
||||
>>> to_camel("content_metrics")
|
||||
|
|
@ -43,28 +43,31 @@ class ContentMetric(BaseModel):
|
|||
대시보드 상단에 표시되는 핵심 성과 지표(KPI) 카드입니다.
|
||||
|
||||
Attributes:
|
||||
id: 지표 고유 ID (예: "total-views", "total-engagement")
|
||||
label: 한글 라벨 (예: "총 조회수")
|
||||
label_en: 영문 라벨 (예: "Total Views")
|
||||
value: 포맷팅된 값 (예: "1.2M", "8.2%")
|
||||
trend: 증감 추세 값 (예: 12.5 → 12.5% 증가)
|
||||
trend_direction: 추세 방향 ("up" 또는 "down")
|
||||
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="총 조회수",
|
||||
... label_en="Total Views",
|
||||
... value="1.2M",
|
||||
... trend=12.5,
|
||||
... label="조회수",
|
||||
... value=1200000.0,
|
||||
... unit="count",
|
||||
... trend=3800.0,
|
||||
... trend_direction="up"
|
||||
... )
|
||||
"""
|
||||
|
||||
id: str
|
||||
label: str
|
||||
label_en: str
|
||||
value: str
|
||||
value: float
|
||||
unit: str = "count"
|
||||
trend: float
|
||||
trend_direction: Literal["up", "down", "-"] = Field(alias="trendDirection")
|
||||
|
||||
|
|
@ -133,7 +136,7 @@ class TopContent(BaseModel):
|
|||
title: 영상 제목
|
||||
thumbnail: 썸네일 이미지 URL
|
||||
platform: 플랫폼 ("youtube" 또는 "instagram")
|
||||
views: 포맷팅된 조회수 (예: "125K")
|
||||
views: 원시 조회수 정수 (포맷팅은 프론트에서 처리, 예: 125400)
|
||||
engagement: 참여율 (예: "8.2%")
|
||||
date: 업로드 날짜 (예: "2026.01.15")
|
||||
|
||||
|
|
@ -143,7 +146,7 @@ class TopContent(BaseModel):
|
|||
... title="힐링 영상",
|
||||
... thumbnail="https://i.ytimg.com/...",
|
||||
... platform="youtube",
|
||||
... views="125K",
|
||||
... views=125400,
|
||||
... engagement="8.2%",
|
||||
... date="2026.01.15"
|
||||
... )
|
||||
|
|
@ -153,7 +156,7 @@ class TopContent(BaseModel):
|
|||
title: str
|
||||
thumbnail: str
|
||||
platform: Literal["youtube", "instagram"]
|
||||
views: str
|
||||
views: int
|
||||
engagement: str
|
||||
date: str
|
||||
|
||||
|
|
@ -169,18 +172,18 @@ class AudienceData(BaseModel):
|
|||
시청자의 연령대, 성별, 지역 분포 데이터입니다.
|
||||
|
||||
Attributes:
|
||||
age_groups: 연령대별 비율 리스트
|
||||
age_groups: 연령대별 시청자 비율 리스트
|
||||
[{"label": "18-24", "percentage": 35}, ...]
|
||||
gender: 성별 조회수
|
||||
{"male": 45000, "female": 55000}
|
||||
top_regions: 상위 지역 리스트
|
||||
[{"region": "서울", "percentage": 42}, ...]
|
||||
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}]
|
||||
... top_regions=[{"region": "대한민국", "percentage": 42}]
|
||||
... )
|
||||
"""
|
||||
|
||||
|
|
@ -194,90 +197,47 @@ class AudienceData(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
class PlatformMetric(BaseModel):
|
||||
"""플랫폼별 메트릭
|
||||
|
||||
플랫폼별 세부 지표입니다 (예: 구독자 증가, 구독 취소).
|
||||
|
||||
Attributes:
|
||||
id: 메트릭 고유 ID (예: "subscribers-gained")
|
||||
label: 지표명 (예: "구독자 증가")
|
||||
value: 포맷팅된 값 (예: "1.2K")
|
||||
unit: 단위 (선택, 예: "명", "%")
|
||||
trend: 증감 추세 값
|
||||
trend_direction: 추세 방향 ("up" 또는 "down")
|
||||
|
||||
Example:
|
||||
>>> metric = PlatformMetric(
|
||||
... id="subscribers-gained",
|
||||
... label="구독자 증가",
|
||||
... value="1.2K",
|
||||
... trend=8.5,
|
||||
... trend_direction="up"
|
||||
... )
|
||||
"""
|
||||
|
||||
id: str
|
||||
label: str
|
||||
value: str
|
||||
unit: Optional[str] = None
|
||||
trend: float
|
||||
trend_direction: Literal["up", "down", "-"] = Field(alias="trendDirection")
|
||||
|
||||
model_config = ConfigDict(
|
||||
alias_generator=to_camel,
|
||||
populate_by_name=True,
|
||||
)
|
||||
|
||||
|
||||
class PlatformData(BaseModel):
|
||||
"""플랫폼별 데이터
|
||||
|
||||
플랫폼별로 그룹화된 메트릭 데이터입니다.
|
||||
|
||||
Attributes:
|
||||
platform: 플랫폼 종류 ("youtube" 또는 "instagram")
|
||||
display_name: 표시할 플랫폼 이름 (예: "YouTube")
|
||||
metrics: 플랫폼별 메트릭 리스트
|
||||
|
||||
Example:
|
||||
>>> data = PlatformData(
|
||||
... platform="youtube",
|
||||
... display_name="YouTube",
|
||||
... metrics=[
|
||||
... PlatformMetric(
|
||||
... id="subscribers",
|
||||
... label="구독자 증가",
|
||||
... value="1.2K",
|
||||
... trend=8.5,
|
||||
... trend_direction="up"
|
||||
... )
|
||||
... ]
|
||||
... )
|
||||
"""
|
||||
|
||||
platform: Literal["youtube", "instagram"]
|
||||
display_name: str = Field(alias="displayName")
|
||||
metrics: list[PlatformMetric]
|
||||
|
||||
model_config = ConfigDict(
|
||||
alias_generator=to_camel,
|
||||
populate_by_name=True,
|
||||
)
|
||||
# class PlatformMetric(BaseModel):
|
||||
# """플랫폼별 메트릭 (미사용 — platform_data 기능 미구현)"""
|
||||
#
|
||||
# id: str
|
||||
# label: str
|
||||
# value: str
|
||||
# unit: Optional[str] = None
|
||||
# trend: float
|
||||
# trend_direction: Literal["up", "down", "-"] = Field(alias="trendDirection")
|
||||
#
|
||||
# model_config = ConfigDict(
|
||||
# alias_generator=to_camel,
|
||||
# populate_by_name=True,
|
||||
# )
|
||||
#
|
||||
#
|
||||
# class PlatformData(BaseModel):
|
||||
# """플랫폼별 데이터 (미사용 — platform_data 기능 미구현)"""
|
||||
#
|
||||
# platform: Literal["youtube", "instagram"]
|
||||
# display_name: str = Field(alias="displayName")
|
||||
# metrics: list[PlatformMetric]
|
||||
#
|
||||
# model_config = ConfigDict(
|
||||
# alias_generator=to_camel,
|
||||
# populate_by_name=True,
|
||||
# )
|
||||
|
||||
|
||||
class DashboardResponse(BaseModel):
|
||||
"""대시보드 전체 응답
|
||||
|
||||
GET /api/dashboard/stats 엔드포인트의 전체 응답 스키마입니다.
|
||||
모든 대시보드 데이터를 포함합니다.
|
||||
GET /dashboard/stats 엔드포인트의 전체 응답 스키마입니다.
|
||||
|
||||
Attributes:
|
||||
content_metrics: KPI 지표 카드 리스트
|
||||
monthly_data: 월별 추이 데이터 (전년 대비)
|
||||
top_content: 인기 영상 리스트
|
||||
audience_data: 시청자 분석 데이터
|
||||
platform_data: 플랫폼별 메트릭 데이터
|
||||
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(
|
||||
|
|
@ -285,7 +245,6 @@ class DashboardResponse(BaseModel):
|
|||
... monthly_data=[...],
|
||||
... top_content=[...],
|
||||
... audience_data=AudienceData(...),
|
||||
... platform_data=[...]
|
||||
... )
|
||||
>>> json_str = response.model_dump_json() # JSON 직렬화
|
||||
"""
|
||||
|
|
@ -295,7 +254,8 @@ class DashboardResponse(BaseModel):
|
|||
daily_data: list[DailyData] = Field(default=[], alias="dailyData")
|
||||
top_content: list[TopContent] = Field(alias="topContent")
|
||||
audience_data: AudienceData = Field(alias="audienceData")
|
||||
platform_data: list[PlatformData] = Field(default=[], alias="platformData")
|
||||
has_uploads: bool = Field(default=True, alias="hasUploads")
|
||||
# platform_data: list[PlatformData] = Field(default=[], alias="platformData") # 미사용
|
||||
|
||||
model_config = ConfigDict(
|
||||
alias_generator=to_camel,
|
||||
|
|
@ -307,11 +267,12 @@ class ConnectedAccount(BaseModel):
|
|||
"""연결된 소셜 계정 정보
|
||||
|
||||
Attributes:
|
||||
id: SocialAccount 테이블 ID (계정 선택 시 이 값을 social_account_id로 전달)
|
||||
id: SocialAccount 테이블 PK
|
||||
platform: 플랫폼 (예: "youtube")
|
||||
platform_username: 플랫폼 사용자명 (예: "@channelname")
|
||||
platform_user_id: 플랫폼 사용자 고유 ID
|
||||
channel_title: YouTube 채널 제목 (platform_data에서 추출)
|
||||
platform_user_id: 플랫폼 채널 고유 ID — 재연동해도 불변.
|
||||
/dashboard/stats?platform_user_id=<값> 으로 계정 선택에 사용
|
||||
channel_title: YouTube 채널 제목 (SocialAccount.platform_data JSON에서 추출)
|
||||
connected_at: 연동 일시
|
||||
is_active: 활성화 상태
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -204,64 +204,64 @@ class DataProcessor:
|
|||
ContentMetric(
|
||||
id="total-views",
|
||||
label="조회수",
|
||||
label_en="Total Views",
|
||||
value=self._format_number(int(views)),
|
||||
value=float(views),
|
||||
unit="count",
|
||||
trend=round(float(views_trend), 1),
|
||||
trend_direction=views_dir,
|
||||
),
|
||||
ContentMetric(
|
||||
id="total-watch-time",
|
||||
label="시청시간",
|
||||
label_en="Watch Time",
|
||||
value=f"{round(estimated_minutes_watched / 60, 1)}시간",
|
||||
value=round(estimated_minutes_watched / 60, 1),
|
||||
unit="hours",
|
||||
trend=round(watch_trend / 60, 1),
|
||||
trend_direction=watch_dir,
|
||||
),
|
||||
ContentMetric(
|
||||
id="avg-view-duration",
|
||||
label="평균 시청시간",
|
||||
label_en="Avg. View Duration",
|
||||
value=f"{round(average_view_duration / 60)}분",
|
||||
value=round(average_view_duration / 60, 1),
|
||||
unit="minutes",
|
||||
trend=round(duration_trend / 60, 1),
|
||||
trend_direction=duration_dir,
|
||||
),
|
||||
ContentMetric(
|
||||
id="new-subscribers",
|
||||
label="신규 구독자",
|
||||
label_en="New Subscribers",
|
||||
value=self._format_number(int(subscribers_gained)),
|
||||
trend=round(float(subs_trend), 1),
|
||||
value=float(subscribers_gained),
|
||||
unit="count",
|
||||
trend=subs_trend,
|
||||
trend_direction=subs_dir,
|
||||
),
|
||||
ContentMetric(
|
||||
id="likes",
|
||||
label="좋아요",
|
||||
label_en="Likes",
|
||||
value=self._format_number(int(likes)),
|
||||
trend=round(float(likes_trend), 1),
|
||||
value=float(likes),
|
||||
unit="count",
|
||||
trend=likes_trend,
|
||||
trend_direction=likes_dir,
|
||||
),
|
||||
ContentMetric(
|
||||
id="comments",
|
||||
label="댓글",
|
||||
label_en="Comments",
|
||||
value=self._format_number(int(comments)),
|
||||
trend=round(float(comments_trend), 1),
|
||||
value=float(comments),
|
||||
unit="count",
|
||||
trend=comments_trend,
|
||||
trend_direction=comments_dir,
|
||||
),
|
||||
ContentMetric(
|
||||
id="shares",
|
||||
label="공유",
|
||||
label_en="Shares",
|
||||
value=self._format_number(int(shares)),
|
||||
trend=round(float(shares_trend), 1),
|
||||
value=float(shares),
|
||||
unit="count",
|
||||
trend=shares_trend,
|
||||
trend_direction=shares_dir,
|
||||
),
|
||||
ContentMetric(
|
||||
id="uploaded-videos",
|
||||
label="업로드 영상",
|
||||
label_en="Uploaded Videos",
|
||||
value=str(period_video_count),
|
||||
value=float(period_video_count),
|
||||
unit="count",
|
||||
trend=0.0,
|
||||
trend_direction="-",
|
||||
),
|
||||
|
|
@ -475,53 +475,6 @@ class DataProcessor:
|
|||
top_regions=top_regions,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _format_number(num: int) -> str:
|
||||
"""숫자 포맷팅 (1234567 → "1.2M")
|
||||
|
||||
조회수, 구독자 수 등 큰 숫자를 읽기 쉽게 포맷팅합니다.
|
||||
|
||||
Args:
|
||||
num: 원본 숫자
|
||||
|
||||
Returns:
|
||||
str: 포맷팅된 문자열
|
||||
- 1,000,000 이상: "1.2M"
|
||||
- 1,000 이상: "12.5K"
|
||||
- 1,000 미만: "123"
|
||||
|
||||
Example:
|
||||
>>> _format_number(1234567)
|
||||
"1.2M"
|
||||
>>> _format_number(12345)
|
||||
"12.3K"
|
||||
>>> _format_number(123)
|
||||
"123"
|
||||
"""
|
||||
if num >= 1_000_000:
|
||||
return f"{num / 1_000_000:.1f}M"
|
||||
elif num >= 1_000:
|
||||
return f"{num / 1_000:.1f}K"
|
||||
else:
|
||||
return str(num)
|
||||
|
||||
@staticmethod
|
||||
def _format_duration(seconds: int) -> str:
|
||||
"""초를 M:SS 형식으로 변환 (평균 시청 시간 표시용)
|
||||
|
||||
Args:
|
||||
seconds: 초 단위 시간
|
||||
|
||||
Returns:
|
||||
str: M:SS 형식 문자열
|
||||
- 204초 → "3:24"
|
||||
- 65초 → "1:05"
|
||||
- 45초 → "0:45"
|
||||
"""
|
||||
minutes = seconds // 60
|
||||
secs = seconds % 60
|
||||
return f"{minutes}:{secs:02d}"
|
||||
|
||||
@staticmethod
|
||||
def _translate_country_code(code: str) -> str:
|
||||
"""국가 코드를 한국어로 변환
|
||||
|
|
|
|||
|
|
@ -44,19 +44,22 @@ class YouTubeAnalyticsService:
|
|||
"""YouTube Analytics API 호출을 병렬로 실행
|
||||
|
||||
Args:
|
||||
video_ids: YouTube 영상 ID 리스트 (최대 30개)
|
||||
video_ids: YouTube 영상 ID 리스트 (최대 30개, 빈 리스트 허용)
|
||||
start_date: 조회 시작일 (YYYY-MM-DD)
|
||||
end_date: 조회 종료일 (YYYY-MM-DD)
|
||||
access_token: YouTube OAuth 2.0 액세스 토큰
|
||||
mode: 조회 모드 ("month" | "day")
|
||||
kpi_end_date: KPI 집계 종료일 (미전달 시 end_date와 동일)
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: API 응답 데이터
|
||||
- kpi: KPI 메트릭 (조회수, 좋아요, 댓글 등)
|
||||
- monthly_recent: 최근 12개월 월별 조회수
|
||||
- monthly_previous: 이전 12개월 월별 조회수
|
||||
- top_videos: 인기 영상 TOP 4
|
||||
- demographics: 연령/성별 분포
|
||||
- region: 지역별 조회수
|
||||
dict[str, Any]: API 응답 데이터 (7개 키)
|
||||
- kpi: 최근 기간 KPI 메트릭 (조회수, 좋아요, 댓글 등)
|
||||
- kpi_previous: 이전 기간 KPI 메트릭 (trend 계산용)
|
||||
- trend_recent: 최근 기간 추이 (월별 또는 일별 조회수)
|
||||
- trend_previous: 이전 기간 추이 (전년 또는 이전 30일)
|
||||
- top_videos: 조회수 기준 인기 영상 TOP 4
|
||||
- demographics: 연령/성별 시청자 분포
|
||||
- region: 지역별 조회수 TOP 5
|
||||
|
||||
Raises:
|
||||
YouTubeAPIError: API 호출 실패
|
||||
|
|
@ -73,7 +76,7 @@ class YouTubeAnalyticsService:
|
|||
... )
|
||||
"""
|
||||
logger.debug(
|
||||
f"[1/6] YouTube Analytics API 병렬 호출 시작 - "
|
||||
f"[1/7] YouTube Analytics API 병렬 호출 시작 - "
|
||||
f"video_count={len(video_ids)}, period={start_date}~{end_date}, mode={mode}"
|
||||
)
|
||||
|
||||
|
|
@ -255,7 +258,7 @@ class YouTubeAnalyticsService:
|
|||
"endDate": end_date,
|
||||
"dimensions": "month",
|
||||
"metrics": "views",
|
||||
# "filters": f"video=={','.join(video_ids)}",
|
||||
"filters": f"video=={','.join(video_ids)}",
|
||||
"sort": "month",
|
||||
}
|
||||
|
||||
|
|
@ -299,7 +302,7 @@ class YouTubeAnalyticsService:
|
|||
"endDate": end_date,
|
||||
"dimensions": "day",
|
||||
"metrics": "views",
|
||||
# "filters": f"video=={','.join(video_ids)}",
|
||||
"filters": f"video=={','.join(video_ids)}",
|
||||
"sort": "day",
|
||||
}
|
||||
|
||||
|
|
@ -340,7 +343,7 @@ class YouTubeAnalyticsService:
|
|||
"endDate": end_date,
|
||||
"dimensions": "video",
|
||||
"metrics": "views,likes,comments",
|
||||
# "filters": f"video=={','.join(video_ids)}",
|
||||
"filters": f"video=={','.join(video_ids)}",
|
||||
"sort": "-views",
|
||||
"maxResults": "4",
|
||||
}
|
||||
|
|
@ -410,7 +413,7 @@ class YouTubeAnalyticsService:
|
|||
rows = [["KR", 1000000], ["US", 500000], ...]
|
||||
조회수 내림차순으로 정렬된 상위 5개 국가
|
||||
"""
|
||||
logger.debug("[YouTubeAnalyticsService._fetch_country] START")
|
||||
logger.debug("[YouTubeAnalyticsService._fetch_region] START")
|
||||
|
||||
params = {
|
||||
"ids": "channel==MINE",
|
||||
|
|
@ -418,7 +421,7 @@ class YouTubeAnalyticsService:
|
|||
"endDate": end_date,
|
||||
"dimensions": "country",
|
||||
"metrics": "views",
|
||||
# "filters": f"video=={','.join(video_ids)}",
|
||||
"filters": f"video=={','.join(video_ids)}",
|
||||
"sort": "-views",
|
||||
"maxResults": "5",
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue