diff --git a/backend/sql/bot_youtube.sql b/backend/sql/bot_youtube.sql index 54e0c68..d06718e 100644 --- a/backend/sql/bot_youtube.sql +++ b/backend/sql/bot_youtube.sql @@ -14,6 +14,7 @@ CREATE TABLE IF NOT EXISTS bot_youtube ( -- 멤버 설정 (선택) default_member_ids JSON, extract_members_from_desc TINYINT(1) DEFAULT 0, + extract_members_from_title TINYINT(1) DEFAULT 0, -- 다음 주 예정 일정 설정 (JSON) auto_schedule_config JSON, diff --git a/backend/src/plugins/scheduler.js b/backend/src/plugins/scheduler.js index 07c20aa..e2ec83d 100644 --- a/backend/src/plugins/scheduler.js +++ b/backend/src/plugins/scheduler.js @@ -58,6 +58,7 @@ async function schedulerPlugin(fastify, opts) { : row.default_member_ids) : [], extractMembersFromDesc: row.extract_members_from_desc === 1, + extractMembersFromTitle: row.extract_members_from_title === 1, autoScheduleNext: row.auto_schedule_config ? (typeof row.auto_schedule_config === 'string' ? JSON.parse(row.auto_schedule_config) diff --git a/backend/src/routes/admin/youtube-bots.js b/backend/src/routes/admin/youtube-bots.js index 7498d76..1158f84 100644 --- a/backend/src/routes/admin/youtube-bots.js +++ b/backend/src/routes/admin/youtube-bots.js @@ -19,6 +19,7 @@ const youtubeBotResponse = { title_filters: { type: 'array', items: { type: 'string' } }, default_member_ids: { type: 'array', items: { type: 'integer' } }, extract_members_from_desc: { type: 'boolean' }, + extract_members_from_title: { type: 'boolean' }, auto_schedule_config: { type: ['object', 'null'], additionalProperties: true }, weekly_schedule_config: { type: ['object', 'null'], additionalProperties: true }, }, @@ -55,6 +56,7 @@ function formatBotResponse(row) { : row.default_member_ids) : [], extract_members_from_desc: row.extract_members_from_desc === 1, + extract_members_from_title: row.extract_members_from_title === 1, auto_schedule_config: row.auto_schedule_config ? (typeof row.auto_schedule_config === 'string' ? JSON.parse(row.auto_schedule_config) @@ -186,6 +188,7 @@ export default async function youtubeBotsRoutes(fastify) { title_filters: { type: ['array', 'null'], items: { type: 'string' } }, default_member_ids: { type: ['array', 'null'], items: { type: 'integer' } }, extract_members_from_desc: { type: 'boolean', default: false }, + extract_members_from_title: { type: 'boolean', default: false }, auto_schedule_config: { type: ['object', 'null'], additionalProperties: true }, weekly_schedule_config: { type: ['object', 'null'], additionalProperties: true }, }, @@ -207,6 +210,7 @@ export default async function youtubeBotsRoutes(fastify) { title_filters, default_member_ids, extract_members_from_desc = false, + extract_members_from_title = false, auto_schedule_config, weekly_schedule_config, } = request.body; @@ -226,9 +230,9 @@ export default async function youtubeBotsRoutes(fastify) { const [result] = await db.query( `INSERT INTO bot_youtube (channel_id, channel_handle, channel_name, banner_url, cron_interval, - title_filters, default_member_ids, extract_members_from_desc, + title_filters, default_member_ids, extract_members_from_desc, extract_members_from_title, auto_schedule_config, weekly_schedule_config, enabled) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`, + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`, [ channel_id, channel_handle || null, @@ -238,6 +242,7 @@ export default async function youtubeBotsRoutes(fastify) { title_filters ? JSON.stringify(title_filters) : null, default_member_ids ? JSON.stringify(default_member_ids) : null, extract_members_from_desc ? 1 : 0, + extract_members_from_title ? 1 : 0, auto_schedule_config ? JSON.stringify(auto_schedule_config) : null, weekly_schedule_config ? JSON.stringify(weekly_schedule_config) : null, ] @@ -278,6 +283,7 @@ export default async function youtubeBotsRoutes(fastify) { title_filters: { type: ['array', 'null'], items: { type: 'string' } }, default_member_ids: { type: ['array', 'null'], items: { type: 'integer' } }, extract_members_from_desc: { type: 'boolean' }, + extract_members_from_title: { type: 'boolean' }, auto_schedule_config: { type: ['object', 'null'], additionalProperties: true }, weekly_schedule_config: { type: ['object', 'null'], additionalProperties: true }, enabled: { type: 'boolean' }, @@ -331,6 +337,10 @@ export default async function youtubeBotsRoutes(fastify) { fields.push('extract_members_from_desc = ?'); values.push(updates.extract_members_from_desc ? 1 : 0); } + if (updates.extract_members_from_title !== undefined) { + fields.push('extract_members_from_title = ?'); + values.push(updates.extract_members_from_title ? 1 : 0); + } if (updates.auto_schedule_config !== undefined) { fields.push('auto_schedule_config = ?'); values.push(updates.auto_schedule_config ? JSON.stringify(updates.auto_schedule_config) : null); diff --git a/backend/src/services/youtube/index.js b/backend/src/services/youtube/index.js index 01ccbd9..a77bf4b 100644 --- a/backend/src/services/youtube/index.js +++ b/backend/src/services/youtube/index.js @@ -272,7 +272,7 @@ async function youtubeBotPlugin(fastify) { // 멤버 이름 맵 미리 조회 (트랜잭션 전에) let nameMap = null; - if (bot.extractMembersFromDesc) { + if (bot.extractMembersFromDesc || bot.extractMembersFromTitle) { nameMap = await getMemberNameMap(); } @@ -295,14 +295,17 @@ async function youtubeBotPlugin(fastify) { // 멤버 연결 (커스텀 설정) const hasDefaultMembers = bot.defaultMemberIds && bot.defaultMemberIds.length > 0; - if (hasDefaultMembers || bot.extractMembersFromDesc) { + if (hasDefaultMembers || bot.extractMembersFromDesc || bot.extractMembersFromTitle) { const memberIds = []; if (hasDefaultMembers) { memberIds.push(...bot.defaultMemberIds); } - if (nameMap) { + if (nameMap && bot.extractMembersFromDesc) { memberIds.push(...extractMemberIds(video.description, nameMap)); } + if (nameMap && bot.extractMembersFromTitle) { + memberIds.push(...extractMemberIds(video.title, nameMap)); + } if (memberIds.length > 0) { const uniqueIds = [...new Set(memberIds)]; const values = uniqueIds.map(id => [newScheduleId, id]); diff --git a/docs/api.md b/docs/api.md index 6487695..a9c59fe 100644 --- a/docs/api.md +++ b/docs/api.md @@ -331,6 +331,7 @@ YouTube 봇 추가 "title_filters": ["fromis_9", "프로미스나인"], "default_member_ids": [1, 2], "extract_members_from_desc": true, + "extract_members_from_title": false, "auto_schedule_config": { "dayOfWeek": 4, "time": "18:00:00", diff --git a/frontend/src/components/pc/admin/bot/YouTubeBotDialog.jsx b/frontend/src/components/pc/admin/bot/YouTubeBotDialog.jsx index 83ac05c..be3e7f3 100644 --- a/frontend/src/components/pc/admin/bot/YouTubeBotDialog.jsx +++ b/frontend/src/components/pc/admin/bot/YouTubeBotDialog.jsx @@ -300,6 +300,7 @@ function YouTubeBotDialog({ isOpen, onClose, botId = null, onSuccess }) { const [filterInput, setFilterInput] = useState(''); const [defaultMemberIds, setDefaultMemberIds] = useState([]); const [extractMembers, setExtractMembers] = useState(false); + const [extractMembersFromTitle, setExtractMembersFromTitle] = useState(false); // 멤버 목록 (탈퇴 멤버 제외) const [members, setMembers] = useState([]); @@ -381,11 +382,13 @@ function YouTubeBotDialog({ isOpen, onClose, botId = null, onSuccess }) { setTitleFilters(bot.title_filters || []); setDefaultMemberIds(bot.default_member_ids || []); setExtractMembers(bot.extract_members_from_desc || false); + setExtractMembersFromTitle(bot.extract_members_from_title || false); // 고급 설정이 있으면 펼침 if ((bot.title_filters && bot.title_filters.length > 0) || (bot.default_member_ids && bot.default_member_ids.length > 0) || - bot.extract_members_from_desc) { + bot.extract_members_from_desc || + bot.extract_members_from_title) { setShowAdvanced(true); } else { setShowAdvanced(false); @@ -410,6 +413,7 @@ function YouTubeBotDialog({ isOpen, onClose, botId = null, onSuccess }) { setFilterInput(''); setDefaultMemberIds([]); setExtractMembers(false); + setExtractMembersFromTitle(false); } }, [isOpen, bot, botId]); @@ -447,6 +451,7 @@ function YouTubeBotDialog({ isOpen, onClose, botId = null, onSuccess }) { title_filters: titleFilters.length > 0 ? titleFilters : null, default_member_ids: defaultMemberIds.length > 0 ? defaultMemberIds : null, extract_members_from_desc: extractMembers, + extract_members_from_title: extractMembersFromTitle, auto_schedule_config: autoScheduleEnabled ? { dayOfWeek: scheduleDayOfWeek, @@ -848,6 +853,28 @@ function YouTubeBotDialog({ isOpen, onClose, botId = null, onSuccess }) { /> + + {/* 제목에서 멤버 추출 */} +
제목에서 멤버 추출
+영상 제목에서 멤버 이름을 찾아 자동 연결
+