refactor(db): 봇 테이블 이름 통일 및 X 봇 스키마 추가
- youtube_bots → bot_youtube, x_bots → bot_x로 테이블 이름 변경 - bot_x 테이블 생성 및 시드 데이터 추가 - 관련 백엔드 코드에서 테이블 참조 업데이트 - X 봇 동적 관리 구현 계획 문서 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2e7fe697fc
commit
4f11e14b12
11 changed files with 288 additions and 250 deletions
12
backend/sql/bot_x.sql
Normal file
12
backend/sql/bot_x.sql
Normal file
|
|
@ -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;
|
||||||
6
backend/sql/bot_x_seed.sql
Normal file
6
backend/sql/bot_x_seed.sql
Normal file
|
|
@ -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);
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
-- YouTube 봇 테이블
|
-- YouTube 봇 테이블
|
||||||
CREATE TABLE IF NOT EXISTS youtube_bots (
|
CREATE TABLE IF NOT EXISTS bot_youtube (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
channel_id VARCHAR(30) NOT NULL,
|
channel_id VARCHAR(30) NOT NULL,
|
||||||
channel_handle VARCHAR(50),
|
channel_handle VARCHAR(50),
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
-- YouTube 봇 시드 데이터
|
-- YouTube 봇 시드 데이터
|
||||||
-- channel_handle은 봇 추가 시 YouTube API로 조회하여 저장
|
-- 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),
|
('UCXbRURMKT3H_w8dT-DWLIxA', 'fromis_9', 2, 1),
|
||||||
('UCtfyAiqf095_0_ux8ruwGfA', 'MUSINSA TV', 2, 1),
|
('UCtfyAiqf095_0_ux8ruwGfA', 'MUSINSA TV', 2, 1),
|
||||||
('UCeUJ8B3krxw8zuDi19AlhaA', '스프 : 스튜디오 프로미스나인', 2, 1)
|
('UCeUJ8B3krxw8zuDi19AlhaA', '스프 : 스튜디오 프로미스나인', 2, 1)
|
||||||
ON DUPLICATE KEY UPDATE channel_name = VALUES(channel_name);
|
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}'
|
SET auto_schedule_config = '{"dayOfWeek":4,"time":"18:00:00","titleTemplate":"{channelName} {episode}화","deadlineDayOfWeek":5,"excludeShorts":true}'
|
||||||
WHERE channel_id = 'UCeUJ8B3krxw8zuDi19AlhaA';
|
WHERE channel_id = 'UCeUJ8B3krxw8zuDi19AlhaA';
|
||||||
|
|
||||||
-- MUSINSA TV - 필터/멤버 설정
|
-- MUSINSA TV - 필터/멤버 설정
|
||||||
UPDATE youtube_bots
|
UPDATE bot_youtube
|
||||||
SET title_filters = '["성수기"]',
|
SET title_filters = '["성수기"]',
|
||||||
default_member_ids = '[7]',
|
default_member_ids = '[7]',
|
||||||
extract_members_from_desc = 1
|
extract_members_from_desc = 1
|
||||||
|
|
@ -16,7 +16,7 @@ async function schedulerPlugin(fastify, opts) {
|
||||||
*/
|
*/
|
||||||
async function getYouTubeBotsFromDB() {
|
async function getYouTubeBotsFromDB() {
|
||||||
const [rows] = await fastify.db.query(
|
const [rows] = await fastify.db.query(
|
||||||
'SELECT * FROM youtube_bots WHERE enabled = 1'
|
'SELECT * FROM bot_youtube WHERE enabled = 1'
|
||||||
);
|
);
|
||||||
return rows.map(row => ({
|
return rows.map(row => ({
|
||||||
id: `youtube-${row.id}`, // DB ID를 문자열 형식으로 변환
|
id: `youtube-${row.id}`, // DB ID를 문자열 형식으로 변환
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ export default async function youtubeBotsRoutes(fastify) {
|
||||||
},
|
},
|
||||||
preHandler: [fastify.authenticate],
|
preHandler: [fastify.authenticate],
|
||||||
}, async (request, reply) => {
|
}, 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);
|
return rows.map(formatBotResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -150,7 +150,7 @@ export default async function youtubeBotsRoutes(fastify) {
|
||||||
preHandler: [fastify.authenticate],
|
preHandler: [fastify.authenticate],
|
||||||
}, async (request, reply) => {
|
}, async (request, reply) => {
|
||||||
const { id } = request.params;
|
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) {
|
if (rows.length === 0) {
|
||||||
return notFound(reply, 'YouTube 봇을 찾을 수 없습니다.');
|
return notFound(reply, 'YouTube 봇을 찾을 수 없습니다.');
|
||||||
|
|
@ -204,7 +204,7 @@ export default async function youtubeBotsRoutes(fastify) {
|
||||||
|
|
||||||
// 중복 체크
|
// 중복 체크
|
||||||
const [existing] = await db.query(
|
const [existing] = await db.query(
|
||||||
'SELECT id FROM youtube_bots WHERE channel_id = ?',
|
'SELECT id FROM bot_youtube WHERE channel_id = ?',
|
||||||
[channel_id]
|
[channel_id]
|
||||||
);
|
);
|
||||||
if (existing.length > 0) {
|
if (existing.length > 0) {
|
||||||
|
|
@ -212,7 +212,7 @@ export default async function youtubeBotsRoutes(fastify) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [result] = await db.query(
|
const [result] = await db.query(
|
||||||
`INSERT INTO youtube_bots
|
`INSERT INTO bot_youtube
|
||||||
(channel_id, channel_handle, channel_name, banner_url, cron_interval,
|
(channel_id, channel_handle, channel_name, banner_url, cron_interval,
|
||||||
title_filters, default_member_ids, extract_members_from_desc, auto_schedule_config, enabled)
|
title_filters, default_member_ids, extract_members_from_desc, auto_schedule_config, enabled)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`,
|
||||||
|
|
@ -238,7 +238,7 @@ export default async function youtubeBotsRoutes(fastify) {
|
||||||
fastify.log.error(`[${botId}] 봇 시작 실패:`, err);
|
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);
|
reply.code(201);
|
||||||
return formatBotResponse(newBot[0]);
|
return formatBotResponse(newBot[0]);
|
||||||
});
|
});
|
||||||
|
|
@ -278,7 +278,7 @@ export default async function youtubeBotsRoutes(fastify) {
|
||||||
const updates = request.body;
|
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) {
|
if (existing.length === 0) {
|
||||||
return notFound(reply, 'YouTube 봇을 찾을 수 없습니다.');
|
return notFound(reply, 'YouTube 봇을 찾을 수 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
@ -327,7 +327,7 @@ export default async function youtubeBotsRoutes(fastify) {
|
||||||
if (fields.length > 0) {
|
if (fields.length > 0) {
|
||||||
values.push(id);
|
values.push(id);
|
||||||
await db.query(
|
await db.query(
|
||||||
`UPDATE youtube_bots SET ${fields.join(', ')} WHERE id = ?`,
|
`UPDATE bot_youtube SET ${fields.join(', ')} WHERE id = ?`,
|
||||||
values
|
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]);
|
return formatBotResponse(updatedBot[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -375,7 +375,7 @@ export default async function youtubeBotsRoutes(fastify) {
|
||||||
const { id } = request.params;
|
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) {
|
if (existing.length === 0) {
|
||||||
return notFound(reply, 'YouTube 봇을 찾을 수 없습니다.');
|
return notFound(reply, 'YouTube 봇을 찾을 수 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
@ -389,7 +389,7 @@ export default async function youtubeBotsRoutes(fastify) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DB에서 삭제
|
// DB에서 삭제
|
||||||
await db.query('DELETE FROM youtube_bots WHERE id = ?', [id]);
|
await db.query('DELETE FROM bot_youtube WHERE id = ?', [id]);
|
||||||
|
|
||||||
// 스케줄러 캐시 무효화
|
// 스케줄러 캐시 무효화
|
||||||
scheduler.invalidateCache();
|
scheduler.invalidateCache();
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ export default async function schedulesRoutes(fastify) {
|
||||||
const [youtubeData] = await db.query(
|
const [youtubeData] = await db.query(
|
||||||
`SELECT sy.channel_id, yb.banner_url
|
`SELECT sy.channel_id, yb.banner_url
|
||||||
FROM schedule_youtube sy
|
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 = ?`,
|
WHERE sy.schedule_id = ?`,
|
||||||
[request.params.id]
|
[request.params.id]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ async function xBotPlugin(fastify, opts) {
|
||||||
*/
|
*/
|
||||||
async function getManagedChannelIds() {
|
async function getManagedChannelIds() {
|
||||||
const [rows] = await fastify.db.query(
|
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);
|
return rows.map(r => r.channel_id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -369,7 +369,7 @@ async function youtubeBotPlugin(fastify) {
|
||||||
*/
|
*/
|
||||||
async function getManagedChannelIds() {
|
async function getManagedChannelIds() {
|
||||||
const [rows] = await fastify.db.query(
|
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);
|
return rows.map(r => r.channel_id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
252
docs/x-bots-plan.md
Normal file
252
docs/x-bots-plan.md
Normal file
|
|
@ -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. **봇 삭제**: 삭제 후 목록에서 제거, 스케줄러 중지 확인
|
||||||
|
|
@ -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. **예정 일정**: 설정된 요일에 예정 일정 자동 생성 확인
|
|
||||||
Loading…
Add table
Reference in a new issue