From f8acb5450f428c4ec41997f0b3c622568fadaf56 Mon Sep 17 00:00:00 2001 From: caadiq Date: Mon, 23 Feb 2026 15:28:27 +0900 Subject: [PATCH] =?UTF-8?q?fix(bot):=20=EB=8F=99=EC=8B=9C=EC=84=B1=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20INSERT=20=EC=8B=9C=20ER=5FDUP=5FENTRY=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=AC=B4=EC=8B=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit YouTube/X 봇의 영상 저장 트랜잭션에서 UNIQUE 제약 위반 발생 시 크래시 대신 null을 반환하여 gracefully 무시하도록 변경. Co-Authored-By: Claude Opus 4.6 --- backend/src/services/x/index.js | 34 +++++++------ backend/src/services/youtube/index.js | 71 +++++++++++++++------------ 2 files changed, 59 insertions(+), 46 deletions(-) diff --git a/backend/src/services/x/index.js b/backend/src/services/x/index.js index 88f3224..782065e 100644 --- a/backend/src/services/x/index.js +++ b/backend/src/services/x/index.js @@ -103,22 +103,28 @@ async function xBotPlugin(fastify, opts) { } // 트랜잭션으로 INSERT 작업 수행 - return withTransaction(fastify.db, async (connection) => { - // schedules 테이블에 저장 - const [result] = await connection.query( - 'INSERT INTO schedules (category_id, title, date, time) VALUES (?, ?, ?, ?)', - [YOUTUBE_CATEGORY_ID, video.title, video.date, video.time] - ); - const scheduleId = result.insertId; + try { + return await withTransaction(fastify.db, async (connection) => { + // schedules 테이블에 저장 + const [result] = await connection.query( + 'INSERT INTO schedules (category_id, title, date, time) VALUES (?, ?, ?, ?)', + [YOUTUBE_CATEGORY_ID, video.title, video.date, video.time] + ); + const scheduleId = result.insertId; - // schedule_youtube 테이블에 저장 - await connection.query( - 'INSERT INTO schedule_youtube (schedule_id, video_id, video_type, channel_id, channel_name) VALUES (?, ?, ?, ?, ?)', - [scheduleId, video.videoId, video.videoType, video.channelId, video.channelTitle] - ); + // schedule_youtube 테이블에 저장 + await connection.query( + 'INSERT INTO schedule_youtube (schedule_id, video_id, video_type, channel_id, channel_name) VALUES (?, ?, ?, ?, ?)', + [scheduleId, video.videoId, video.videoType, video.channelId, video.channelTitle] + ); - return scheduleId; - }); + return scheduleId; + }); + } catch (err) { + // UNIQUE 제약 위반 (동시성 중복) → 무시 + if (err.code === 'ER_DUP_ENTRY') return null; + throw err; + } } /** diff --git a/backend/src/services/youtube/index.js b/backend/src/services/youtube/index.js index 295deb1..51e6f08 100644 --- a/backend/src/services/youtube/index.js +++ b/backend/src/services/youtube/index.js @@ -276,42 +276,49 @@ async function youtubeBotPlugin(fastify) { } // 트랜잭션으로 INSERT 작업 수행 - const scheduleId = await withTransaction(fastify.db, async (connection) => { - // schedules 테이블에 저장 - const [result] = await connection.query( - 'INSERT INTO schedules (category_id, title, date, time) VALUES (?, ?, ?, ?)', - [YOUTUBE_CATEGORY_ID, video.title, video.date, video.time] - ); - const newScheduleId = result.insertId; + let scheduleId; + try { + scheduleId = await withTransaction(fastify.db, async (connection) => { + // schedules 테이블에 저장 + const [result] = await connection.query( + 'INSERT INTO schedules (category_id, title, date, time) VALUES (?, ?, ?, ?)', + [YOUTUBE_CATEGORY_ID, video.title, video.date, video.time] + ); + const newScheduleId = result.insertId; - // schedule_youtube 테이블에 저장 - await connection.query( - 'INSERT INTO schedule_youtube (schedule_id, video_id, video_type, channel_id, channel_name) VALUES (?, ?, ?, ?, ?)', - [newScheduleId, video.videoId, video.videoType, video.channelId, bot.channelName] - ); + // schedule_youtube 테이블에 저장 + await connection.query( + 'INSERT INTO schedule_youtube (schedule_id, video_id, video_type, channel_id, channel_name) VALUES (?, ?, ?, ?, ?)', + [newScheduleId, video.videoId, video.videoType, video.channelId, bot.channelName] + ); - // 멤버 연결 (커스텀 설정) - const hasDefaultMembers = bot.defaultMemberIds && bot.defaultMemberIds.length > 0; - if (hasDefaultMembers || bot.extractMembersFromDesc) { - const memberIds = []; - if (hasDefaultMembers) { - memberIds.push(...bot.defaultMemberIds); + // 멤버 연결 (커스텀 설정) + const hasDefaultMembers = bot.defaultMemberIds && bot.defaultMemberIds.length > 0; + if (hasDefaultMembers || bot.extractMembersFromDesc) { + const memberIds = []; + if (hasDefaultMembers) { + memberIds.push(...bot.defaultMemberIds); + } + if (nameMap) { + memberIds.push(...extractMemberIds(video.description, nameMap)); + } + if (memberIds.length > 0) { + const uniqueIds = [...new Set(memberIds)]; + const values = uniqueIds.map(id => [newScheduleId, id]); + await connection.query( + 'INSERT INTO schedule_members (schedule_id, member_id) VALUES ?', + [values] + ); + } } - if (nameMap) { - memberIds.push(...extractMemberIds(video.description, nameMap)); - } - if (memberIds.length > 0) { - const uniqueIds = [...new Set(memberIds)]; - const values = uniqueIds.map(id => [newScheduleId, id]); - await connection.query( - 'INSERT INTO schedule_members (schedule_id, member_id) VALUES ?', - [values] - ); - } - } - return newScheduleId; - }); + return newScheduleId; + }); + } catch (err) { + // UNIQUE 제약 위반 (동시성 중복) → 무시 + if (err.code === 'ER_DUP_ENTRY') return null; + throw err; + } // 새 영상 추가 후 다음 주 예정 일정 생성 (쇼츠 제외) if (autoScheduleNext && isVideoType && scheduleId) {