From ba7def935c3977a16e996898b5de563b351730d0 Mon Sep 17 00:00:00 2001 From: caadiq Date: Sun, 8 Feb 2026 22:40:52 +0900 Subject: [PATCH] =?UTF-8?q?feat(x-bot):=20YouTube=20=EC=98=81=EC=83=81=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=20=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X 봇 설정에서 트윗 내 YouTube 링크 자동 추출 기능을 온/오프 가능하게 함: - bot_x 테이블에 extract_youtube 컬럼 추가 (기본값: false) - 고급 설정에 "YouTube 영상 추출" 토글 추가 - extractYoutube가 true일 때만 YouTube 일정 자동 생성 Co-Authored-By: Claude Opus 4.5 --- backend/src/plugins/scheduler.js | 1 + backend/src/routes/admin/x-bots.js | 15 +++++++++-- backend/src/services/x/index.js | 11 +++++--- .../components/pc/admin/bot/XBotDialog.jsx | 27 ++++++++++++++++++- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/backend/src/plugins/scheduler.js b/backend/src/plugins/scheduler.js index dc14e25..c7e485a 100644 --- a/backend/src/plugins/scheduler.js +++ b/backend/src/plugins/scheduler.js @@ -70,6 +70,7 @@ async function schedulerPlugin(fastify, opts) { : row.text_filters) : [], includeRetweets: row.include_retweets === 1, + extractYoutube: row.extract_youtube === 1, })); } diff --git a/backend/src/routes/admin/x-bots.js b/backend/src/routes/admin/x-bots.js index d2f873a..e1b5d90 100644 --- a/backend/src/routes/admin/x-bots.js +++ b/backend/src/routes/admin/x-bots.js @@ -14,6 +14,7 @@ const xBotResponse = { avatar_url: { type: 'string' }, text_filters: { type: 'array', items: { type: 'string' } }, include_retweets: { type: 'boolean' }, + extract_youtube: { type: 'boolean' }, cron_interval: { type: 'integer' }, enabled: { type: 'boolean' }, }, @@ -42,6 +43,7 @@ function formatBotResponse(row) { : row.text_filters) : [], include_retweets: row.include_retweets === 1, + extract_youtube: row.extract_youtube === 1, cron_interval: row.cron_interval, enabled: row.enabled === 1, }; @@ -160,6 +162,7 @@ export default async function xBotsRoutes(fastify) { avatar_url: { type: ['string', 'null'] }, text_filters: { type: ['array', 'null'], items: { type: 'string' } }, include_retweets: { type: 'boolean', default: false }, + extract_youtube: { type: 'boolean', default: false }, cron_interval: { type: 'integer', default: 1 }, }, required: ['username'], @@ -177,6 +180,7 @@ export default async function xBotsRoutes(fastify) { avatar_url, text_filters, include_retweets = false, + extract_youtube = false, cron_interval = 1, } = request.body; @@ -190,14 +194,15 @@ export default async function xBotsRoutes(fastify) { } const [result] = await db.query( - `INSERT INTO bot_x (username, display_name, avatar_url, text_filters, include_retweets, cron_interval, enabled) - VALUES (?, ?, ?, ?, ?, ?, 1)`, + `INSERT INTO bot_x (username, display_name, avatar_url, text_filters, include_retweets, extract_youtube, cron_interval, enabled) + VALUES (?, ?, ?, ?, ?, ?, ?, 1)`, [ username, display_name || null, avatar_url || null, text_filters && text_filters.length > 0 ? JSON.stringify(text_filters) : null, include_retweets ? 1 : 0, + extract_youtube ? 1 : 0, cron_interval, ] ); @@ -215,6 +220,7 @@ export default async function xBotsRoutes(fastify) { nitterUrl: process.env.NITTER_URL || 'http://nitter:8080', textFilters: text_filters || [], includeRetweets: include_retweets, + extractYoutube: extract_youtube, }; // 전체 동기화 (async, 응답 대기하지 않음) @@ -255,6 +261,7 @@ export default async function xBotsRoutes(fastify) { avatar_url: { type: ['string', 'null'] }, text_filters: { type: ['array', 'null'], items: { type: 'string' } }, include_retweets: { type: 'boolean' }, + extract_youtube: { type: 'boolean' }, cron_interval: { type: 'integer' }, enabled: { type: 'boolean' }, }, @@ -297,6 +304,10 @@ export default async function xBotsRoutes(fastify) { fields.push('include_retweets = ?'); values.push(updates.include_retweets ? 1 : 0); } + if (updates.extract_youtube !== undefined) { + fields.push('extract_youtube = ?'); + values.push(updates.extract_youtube ? 1 : 0); + } if (updates.cron_interval !== undefined) { fields.push('cron_interval = ?'); values.push(updates.cron_interval); diff --git a/backend/src/services/x/index.js b/backend/src/services/x/index.js index b228d35..88f3224 100644 --- a/backend/src/services/x/index.js +++ b/backend/src/services/x/index.js @@ -186,8 +186,10 @@ async function xBotPlugin(fastify, opts) { // Meilisearch 동기화 await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId); addedCount++; - // YouTube 링크 처리 - ytAddedCount += await processYoutubeLinks(tweet); + // YouTube 링크 처리 (옵션이 켜져 있을 때만) + if (bot.extractYoutube === true) { + ytAddedCount += await processYoutubeLinks(tweet); + } } } @@ -215,7 +217,10 @@ async function xBotPlugin(fastify, opts) { // Meilisearch 동기화 await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId); addedCount++; - ytAddedCount += await processYoutubeLinks(tweet); + // YouTube 링크 처리 (옵션이 켜져 있을 때만) + if (bot.extractYoutube === true) { + ytAddedCount += await processYoutubeLinks(tweet); + } } } diff --git a/frontend/src/components/pc/admin/bot/XBotDialog.jsx b/frontend/src/components/pc/admin/bot/XBotDialog.jsx index a74e52c..45023cd 100644 --- a/frontend/src/components/pc/admin/bot/XBotDialog.jsx +++ b/frontend/src/components/pc/admin/bot/XBotDialog.jsx @@ -133,6 +133,7 @@ function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) { const [textFilters, setTextFilters] = useState([]); const [filterInput, setFilterInput] = useState(''); const [includeRetweets, setIncludeRetweets] = useState(false); + const [extractYoutube, setExtractYoutube] = useState(false); // X 봇 상세 조회 (수정 모드) const { data: bot, isLoading: botLoading } = useQuery({ @@ -157,7 +158,8 @@ function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) { setInterval(bot.cron_interval || 1); setTextFilters(bot.text_filters || []); setIncludeRetweets(bot.include_retweets || false); - setShowAdvanced((bot.text_filters && bot.text_filters.length > 0) || bot.include_retweets || false); + setExtractYoutube(bot.extract_youtube || false); + setShowAdvanced((bot.text_filters && bot.text_filters.length > 0) || bot.include_retweets || bot.extract_youtube || false); } else if (!botId) { // 추가 모드 setUsername(''); @@ -166,6 +168,7 @@ function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) { setTextFilters([]); setFilterInput(''); setIncludeRetweets(false); + setExtractYoutube(false); setShowAdvanced(false); } }, [isOpen, bot, botId]); @@ -202,6 +205,7 @@ function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) { avatar_url: profileInfo.avatarUrl, text_filters: textFilters.length > 0 ? textFilters : null, include_retweets: includeRetweets, + extract_youtube: extractYoutube, cron_interval: interval, }; @@ -374,6 +378,27 @@ function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) { + {/* YouTube 영상 추출 */} +
+
+ +

트윗에 YouTube 링크가 있으면 유튜브 일정에 추가합니다

+
+ +
+ {/* 텍스트 필터 */}