From 4f11e14b12af78be91590efab0f0b0e74b3482bf Mon Sep 17 00:00:00 2001 From: caadiq Date: Sat, 7 Feb 2026 19:52:41 +0900 Subject: [PATCH] =?UTF-8?q?refactor(db):=20=EB=B4=87=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EC=9D=B4=EB=A6=84=20=ED=86=B5=EC=9D=BC=20=EB=B0=8F?= =?UTF-8?q?=20X=20=EB=B4=87=20=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - youtube_bots → bot_youtube, x_bots → bot_x로 테이블 이름 변경 - bot_x 테이블 생성 및 시드 데이터 추가 - 관련 백엔드 코드에서 테이블 참조 업데이트 - X 봇 동적 관리 구현 계획 문서 추가 Co-Authored-By: Claude Opus 4.5 --- backend/sql/bot_x.sql | 12 + backend/sql/bot_x_seed.sql | 6 + .../sql/{youtube_bots.sql => bot_youtube.sql} | 2 +- ...ube_bots_seed.sql => bot_youtube_seed.sql} | 6 +- backend/src/plugins/scheduler.js | 2 +- backend/src/routes/admin/youtube-bots.js | 20 +- backend/src/routes/schedules/index.js | 2 +- backend/src/services/x/index.js | 2 +- backend/src/services/youtube/index.js | 2 +- docs/x-bots-plan.md | 252 ++++++++++++++++++ docs/youtube-bots-plan.md | 232 ---------------- 11 files changed, 288 insertions(+), 250 deletions(-) create mode 100644 backend/sql/bot_x.sql create mode 100644 backend/sql/bot_x_seed.sql rename backend/sql/{youtube_bots.sql => bot_youtube.sql} (94%) rename backend/sql/{youtube_bots_seed.sql => bot_youtube_seed.sql} (86%) create mode 100644 docs/x-bots-plan.md delete mode 100644 docs/youtube-bots-plan.md diff --git a/backend/sql/bot_x.sql b/backend/sql/bot_x.sql new file mode 100644 index 0000000..dc1266c --- /dev/null +++ b/backend/sql/bot_x.sql @@ -0,0 +1,12 @@ +-- X 봇 테이블 +CREATE TABLE IF NOT EXISTS bot_x ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL, + display_name VARCHAR(100), + avatar_url VARCHAR(500), + cron_interval INT DEFAULT 1, + enabled TINYINT(1) DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uk_username (username) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/backend/sql/bot_x_seed.sql b/backend/sql/bot_x_seed.sql new file mode 100644 index 0000000..669cd5a --- /dev/null +++ b/backend/sql/bot_x_seed.sql @@ -0,0 +1,6 @@ +-- X 봇 초기 데이터 +-- 기존 config/bots.js에 하드코딩된 X 봇을 DB로 마이그레이션 + +INSERT INTO bot_x (username, display_name, cron_interval, enabled) +VALUES ('realfromis_9', 'fromis_9', 1, 1) +ON DUPLICATE KEY UPDATE display_name = VALUES(display_name); diff --git a/backend/sql/youtube_bots.sql b/backend/sql/bot_youtube.sql similarity index 94% rename from backend/sql/youtube_bots.sql rename to backend/sql/bot_youtube.sql index 49f23ab..c8469f6 100644 --- a/backend/sql/youtube_bots.sql +++ b/backend/sql/bot_youtube.sql @@ -1,5 +1,5 @@ -- YouTube 봇 테이블 -CREATE TABLE IF NOT EXISTS youtube_bots ( +CREATE TABLE IF NOT EXISTS bot_youtube ( id INT AUTO_INCREMENT PRIMARY KEY, channel_id VARCHAR(30) NOT NULL, channel_handle VARCHAR(50), diff --git a/backend/sql/youtube_bots_seed.sql b/backend/sql/bot_youtube_seed.sql similarity index 86% rename from backend/sql/youtube_bots_seed.sql rename to backend/sql/bot_youtube_seed.sql index 6ed42d4..7dcecb6 100644 --- a/backend/sql/youtube_bots_seed.sql +++ b/backend/sql/bot_youtube_seed.sql @@ -1,19 +1,19 @@ -- YouTube 봇 시드 데이터 -- channel_handle은 봇 추가 시 YouTube API로 조회하여 저장 -INSERT INTO youtube_bots (channel_id, channel_name, cron_interval, enabled) VALUES +INSERT INTO bot_youtube (channel_id, channel_name, cron_interval, enabled) VALUES ('UCXbRURMKT3H_w8dT-DWLIxA', 'fromis_9', 2, 1), ('UCtfyAiqf095_0_ux8ruwGfA', 'MUSINSA TV', 2, 1), ('UCeUJ8B3krxw8zuDi19AlhaA', '스프 : 스튜디오 프로미스나인', 2, 1) ON DUPLICATE KEY UPDATE channel_name = VALUES(channel_name); -- 스프 : 스튜디오 프로미스나인 - 예정 일정 설정 -UPDATE youtube_bots +UPDATE bot_youtube SET auto_schedule_config = '{"dayOfWeek":4,"time":"18:00:00","titleTemplate":"{channelName} {episode}화","deadlineDayOfWeek":5,"excludeShorts":true}' WHERE channel_id = 'UCeUJ8B3krxw8zuDi19AlhaA'; -- MUSINSA TV - 필터/멤버 설정 -UPDATE youtube_bots +UPDATE bot_youtube SET title_filters = '["성수기"]', default_member_ids = '[7]', extract_members_from_desc = 1 diff --git a/backend/src/plugins/scheduler.js b/backend/src/plugins/scheduler.js index ae25c83..69ceece 100644 --- a/backend/src/plugins/scheduler.js +++ b/backend/src/plugins/scheduler.js @@ -16,7 +16,7 @@ async function schedulerPlugin(fastify, opts) { */ async function getYouTubeBotsFromDB() { const [rows] = await fastify.db.query( - 'SELECT * FROM youtube_bots WHERE enabled = 1' + 'SELECT * FROM bot_youtube WHERE enabled = 1' ); return rows.map(row => ({ id: `youtube-${row.id}`, // DB ID를 문자열 형식으로 변환 diff --git a/backend/src/routes/admin/youtube-bots.js b/backend/src/routes/admin/youtube-bots.js index 0d87a33..71fe98a 100644 --- a/backend/src/routes/admin/youtube-bots.js +++ b/backend/src/routes/admin/youtube-bots.js @@ -128,7 +128,7 @@ export default async function youtubeBotsRoutes(fastify) { }, preHandler: [fastify.authenticate], }, async (request, reply) => { - const [rows] = await db.query('SELECT * FROM youtube_bots ORDER BY id'); + const [rows] = await db.query('SELECT * FROM bot_youtube ORDER BY id'); return rows.map(formatBotResponse); }); @@ -150,7 +150,7 @@ export default async function youtubeBotsRoutes(fastify) { preHandler: [fastify.authenticate], }, async (request, reply) => { const { id } = request.params; - const [rows] = await db.query('SELECT * FROM youtube_bots WHERE id = ?', [id]); + const [rows] = await db.query('SELECT * FROM bot_youtube WHERE id = ?', [id]); if (rows.length === 0) { return notFound(reply, 'YouTube 봇을 찾을 수 없습니다.'); @@ -204,7 +204,7 @@ export default async function youtubeBotsRoutes(fastify) { // 중복 체크 const [existing] = await db.query( - 'SELECT id FROM youtube_bots WHERE channel_id = ?', + 'SELECT id FROM bot_youtube WHERE channel_id = ?', [channel_id] ); if (existing.length > 0) { @@ -212,7 +212,7 @@ export default async function youtubeBotsRoutes(fastify) { } const [result] = await db.query( - `INSERT INTO youtube_bots + `INSERT INTO bot_youtube (channel_id, channel_handle, channel_name, banner_url, cron_interval, title_filters, default_member_ids, extract_members_from_desc, auto_schedule_config, enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`, @@ -238,7 +238,7 @@ export default async function youtubeBotsRoutes(fastify) { fastify.log.error(`[${botId}] 봇 시작 실패:`, err); } - const [newBot] = await db.query('SELECT * FROM youtube_bots WHERE id = ?', [result.insertId]); + const [newBot] = await db.query('SELECT * FROM bot_youtube WHERE id = ?', [result.insertId]); reply.code(201); return formatBotResponse(newBot[0]); }); @@ -278,7 +278,7 @@ export default async function youtubeBotsRoutes(fastify) { const updates = request.body; // 존재 확인 - const [existing] = await db.query('SELECT * FROM youtube_bots WHERE id = ?', [id]); + const [existing] = await db.query('SELECT * FROM bot_youtube WHERE id = ?', [id]); if (existing.length === 0) { return notFound(reply, 'YouTube 봇을 찾을 수 없습니다.'); } @@ -327,7 +327,7 @@ export default async function youtubeBotsRoutes(fastify) { if (fields.length > 0) { values.push(id); await db.query( - `UPDATE youtube_bots SET ${fields.join(', ')} WHERE id = ?`, + `UPDATE bot_youtube SET ${fields.join(', ')} WHERE id = ?`, values ); @@ -346,7 +346,7 @@ export default async function youtubeBotsRoutes(fastify) { } } - const [updatedBot] = await db.query('SELECT * FROM youtube_bots WHERE id = ?', [id]); + const [updatedBot] = await db.query('SELECT * FROM bot_youtube WHERE id = ?', [id]); return formatBotResponse(updatedBot[0]); }); @@ -375,7 +375,7 @@ export default async function youtubeBotsRoutes(fastify) { const { id } = request.params; // 존재 확인 - const [existing] = await db.query('SELECT * FROM youtube_bots WHERE id = ?', [id]); + const [existing] = await db.query('SELECT * FROM bot_youtube WHERE id = ?', [id]); if (existing.length === 0) { return notFound(reply, 'YouTube 봇을 찾을 수 없습니다.'); } @@ -389,7 +389,7 @@ export default async function youtubeBotsRoutes(fastify) { } // DB에서 삭제 - await db.query('DELETE FROM youtube_bots WHERE id = ?', [id]); + await db.query('DELETE FROM bot_youtube WHERE id = ?', [id]); // 스케줄러 캐시 무효화 scheduler.invalidateCache(); diff --git a/backend/src/routes/schedules/index.js b/backend/src/routes/schedules/index.js index dedf49a..089af24 100644 --- a/backend/src/routes/schedules/index.js +++ b/backend/src/routes/schedules/index.js @@ -156,7 +156,7 @@ export default async function schedulesRoutes(fastify) { const [youtubeData] = await db.query( `SELECT sy.channel_id, yb.banner_url FROM schedule_youtube sy - LEFT JOIN youtube_bots yb ON sy.channel_id = yb.channel_id + LEFT JOIN bot_youtube yb ON sy.channel_id = yb.channel_id WHERE sy.schedule_id = ?`, [request.params.id] ); diff --git a/backend/src/services/x/index.js b/backend/src/services/x/index.js index 3eaab15..1904e17 100644 --- a/backend/src/services/x/index.js +++ b/backend/src/services/x/index.js @@ -16,7 +16,7 @@ async function xBotPlugin(fastify, opts) { */ async function getManagedChannelIds() { const [rows] = await fastify.db.query( - 'SELECT channel_id FROM youtube_bots WHERE enabled = 1' + 'SELECT channel_id FROM bot_youtube WHERE enabled = 1' ); return rows.map(r => r.channel_id); } diff --git a/backend/src/services/youtube/index.js b/backend/src/services/youtube/index.js index 5e9c0a7..295deb1 100644 --- a/backend/src/services/youtube/index.js +++ b/backend/src/services/youtube/index.js @@ -369,7 +369,7 @@ async function youtubeBotPlugin(fastify) { */ async function getManagedChannelIds() { const [rows] = await fastify.db.query( - 'SELECT channel_id FROM youtube_bots WHERE enabled = 1' + 'SELECT channel_id FROM bot_youtube WHERE enabled = 1' ); return rows.map(r => r.channel_id); } diff --git a/docs/x-bots-plan.md b/docs/x-bots-plan.md new file mode 100644 index 0000000..af6cddd --- /dev/null +++ b/docs/x-bots-plan.md @@ -0,0 +1,252 @@ +# X 봇 동적 관리 기능 구현 계획 + +## 개요 +현재 `config/bots.js`에 하드코딩된 X 봇 설정을 DB 기반으로 변경하여, 관리 페이지에서 X 계정을 추가/수정/삭제할 수 있도록 함. + +## 현재 구조 +```javascript +// config/bots.js +{ + id: 'x-fromis9', + type: 'x', + username: 'realfromis_9', + nitterUrl: process.env.NITTER_URL || 'http://nitter:8080', + cron: '*/1 * * * *', + enabled: true, +} +``` + +## 주요 기능 +1. X username 입력 → Nitter를 통해 프로필 정보 조회 (displayName, avatarUrl) +2. 동기화 간격 설정 (분 단위) +3. 봇 활성화/비활성화 +4. 봇 삭제 + +--- + +## 1. DB 스키마 + +### `bot_x` 테이블 생성 + +```sql +CREATE TABLE IF NOT EXISTS bot_x ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL, + display_name VARCHAR(100), + avatar_url VARCHAR(500), + cron_interval INT DEFAULT 1, + enabled TINYINT(1) DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uk_username (username) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +**파일**: `backend/sql/bot_x.sql` + +### 시드 데이터 + +```sql +INSERT INTO bot_x (username, display_name, cron_interval, enabled) +VALUES ('realfromis_9', 'fromis_9', 1, 1) +ON DUPLICATE KEY UPDATE display_name = VALUES(display_name); +``` + +**파일**: `backend/sql/bot_x_seed.sql` + +--- + +## 2. 백엔드 API + +### 2.1 프로필 조회 API (신규) + +**POST /api/admin/x-bots/lookup** + +Nitter를 통해 X 계정 프로필 정보 조회. + +```javascript +// 요청 +{ "username": "realfromis_9" } + +// 응답 +{ + "username": "realfromis_9", + "displayName": "fromis_9", + "avatarUrl": "https://pbs.twimg.com/profile_images/..." +} +``` + +**구현**: `services/x/scraper.js`의 `fetchTweets` 함수 활용 + +### 2.2 봇 CRUD API (신규) + +**파일**: `backend/src/routes/admin/x-bots.js` + +| 메서드 | 경로 | 설명 | +|--------|------|------| +| GET | /api/admin/x-bots | 봇 목록 조회 | +| GET | /api/admin/x-bots/:id | 봇 상세 조회 | +| POST | /api/admin/x-bots | 봇 추가 | +| PUT | /api/admin/x-bots/:id | 봇 수정 | +| DELETE | /api/admin/x-bots/:id | 봇 삭제 | + +### 2.3 기존 봇 API 수정 + +**파일**: `backend/src/routes/admin/bots.js` + +- `GET /api/admin/bots`: DB에서 X 봇 목록도 조회하도록 수정 +- X 봇 상세 정보 추가 (db_id, username, display_name, avatar_url 등) + +--- + +## 3. 스케줄러 수정 + +**파일**: `backend/src/plugins/scheduler.js` + +### 변경 사항 +1. `getXBotsFromDB()` 함수 추가 +2. `getAllBots()`에서 X 봇도 DB에서 로드 +3. `config/bots.js`에서 X 봇 제거 + +```javascript +async function getXBotsFromDB() { + const [rows] = await fastify.db.query( + 'SELECT * FROM bot_x WHERE enabled = 1' + ); + return rows.map(row => ({ + id: `x-${row.id}`, + dbId: row.id, + type: 'x', + username: row.username, + displayName: row.display_name, + avatarUrl: row.avatar_url, + nitterUrl: process.env.NITTER_URL || 'http://nitter:8080', + cron: `*/${row.cron_interval} * * * *`, + enabled: row.enabled === 1, + })); +} + +async function getAllBots(forceRefresh = false) { + if (cachedBots && !forceRefresh) { + return cachedBots; + } + const xBots = await getXBotsFromDB(); + const youtubeBots = await getYouTubeBotsFromDB(); + cachedBots = [...staticBots, ...xBots, ...youtubeBots]; + return cachedBots; +} +``` + +--- + +## 4. 프론트엔드 UI + +### 4.1 봇 추가/수정 다이얼로그 + +**파일**: `frontend/src/components/pc/admin/bot/XBotDialog.jsx` + +**입력 필드**: +- X username 입력 → "조회" 버튼 → Nitter에서 프로필 정보 가져와 표시 +- 동기화 간격 (분): 드롭다운 (1, 2, 5, 10, 30, 60) + +### 4.2 봇 목록 페이지 수정 + +**파일**: `frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx` + +- X 섹션에 "봇 추가" 버튼 추가 (`canAdd: true`) +- X 봇도 수정/삭제 버튼 표시 + +### 4.3 API 클라이언트 + +**파일**: `frontend/src/api/admin/bots.js` + +```javascript +export const lookupXProfile = (username) => + fetchAuthApi('/admin/x-bots/lookup', { method: 'POST', body: JSON.stringify({ username }) }); +export const getXBot = (id) => fetchAuthApi(`/admin/x-bots/${id}`); +export const createXBot = (data) => fetchAuthApi('/admin/x-bots', { method: 'POST', body: JSON.stringify(data) }); +export const updateXBot = (id, data) => fetchAuthApi(`/admin/x-bots/${id}`, { method: 'PUT', body: JSON.stringify(data) }); +export const deleteXBot = (id) => fetchAuthApi(`/admin/x-bots/${id}`, { method: 'DELETE' }); +``` + +--- + +## 5. 파일 변경 목록 + +### 신규 파일 +| 파일 | 설명 | +|------|------| +| `backend/sql/bot_x.sql` | 테이블 생성 SQL | +| `backend/sql/bot_x_seed.sql` | 초기 데이터 SQL | +| `backend/src/routes/admin/x-bots.js` | X 봇 CRUD API | +| `frontend/src/components/pc/admin/bot/XBotDialog.jsx` | 봇 추가/수정 다이얼로그 | + +### 수정 파일 +| 파일 | 변경 내용 | +|------|-----------| +| `backend/src/plugins/scheduler.js` | X 봇 DB 로드 추가 | +| `backend/src/routes/admin/bots.js` | X 봇 상세 정보 추가 | +| `backend/src/routes/index.js` | x-bots 라우트 등록 | +| `backend/src/config/bots.js` | X 봇 제거 (meilisearch만 남김) | +| `frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx` | X 섹션 canAdd, 다이얼로그 연결 | +| `frontend/src/components/pc/admin/bot/index.js` | XBotDialog export | +| `frontend/src/api/admin/bots.js` | X 봇 API 함수 추가 | + +--- + +## 6. 구현 순서 + +### 단계 1: DB 스키마 생성 +1. `backend/sql/bot_x.sql` 파일 생성 +2. `backend/sql/bot_x_seed.sql` 파일 생성 +3. DB에 테이블 생성 및 시드 데이터 삽입 + +### 단계 2: Nitter 프로필 조회 함수 추가 +1. `backend/src/services/x/scraper.js`에 `fetchProfile()` 함수 추가 + - Nitter에서 username으로 프로필 정보(displayName, avatarUrl) 조회 + +### 단계 3: X 봇 CRUD API 생성 +1. `backend/src/routes/admin/x-bots.js` 파일 생성 + - POST `/lookup`: 프로필 조회 + - GET `/`: 봇 목록 조회 + - GET `/:id`: 봇 상세 조회 + - POST `/`: 봇 추가 + - PUT `/:id`: 봇 수정 + - DELETE `/:id`: 봇 삭제 +2. `backend/src/routes/index.js`에 x-bots 라우트 등록 + +### 단계 4: 스케줄러 수정 +1. `backend/src/plugins/scheduler.js`에 `getXBotsFromDB()` 함수 추가 +2. `getBots()`에서 X 봇도 DB에서 로드하도록 수정 +3. `backend/src/config/bots.js`에서 X 봇 제거 + +### 단계 5: 기존 봇 API 수정 +1. `backend/src/routes/admin/bots.js` 스키마에 X 봇 필드 추가 + - `db_id`, `username`, `display_name`, `avatar_url` 등 + +### 단계 6: 프론트엔드 API 클라이언트 +1. `frontend/src/api/admin/bots.js`에 X 봇 API 함수 추가 + - `lookupXProfile`, `getXBot`, `createXBot`, `updateXBot`, `deleteXBot` + +### 단계 7: X 봇 다이얼로그 컴포넌트 생성 +1. `frontend/src/components/pc/admin/bot/XBotDialog.jsx` 생성 + - username 입력 → "조회" 버튼 → 프로필 정보 표시 + - 동기화 간격 선택 + - 추가/수정 폼 제출 +2. `frontend/src/components/pc/admin/bot/index.js`에 export 추가 + +### 단계 8: 봇 목록 페이지 수정 +1. `frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx` 수정 + - X 섹션에 `canAdd: true` 추가 + - XBotDialog 연결 + - 수정/삭제 핸들러 연결 + +--- + +## 7. 검증 방법 + +1. **DB 테이블 확인**: `SELECT * FROM bot_x` +2. **프로필 조회 API**: username 입력 → displayName, avatarUrl 반환 확인 +3. **봇 추가**: 새 X 계정 추가 후 목록에 표시, 스케줄러 동작 확인 +4. **봇 수정**: 동기화 간격 변경 후 cron 재등록 확인 +5. **봇 삭제**: 삭제 후 목록에서 제거, 스케줄러 중지 확인 diff --git a/docs/youtube-bots-plan.md b/docs/youtube-bots-plan.md deleted file mode 100644 index e368b66..0000000 --- a/docs/youtube-bots-plan.md +++ /dev/null @@ -1,232 +0,0 @@ -# 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. **예정 일정**: 설정된 요일에 예정 일정 자동 생성 확인