From 1c04f4ed6ded5698e7beb8215fb00bab61379f1b Mon Sep 17 00:00:00 2001 From: caadiq Date: Fri, 6 Feb 2026 17:58:30 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EB=B4=87=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=83=80=EC=9E=85=EB=B3=84=20?= =?UTF-8?q?=EC=84=B9=EC=85=98=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Meilisearch, YouTube, X 세 섹션으로 분리 - 각 섹션에 아이콘 및 색상 적용 - YouTube 섹션에 "봇 추가" 버튼 추가 (기능은 추후 구현) - YouTube 봇 동적 관리 계획서 추가 (docs/youtube-bots-plan.md) Co-Authored-By: Claude Opus 4.5 --- docs/youtube-bots-plan.md | 232 ++++++++++++++++++ .../pages/pc/admin/schedules/ScheduleBots.jsx | 185 ++++++++++---- 2 files changed, 366 insertions(+), 51 deletions(-) create mode 100644 docs/youtube-bots-plan.md diff --git a/docs/youtube-bots-plan.md b/docs/youtube-bots-plan.md new file mode 100644 index 0000000..e368b66 --- /dev/null +++ b/docs/youtube-bots-plan.md @@ -0,0 +1,232 @@ +# YouTube 봇 동적 관리 기능 구현 계획 + +## 개요 +현재 `config/bots.js`에 하드코딩된 YouTube 봇 설정을 DB 기반으로 변경하여, 관리 페이지에서 채널을 추가/수정/삭제할 수 있도록 함. + +## 주요 기능 +1. YouTube 핸들(@username) 입력 → 채널 정보 자동 조회 +2. 동기화 간격 설정 (분 단위) +3. 다음 주 예정 일정 자동 생성 옵션 (요일, 시간, 제목 템플릿) +4. 봇 활성화/비활성화 +5. 봇 삭제 + +--- + +## 1. DB 스키마 + +### `youtube_bots` 테이블 생성 + +```sql +CREATE TABLE youtube_bots ( + id VARCHAR(50) PRIMARY KEY, -- 봇 ID (youtube-{handle}) + channel_id VARCHAR(30) NOT NULL, -- UC... + channel_handle VARCHAR(50) NOT NULL, -- @username + channel_name VARCHAR(100) NOT NULL, -- 채널 이름 + uploads_playlist_id VARCHAR(50), -- UU... (캐싱용) + cron_interval INT DEFAULT 2, -- 분 단위 (2 = */2 * * * *) + enabled TINYINT(1) DEFAULT 1, + + -- 제목 필터 (선택) + title_filter VARCHAR(100), + + -- 멤버 설정 (선택) + default_member_id INT, + extract_members_from_desc TINYINT(1) DEFAULT 0, + + -- 다음 주 예정 일정 설정 (JSON) + auto_schedule_config JSON, + -- 예: {"dayOfWeek": 4, "time": "18:00:00", "titleTemplate": "{channelName} {episode}화", "deadlineDayOfWeek": 5} + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + UNIQUE KEY uk_channel_id (channel_id) +); +``` + +**파일**: `backend/sql/youtube_bots.sql` + +--- + +## 2. 백엔드 API + +### 2.1 채널 정보 조회 API (신규) + +**POST /api/admin/youtube-bots/lookup** + +YouTube API `forHandle` 파라미터로 채널 정보 조회. + +```js +// 요청 +{ "handle": "@studiofromis_9" } + +// 응답 +{ + "channelId": "UCeUJ8B3krxw8zuDi19AlhaA", + "handle": "studiofromis_9", + "title": "스프 : 스튜디오 프로미스나인", + "thumbnailUrl": "...", + "uploadsPlaylistId": "UUeUJ8B3krxw8zuDi19AlhaA" +} +``` + +**파일**: `backend/src/services/youtube/api.js` - `getChannelByHandle()` 추가 + +### 2.2 봇 CRUD API (신규) + +**파일**: `backend/src/routes/admin/youtube-bots.js` + +| 메서드 | 경로 | 설명 | +|--------|------|------| +| GET | /api/admin/youtube-bots | 봇 목록 조회 | +| POST | /api/admin/youtube-bots | 봇 추가 | +| PUT | /api/admin/youtube-bots/:id | 봇 수정 | +| DELETE | /api/admin/youtube-bots/:id | 봇 삭제 | + +### 2.3 기존 봇 API 수정 + +**파일**: `backend/src/routes/admin/bots.js` + +- `GET /api/admin/bots`: DB에서 YouTube 봇 목록 조회하도록 수정 +- 기존 `bots.js`의 meilisearch-sync, x-fromis9는 유지 (YouTube 봇만 DB로 이동) + +--- + +## 3. 스케줄러 수정 + +**파일**: `backend/src/plugins/scheduler.js` + +### 변경 사항 +1. 시작 시 DB에서 YouTube 봇 목록 로드 +2. `bots.js`의 meilisearch-sync, x-fromis9는 기존대로 사용 +3. 봇 추가/수정/삭제 시 스케줄 동적 업데이트 + +```js +// getBots() 함수를 async로 변경하여 DB에서 조회 +async getBots() { + const staticBots = bots.filter(b => b.type !== 'youtube'); + const [youtubeBots] = await fastify.db.query('SELECT * FROM youtube_bots WHERE enabled = 1'); + return [...staticBots, ...youtubeBots.map(convertDbBotToConfig)]; +} + +// 봇 추가/삭제 시 스케줄러 동적 업데이트 메서드 추가 +async addBot(bot) { ... } +async removeBot(botId) { ... } +async updateBot(botId, config) { ... } +``` + +--- + +## 4. YouTube 서비스 수정 + +**파일**: `backend/src/services/youtube/index.js` + +### 변경 사항 +- `getManagedChannelIds()`: DB에서 조회하도록 수정 + +--- + +## 5. 프론트엔드 UI + +### 5.1 봇 추가/수정 모달 + +**파일**: `frontend/src/components/pc/admin/YouTubeBotModal.jsx` + +**입력 필드**: +- 채널 핸들 (@username) - 입력 후 "조회" 버튼 → 채널 정보 표시 +- 동기화 간격 (분): 드롭다운 (1, 2, 5, 10, 30, 60) +- 다음 주 예정 일정 활성화 (토글) + - 요일 선택 (일~토) + - 시간 입력 (HH:MM) + - 제목 템플릿 (예: {channelName} {episode}화) + - 마감 요일 선택 +- 고급 설정 (접기/펼치기) + - 제목 필터 (특정 키워드 포함 영상만) + - 기본 멤버 선택 + - description에서 멤버 추출 (토글) + +### 5.2 봇 목록 페이지 수정 + +**파일**: `frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx` + +- 섹션별로 분리: Meilisearch, YouTube, X +- YouTube 섹션에만 "봇 추가" 버튼 +- BotCard에 "수정", "삭제" 버튼 추가 (YouTube 봇만) +- meilisearch, x 봇은 수정/삭제 버튼 없이 읽기 전용 + +### 5.3 BotCard 컴포넌트 수정 + +**파일**: `frontend/src/components/pc/admin/BotCard.jsx` + +- `onEdit`, `onDelete` props 추가 +- YouTube 타입일 때만 수정/삭제 버튼 표시 + +### 5.4 API 클라이언트 + +**파일**: `frontend/src/api/admin/bots.js` + +```js +// 추가할 함수 +export const lookupChannel = (handle) => fetch('/admin/youtube-bots/lookup', { method: 'POST', body: { handle } }) +export const createYouTubeBot = (data) => fetch('/admin/youtube-bots', { method: 'POST', body: data }) +export const updateYouTubeBot = (id, data) => fetch(`/admin/youtube-bots/${id}`, { method: 'PUT', body: data }) +export const deleteYouTubeBot = (id) => fetch(`/admin/youtube-bots/${id}`, { method: 'DELETE' }) +``` + +--- + +## 6. 마이그레이션 + +기존 `bots.js`의 YouTube 봇 3개를 DB로 이동: + +```sql +INSERT INTO youtube_bots (id, channel_id, channel_handle, channel_name, cron_interval, enabled) +VALUES + ('youtube-fromis9', 'UCXbRURMKT3H_w8dT-DWLIxA', 'fromis9', 'fromis_9', 2, 1), + ('youtube-studio', 'UCeUJ8B3krxw8zuDi19AlhaA', 'studiofromis_9', '스프 : 스튜디오 프로미스나인', 2, 1), + ('youtube-musinsa', 'UCtfyAiqf095_0_ux8ruwGfA', 'maboroshimusinsaTV', 'MUSINSA TV', 2, 1); + +-- youtube-studio 예정 일정 설정 +UPDATE youtube_bots SET auto_schedule_config = '{"dayOfWeek":4,"time":"18:00:00","titleTemplate":"{channelName} {episode}화","deadlineDayOfWeek":5}' WHERE id = 'youtube-studio'; + +-- youtube-musinsa 필터/멤버 설정 +UPDATE youtube_bots SET title_filter = '성수기', default_member_id = 7, extract_members_from_desc = 1 WHERE id = 'youtube-musinsa'; +``` + +**파일**: `backend/sql/youtube_bots_seed.sql` + +--- + +## 파일 변경 목록 + +### 신규 파일 +| 파일 | 설명 | +|------|------| +| `backend/sql/youtube_bots.sql` | 테이블 생성 SQL | +| `backend/sql/youtube_bots_seed.sql` | 초기 데이터 SQL | +| `backend/src/routes/admin/youtube-bots.js` | YouTube 봇 CRUD API | +| `frontend/src/components/pc/admin/YouTubeBotModal.jsx` | 봇 추가/수정 모달 | + +### 수정 파일 +| 파일 | 변경 내용 | +|------|-----------| +| `backend/src/services/youtube/api.js` | `getChannelByHandle()` 추가 | +| `backend/src/services/youtube/index.js` | DB 기반으로 변경 | +| `backend/src/plugins/scheduler.js` | DB에서 봇 로드, 동적 업데이트 | +| `backend/src/routes/admin/bots.js` | DB 통합 조회 | +| `backend/src/routes/index.js` | youtube-bots 라우트 등록 | +| `backend/src/config/bots.js` | YouTube 봇 제거 | +| `frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx` | 섹션별 분리, 추가/수정/삭제 UI | +| `frontend/src/components/pc/admin/BotCard.jsx` | 수정/삭제 버튼 | +| `frontend/src/api/admin/bots.js` | API 함수 추가 | + +--- + +## 검증 방법 + +1. **DB 테이블 생성**: `docker compose exec fromis9-db mysql -u... -e "DESC youtube_bots"` +2. **채널 조회 API**: `@studiofromis_9` 입력 → 채널 정보 반환 확인 +3. **봇 추가**: 새 채널 추가 후 목록에 표시, 스케줄러 동작 확인 +4. **봇 수정**: 동기화 간격 변경 후 cron 재등록 확인 +5. **봇 삭제**: 삭제 후 목록에서 제거, 스케줄러 중지 확인 +6. **예정 일정**: 설정된 요일에 예정 일정 자동 생성 확인 diff --git a/frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx b/frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx index 9271797..e2b0a58 100644 --- a/frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx +++ b/frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx @@ -1,14 +1,40 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { motion, AnimatePresence } from 'framer-motion'; -import { Home, ChevronRight, Bot, CheckCircle, XCircle, RefreshCw } from 'lucide-react'; +import { Home, ChevronRight, Bot, CheckCircle, XCircle, RefreshCw, Plus, Search, Database, Youtube, Twitter } from 'lucide-react'; import { Toast, Tooltip, AnimatedNumber } from '@/components/common'; import { AdminLayout, BotCard } from '@/components/pc/admin'; import { useAdminAuth } from '@/hooks/pc/admin'; import { useToast } from '@/hooks/common'; import * as botsApi from '@/api/admin/bots'; +// 섹션 설정 +const SECTIONS = { + meilisearch: { + title: 'Meilisearch', + icon: Database, + color: 'text-purple-500', + bgColor: 'bg-purple-50', + borderColor: 'border-purple-100', + }, + youtube: { + title: 'YouTube', + icon: Youtube, + color: 'text-red-500', + bgColor: 'bg-red-50', + borderColor: 'border-red-100', + canAdd: true, + }, + x: { + title: 'X (Twitter)', + icon: Twitter, + color: 'text-gray-700', + bgColor: 'bg-gray-50', + borderColor: 'border-gray-200', + }, +}; + // 애니메이션 variants const containerVariants = { hidden: { opacity: 0 }, @@ -195,6 +221,17 @@ function ScheduleBots() { const stoppedCount = bots.filter((b) => b.status === 'stopped').length; const errorCount = bots.filter((b) => b.status === 'error').length; + // 봇을 타입별로 그룹화 + const botsByType = useMemo(() => { + const grouped = { meilisearch: [], youtube: [], x: [] }; + bots.forEach((bot) => { + if (grouped[bot.type]) { + grouped[bot.type].push(bot); + } + }); + return grouped; + }, [bots]); + return ( setToast(null)} /> @@ -281,56 +318,102 @@ function ScheduleBots() { )} - {/* 봇 목록 */} - -
-

