diff --git a/backend/sql/bot_x.sql b/backend/sql/bot_x.sql index 12e437b..0e388de 100644 --- a/backend/sql/bot_x.sql +++ b/backend/sql/bot_x.sql @@ -7,6 +7,8 @@ CREATE TABLE IF NOT EXISTS bot_x ( text_filters LONGTEXT, include_retweets TINYINT(1) DEFAULT 0, extract_youtube TINYINT(1) NOT NULL DEFAULT 0, + -- extract_youtube가 켜졌을 때, YouTube 봇으로 등록된 채널 영상이면 추가에서 제외 + exclude_managed_channels TINYINT(1) NOT NULL DEFAULT 1, cron_interval INT DEFAULT 1, enabled TINYINT(1) DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, diff --git a/backend/src/plugins/scheduler.js b/backend/src/plugins/scheduler.js index b65b293..07c20aa 100644 --- a/backend/src/plugins/scheduler.js +++ b/backend/src/plugins/scheduler.js @@ -92,6 +92,7 @@ async function schedulerPlugin(fastify, opts) { : [], includeRetweets: row.include_retweets === 1, extractYoutube: row.extract_youtube === 1, + excludeManagedChannels: row.exclude_managed_channels === 1, })); } diff --git a/backend/src/routes/admin/x-bots.js b/backend/src/routes/admin/x-bots.js index d1dbbfa..730b39a 100644 --- a/backend/src/routes/admin/x-bots.js +++ b/backend/src/routes/admin/x-bots.js @@ -16,6 +16,7 @@ const xBotResponse = { text_filters: { type: 'array', items: { type: 'string' } }, include_retweets: { type: 'boolean' }, extract_youtube: { type: 'boolean' }, + exclude_managed_channels: { type: 'boolean' }, cron_interval: { type: 'integer' }, enabled: { type: 'boolean' }, }, @@ -45,6 +46,7 @@ function formatBotResponse(row) { : [], include_retweets: row.include_retweets === 1, extract_youtube: row.extract_youtube === 1, + exclude_managed_channels: row.exclude_managed_channels === 1, cron_interval: row.cron_interval, enabled: row.enabled === 1, }; @@ -195,8 +197,8 @@ export default async function xBotsRoutes(fastify) { } const [result] = await db.query( - `INSERT INTO bot_x (username, display_name, avatar_url, text_filters, include_retweets, extract_youtube, cron_interval, enabled) - VALUES (?, ?, ?, ?, ?, ?, ?, 1)`, + `INSERT INTO bot_x (username, display_name, avatar_url, text_filters, include_retweets, extract_youtube, exclude_managed_channels, cron_interval, enabled) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)`, [ username, display_name || null, @@ -204,6 +206,7 @@ export default async function xBotsRoutes(fastify) { text_filters && text_filters.length > 0 ? JSON.stringify(text_filters) : null, include_retweets ? 1 : 0, extract_youtube ? 1 : 0, + exclude_managed_channels ? 1 : 0, cron_interval, ] ); @@ -222,6 +225,7 @@ export default async function xBotsRoutes(fastify) { textFilters: text_filters || [], includeRetweets: include_retweets, extractYoutube: extract_youtube, + excludeManagedChannels: exclude_managed_channels, }; // 전체 동기화 (async, 응답 대기하지 않음) @@ -264,6 +268,7 @@ export default async function xBotsRoutes(fastify) { text_filters: { type: ['array', 'null'], items: { type: 'string' } }, include_retweets: { type: 'boolean' }, extract_youtube: { type: 'boolean' }, + exclude_managed_channels: { type: 'boolean' }, cron_interval: { type: 'integer' }, enabled: { type: 'boolean' }, }, @@ -310,6 +315,10 @@ export default async function xBotsRoutes(fastify) { fields.push('extract_youtube = ?'); values.push(updates.extract_youtube ? 1 : 0); } + if (updates.exclude_managed_channels !== undefined) { + fields.push('exclude_managed_channels = ?'); + values.push(updates.exclude_managed_channels ? 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 822c5b5..090faf2 100644 --- a/backend/src/services/x/index.js +++ b/backend/src/services/x/index.js @@ -134,11 +134,11 @@ async function xBotPlugin(fastify, opts) { /** * 트윗에서 YouTube 링크 처리 */ - async function processYoutubeLinks(tweet) { + async function processYoutubeLinks(tweet, { excludeManagedChannels = true } = {}) { const videoIds = extractYoutubeVideoIds(tweet.text); if (videoIds.length === 0) return 0; - const managedChannels = await getManagedChannelIds(); + const managedChannels = excludeManagedChannels ? await getManagedChannelIds() : []; let addedCount = 0; for (const videoId of videoIds) { @@ -146,8 +146,8 @@ async function xBotPlugin(fastify, opts) { const video = await fetchVideoInfo(videoId); if (!video) continue; - // 관리 중인 채널이면 스킵 - if (managedChannels.includes(video.channelId)) continue; + // 옵션에 따라 관리 중인 채널 영상은 스킵 + if (excludeManagedChannels && managedChannels.includes(video.channelId)) continue; const scheduleId = await saveYoutubeFromTweet(video); if (scheduleId) { @@ -207,7 +207,9 @@ async function xBotPlugin(fastify, opts) { addedCount++; // YouTube 링크 처리 (옵션이 켜져 있을 때만) if (bot.extractYoutube === true) { - ytAddedCount += await processYoutubeLinks(tweet); + ytAddedCount += await processYoutubeLinks(tweet, { + excludeManagedChannels: bot.excludeManagedChannels !== false, + }); } } } @@ -238,7 +240,9 @@ async function xBotPlugin(fastify, opts) { addedCount++; // YouTube 링크 처리 (옵션이 켜져 있을 때만) if (bot.extractYoutube === true) { - ytAddedCount += await processYoutubeLinks(tweet); + ytAddedCount += await processYoutubeLinks(tweet, { + excludeManagedChannels: bot.excludeManagedChannels !== false, + }); } } } diff --git a/docs/api.md b/docs/api.md index 89e2253..6487695 100644 --- a/docs/api.md +++ b/docs/api.md @@ -426,6 +426,7 @@ X 봇 추가 | `text_filters` | string[]\|null | null | 텍스트 필터 (하나라도 포함 시 추가, 비어있으면 모든 트윗) | | `include_retweets` | boolean | false | 리트윗 포함 여부 | | `extract_youtube` | boolean | false | 트윗 내 YouTube 링크 자동 추출하여 유튜브 일정 추가 | +| `exclude_managed_channels` | boolean | true | `extract_youtube`가 true일 때, 등록된 YouTube 봇 채널의 영상은 중복 추가에서 제외 | | `cron_interval` | integer | 1 | 동기화 간격 (분) | ### PUT /admin/x-bots/:id diff --git a/frontend/src/components/pc/admin/bot/XBotDialog.jsx b/frontend/src/components/pc/admin/bot/XBotDialog.jsx index 45023cd..d877c39 100644 --- a/frontend/src/components/pc/admin/bot/XBotDialog.jsx +++ b/frontend/src/components/pc/admin/bot/XBotDialog.jsx @@ -134,6 +134,7 @@ function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) { const [filterInput, setFilterInput] = useState(''); const [includeRetweets, setIncludeRetweets] = useState(false); const [extractYoutube, setExtractYoutube] = useState(false); + const [excludeManagedChannels, setExcludeManagedChannels] = useState(true); // X 봇 상세 조회 (수정 모드) const { data: bot, isLoading: botLoading } = useQuery({ @@ -159,6 +160,7 @@ function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) { setTextFilters(bot.text_filters || []); setIncludeRetweets(bot.include_retweets || false); setExtractYoutube(bot.extract_youtube || false); + setExcludeManagedChannels(bot.exclude_managed_channels ?? true); setShowAdvanced((bot.text_filters && bot.text_filters.length > 0) || bot.include_retweets || bot.extract_youtube || false); } else if (!botId) { // 추가 모드 @@ -206,6 +208,7 @@ function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) { text_filters: textFilters.length > 0 ? textFilters : null, include_retweets: includeRetweets, extract_youtube: extractYoutube, + exclude_managed_channels: excludeManagedChannels, cron_interval: interval, }; @@ -399,6 +402,29 @@ function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) { + {/* 관리 중인 채널 제외 (extractYoutube 활성 시만) */} + {extractYoutube && ( +
등록된 YouTube 봇 채널의 영상은 트윗에서 중복 추가하지 않습니다
+