From 1a4652c185f30d19e29bb449bb93241a560fc2cc Mon Sep 17 00:00:00 2001 From: mineseo-kim Date: Thu, 9 Apr 2026 09:09:32 +0900 Subject: [PATCH] Fix slash command timeouts: defer before locale DB reads - interactionCreate: deferReply(ephemeral) immediately for chat commands, then getInteractionLocale. - All slash handlers use editReply; remove redundant deferReply in audit/fishing/music. - localeHelper: parallelize user/guild Prisma lookups for getInteractionLocale and getContextLocale. Note: deferred replies are ephemeral; public-only responses (e.g. music queue) are now user-visible only. Made-with: Cursor --- src/commands/audit.ts | 12 ++--- src/commands/config.ts | 10 ++-- src/commands/event.ts | 34 +++++------- src/commands/fishing.ts | 6 --- src/commands/language.ts | 5 +- src/commands/minigame.ts | 6 +-- src/commands/music.ts | 91 +++++++++++---------------------- src/commands/refine.ts | 30 +++++------ src/commands/setup.ts | 3 +- src/commands/voice.ts | 25 ++++----- src/events/interactionCreate.ts | 2 + src/i18n/localeHelper.ts | 58 ++++++++++----------- 12 files changed, 111 insertions(+), 171 deletions(-) diff --git a/src/commands/audit.ts b/src/commands/audit.ts index c680994..7ae9409 100644 --- a/src/commands/audit.ts +++ b/src/commands/audit.ts @@ -118,9 +118,8 @@ export default { if (action === 'set') { const channel = interaction.options.getChannel('channel') as TextChannel; - if (!channel) return interaction.reply({ content: '❌ `channel` 옵션을 선택해주세요.', ephemeral: true }); + if (!channel) return interaction.editReply({ content: '❌ `channel` 옵션을 선택해주세요.' }); - await interaction.deferReply({ ephemeral: true }); const botMember = guild.members.me; if (!botMember) return; const perms = channel.permissionsFor(botMember); @@ -141,13 +140,11 @@ export default { } if (action === 'clear') { - await interaction.deferReply({ ephemeral: true }); await auditLogService.clearChannel(guild.id); return interaction.editReply({ content: `✅ 감사 채널 설정이 해제되었습니다.` }); } if (action === 'status') { - await interaction.deferReply({ ephemeral: true }); const config = await auditLogService.getChannel(guild.id); if (!config) return interaction.editReply({ content: `설정된 감사 채널이 없습니다. 먼저 \`/audit channel action:set\` 명령어로 채널을 설정해주세요.` }); @@ -168,10 +165,9 @@ export default { const enable = interaction.options.getBoolean('enable'); if (!category || enable === null) { - return interaction.reply({ content: '❌ `category` 및 `enable` 옵션을 모두 입력해주세요.', ephemeral: true }); + return interaction.editReply({ content: '❌ `category` 및 `enable` 옵션을 모두 입력해주세요.' }); } - await interaction.deferReply({ ephemeral: true }); const config = await auditLogService.getChannel(guild.id); if (!config) return interaction.editReply({ content: `설정된 감사 채널이 없습니다. 먼저 \`/audit channel action:set\` 명령어로 채널을 설정해주세요.` }); @@ -185,7 +181,6 @@ export default { const action = interaction.options.getString('action', true); if (action === 'permissions') { - await interaction.deferReply({ ephemeral: true }); const results = await PermissionAuditService.auditGuild(guild); if (results.length === 0) return interaction.editReply({ content: t(locale, 'commands.permissionAudit.noResults') }); @@ -230,8 +225,7 @@ export default { } } catch (error) { console.error('Error in audit command', error); - const reply = interaction.deferred ? interaction.editReply : interaction.reply; - return (reply as any)({ content: '명령 실행 중 오류가 발생했습니다.', ephemeral: true }); + return interaction.editReply({ content: '명령 실행 중 오류가 발생했습니다.' }); } }, }; diff --git a/src/commands/config.ts b/src/commands/config.ts index 3d687c2..66d7c42 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -76,14 +76,13 @@ export default { if (action === 'language') { const newLocale = interaction.options.getString('locale') as SupportedLocale; if (!newLocale) { - return interaction.reply({ + return interaction.editReply({ content: '❌ `locale` 옵션을 선택해주세요.', - ephemeral: true }); } if (!SUPPORTED_LOCALES.includes(newLocale)) { - return interaction.reply({ content: `Unsupported locale: ${newLocale}`, ephemeral: true }); + return interaction.editReply({ content: `Unsupported locale: ${newLocale}` }); } await prisma.guildConfig.upsert({ @@ -92,9 +91,8 @@ export default { create: { guildId: interaction.guildId, locale: newLocale }, }); - return interaction.reply({ + return interaction.editReply({ content: t(newLocale, 'commands.language.serverSet', { locale: newLocale === 'ko' ? '한국어' : 'English' }), - ephemeral: true, }); } } @@ -132,7 +130,7 @@ export default { .setTitle(t(locale, 'commands.config.title')) .setDescription(`${label}: **${state}**`); - return interaction.reply({ embeds: [embed], ephemeral: true }); + return interaction.editReply({ embeds: [embed] }); } }, }; diff --git a/src/commands/event.ts b/src/commands/event.ts index 7986e4c..411ba87 100644 --- a/src/commands/event.ts +++ b/src/commands/event.ts @@ -201,26 +201,23 @@ export default { const startsAt = parseSeoulDateTime(date, time); if (!startsAt) { - await interaction.reply({ + await interaction.editReply({ content: `${t(locale, 'commands.event.invalidDateTime')}\n${t(locale, 'commands.event.invalidDateTimeResolution')}`, - ephemeral: true, }); return; } if (startsAt.getTime() <= Date.now()) { - await interaction.reply({ + await interaction.editReply({ content: `${t(locale, 'commands.event.invalidPastDateTime')}\n${t(locale, 'commands.event.invalidPastDateTimeResolution')}`, - ephemeral: true, }); return; } const reminderOffsets = parseReminderOffsets(reminderRaw); if (!reminderOffsets) { - await interaction.reply({ + await interaction.editReply({ content: `${t(locale, 'commands.event.invalidReminderOffsets')}\n${t(locale, 'commands.event.invalidReminderOffsetsResolution')}`, - ephemeral: true, }); return; } @@ -252,7 +249,7 @@ export default { ) .setTimestamp(); - await interaction.reply({ embeds: [embed], ephemeral: true }); + await interaction.editReply({ embeds: [embed] }); return; } @@ -268,9 +265,8 @@ export default { }); if (events.length === 0) { - await interaction.reply({ + await interaction.editReply({ content: t(locale, 'commands.event.listEmpty'), - ephemeral: true, }); return; } @@ -295,7 +291,7 @@ export default { }); } - await interaction.reply({ embeds: [embed], ephemeral: true }); + await interaction.editReply({ embeds: [embed] }); return; } @@ -311,9 +307,8 @@ export default { }); if (!event) { - await interaction.reply({ + await interaction.editReply({ content: t(locale, 'commands.event.cancelNotFound', { id }), - ephemeral: true, }); return; } @@ -323,9 +318,8 @@ export default { data: { status: 'CANCELLED' }, }); - await interaction.reply({ + await interaction.editReply({ content: t(locale, 'commands.event.cancelSuccess', { id }), - ephemeral: true, }); return; } @@ -340,34 +334,30 @@ export default { }); if (!event) { - await interaction.reply({ + await interaction.editReply({ content: t(locale, 'commands.event.cancelNotFound', { id }), - ephemeral: true, }); return; } if (!event.announcementChannelId) { - await interaction.reply({ + await interaction.editReply({ content: t(locale, 'commands.event.announceNotAvailable'), - ephemeral: true, }); return; } try { await EventService.announceEvent(interaction.guild!, event.id); - await interaction.reply({ + await interaction.editReply({ content: t(locale, 'commands.event.announceSuccess', { id, channel: `<#${event.announcementChannelId}>`, }), - ephemeral: true, }); } catch { - await interaction.reply({ + await interaction.editReply({ content: t(locale, 'commands.event.announceNotAvailable'), - ephemeral: true, }); } } diff --git a/src/commands/fishing.ts b/src/commands/fishing.ts index e31bc17..291f9fc 100644 --- a/src/commands/fishing.ts +++ b/src/commands/fishing.ts @@ -48,8 +48,6 @@ export default { const subcommand = interaction.options.getSubcommand(); if (subcommand === 'enter') { - await interaction.deferReply({ ephemeral: true }); - if (!config || !config.enabled) { await interaction.editReply({ content: t(locale, 'commands.fishing.disabled'), @@ -83,8 +81,6 @@ export default { } if (subcommand === 'cast') { - await interaction.deferReply({ ephemeral: true }); - if (!config || !config.enabled) { await interaction.editReply({ content: t(locale, 'commands.fishing.disabled'), @@ -109,8 +105,6 @@ export default { } if (subcommand === 'end') { - await interaction.deferReply({ ephemeral: true }); - const ended = await FishingService.endThreadByUser(interaction, locale); if (!ended) { await interaction.editReply({ diff --git a/src/commands/language.ts b/src/commands/language.ts index 476670c..7701053 100644 --- a/src/commands/language.ts +++ b/src/commands/language.ts @@ -27,7 +27,7 @@ export default { // Validate locale (safety check) if (!SUPPORTED_LOCALES.includes(newLocale)) { - await interaction.reply({ content: `Unsupported locale: ${newLocale}`, ephemeral: true }); + await interaction.editReply({ content: `Unsupported locale: ${newLocale}` }); return; } @@ -38,9 +38,8 @@ export default { }); // Respond in the NEWLY selected locale - await interaction.reply({ + await interaction.editReply({ content: t(newLocale, 'commands.language.userSet', { locale: newLocale === 'ko' ? '한국어' : 'English' }), - ephemeral: true, }); }, }; diff --git a/src/commands/minigame.ts b/src/commands/minigame.ts index 7fb9086..d57085f 100644 --- a/src/commands/minigame.ts +++ b/src/commands/minigame.ts @@ -81,7 +81,7 @@ export default { .setTitle('🎮 미니게임 설정 변경') .setDescription(`${game.name} 미니게임이 **${state}**되었습니다.`); - return interaction.reply({ embeds: [embed], ephemeral: true }); + return interaction.editReply({ embeds: [embed] }); } if (subcommand === 'status') { @@ -106,7 +106,7 @@ export default { }); }); - return interaction.reply({ embeds: [embed], ephemeral: true }); + return interaction.editReply({ embeds: [embed] }); } if (subcommand === 'channel') { @@ -127,7 +127,7 @@ export default { .setTitle('🎮 미니게임 채널 설정') .setDescription(`${game.name} 미니게임 전용 채널이 **${channelMsg}**로 설정되었습니다.`); - return interaction.reply({ embeds: [embed], ephemeral: true }); + return interaction.editReply({ embeds: [embed] }); } }, }; diff --git a/src/commands/music.ts b/src/commands/music.ts index 0beb3c4..81b6928 100644 --- a/src/commands/music.ts +++ b/src/commands/music.ts @@ -26,7 +26,7 @@ async function respond( return; } - await interaction.reply({ content, ephemeral }); + await interaction.editReply({ content }); } export default { @@ -136,33 +136,28 @@ export default { const url = interaction.options.getString('url'); if ((!query && !url) || (query && url)) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'addMutuallyExclusive'), - ephemeral: true, }); return; } const member = interaction.member as GuildMember; if (!member.voice.channel) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'notInVoice'), - ephemeral: true, }); return; } const textChannel = interaction.channel as any; if (!textChannel?.send) { - await interaction.reply({ + await interaction.editReply({ content: t(locale, 'errors.E3003.userMessage'), - ephemeral: true, }); return; } - await interaction.deferReply(); - const result = query ? await MusicService.addFromQuery(member, textChannel, query, locale) : await MusicService.addFromUrl(member, textChannel, url!, locale); @@ -191,7 +186,7 @@ export default { } if (subcommand === 'queue') { - await interaction.reply({ + await interaction.editReply({ embeds: [MusicService.getQueueEmbed(interaction.guildId!, locale)], }); return; @@ -200,18 +195,16 @@ export default { if (subcommand === 'remove') { const member = interaction.member as GuildMember; if (!member.voice.channel) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'notInVoice'), - ephemeral: true, }); return; } const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!); if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'differentVoiceChannel'), - ephemeral: true, }); return; } @@ -219,14 +212,13 @@ export default { const index = interaction.options.getInteger('index', true); const removed = await MusicService.remove(interaction.guildId!, index); if (!removed) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'noActiveSession'), - ephemeral: true, }); return; } - await interaction.reply({ + await interaction.editReply({ content: t(locale, 'commands.music.queueRemoved', { title: removed.title, }), @@ -237,160 +229,145 @@ export default { if (subcommand === 'pause') { const member = interaction.member as GuildMember; if (!member.voice.channel) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'notInVoice'), - ephemeral: true, }); return; } const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!); if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'differentVoiceChannel'), - ephemeral: true, }); return; } const paused = await MusicService.pause(interaction.guildId!, locale); if (!paused) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'noActiveSession'), - ephemeral: true, }); return; } - await interaction.reply({ content: t(locale, 'commands.music.pauseSuccess') }); + await interaction.editReply({ content: t(locale, 'commands.music.pauseSuccess') }); return; } if (subcommand === 'resume') { const member = interaction.member as GuildMember; if (!member.voice.channel) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'notInVoice'), - ephemeral: true, }); return; } const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!); if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'differentVoiceChannel'), - ephemeral: true, }); return; } const resumed = await MusicService.resume(interaction.guildId!, locale); if (!resumed) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'noActiveSession'), - ephemeral: true, }); return; } - await interaction.reply({ content: t(locale, 'commands.music.resumeSuccess') }); + await interaction.editReply({ content: t(locale, 'commands.music.resumeSuccess') }); return; } if (subcommand === 'skip') { const member = interaction.member as GuildMember; if (!member.voice.channel) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'notInVoice'), - ephemeral: true, }); return; } const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!); if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'differentVoiceChannel'), - ephemeral: true, }); return; } const skipped = await MusicService.skip(interaction.guildId!); if (!skipped) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'noActiveSession'), - ephemeral: true, }); return; } - await interaction.reply({ content: t(locale, 'commands.music.skipSuccess') }); + await interaction.editReply({ content: t(locale, 'commands.music.skipSuccess') }); return; } if (subcommand === 'stop') { const member = interaction.member as GuildMember; if (!member.voice.channel) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'notInVoice'), - ephemeral: true, }); return; } const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!); if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'differentVoiceChannel'), - ephemeral: true, }); return; } const stopped = await MusicService.stop(interaction.guildId!, locale); if (!stopped) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'noActiveSession'), - ephemeral: true, }); return; } - await interaction.reply({ content: t(locale, 'commands.music.stopSuccess') }); + await interaction.editReply({ content: t(locale, 'commands.music.stopSuccess') }); return; } if (subcommand === 'leave') { const member = interaction.member as GuildMember; if (!member.voice.channel) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'notInVoice'), - ephemeral: true, }); return; } const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!); if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'differentVoiceChannel'), - ephemeral: true, }); return; } const left = await MusicService.leave(interaction.guildId!, locale); if (!left) { - await interaction.reply({ + await interaction.editReply({ content: buildErrorMessage(locale, 'noActiveSession'), - ephemeral: true, }); return; } - await interaction.reply({ content: t(locale, 'commands.music.leaveSuccess') }); + await interaction.editReply({ content: t(locale, 'commands.music.leaveSuccess') }); } } catch (error) { logger.error('Error in music command:', error); @@ -417,15 +394,7 @@ export default { return; } - if (interaction.replied || interaction.deferred) { - await respond(interaction, t(locale, 'errors.E3003.userMessage'), true); - return; - } - - await interaction.reply({ - content: t(locale, 'errors.E3003.userMessage'), - ephemeral: true, - }); + await respond(interaction, t(locale, 'errors.E3003.userMessage'), true); } }, }; diff --git a/src/commands/refine.ts b/src/commands/refine.ts index b576f9e..8022c5a 100644 --- a/src/commands/refine.ts +++ b/src/commands/refine.ts @@ -97,11 +97,11 @@ export default { }); if (!config || !config.enabled) { - return interaction.reply({ content: '❌ 이 서버에서는 재련 미니게임이 비활성화되어 있습니다.', ephemeral: true }); + return interaction.editReply({ content: '❌ 이 서버에서는 재련 미니게임이 비활성화되어 있습니다.' }); } if (config.channelId && config.channelId !== interaction.channelId) { - return interaction.reply({ content: `❌ 재련 미니게임은 <#${config.channelId}> 채널에서만 이용 가능합니다.`, ephemeral: true }); + return interaction.editReply({ content: `❌ 재련 미니게임은 <#${config.channelId}> 채널에서만 이용 가능합니다.` }); } const subcommand = interaction.options.getSubcommand(); @@ -132,9 +132,9 @@ export default { const row = new ActionRowBuilder().addComponents(retryBtn); - return interaction.reply({ embeds: [embed], components: [row] }); + return interaction.editReply({ embeds: [embed], components: [row] }); } catch (err: any) { - return interaction.reply({ content: `❌ 오류: ${err.message}`, ephemeral: true }); + return interaction.editReply({ content: `❌ 오류: ${err.message}` }); } } @@ -142,10 +142,10 @@ export default { if (subcommand === 'battle') { const targetUser = interaction.options.getUser('target', true); if (targetUser.id === interaction.user.id) { - return interaction.reply({ content: '❌ 자신을 공격할 수 없습니다.', ephemeral: true }); + return interaction.editReply({ content: '❌ 자신을 공격할 수 없습니다.' }); } if (targetUser.bot) { - return interaction.reply({ content: '❌ 봇과 전투할 수 없습니다.', ephemeral: true }); + return interaction.editReply({ content: '❌ 봇과 전투할 수 없습니다.' }); } try { @@ -170,9 +170,9 @@ export default { if (result.attackerDurability <= 0) embed.setFooter({ text: '⚠️ [경고] 무기가 전투 불능이 되었습니다. 내구도 0에서 다시 공격하면 파괴됩니다!' }); } - return interaction.reply({ embeds: [embed] }); + return interaction.editReply({ embeds: [embed] }); } catch (err: any) { - return interaction.reply({ content: `❌ 오류: ${err.message}`, ephemeral: true }); + return interaction.editReply({ content: `❌ 오류: ${err.message}` }); } } @@ -180,9 +180,9 @@ export default { if (subcommand === 'checkin') { try { const res = await RefinementService.checkIn(interaction.user.id, interaction.guildId); - return interaction.reply({ content: `✅ 출석 완료! **${res.goldAdded} G**를 수령했습니다. (총: ${res.totalGold} G)`, ephemeral: true }); + return interaction.editReply({ content: `✅ 출석 완료! **${res.goldAdded} G**를 수령했습니다. (총: ${res.totalGold} G)` }); } catch (err: any) { - return interaction.reply({ content: `❌ 오류: ${err.message}`, ephemeral: true }); + return interaction.editReply({ content: `❌ 오류: ${err.message}` }); } } @@ -208,7 +208,7 @@ export default { { name: '전투', value: `승리: ${profile.battleWin} | 패배: ${profile.battleLoss}`, inline: false } ); - return interaction.reply({ embeds: [embed] }); + return interaction.editReply({ embeds: [embed] }); } // --- RANKING --- @@ -243,16 +243,16 @@ export default { }).join('\n') || '데이터가 없습니다.'; embed.setDescription(listStr); - return interaction.reply({ embeds: [embed] }); + return interaction.editReply({ embeds: [embed] }); } // --- SELL --- if (subcommand === 'sell') { try { const res = await RefinementService.sellWeapon(interaction.user.id, interaction.guildId); - return interaction.reply({ content: `💰 **${res.level}단계** 무기를 판매하고 **${res.price} G**를 받았습니다. (총: ${res.gold} G)`, ephemeral: true }); + return interaction.editReply({ content: `💰 **${res.level}단계** 무기를 판매하고 **${res.price} G**를 받았습니다. (총: ${res.gold} G)` }); } catch (err: any) { - return interaction.reply({ content: `❌ 오류: ${err.message}`, ephemeral: true }); + return interaction.editReply({ content: `❌ 오류: ${err.message}` }); } } @@ -271,7 +271,7 @@ export default { { name: '🔥 피버 타임', value: '서버가 가장 활발한 시간대에 재련 성공 확률이 10% 증가합니다!' } ); - return interaction.reply({ embeds: [embed] }); + return interaction.editReply({ embeds: [embed] }); } }, }; diff --git a/src/commands/setup.ts b/src/commands/setup.ts index d676a51..f814f23 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -19,10 +19,9 @@ export default { if (!interaction.guildId) return; const { embed, components } = await SetupWizardRenderer.renderStep(0, interaction, locale); - return interaction.reply({ + return interaction.editReply({ embeds: [embed], components, - ephemeral: true, }); }, }; diff --git a/src/commands/voice.ts b/src/commands/voice.ts index ba86aff..573b7e4 100644 --- a/src/commands/voice.ts +++ b/src/commands/voice.ts @@ -88,22 +88,21 @@ export default { if (action === 'set') { const channel = interaction.options.getChannel('channel'); - if (!channel) return interaction.reply({ content: '❌ `channel` 옵션을 선택해주세요.', ephemeral: true }); + if (!channel) return interaction.editReply({ content: '❌ `channel` 옵션을 선택해주세요.' }); await prisma.voiceGenerator.upsert({ where: { channelId: channel.id }, update: { categoryId: category?.id || null, guildId }, create: { channelId: channel.id, guildId, categoryId: category?.id || null } }); - return interaction.reply({ + return interaction.editReply({ content: t(locale, 'commands.voiceSetup.setSuccess', { channel: `${channel}` }), - ephemeral: true }); } if (action === 'create') { const name = interaction.options.getString('name'); - if (!name) return interaction.reply({ content: '❌ `name` 옵션을 입력해주세요.', ephemeral: true }); + if (!name) return interaction.editReply({ content: '❌ `name` 옵션을 입력해주세요.' }); const newChannel = await interaction.guild!.channels.create({ name, @@ -113,9 +112,8 @@ export default { await prisma.voiceGenerator.create({ data: { channelId: newChannel.id, guildId, categoryId: category?.id || null } }); - return interaction.reply({ + return interaction.editReply({ content: t(locale, 'commands.voiceSetup.createSuccess', { channel: `${newChannel}` }), - ephemeral: true }); } } @@ -126,31 +124,29 @@ export default { if (action === 'name') { const template = interaction.options.getString('template'); - if (!template) return interaction.reply({ content: '❌ `template` 옵션을 입력해주세요.', ephemeral: true }); + if (!template) return interaction.editReply({ content: '❌ `template` 옵션을 입력해주세요.' }); await prisma.voiceGuildConfig.upsert({ where: { guildId }, update: { defaultNameTemplate: template }, create: { guildId, defaultNameTemplate: template } }); - return interaction.reply({ + return interaction.editReply({ content: t(locale, 'commands.voiceConfig.setSuccess'), - ephemeral: true }); } if (action === 'limit') { const limit = interaction.options.getInteger('limit'); - if (limit === null) return interaction.reply({ content: '❌ `limit` 옵션을 입력해주세요.', ephemeral: true }); + if (limit === null) return interaction.editReply({ content: '❌ `limit` 옵션을 입력해주세요.' }); await prisma.voiceGuildConfig.upsert({ where: { guildId }, update: { defaultUserLimit: limit }, create: { guildId, defaultUserLimit: limit } }); - return interaction.reply({ + return interaction.editReply({ content: t(locale, 'commands.voiceConfig.setSuccess'), - ephemeral: true }); } @@ -171,14 +167,13 @@ export default { inline: true } ); - return interaction.reply({ embeds: [embed], ephemeral: true }); + return interaction.editReply({ embeds: [embed] }); } } } catch (error) { logger.error('Error in voice command', error); - return interaction.reply({ + return interaction.editReply({ content: t(locale, 'errors.E3003.userMessage'), - ephemeral: true }); } }, diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 663a3a9..ef0a16f 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -18,6 +18,8 @@ export default { const command = client.commands.get(interaction.commandName); if (!command) return; + // Acknowledge before locale DB reads so Discord's ~3s interaction window is never missed. + await interaction.deferReply({ ephemeral: true }); const locale = await getInteractionLocale(interaction); await withErrorHandler(interaction, async () => { await command.execute(interaction, locale); diff --git a/src/i18n/localeHelper.ts b/src/i18n/localeHelper.ts index 663fd8d..c3391d6 100644 --- a/src/i18n/localeHelper.ts +++ b/src/i18n/localeHelper.ts @@ -23,21 +23,20 @@ export async function getInteractionLocale(interaction: Interaction): Promise