봇 목록

- - - -
+ {/* 로딩 상태 */} + {loading ? ( + +
+
+ ) : bots.length === 0 ? ( + + +

등록된 봇이 없습니다

+
+ ) : ( + /* 섹션별 봇 목록 */ +
+ {Object.entries(SECTIONS).map(([type, section]) => { + const sectionBots = botsByType[type] || []; + if (sectionBots.length === 0 && !section.canAdd) return null; - {loading ? ( -
-
-
- ) : bots.length === 0 ? ( -
- -

등록된 봇이 없습니다

-

위의 버튼을 클릭하여 봇을 추가하세요

-
- ) : ( -
- {bots.map((bot, index) => ( - - isInitialLoad && index === bots.length - 1 && setIsInitialLoad(false) - } - formatTime={formatTime} - formatInterval={formatInterval} - /> - ))} -
- )} - + const SectionIcon = section.icon; + + return ( + + {/* 섹션 헤더 */} +
+
+
+ +
+
+

{section.title}

+

{sectionBots.length}개의 봇

+
+
+
+ {section.canAdd && ( + + )} + + + +
+
+ + {/* 봇 카드 목록 */} + {sectionBots.length === 0 ? ( +
+ +

등록된 봇이 없습니다

+ {section.canAdd && ( +

위의 버튼을 클릭하여 봇을 추가하세요

+ )} +
+ ) : ( +
+ {sectionBots.map((bot, index) => ( + + isInitialLoad && index === sectionBots.length - 1 && setIsInitialLoad(false) + } + formatTime={formatTime} + formatInterval={formatInterval} + /> + ))} +
+ )} +
+ ); + })} +
+ )}
);