audience_data 범위 및 기타 처리수정
parent
8fe0512608
commit
c705ce40f8
|
|
@ -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 "-")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue