Compare commits
4 Commits
cb267192d2
...
8fe0512608
| Author | SHA1 | Date |
|---|---|---|
|
|
8fe0512608 | |
|
|
a1192193e5 | |
|
|
ed616e4f66 | |
|
|
1512691504 |
|
|
@ -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 <= kpi_end_dt,
|
Dashboard.uploaded_at <= end_dt,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
period_video_count = count_result.scalar() or 0
|
period_video_count = count_result.scalar() or 0
|
||||||
|
|
|
||||||
|
|
@ -418,8 +418,8 @@ class DataProcessor:
|
||||||
# === 연령/성별 데이터 처리 ===
|
# === 연령/성별 데이터 처리 ===
|
||||||
demo_rows = demographics_data.get("rows", [])
|
demo_rows = demographics_data.get("rows", [])
|
||||||
|
|
||||||
age_map: dict[str, int] = {}
|
age_map: dict[str, float] = {}
|
||||||
gender_map = {"male": 0, "female": 0}
|
gender_map_f: dict[str, float] = {"male": 0.0, "female": 0.0}
|
||||||
|
|
||||||
for row in demo_rows:
|
for row in demo_rows:
|
||||||
if len(row) < 3:
|
if len(row) < 3:
|
||||||
|
|
@ -427,27 +427,21 @@ class DataProcessor:
|
||||||
|
|
||||||
age_group = row[0] # "age18-24"
|
age_group = row[0] # "age18-24"
|
||||||
gender = row[1] # "male" or "female"
|
gender = row[1] # "male" or "female"
|
||||||
views = row[2]
|
viewer_pct = row[2] # viewerPercentage (이미 % 값, 예: 45.5)
|
||||||
|
|
||||||
# 연령대별 집계 (age18-24 → 18-24)
|
# 연령대별 집계: 남녀 비율 합산 (age18-24 → 18-24)
|
||||||
age_label = age_group.replace("age", "")
|
age_label = age_group.replace("age", "")
|
||||||
age_map[age_label] = age_map.get(age_label, 0) + views
|
age_map[age_label] = age_map.get(age_label, 0.0) + viewer_pct
|
||||||
|
|
||||||
# 성별 집계
|
# 성별 집계
|
||||||
if gender in gender_map:
|
if gender in gender_map_f:
|
||||||
gender_map[gender] += views
|
gender_map_f[gender] += viewer_pct
|
||||||
|
|
||||||
# 연령대별 비율 계산
|
|
||||||
total_demo_views = sum(age_map.values())
|
|
||||||
age_groups = [
|
age_groups = [
|
||||||
{
|
{"label": age, "percentage": int(round(pct))}
|
||||||
"label": age,
|
for age, pct in sorted(age_map.items())
|
||||||
"percentage": int(
|
|
||||||
(count / total_demo_views * 100) if total_demo_views > 0 else 0
|
|
||||||
),
|
|
||||||
}
|
|
||||||
for age, count in sorted(age_map.items())
|
|
||||||
]
|
]
|
||||||
|
gender_map = {k: int(round(v)) for k, v in gender_map_f.items()}
|
||||||
|
|
||||||
# === 지역 데이터 처리 ===
|
# === 지역 데이터 처리 ===
|
||||||
geo_rows = geography_data.get("rows", [])
|
geo_rows = geography_data.get("rows", [])
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ class YouTubeAnalyticsService:
|
||||||
self._fetch_monthly_data(video_ids, previous_start, previous_end, access_token),
|
self._fetch_monthly_data(video_ids, previous_start, previous_end, access_token),
|
||||||
self._fetch_top_videos(video_ids, start_date, _kpi_end, access_token),
|
self._fetch_top_videos(video_ids, start_date, _kpi_end, access_token),
|
||||||
self._fetch_demographics(start_date, _kpi_end, access_token),
|
self._fetch_demographics(start_date, _kpi_end, access_token),
|
||||||
self._fetch_region(video_ids, start_date, _kpi_end, access_token),
|
self._fetch_region(start_date, _kpi_end, access_token),
|
||||||
]
|
]
|
||||||
else: # mode == "day"
|
else: # mode == "day"
|
||||||
tasks = [
|
tasks = [
|
||||||
|
|
@ -134,7 +134,7 @@ class YouTubeAnalyticsService:
|
||||||
self._fetch_daily_data(video_ids, day_previous_start, day_previous_end, access_token),
|
self._fetch_daily_data(video_ids, day_previous_start, day_previous_end, access_token),
|
||||||
self._fetch_top_videos(video_ids, start_date, end_date, access_token),
|
self._fetch_top_videos(video_ids, start_date, end_date, access_token),
|
||||||
self._fetch_demographics(start_date, end_date, access_token),
|
self._fetch_demographics(start_date, end_date, access_token),
|
||||||
self._fetch_region(video_ids, start_date, end_date, access_token),
|
self._fetch_region(start_date, end_date, access_token),
|
||||||
]
|
]
|
||||||
|
|
||||||
# 병렬 실행
|
# 병렬 실행
|
||||||
|
|
@ -206,8 +206,6 @@ class YouTubeAnalyticsService:
|
||||||
estimatedMinutesWatched, averageViewDuration,
|
estimatedMinutesWatched, averageViewDuration,
|
||||||
subscribersGained]
|
subscribersGained]
|
||||||
|
|
||||||
Note:
|
|
||||||
annotationClickThroughRate는 2019년 annotations 기능 제거로 deprecated.
|
|
||||||
"""
|
"""
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"[YouTubeAnalyticsService._fetch_kpi] START - video_count={len(video_ids)}"
|
f"[YouTubeAnalyticsService._fetch_kpi] START - video_count={len(video_ids)}"
|
||||||
|
|
@ -218,7 +216,7 @@ class YouTubeAnalyticsService:
|
||||||
"startDate": start_date,
|
"startDate": start_date,
|
||||||
"endDate": end_date,
|
"endDate": end_date,
|
||||||
"metrics": "views,likes,comments,shares,estimatedMinutesWatched,averageViewDuration,subscribersGained",
|
"metrics": "views,likes,comments,shares,estimatedMinutesWatched,averageViewDuration,subscribersGained",
|
||||||
# "filters": f"video=={','.join(video_ids)}",
|
"filters": f"video=={','.join(video_ids)}",
|
||||||
}
|
}
|
||||||
|
|
||||||
result = await self._call_api(params, access_token)
|
result = await self._call_api(params, access_token)
|
||||||
|
|
@ -393,7 +391,6 @@ class YouTubeAnalyticsService:
|
||||||
|
|
||||||
async def _fetch_region(
|
async def _fetch_region(
|
||||||
self,
|
self,
|
||||||
video_ids: list[str],
|
|
||||||
start_date: str,
|
start_date: str,
|
||||||
end_date: str,
|
end_date: str,
|
||||||
access_token: str,
|
access_token: str,
|
||||||
|
|
@ -403,7 +400,6 @@ class YouTubeAnalyticsService:
|
||||||
지역별 조회수 분포를 조회합니다 (상위 5개).
|
지역별 조회수 분포를 조회합니다 (상위 5개).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
video_ids: YouTube 영상 ID 리스트
|
|
||||||
start_date: 조회 시작일 (YYYY-MM-DD)
|
start_date: 조회 시작일 (YYYY-MM-DD)
|
||||||
end_date: 조회 종료일 (YYYY-MM-DD)
|
end_date: 조회 종료일 (YYYY-MM-DD)
|
||||||
access_token: OAuth 2.0 액세스 토큰
|
access_token: OAuth 2.0 액세스 토큰
|
||||||
|
|
@ -421,9 +417,7 @@ class YouTubeAnalyticsService:
|
||||||
"endDate": end_date,
|
"endDate": end_date,
|
||||||
"dimensions": "country",
|
"dimensions": "country",
|
||||||
"metrics": "views",
|
"metrics": "views",
|
||||||
"filters": f"video=={','.join(video_ids)}",
|
|
||||||
"sort": "-views",
|
"sort": "-views",
|
||||||
"maxResults": "5",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result = await self._call_api(params, access_token)
|
result = await self._call_api(params, access_token)
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ async def make_youtube_seo_description(
|
||||||
"language" : project.language,
|
"language" : project.language,
|
||||||
"target_keywords" : hashtags
|
"target_keywords" : hashtags
|
||||||
}
|
}
|
||||||
chatgpt = ChatgptService()
|
chatgpt = ChatgptService(timeout = 180)
|
||||||
yt_seo_output = await chatgpt.generate_structured_output(yt_upload_prompt, yt_seo_input_data)
|
yt_seo_output = await chatgpt.generate_structured_output(yt_upload_prompt, yt_seo_input_data)
|
||||||
result_dict = {
|
result_dict = {
|
||||||
"title" : yt_seo_output.title,
|
"title" : yt_seo_output.title,
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ class ChatgptService:
|
||||||
text_format=output_format
|
text_format=output_format
|
||||||
)
|
)
|
||||||
# Response 디버그 로깅
|
# Response 디버그 로깅
|
||||||
|
logger.debug(f"[ChatgptService] attempt: {attempt}")
|
||||||
logger.debug(f"[ChatgptService] Response ID: {response.id}")
|
logger.debug(f"[ChatgptService] Response ID: {response.id}")
|
||||||
logger.debug(f"[ChatgptService] Response status: {response.status}")
|
logger.debug(f"[ChatgptService] Response status: {response.status}")
|
||||||
logger.debug(f"[ChatgptService] Response model: {response.model}")
|
logger.debug(f"[ChatgptService] Response model: {response.model}")
|
||||||
|
|
|
||||||
|
|
@ -550,6 +550,7 @@ async def get_video_status(
|
||||||
}
|
}
|
||||||
message = status_messages.get(status, f"상태: {status}")
|
message = status_messages.get(status, f"상태: {status}")
|
||||||
|
|
||||||
|
video_id = None
|
||||||
# succeeded 상태인 경우 백그라운드 태스크 실행
|
# succeeded 상태인 경우 백그라운드 태스크 실행
|
||||||
if status == "succeeded" and video_url:
|
if status == "succeeded" and video_url:
|
||||||
# creatomate_render_id로 Video 조회하여 task_id 가져오기
|
# creatomate_render_id로 Video 조회하여 task_id 가져오기
|
||||||
|
|
@ -561,6 +562,8 @@ async def get_video_status(
|
||||||
)
|
)
|
||||||
video = video_result.scalar_one_or_none()
|
video = video_result.scalar_one_or_none()
|
||||||
|
|
||||||
|
video_id = video.id
|
||||||
|
|
||||||
if video and video.status != "completed":
|
if video and video.status != "completed":
|
||||||
# 이미 완료된 경우 백그라운드 작업 중복 실행 방지
|
# 이미 완료된 경우 백그라운드 작업 중복 실행 방지
|
||||||
# 백그라운드 태스크로 MP4 다운로드 → Blob 업로드 → DB 업데이트 → 임시 파일 삭제
|
# 백그라운드 태스크로 MP4 다운로드 → Blob 업로드 → DB 업데이트 → 임시 파일 삭제
|
||||||
|
|
@ -584,6 +587,7 @@ async def get_video_status(
|
||||||
status=status,
|
status=status,
|
||||||
url=video_url,
|
url=video_url,
|
||||||
snapshot_url=result.get("snapshot_url"),
|
snapshot_url=result.get("snapshot_url"),
|
||||||
|
video_id = video_id if video_id else None
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ class VideoRenderData(BaseModel):
|
||||||
status: Optional[str] = Field(None, description="렌더 상태")
|
status: Optional[str] = Field(None, description="렌더 상태")
|
||||||
url: Optional[str] = Field(None, description="영상 URL")
|
url: Optional[str] = Field(None, description="영상 URL")
|
||||||
snapshot_url: Optional[str] = Field(None, description="스냅샷 URL")
|
snapshot_url: Optional[str] = Field(None, description="스냅샷 URL")
|
||||||
|
video_id: Optional[int] = Field(None, description="Video id(DB)")
|
||||||
|
|
||||||
|
|
||||||
class PollingVideoResponse(BaseModel):
|
class PollingVideoResponse(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ API 요청 시 `dimensions` 파라미터에 들어갈 수 있는 값들입니다
|
||||||
| -------------- | ------- | --------------------------- | -------------- |
|
| -------------- | ------- | --------------------------- | -------------- |
|
||||||
| `**ageGroup`** | **연령대** | 시청자 연령 분포 (18-24, 25-34...) | `video`와 혼용 불가 |
|
| `**ageGroup`** | **연령대** | 시청자 연령 분포 (18-24, 25-34...) | `video`와 혼용 불가 |
|
||||||
| `**gender`** | **성별** | 남녀 성비 (male, female) | `video`와 혼용 불가 |
|
| `**gender`** | **성별** | 남녀 성비 (male, female) | `video`와 혼용 불가 |
|
||||||
| `**country`** | **국가** | 국가별 시청자 수 (KR, US...) | 지도 차트용 |
|
| `**country`** | **국가** | 국가별 시청자 수 (KR, US...) | 지도 차트용, 채널 전체 기준 |
|
||||||
|
|
||||||
|
|
||||||
### C. 유입 및 기기 (Traffic Device)
|
### C. 유입 및 기기 (Traffic Device)
|
||||||
|
|
@ -84,22 +84,76 @@ API 요청 시 `dimensions` 파라미터에 들어갈 수 있는 값들입니다
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 자주 쓰는 조합 (Best Practice)
|
## 3. 현재 사용 중인 API 호출 조합
|
||||||
|
|
||||||
|
대시보드에서 실제로 사용하는 7가지 호출 조합입니다. 모두 `ids=channel==MINE`으로 고정합니다.
|
||||||
|
|
||||||
|
### 1. KPI 요약 (`_fetch_kpi`) — 현재/이전 기간 각 1회
|
||||||
|
|
||||||
|
| 파라미터 | 값 |
|
||||||
|
| ---------- | ------------------------------------------------------------------------------------- |
|
||||||
|
| dimensions | (없음) |
|
||||||
|
| metrics | `views, likes, comments, shares, estimatedMinutesWatched, averageViewDuration, subscribersGained` |
|
||||||
|
| filters | `video==ID1,ID2,...` (업로드된 영상 ID 최대 30개) |
|
||||||
|
|
||||||
|
> 현재/이전 기간을 각각 호출하여 trend(증감률) 계산에 사용.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 월별 추이 차트 (`_fetch_monthly_data`) — 최근 12개월 / 이전 12개월 각 1회
|
||||||
|
|
||||||
|
| 파라미터 | 값 |
|
||||||
|
| ---------- | -------------------- |
|
||||||
|
| dimensions | `month` |
|
||||||
|
| metrics | `views` |
|
||||||
|
| filters | `video==ID1,ID2,...` |
|
||||||
|
| sort | `month` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 일별 추이 차트 (`_fetch_daily_data`) — 최근 30일 / 이전 30일 각 1회
|
||||||
|
|
||||||
|
| 파라미터 | 값 |
|
||||||
|
| ---------- | -------------------- |
|
||||||
|
| dimensions | `day` |
|
||||||
|
| metrics | `views` |
|
||||||
|
| filters | `video==ID1,ID2,...` |
|
||||||
|
| sort | `day` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 인기 영상 TOP 4 (`_fetch_top_videos`)
|
||||||
|
|
||||||
|
| 파라미터 | 값 |
|
||||||
|
| ---------- | ------------------------ |
|
||||||
|
| dimensions | `video` |
|
||||||
|
| metrics | `views, likes, comments` |
|
||||||
|
| filters | `video==ID1,ID2,...` |
|
||||||
|
| sort | `-views` |
|
||||||
|
| maxResults | `4` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 시청자 연령/성별 분포 (`_fetch_demographics`) — 채널 전체 기준
|
||||||
|
|
||||||
|
| 파라미터 | 값 |
|
||||||
|
| ---------- | ----------------------- |
|
||||||
|
| dimensions | `ageGroup, gender` |
|
||||||
|
| metrics | `viewerPercentage` |
|
||||||
|
|
||||||
|
> `ageGroup`, `gender` 차원은 `video` 필터와 혼용 불가 → 채널 전체 시청자 기준.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. 지역별 조회수 TOP 5 (`_fetch_region`) — 채널 전체 기준
|
||||||
|
|
||||||
|
| 파라미터 | 값 |
|
||||||
|
| ---------- | -------------------- |
|
||||||
|
| dimensions | `country` |
|
||||||
|
| metrics | `views` |
|
||||||
|
| sort | `-views` |
|
||||||
|
| maxResults | `5` |
|
||||||
|
|
||||||
1. **프로젝트 전체 요약 (KPI)**
|
|
||||||
- `dimensions`: (없음)
|
|
||||||
- `metrics`: `views,likes,estimatedRevenue`
|
|
||||||
- `filters`: `video==ID1,ID2...`
|
|
||||||
2. **일별 성장 그래프 (Line Chart)**
|
|
||||||
- `dimensions`: `day`
|
|
||||||
- `metrics`: `views`
|
|
||||||
- `filters`: `video==ID1,ID2...`
|
|
||||||
- `sort`: `day`
|
|
||||||
3. **인기 영상 랭킹 (Table)**
|
|
||||||
- `dimensions`: `video`
|
|
||||||
- `metrics`: `views,averageViewDuration`
|
|
||||||
- `filters`: `video==ID1,ID2...`
|
|
||||||
- `sort`: `-views`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue