feat: YouTube API 할당량 절감을 위한 playlist ID 캐싱
- uploads playlist ID를 Redis에 영구 캐싱 (불변값) - 일일 API 사용량 6,480 → 4,320 units (33% 절감) - 문서 업데이트 (컨테이너 분리 구조, X source.name 빈 문자열) - CLAUDE.md에 문서 업데이트 필수 안내 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
149e85ebd9
commit
c7b0a51924
5 changed files with 61 additions and 25 deletions
|
|
@ -29,3 +29,7 @@ DB 및 외부 서비스 접근 정보는 `.env` 파일 참조:
|
|||
- [docs/architecture.md](docs/architecture.md) - 프로젝트 구조
|
||||
- [docs/api.md](docs/api.md) - API 명세
|
||||
- [docs/development.md](docs/development.md) - 개발/배포 가이드
|
||||
|
||||
## 작업 시 주의사항
|
||||
|
||||
- **문서 업데이트 필수**: 작업이 완료되면 항상 `docs/` 폴더의 관련 문서를 업데이트할 것
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ function getVideoUrl(videoId, isShorts) {
|
|||
/**
|
||||
* 채널의 업로드 플레이리스트 ID 조회
|
||||
*/
|
||||
async function getUploadsPlaylistId(channelId) {
|
||||
export async function getUploadsPlaylistId(channelId) {
|
||||
const url = `${API_BASE}/channels?part=contentDetails&id=${channelId}&key=${API_KEY}`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
|
|
@ -64,9 +64,12 @@ async function getVideoDurations(videoIds) {
|
|||
|
||||
/**
|
||||
* 최근 N개 영상 조회
|
||||
* @param {string} channelId - 채널 ID
|
||||
* @param {number} maxResults - 최대 결과 수
|
||||
* @param {string} uploadsPlaylistId - 캐싱된 uploads playlist ID (선택)
|
||||
*/
|
||||
export async function fetchRecentVideos(channelId, maxResults = 10) {
|
||||
const uploadsId = await getUploadsPlaylistId(channelId);
|
||||
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}`;
|
||||
const res = await fetch(url);
|
||||
|
|
@ -102,9 +105,11 @@ export async function fetchRecentVideos(channelId, maxResults = 10) {
|
|||
|
||||
/**
|
||||
* 전체 영상 조회 (페이지네이션)
|
||||
* @param {string} channelId - 채널 ID
|
||||
* @param {string} uploadsPlaylistId - 캐싱된 uploads playlist ID (선택)
|
||||
*/
|
||||
export async function fetchAllVideos(channelId) {
|
||||
const uploadsId = await getUploadsPlaylistId(channelId);
|
||||
export async function fetchAllVideos(channelId, uploadsPlaylistId = null) {
|
||||
const uploadsId = uploadsPlaylistId || await getUploadsPlaylistId(channelId);
|
||||
const videos = [];
|
||||
let pageToken = '';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,30 @@
|
|||
import fp from 'fastify-plugin';
|
||||
import { fetchRecentVideos, fetchAllVideos } from './api.js';
|
||||
import { fetchRecentVideos, fetchAllVideos, getUploadsPlaylistId } from './api.js';
|
||||
import bots from '../../config/bots.js';
|
||||
|
||||
const YOUTUBE_CATEGORY_ID = 2;
|
||||
const PLAYLIST_CACHE_PREFIX = 'yt_uploads:';
|
||||
|
||||
async function youtubeBotPlugin(fastify, opts) {
|
||||
/**
|
||||
* uploads playlist ID 조회 (Redis 캐싱)
|
||||
*/
|
||||
async function getCachedUploadsPlaylistId(channelId) {
|
||||
const cacheKey = `${PLAYLIST_CACHE_PREFIX}${channelId}`;
|
||||
|
||||
// Redis 캐시 확인
|
||||
const cached = await fastify.redis.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// API 호출 후 캐싱 (영구 저장 - 값이 변하지 않음)
|
||||
const playlistId = await getUploadsPlaylistId(channelId);
|
||||
await fastify.redis.set(cacheKey, playlistId);
|
||||
|
||||
return playlistId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 멤버 이름 맵 조회
|
||||
*/
|
||||
|
|
@ -89,7 +109,8 @@ async function youtubeBotPlugin(fastify, opts) {
|
|||
* 최근 영상 동기화 (정기 실행)
|
||||
*/
|
||||
async function syncNewVideos(bot) {
|
||||
const videos = await fetchRecentVideos(bot.channelId, 10);
|
||||
const uploadsPlaylistId = await getCachedUploadsPlaylistId(bot.channelId);
|
||||
const videos = await fetchRecentVideos(bot.channelId, 10, uploadsPlaylistId);
|
||||
let addedCount = 0;
|
||||
|
||||
for (const video of videos) {
|
||||
|
|
@ -106,7 +127,8 @@ async function youtubeBotPlugin(fastify, opts) {
|
|||
* 전체 영상 동기화 (초기화)
|
||||
*/
|
||||
async function syncAllVideos(bot) {
|
||||
const videos = await fetchAllVideos(bot.channelId);
|
||||
const uploadsPlaylistId = await getCachedUploadsPlaylistId(bot.channelId);
|
||||
const videos = await fetchAllVideos(bot.channelId, uploadsPlaylistId);
|
||||
let addedCount = 0;
|
||||
|
||||
for (const video of videos) {
|
||||
|
|
@ -137,5 +159,5 @@ async function youtubeBotPlugin(fastify, opts) {
|
|||
|
||||
export default fp(youtubeBotPlugin, {
|
||||
name: 'youtubeBot',
|
||||
dependencies: ['db'],
|
||||
dependencies: ['db', 'redis'],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ Base URL: `/api`
|
|||
|
||||
**source 객체 (카테고리별):**
|
||||
- YouTube (category_id=2): `{ name: "채널명", url: "https://www.youtube.com/..." }`
|
||||
- X (category_id=3): `{ name: "X", url: "https://x.com/realfromis_9/status/..." }`
|
||||
- X (category_id=3): `{ name: "", url: "https://x.com/realfromis_9/status/..." }` (name 빈 문자열)
|
||||
- 기타 카테고리: source 없음
|
||||
|
||||
**검색 응답:**
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ fromis_9/
|
|||
│ │ │ └── suggestions/ # 추천 검색어
|
||||
│ │ ├── app.js # Fastify 앱 설정
|
||||
│ │ └── server.js # 진입점
|
||||
│ ├── Dockerfile # 백엔드 컨테이너
|
||||
│ └── package.json
|
||||
│
|
||||
├── backend-backup/ # Express 백엔드 (참조용, 마이그레이션 원본)
|
||||
|
|
@ -47,9 +48,9 @@ fromis_9/
|
|||
│ │ ├── stores/ # Zustand 스토어
|
||||
│ │ └── App.jsx
|
||||
│ ├── vite.config.js
|
||||
│ ├── Dockerfile # 프론트엔드 컨테이너
|
||||
│ └── package.json
|
||||
│
|
||||
├── Dockerfile # 개발/배포 통합 (주석 전환)
|
||||
├── docker-compose.yml
|
||||
└── .env
|
||||
```
|
||||
|
|
@ -64,20 +65,24 @@ fromis_9/
|
|||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ fromis9-frontend (Docker) │
|
||||
│ ┌─────────────────┐ ┌─────────────────────────────┐ │
|
||||
│ │ Vite (:80) │───▶│ Fastify (:3000) │ │
|
||||
│ │ 프론트엔드 │ │ 백엔드 API │ │
|
||||
│ └─────────────────┘ └──────────┬──────────────────┘ │
|
||||
└─────────────────────────────────────┼───────────────────┘
|
||||
│ fromis9-frontend (:80) │
|
||||
│ Vite 개발서버 │
|
||||
│ (프록시: /api → backend) │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────────────┼────────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ MariaDB │ │ Meilisearch │ │ Redis │
|
||||
│ (외부 DB망) │ │ (검색 엔진) │ │ (캐시) │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ fromis9-backend (:80) │
|
||||
│ Fastify API │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
┌────────────┼────────────┬────────────┐
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌───────────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
|
||||
│ MariaDB │ │Meilisearch│ │ Redis │ │ Nitter │
|
||||
│ (외부 DB망) │ │ (검색엔진) │ │ (캐시) │ │ (X 스크랩) │
|
||||
└───────────────┘ └───────────┘ └───────────┘ └───────────┘
|
||||
```
|
||||
|
||||
## 데이터베이스
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue