fix(bot): 동시성 중복 INSERT 시 ER_DUP_ENTRY 에러 무시 처리

YouTube/X 봇의 영상 저장 트랜잭션에서 UNIQUE 제약 위반 발생 시
크래시 대신 null을 반환하여 gracefully 무시하도록 변경.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-02-23 15:28:27 +09:00
parent 3feb23f67f
commit f8acb5450f
2 changed files with 59 additions and 46 deletions

View file

@ -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;
}
}
/**

View file

@ -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) {