From 730da864a41504981e1491fd3d4a309a651164da Mon Sep 17 00:00:00 2001 From: caadiq Date: Fri, 6 Feb 2026 21:56:27 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20YouTube=20=EB=B4=87=20fetchRecentVideos?= =?UTF-8?q?=EB=A5=BC=20Activities=20API=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - playlistItems API 대신 Activities API 사용 - playlistItems는 새 영상 반영이 지연되는 문제가 있었음 - Activities API는 새 업로드를 즉시 반영함 - upload 외 다른 활동(좋아요, 플레이리스트 등)도 포함되므로 2배로 조회 후 필터링 - API 할당량 비용은 동일 (1 단위) Co-Authored-By: Claude Opus 4.5 --- backend/src/services/youtube/api.js | 44 ++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/backend/src/services/youtube/api.js b/backend/src/services/youtube/api.js index 5ace974..90d6455 100644 --- a/backend/src/services/youtube/api.js +++ b/backend/src/services/youtube/api.js @@ -94,15 +94,15 @@ async function getVideoDurations(videoIds) { } /** - * 최근 N개 영상 조회 + * 최근 N개 영상 조회 (Activities API 사용 - playlistItems보다 빠른 반영) * @param {string} channelId - 채널 ID * @param {number} maxResults - 최대 결과 수 - * @param {string} uploadsPlaylistId - 캐싱된 uploads playlist ID (선택) + * @param {string} uploadsPlaylistId - 미사용 (하위 호환성 유지) */ export async function fetchRecentVideos(channelId, maxResults = 10, uploadsPlaylistId = null) { - const uploadsId = uploadsPlaylistId || await getUploadsPlaylistId(channelId); - - const url = `${API_BASE}/playlistItems?part=snippet&playlistId=${uploadsId}&maxResults=${maxResults}&key=${API_KEY}`; + // Activities API에 type=upload 지정 (다른 활동이 섞일 수 있어 2배 조회) + const fetchCount = Math.min(maxResults * 2, 50); + const url = `${API_BASE}/activities?part=snippet,contentDetails&channelId=${channelId}&type=upload&maxResults=${fetchCount}&key=${API_KEY}`; const res = await fetch(url); const data = await res.json(); @@ -110,17 +110,35 @@ export async function fetchRecentVideos(channelId, maxResults = 10, uploadsPlayl throw new Error(data.error.message); } - const videoIds = data.items.map(item => item.snippet.resourceId.videoId); - const shortsMap = await getVideoDurations(videoIds); + // 'upload' 타입만 필터링 (API가 완벽히 필터링하지 않을 수 있음) + const uploadItems = (data.items || []) + .filter(item => item.snippet.type === 'upload') + .slice(0, maxResults); - return data.items.map(item => { - const { snippet } = item; - const videoId = snippet.resourceId.videoId; - const isShorts = shortsMap[videoId] || false; + if (uploadItems.length === 0) { + return []; + } + + // video ID 목록 추출 + const videoIds = uploadItems.map(item => item.contentDetails.upload.videoId); + + // videos API로 상세 정보 조회 (duration, description 등) + const videosUrl = `${API_BASE}/videos?part=snippet,contentDetails&id=${videoIds.join(',')}&key=${API_KEY}`; + const videosRes = await fetch(videosUrl); + const videosData = await videosRes.json(); + + if (videosData.error) { + throw new Error(videosData.error.message); + } + + return (videosData.items || []).map(video => { + const { snippet, contentDetails } = video; + const seconds = parseDuration(contentDetails.duration); + const isShorts = seconds > 0 && seconds <= 60; const publishedAt = new Date(snippet.publishedAt); return { - videoId, + videoId: video.id, title: snippet.title, description: snippet.description || '', channelId: snippet.channelId, @@ -129,7 +147,7 @@ export async function fetchRecentVideos(channelId, maxResults = 10, uploadsPlayl date: formatDate(publishedAt), time: formatTime(publishedAt), videoType: isShorts ? 'shorts' : 'video', - videoUrl: getVideoUrl(videoId, isShorts), + videoUrl: getVideoUrl(video.id, isShorts), }; }); }