audience_data 범위 및 기타 처리수정

subtitle
김성경 2026-03-03 15:23:31 +09:00
parent 8fe0512608
commit c705ce40f8
2 changed files with 80 additions and 50 deletions

View File

@ -267,7 +267,7 @@ async def get_dashboard_stats(
Dashboard.platform == "youtube", Dashboard.platform == "youtube",
Dashboard.platform_user_id == social_account.platform_user_id, Dashboard.platform_user_id == social_account.platform_user_id,
Dashboard.uploaded_at >= start_dt, Dashboard.uploaded_at >= start_dt,
Dashboard.uploaded_at <= end_dt, Dashboard.uploaded_at < today + timedelta(days=1),
) )
) )
period_video_count = count_result.scalar() or 0 period_video_count = count_result.scalar() or 0
@ -451,7 +451,7 @@ async def get_dashboard_stats(
# 12. 업로드 영상 수 및 trend 주입 (캐시 저장 후 — 항상 DB에서 직접 집계) # 12. 업로드 영상 수 및 trend 주입 (캐시 저장 후 — 항상 DB에서 직접 집계)
for metric in dashboard_data.content_metrics: for metric in dashboard_data.content_metrics:
if metric.id == "uploaded-videos": 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) video_trend = float(period_video_count - prev_period_video_count)
metric.trend = video_trend metric.trend = video_trend
metric.trend_direction = "up" if video_trend > 0 else ("down" if video_trend < 0 else "-") metric.trend_direction = "up" if video_trend > 0 else ("down" if video_trend < 0 else "-")

View File

