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_user_id == social_account.platform_user_id,
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
@ -451,7 +451,7 @@ async def get_dashboard_stats(
# 12. 업로드 영상 수 및 trend 주입 (캐시 저장 후 — 항상 DB에서 직접 집계)
for metric in dashboard_data.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 "-")

View File

@ -4,6 +4,7 @@ YouTube Analytics 데이터 가공 프로세서
YouTube Analytics API의 원본 데이터를 프론트엔드용 Pydantic 스키마로 변환합니다.
"""
from collections import defaultdict
from datetime import datetime, timedelta
from typing import Any, Literal
@ -19,6 +20,46 @@ from app.utils.logger import get_logger
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:
"""YouTube Analytics 데이터 가공 프로세서
@ -90,6 +131,7 @@ class DataProcessor:
monthly_data = self._merge_monthly_data(
raw_data.get("trend_recent", {}),
raw_data.get("trend_previous", {}),
end_date=end_date,
)
daily_data: list[DailyData] = []
else: # mode == "day"
@ -271,49 +313,49 @@ class DataProcessor:
self,
data_recent: dict[str, Any],
data_previous: dict[str, Any],
end_date: str = "",
) -> list[MonthlyData]:
"""최근 12개월과 이전 12개월의 월별 데이터를 병합
최근 12개월 대비 이전 12개월의 월별 조회수 비교 차트를 위한 데이터를 생성합니다.
실제 API 응답의 데이터를 기준으로 매핑합니다.
end_date 기준 12개월을 명시 생성하여 API가 반환하지 않은 (당월 ) 0으로 포함합니다.
Args:
data_recent: 최근 12개월 월별 조회수 데이터
rows = [["2026-01", 150000], ["2026-02", 180000], ...]
data_previous: 이전 12개월 월별 조회수 데이터
rows = [["2025-01", 120000], ["2025-02", 140000], ...]
end_date: 기준 종료일 (YYYY-MM-DD). 미전달 오늘 사용
Returns:
list[MonthlyData]: 월별 비교 데이터 (최대 12)
list[MonthlyData]: 월별 비교 데이터 (12, API 미반환 월은 0)
"""
logger.debug("[DataProcessor._merge_monthly_data] START")
rows_recent = data_recent.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_previous = {row[0]: row[1] for row in rows_previous if len(row) >= 2}
# 최근 기간의 월 키만 기준으로 정렬 (24개 합집합 방지)
# 각 월의 이전 연도 키는 1년 전으로 계산: "2025-02" → "2024-02"
recent_months = sorted(map_recent.keys())
# end_date 기준 12개월 명시 생성 (API 미반환 당월도 0으로 포함)
if end_date:
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
else:
end_dt = datetime.today()
# 월별 데이터 생성
result = []
for month_key in recent_months:
year, month = month_key.split("-")
month_num = int(month)
month_label = f"{month_num}"
# 이전 연도 동일 월: "2025-02" → "2024-02"
prev_year_key = f"{int(year) - 1}-{month}"
for i in range(11, -1, -1):
m = end_dt.month - i
y = end_dt.year
if m <= 0:
m += 12
y -= 1
month_key = f"{y}-{m:02d}"
result.append(
MonthlyData(
month=month_label,
month=f"{m}",
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:
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 = [
{"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()}
@ -447,15 +497,17 @@ class DataProcessor:
geo_rows = geography_data.get("rows", [])
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 = [
{
"region": self._translate_country_code(row[0]),
"percentage": int(
(row[1] / total_geo_views * 100) if total_geo_views > 0 else 0
),
"region": region,
"percentage": int((views / total_geo_views * 100) if total_geo_views > 0 else 0),
}
for row in geo_rows[:5] # 상위 5개
if len(row) >= 2
for region, views in sorted(merged_geo.items(), key=lambda x: x[1], reverse=True)[:5]
]
logger.debug(
@ -487,26 +539,4 @@ class DataProcessor:
>>> _translate_country_code("US")
"미국"
"""
country_map = {
"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)
return _COUNTRY_CODE_MAP.get(code, "기타")