From cb184e4fa51f3b7aafa74e238eefce8022a1d27b Mon Sep 17 00:00:00 2001 From: caadiq Date: Tue, 3 Feb 2026 18:20:49 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9C=A0=ED=8A=9C=EB=B8=8C=20=EC=98=88?= =?UTF-8?q?=EC=A0=95=20=EC=9D=BC=EC=A0=95=EC=97=90=20=EC=B1=84=EB=84=90=20?= =?UTF-8?q?=EB=B0=B0=EB=84=88=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - YouTube API에서 채널 정보(배너 이미지) 조회 함수 추가 - 채널 정보 Redis 캐싱 (24시간) - 일정 상세 API에 bannerUrl 필드 추가 - 예정 일정 placeholder에 배너 이미지 배경 표시 Co-Authored-By: Claude Opus 4.5 --- backend/src/routes/schedules/index.js | 18 +++++++++++++ backend/src/services/youtube/api.js | 27 +++++++++++++++++++ backend/src/services/youtube/index.js | 23 +++++++++++++++- .../schedule/sections/YoutubeSection.jsx | 27 ++++++++++++------- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/backend/src/routes/schedules/index.js b/backend/src/routes/schedules/index.js index 7df19cf..47237e0 100644 --- a/backend/src/routes/schedules/index.js +++ b/backend/src/routes/schedules/index.js @@ -151,6 +151,24 @@ export default async function schedulesRoutes(fastify) { return notFound(reply, '일정을 찾을 수 없습니다.'); } + // 유튜브 카테고리인 경우 채널 배너 이미지 추가 + if (result.category?.id === CATEGORY_IDS.YOUTUBE) { + const [youtubeData] = await db.query( + 'SELECT channel_id FROM schedule_youtube WHERE schedule_id = ?', + [request.params.id] + ); + if (youtubeData.length > 0 && youtubeData[0].channel_id) { + try { + const channelInfo = await fastify.youtubeBot.getChannelInfo(youtubeData[0].channel_id); + if (channelInfo?.bannerUrl) { + result.bannerUrl = channelInfo.bannerUrl; + } + } catch (err) { + fastify.log.warn(`채널 정보 조회 실패: ${err.message}`); + } + } + } + return result; } catch (err) { fastify.log.error(err); diff --git a/backend/src/services/youtube/api.js b/backend/src/services/youtube/api.js index 5e8e277..bb46e85 100644 --- a/backend/src/services/youtube/api.js +++ b/backend/src/services/youtube/api.js @@ -44,6 +44,33 @@ export async function getUploadsPlaylistId(channelId) { return data.items[0].contentDetails.relatedPlaylists.uploads; } +/** + * 채널 정보 조회 (배너 이미지 포함) + */ +export async function getChannelInfo(channelId) { + const url = `${API_BASE}/channels?part=snippet,brandingSettings&id=${channelId}&key=${API_KEY}`; + const res = await fetch(url); + const data = await res.json(); + + if (data.error) { + throw new Error(data.error.message); + } + if (!data.items?.length) { + throw new Error('채널을 찾을 수 없습니다'); + } + + const channel = data.items[0]; + const { snippet, brandingSettings } = channel; + + return { + channelId, + title: snippet.title, + description: snippet.description, + thumbnailUrl: snippet.thumbnails?.high?.url || snippet.thumbnails?.default?.url, + bannerUrl: brandingSettings?.image?.bannerExternalUrl || null, + }; +} + /** * 영상 ID 목록으로 duration 조회 (Shorts 판별용) */ diff --git a/backend/src/services/youtube/index.js b/backend/src/services/youtube/index.js index b1e9e5e..37a41a5 100644 --- a/backend/src/services/youtube/index.js +++ b/backend/src/services/youtube/index.js @@ -1,5 +1,5 @@ import fp from 'fastify-plugin'; -import { fetchRecentVideos, fetchAllVideos, getUploadsPlaylistId } from './api.js'; +import { fetchRecentVideos, fetchAllVideos, getUploadsPlaylistId, getChannelInfo } from './api.js'; import bots from '../../config/bots.js'; import { CATEGORY_IDS } from '../../config/index.js'; import { withTransaction } from '../../utils/transaction.js'; @@ -7,6 +7,7 @@ import { syncScheduleById, deleteSchedule } from '../meilisearch/index.js'; const YOUTUBE_CATEGORY_ID = CATEGORY_IDS.YOUTUBE; const PLAYLIST_CACHE_PREFIX = 'yt_uploads:'; +const CHANNEL_INFO_PREFIX = 'yt_channel:'; async function youtubeBotPlugin(fastify, opts) { /** @@ -28,6 +29,25 @@ async function youtubeBotPlugin(fastify, opts) { return playlistId; } + /** + * 채널 정보 조회 (Redis 캐싱, 24시간) + */ + async function getCachedChannelInfo(channelId) { + const cacheKey = `${CHANNEL_INFO_PREFIX}${channelId}`; + + // Redis 캐시 확인 + const cached = await fastify.redis.get(cacheKey); + if (cached) { + return JSON.parse(cached); + } + + // API 호출 후 캐싱 (24시간) + const channelInfo = await getChannelInfo(channelId); + await fastify.redis.set(cacheKey, JSON.stringify(channelInfo), 'EX', 86400); + + return channelInfo; + } + /** * 다음 특정 요일 날짜 계산 (KST 기준) * @param {number} targetDay - 목표 요일 (0=일, 4=목) @@ -395,6 +415,7 @@ async function youtubeBotPlugin(fastify, opts) { syncNewVideos, syncAllVideos, getManagedChannelIds, + getChannelInfo: getCachedChannelInfo, }); } diff --git a/frontend/src/pages/pc/public/schedule/sections/YoutubeSection.jsx b/frontend/src/pages/pc/public/schedule/sections/YoutubeSection.jsx index 3567c97..ebb0ad8 100644 --- a/frontend/src/pages/pc/public/schedule/sections/YoutubeSection.jsx +++ b/frontend/src/pages/pc/public/schedule/sections/YoutubeSection.jsx @@ -83,19 +83,28 @@ function VideoInfo({ schedule, isShorts, isScheduled = false }) { /** * 예정 일정 Placeholder 컴포넌트 */ -function ScheduledPlaceholder() { +function ScheduledPlaceholder({ bannerUrl }) { return (
- {/* 배경 패턴 */} -
-
-
+ {/* 배경: 배너 이미지 또는 패턴 */} + {bannerUrl ? ( +
+
+
+ ) : ( +
+
+
+ )} {/* 유튜브 아이콘 */}
-
+
@@ -132,7 +141,7 @@ function YoutubeSection({ schedule }) { transition={{ delay: 0.1 }} className="w-full" > - + {/* 영상 정보 카드 */}