@ -4,6 +4,7 @@ YouTube Analytics 데이터 가공 프로세서
YouTube Analytics API의 원본 데이터를 프론트엔드용 Pydantic 스키마로 변환합니다. YouTube Analytics API의 원본 데이터를 프론트엔드용 Pydantic 스키마로 변환합니다.
""" """
from collections import defaultdict
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Literal from typing import Any, Literal
@ -19,6 +20,46 @@ from app.utils.logger import get_logger
logger = get_logger("dashboard") logger = get_logger("dashboard")
_COUNTRY_CODE_MAP: dict[str, str] = {
"KR": "대한민국",
"US": "미국",
"JP": "일본",
"CN": "중국",
"GB": "영국",
"DE": "독일",
"FR": "프랑스",
"CA": "캐나다",
"AU": "호주",
"IN": "인도",
"ID": "인도네시아",
"TH": "태국",
"VN": "베트남",
"PH": "필리핀",
"MY": "말레이시아",
"SG": "싱가포르",
"TW": "대만",
"HK": "홍콩",
"BR": "브라질",
"MX": "멕시코",
"NL": "네덜란드",
"BE": "벨기에",
"SE": "스웨덴",
"NO": "노르웨이",
"FI": "핀란드",
"DK": "덴마크",
"IE": "아일랜드",
"PL": "폴란드",
"CZ": "체코",
"RO": "루마니아",
"HU": "헝가리",
"SK": "슬로바키아",
"SI": "슬로베니아",
"HR": "크로아티아",
"GR": "그리스",
"PT": "포르투갈",
"ES": "스페인",
"IT": "이탈리아",
}
class DataProcessor: class DataProcessor:
"""YouTube Analytics 데이터 가공 프로세서 """YouTube Analytics 데이터 가공 프로세서
@ -90,6 +131,7 @@ class DataProcessor:
monthly_data = self._merge_monthly_data( monthly_data = self._merge_monthly_data(
raw_data.get("trend_recent", {}), raw_data.get("trend_recent", {}),
raw_data.get("trend_previous", {}), raw_data.get("trend_previous", {}),
end_date=end_date,
) )
daily_data: list[DailyData] = [] daily_data: list[DailyData] = []
else: # mode == "day" else: # mode == "day"
@ -271,49 +313,49 @@ class DataProcessor:
self, self,
data_recent: dict[str, Any], data_recent: dict[str, Any],
data_previous: dict[str, Any], data_previous: dict[str, Any],
end_date: str = "",
) -> list[MonthlyData]: ) -> list[MonthlyData]:
"""최근 12개월과 이전 12개월의 월별 데이터를 병합 """최근 12개월과 이전 12개월의 월별 데이터를 병합
최근 12개월 대비 이전 12개월의 월별 조회수 비교 차트를 위한 데이터를 생성합니다. end_date 기준 12개월을 명시 생성하여 API가 반환하지 않은 (당월 ) 0으로 포함합니다.
실제 API 응답의 데이터를 기준으로 매핑합니다.
Args: Args:
data_recent: 최근 12개월 월별 조회수 데이터 data_recent: 최근 12개월 월별 조회수 데이터
rows = [["2026-01", 150000], ["2026-02", 180000], ...] rows = [["2026-01", 150000], ["2026-02", 180000], ...]
data_previous: 이전 12개월 월별 조회수 데이터 data_previous: 이전 12개월 월별 조회수 데이터
rows = [["2025-01", 120000], ["2025-02", 140000], ...] rows = [["2025-01", 120000], ["2025-02", 140000], ...]
end_date: 기준 종료일 (YYYY-MM-DD). 미전달 오늘 사용
Returns: Returns:
list[MonthlyData]: 월별 비교 데이터 (최대 12) list[MonthlyData]: 월별 비교 데이터 (12, API 미반환 월은 0)
""" """
logger.debug("[DataProcessor._merge_monthly_data] START") logger.debug("[DataProcessor._merge_monthly_data] START")
rows_recent = data_recent.get("rows", []) rows_recent = data_recent.get("rows", [])
rows_previous = data_previous.get("rows", []) rows_previous = data_previous.get("rows", [])
# 월별 맵 생성: {"2025-02": 150000, "2025-03": 180000}
map_recent = {row[0]: row[1] for row in rows_recent if len(row) >= 2} map_recent = {row[0]: row[1] for row in rows_recent if len(row) >= 2}
map_previous = {row[0]: row[1] for row in rows_previous if len(row) >= 2} map_previous = {row[0]: row[1] for row in rows_previous if len(row) >= 2}
# 최근 기간의 월 키만 기준으로 정렬 (24개 합집합 방지) # end_date 기준 12개월 명시 생성 (API 미반환 당월도 0으로 포함)
# 각 월의 이전 연도 키는 1년 전으로 계산: "2025-02" → "2024-02" if end_date:
recent_months = sorted(map_recent.keys()) end_dt = datetime.strptime(end_date, "%Y-%m-%d")
else:
end_dt = datetime.today()
# 월별 데이터 생성
result = [] result = []
for month_key in recent_months: for i in range(11, -1, -1):
year, month = month_key.split("-") m = end_dt.month - i
month_num = int(month) y = end_dt.year
month_label = f"{month_num}" if m <= 0:
m += 12
# 이전 연도 동일 월: "2025-02" → "2024-02" y -= 1
prev_year_key = f"{int(year) - 1}-{month}" month_key = f"{y}-{m:02d}"
result.append( result.append(
MonthlyData( MonthlyData(
month=month_label, month=f"{m}",
this_year=map_recent.get(month_key, 0), this_year=map_recent.get(month_key, 0),
last_year=map_previous.get(prev_year_key, 0), last_year=map_previous.get(f"{y - 1}-{m:02d}", 0),
) )
) )
@ -437,9 +479,17 @@ class DataProcessor:
if gender in gender_map_f: if gender in gender_map_f:
gender_map_f[gender] += viewer_pct gender_map_f[gender] += viewer_pct
# 연령대 5개로 통합: 13-17+18-24 → 13-24, 55-64+65- → 55+
merged_age: dict[str, float] = {
"13-24": age_map.get("13-17", 0.0) + age_map.get("18-24", 0.0),
"25-34": age_map.get("25-34", 0.0),
"35-44": age_map.get("35-44", 0.0),
"45-54": age_map.get("45-54", 0.0),
"55+": age_map.get("55-64", 0.0) + age_map.get("65-", 0.0),
}
age_groups = [ age_groups = [
{"label": age, "percentage": int(round(pct))} {"label": age, "percentage": int(round(pct))}
for age, pct in sorted(age_map.items()) for age, pct in merged_age.items()
] ]
gender_map = {k: int(round(v)) for k, v in gender_map_f.items()} gender_map = {k: int(round(v)) for k, v in gender_map_f.items()}
@ -447,15 +497,17 @@ class DataProcessor:
geo_rows = geography_data.get("rows", []) geo_rows = geography_data.get("rows", [])
total_geo_views = sum(row[1] for row in geo_rows if len(row) >= 2) total_geo_views = sum(row[1] for row in geo_rows if len(row) >= 2)
merged_geo: defaultdict[str, int] = defaultdict(int)
for row in geo_rows:
if len(row) >= 2:
merged_geo[self._translate_country_code(row[0])] += row[1]
top_regions = [ top_regions = [
{ {
"region": self._translate_country_code(row[0]), "region": region,
"percentage": int( "percentage": int((views / total_geo_views * 100) if total_geo_views > 0 else 0),
(row[1] / total_geo_views * 100) if total_geo_views > 0 else 0
),
} }
for row in geo_rows[:5] # 상위 5개 for region, views in sorted(merged_geo.items(), key=lambda x: x[1], reverse=True)[:5]
if len(row) >= 2
] ]
logger.debug( logger.debug(
@ -487,26 +539,4 @@ class DataProcessor:
>>> _translate_country_code("US") >>> _translate_country_code("US")
"미국" "미국"
""" """
country_map = { return _COUNTRY_CODE_MAP.get(code, "기타")
"KR": "대한민국",
"US": "미국",
"JP": "일본",
"CN": "중국",
"GB": "영국",
"DE": "독일",
"FR": "프랑스",
"CA": "캐나다",
"AU": "호주",
"IN": "인도",
"ID": "인도네시아",
"TH": "태국",
"VN": "베트남",
"PH": "필리핀",
"MY": "말레이시아",
"SG": "싱가포르",
"TW": "대만",
"HK": "홍콩",
"BR": "브라질",
"MX": "멕시코",
}
return country_map.get(code, code)