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
This commit is contained in:
mineseo-kim 2026-04-09 09:09:32 +09:00
parent a2e755b708
commit 1a4652c185
12 changed files with 111 additions and 171 deletions

View File

@ -118,9 +118,8 @@ export default {
if (action === 'set') { if (action === 'set') {
const channel = interaction.options.getChannel('channel') as TextChannel; 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; const botMember = guild.members.me;
if (!botMember) return; if (!botMember) return;
const perms = channel.permissionsFor(botMember); const perms = channel.permissionsFor(botMember);
@ -141,13 +140,11 @@ export default {
} }
if (action === 'clear') { if (action === 'clear') {
await interaction.deferReply({ ephemeral: true });
await auditLogService.clearChannel(guild.id); await auditLogService.clearChannel(guild.id);
return interaction.editReply({ content: `✅ 감사 채널 설정이 해제되었습니다.` }); return interaction.editReply({ content: `✅ 감사 채널 설정이 해제되었습니다.` });
} }
if (action === 'status') { if (action === 'status') {
await interaction.deferReply({ ephemeral: true });
const config = await auditLogService.getChannel(guild.id); const config = await auditLogService.getChannel(guild.id);
if (!config) return interaction.editReply({ content: `설정된 감사 채널이 없습니다. 먼저 \`/audit channel action:set\` 명령어로 채널을 설정해주세요.` }); if (!config) return interaction.editReply({ content: `설정된 감사 채널이 없습니다. 먼저 \`/audit channel action:set\` 명령어로 채널을 설정해주세요.` });
@ -168,10 +165,9 @@ export default {
const enable = interaction.options.getBoolean('enable'); const enable = interaction.options.getBoolean('enable');
if (!category || enable === null) { 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); const config = await auditLogService.getChannel(guild.id);
if (!config) return interaction.editReply({ content: `설정된 감사 채널이 없습니다. 먼저 \`/audit channel action:set\` 명령어로 채널을 설정해주세요.` }); if (!config) return interaction.editReply({ content: `설정된 감사 채널이 없습니다. 먼저 \`/audit channel action:set\` 명령어로 채널을 설정해주세요.` });
@ -185,7 +181,6 @@ export default {
const action = interaction.options.getString('action', true); const action = interaction.options.getString('action', true);
if (action === 'permissions') { if (action === 'permissions') {
await interaction.deferReply({ ephemeral: true });
const results = await PermissionAuditService.auditGuild(guild); const results = await PermissionAuditService.auditGuild(guild);
if (results.length === 0) return interaction.editReply({ content: t(locale, 'commands.permissionAudit.noResults') }); if (results.length === 0) return interaction.editReply({ content: t(locale, 'commands.permissionAudit.noResults') });
@ -230,8 +225,7 @@ export default {
} }
} catch (error) { } catch (error) {
console.error('Error in audit command', error); console.error('Error in audit command', error);
const reply = interaction.deferred ? interaction.editReply : interaction.reply; return interaction.editReply({ content: '명령 실행 중 오류가 발생했습니다.' });
return (reply as any)({ content: '명령 실행 중 오류가 발생했습니다.', ephemeral: true });
} }
}, },
}; };

View File

@ -76,14 +76,13 @@ export default {
if (action === 'language') { if (action === 'language') {
const newLocale = interaction.options.getString('locale') as SupportedLocale; const newLocale = interaction.options.getString('locale') as SupportedLocale;
if (!newLocale) { if (!newLocale) {
return interaction.reply({ return interaction.editReply({
content: '❌ `locale` 옵션을 선택해주세요.', content: '❌ `locale` 옵션을 선택해주세요.',
ephemeral: true
}); });
} }
if (!SUPPORTED_LOCALES.includes(newLocale)) { 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({ await prisma.guildConfig.upsert({
@ -92,9 +91,8 @@ export default {
create: { guildId: interaction.guildId, locale: newLocale }, create: { guildId: interaction.guildId, locale: newLocale },
}); });
return interaction.reply({ return interaction.editReply({
content: t(newLocale, 'commands.language.serverSet', { locale: newLocale === 'ko' ? '한국어' : 'English' }), content: t(newLocale, 'commands.language.serverSet', { locale: newLocale === 'ko' ? '한국어' : 'English' }),
ephemeral: true,
}); });
} }
} }
@ -132,7 +130,7 @@ export default {
.setTitle(t(locale, 'commands.config.title')) .setTitle(t(locale, 'commands.config.title'))
.setDescription(`${label}: **${state}**`); .setDescription(`${label}: **${state}**`);
return interaction.reply({ embeds: [embed], ephemeral: true }); return interaction.editReply({ embeds: [embed] });
} }
}, },
}; };

View File

@ -201,26 +201,23 @@ export default {
const startsAt = parseSeoulDateTime(date, time); const startsAt = parseSeoulDateTime(date, time);
if (!startsAt) { if (!startsAt) {
await interaction.reply({ await interaction.editReply({
content: `${t(locale, 'commands.event.invalidDateTime')}\n${t(locale, 'commands.event.invalidDateTimeResolution')}`, content: `${t(locale, 'commands.event.invalidDateTime')}\n${t(locale, 'commands.event.invalidDateTimeResolution')}`,
ephemeral: true,
}); });
return; return;
} }
if (startsAt.getTime() <= Date.now()) { if (startsAt.getTime() <= Date.now()) {
await interaction.reply({ await interaction.editReply({
content: `${t(locale, 'commands.event.invalidPastDateTime')}\n${t(locale, 'commands.event.invalidPastDateTimeResolution')}`, content: `${t(locale, 'commands.event.invalidPastDateTime')}\n${t(locale, 'commands.event.invalidPastDateTimeResolution')}`,
ephemeral: true,
}); });
return; return;
} }
const reminderOffsets = parseReminderOffsets(reminderRaw); const reminderOffsets = parseReminderOffsets(reminderRaw);
if (!reminderOffsets) { if (!reminderOffsets) {
await interaction.reply({ await interaction.editReply({
content: `${t(locale, 'commands.event.invalidReminderOffsets')}\n${t(locale, 'commands.event.invalidReminderOffsetsResolution')}`, content: `${t(locale, 'commands.event.invalidReminderOffsets')}\n${t(locale, 'commands.event.invalidReminderOffsetsResolution')}`,
ephemeral: true,
}); });
return; return;
} }
@ -252,7 +249,7 @@ export default {
) )
.setTimestamp(); .setTimestamp();
await interaction.reply({ embeds: [embed], ephemeral: true }); await interaction.editReply({ embeds: [embed] });
return; return;
} }
@ -268,9 +265,8 @@ export default {
}); });
if (events.length === 0) { if (events.length === 0) {
await interaction.reply({ await interaction.editReply({
content: t(locale, 'commands.event.listEmpty'), content: t(locale, 'commands.event.listEmpty'),
ephemeral: true,
}); });
return; return;
} }
@ -295,7 +291,7 @@ export default {
}); });
} }
await interaction.reply({ embeds: [embed], ephemeral: true }); await interaction.editReply({ embeds: [embed] });
return; return;
} }
@ -311,9 +307,8 @@ export default {
}); });
if (!event) { if (!event) {
await interaction.reply({ await interaction.editReply({
content: t(locale, 'commands.event.cancelNotFound', { id }), content: t(locale, 'commands.event.cancelNotFound', { id }),
ephemeral: true,
}); });
return; return;
} }
@ -323,9 +318,8 @@ export default {
data: { status: 'CANCELLED' }, data: { status: 'CANCELLED' },
}); });
await interaction.reply({ await interaction.editReply({
content: t(locale, 'commands.event.cancelSuccess', { id }), content: t(locale, 'commands.event.cancelSuccess', { id }),
ephemeral: true,
}); });
return; return;
} }
@ -340,34 +334,30 @@ export default {
}); });
if (!event) { if (!event) {
await interaction.reply({ await interaction.editReply({
content: t(locale, 'commands.event.cancelNotFound', { id }), content: t(locale, 'commands.event.cancelNotFound', { id }),
ephemeral: true,
}); });
return; return;
} }
if (!event.announcementChannelId) { if (!event.announcementChannelId) {
await interaction.reply({ await interaction.editReply({
content: t(locale, 'commands.event.announceNotAvailable'), content: t(locale, 'commands.event.announceNotAvailable'),
ephemeral: true,
}); });
return; return;
} }
try { try {
await EventService.announceEvent(interaction.guild!, event.id); await EventService.announceEvent(interaction.guild!, event.id);
await interaction.reply({ await interaction.editReply({
content: t(locale, 'commands.event.announceSuccess', { content: t(locale, 'commands.event.announceSuccess', {
id, id,
channel: `<#${event.announcementChannelId}>`, channel: `<#${event.announcementChannelId}>`,
}), }),
ephemeral: true,
}); });
} catch { } catch {
await interaction.reply({ await interaction.editReply({
content: t(locale, 'commands.event.announceNotAvailable'), content: t(locale, 'commands.event.announceNotAvailable'),
ephemeral: true,
}); });
} }
} }

View File

@ -48,8 +48,6 @@ export default {
const subcommand = interaction.options.getSubcommand(); const subcommand = interaction.options.getSubcommand();
if (subcommand === 'enter') { if (subcommand === 'enter') {
await interaction.deferReply({ ephemeral: true });
if (!config || !config.enabled) { if (!config || !config.enabled) {
await interaction.editReply({ await interaction.editReply({
content: t(locale, 'commands.fishing.disabled'), content: t(locale, 'commands.fishing.disabled'),
@ -83,8 +81,6 @@ export default {
} }
if (subcommand === 'cast') { if (subcommand === 'cast') {
await interaction.deferReply({ ephemeral: true });
if (!config || !config.enabled) { if (!config || !config.enabled) {
await interaction.editReply({ await interaction.editReply({
content: t(locale, 'commands.fishing.disabled'), content: t(locale, 'commands.fishing.disabled'),
@ -109,8 +105,6 @@ export default {
} }
if (subcommand === 'end') { if (subcommand === 'end') {
await interaction.deferReply({ ephemeral: true });
const ended = await FishingService.endThreadByUser(interaction, locale); const ended = await FishingService.endThreadByUser(interaction, locale);
if (!ended) { if (!ended) {
await interaction.editReply({ await interaction.editReply({

View File

@ -27,7 +27,7 @@ export default {
// Validate locale (safety check) // Validate locale (safety check)
if (!SUPPORTED_LOCALES.includes(newLocale)) { if (!SUPPORTED_LOCALES.includes(newLocale)) {
await interaction.reply({ content: `Unsupported locale: ${newLocale}`, ephemeral: true }); await interaction.editReply({ content: `Unsupported locale: ${newLocale}` });
return; return;
} }
@ -38,9 +38,8 @@ export default {
}); });
// Respond in the NEWLY selected locale // Respond in the NEWLY selected locale
await interaction.reply({ await interaction.editReply({
content: t(newLocale, 'commands.language.userSet', { locale: newLocale === 'ko' ? '한국어' : 'English' }), content: t(newLocale, 'commands.language.userSet', { locale: newLocale === 'ko' ? '한국어' : 'English' }),
ephemeral: true,
}); });
}, },
}; };

View File

@ -81,7 +81,7 @@ export default {
.setTitle('🎮 미니게임 설정 변경') .setTitle('🎮 미니게임 설정 변경')
.setDescription(`${game.name} 미니게임이 **${state}**되었습니다.`); .setDescription(`${game.name} 미니게임이 **${state}**되었습니다.`);
return interaction.reply({ embeds: [embed], ephemeral: true }); return interaction.editReply({ embeds: [embed] });
} }
if (subcommand === 'status') { if (subcommand === 'status') {
@ -106,7 +106,7 @@ export default {
}); });
}); });
return interaction.reply({ embeds: [embed], ephemeral: true }); return interaction.editReply({ embeds: [embed] });
} }
if (subcommand === 'channel') { if (subcommand === 'channel') {
@ -127,7 +127,7 @@ export default {
.setTitle('🎮 미니게임 채널 설정') .setTitle('🎮 미니게임 채널 설정')
.setDescription(`${game.name} 미니게임 전용 채널이 **${channelMsg}**로 설정되었습니다.`); .setDescription(`${game.name} 미니게임 전용 채널이 **${channelMsg}**로 설정되었습니다.`);
return interaction.reply({ embeds: [embed], ephemeral: true }); return interaction.editReply({ embeds: [embed] });
} }
}, },
}; };

View File

@ -26,7 +26,7 @@ async function respond(
return; return;
} }
await interaction.reply({ content, ephemeral }); await interaction.editReply({ content });
} }
export default { export default {
@ -136,33 +136,28 @@ export default {
const url = interaction.options.getString('url'); const url = interaction.options.getString('url');
if ((!query && !url) || (query && url)) { if ((!query && !url) || (query && url)) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'addMutuallyExclusive'), content: buildErrorMessage(locale, 'addMutuallyExclusive'),
ephemeral: true,
}); });
return; return;
} }
const member = interaction.member as GuildMember; const member = interaction.member as GuildMember;
if (!member.voice.channel) { if (!member.voice.channel) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'notInVoice'), content: buildErrorMessage(locale, 'notInVoice'),
ephemeral: true,
}); });
return; return;
} }
const textChannel = interaction.channel as any; const textChannel = interaction.channel as any;
if (!textChannel?.send) { if (!textChannel?.send) {
await interaction.reply({ await interaction.editReply({
content: t(locale, 'errors.E3003.userMessage'), content: t(locale, 'errors.E3003.userMessage'),
ephemeral: true,
}); });
return; return;
} }
await interaction.deferReply();
const result = query const result = query
? await MusicService.addFromQuery(member, textChannel, query, locale) ? await MusicService.addFromQuery(member, textChannel, query, locale)
: await MusicService.addFromUrl(member, textChannel, url!, locale); : await MusicService.addFromUrl(member, textChannel, url!, locale);
@ -191,7 +186,7 @@ export default {
} }
if (subcommand === 'queue') { if (subcommand === 'queue') {
await interaction.reply({ await interaction.editReply({
embeds: [MusicService.getQueueEmbed(interaction.guildId!, locale)], embeds: [MusicService.getQueueEmbed(interaction.guildId!, locale)],
}); });
return; return;
@ -200,18 +195,16 @@ export default {
if (subcommand === 'remove') { if (subcommand === 'remove') {
const member = interaction.member as GuildMember; const member = interaction.member as GuildMember;
if (!member.voice.channel) { if (!member.voice.channel) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'notInVoice'), content: buildErrorMessage(locale, 'notInVoice'),
ephemeral: true,
}); });
return; return;
} }
const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!); const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!);
if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) { if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'differentVoiceChannel'), content: buildErrorMessage(locale, 'differentVoiceChannel'),
ephemeral: true,
}); });
return; return;
} }
@ -219,14 +212,13 @@ export default {
const index = interaction.options.getInteger('index', true); const index = interaction.options.getInteger('index', true);
const removed = await MusicService.remove(interaction.guildId!, index); const removed = await MusicService.remove(interaction.guildId!, index);
if (!removed) { if (!removed) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'noActiveSession'), content: buildErrorMessage(locale, 'noActiveSession'),
ephemeral: true,
}); });
return; return;
} }
await interaction.reply({ await interaction.editReply({
content: t(locale, 'commands.music.queueRemoved', { content: t(locale, 'commands.music.queueRemoved', {
title: removed.title, title: removed.title,
}), }),
@ -237,160 +229,145 @@ export default {
if (subcommand === 'pause') { if (subcommand === 'pause') {
const member = interaction.member as GuildMember; const member = interaction.member as GuildMember;
if (!member.voice.channel) { if (!member.voice.channel) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'notInVoice'), content: buildErrorMessage(locale, 'notInVoice'),
ephemeral: true,
}); });
return; return;
} }
const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!); const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!);
if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) { if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'differentVoiceChannel'), content: buildErrorMessage(locale, 'differentVoiceChannel'),
ephemeral: true,
}); });
return; return;
} }
const paused = await MusicService.pause(interaction.guildId!, locale); const paused = await MusicService.pause(interaction.guildId!, locale);
if (!paused) { if (!paused) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'noActiveSession'), content: buildErrorMessage(locale, 'noActiveSession'),
ephemeral: true,
}); });
return; return;
} }
await interaction.reply({ content: t(locale, 'commands.music.pauseSuccess') }); await interaction.editReply({ content: t(locale, 'commands.music.pauseSuccess') });
return; return;
} }
if (subcommand === 'resume') { if (subcommand === 'resume') {
const member = interaction.member as GuildMember; const member = interaction.member as GuildMember;
if (!member.voice.channel) { if (!member.voice.channel) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'notInVoice'), content: buildErrorMessage(locale, 'notInVoice'),
ephemeral: true,
}); });
return; return;
} }
const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!); const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!);
if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) { if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'differentVoiceChannel'), content: buildErrorMessage(locale, 'differentVoiceChannel'),
ephemeral: true,
}); });
return; return;
} }
const resumed = await MusicService.resume(interaction.guildId!, locale); const resumed = await MusicService.resume(interaction.guildId!, locale);
if (!resumed) { if (!resumed) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'noActiveSession'), content: buildErrorMessage(locale, 'noActiveSession'),
ephemeral: true,
}); });
return; return;
} }
await interaction.reply({ content: t(locale, 'commands.music.resumeSuccess') }); await interaction.editReply({ content: t(locale, 'commands.music.resumeSuccess') });
return; return;
} }
if (subcommand === 'skip') { if (subcommand === 'skip') {
const member = interaction.member as GuildMember; const member = interaction.member as GuildMember;
if (!member.voice.channel) { if (!member.voice.channel) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'notInVoice'), content: buildErrorMessage(locale, 'notInVoice'),
ephemeral: true,
}); });
return; return;
} }
const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!); const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!);
if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) { if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'differentVoiceChannel'), content: buildErrorMessage(locale, 'differentVoiceChannel'),
ephemeral: true,
}); });
return; return;
} }
const skipped = await MusicService.skip(interaction.guildId!); const skipped = await MusicService.skip(interaction.guildId!);
if (!skipped) { if (!skipped) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'noActiveSession'), content: buildErrorMessage(locale, 'noActiveSession'),
ephemeral: true,
}); });
return; return;
} }
await interaction.reply({ content: t(locale, 'commands.music.skipSuccess') }); await interaction.editReply({ content: t(locale, 'commands.music.skipSuccess') });
return; return;
} }
if (subcommand === 'stop') { if (subcommand === 'stop') {
const member = interaction.member as GuildMember; const member = interaction.member as GuildMember;
if (!member.voice.channel) { if (!member.voice.channel) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'notInVoice'), content: buildErrorMessage(locale, 'notInVoice'),
ephemeral: true,
}); });
return; return;
} }
const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!); const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!);
if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) { if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'differentVoiceChannel'), content: buildErrorMessage(locale, 'differentVoiceChannel'),
ephemeral: true,
}); });
return; return;
} }
const stopped = await MusicService.stop(interaction.guildId!, locale); const stopped = await MusicService.stop(interaction.guildId!, locale);
if (!stopped) { if (!stopped) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'noActiveSession'), content: buildErrorMessage(locale, 'noActiveSession'),
ephemeral: true,
}); });
return; return;
} }
await interaction.reply({ content: t(locale, 'commands.music.stopSuccess') }); await interaction.editReply({ content: t(locale, 'commands.music.stopSuccess') });
return; return;
} }
if (subcommand === 'leave') { if (subcommand === 'leave') {
const member = interaction.member as GuildMember; const member = interaction.member as GuildMember;
if (!member.voice.channel) { if (!member.voice.channel) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'notInVoice'), content: buildErrorMessage(locale, 'notInVoice'),
ephemeral: true,
}); });
return; return;
} }
const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!); const activeVoiceChannelId = MusicService.getActiveVoiceChannelId(interaction.guildId!);
if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) { if (activeVoiceChannelId && activeVoiceChannelId !== member.voice.channelId) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'differentVoiceChannel'), content: buildErrorMessage(locale, 'differentVoiceChannel'),
ephemeral: true,
}); });
return; return;
} }
const left = await MusicService.leave(interaction.guildId!, locale); const left = await MusicService.leave(interaction.guildId!, locale);
if (!left) { if (!left) {
await interaction.reply({ await interaction.editReply({
content: buildErrorMessage(locale, 'noActiveSession'), content: buildErrorMessage(locale, 'noActiveSession'),
ephemeral: true,
}); });
return; return;
} }
await interaction.reply({ content: t(locale, 'commands.music.leaveSuccess') }); await interaction.editReply({ content: t(locale, 'commands.music.leaveSuccess') });
} }
} catch (error) { } catch (error) {
logger.error('Error in music command:', error); logger.error('Error in music command:', error);
@ -417,15 +394,7 @@ export default {
return; return;
} }
if (interaction.replied || interaction.deferred) { await respond(interaction, t(locale, 'errors.E3003.userMessage'), true);
await respond(interaction, t(locale, 'errors.E3003.userMessage'), true);
return;
}
await interaction.reply({
content: t(locale, 'errors.E3003.userMessage'),
ephemeral: true,
});
} }
}, },
}; };

View File

@ -97,11 +97,11 @@ export default {
}); });
if (!config || !config.enabled) { if (!config || !config.enabled) {
return interaction.reply({ content: '❌ 이 서버에서는 재련 미니게임이 비활성화되어 있습니다.', ephemeral: true }); return interaction.editReply({ content: '❌ 이 서버에서는 재련 미니게임이 비활성화되어 있습니다.' });
} }
if (config.channelId && config.channelId !== interaction.channelId) { 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(); const subcommand = interaction.options.getSubcommand();
@ -132,9 +132,9 @@ export default {
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(retryBtn); const row = new ActionRowBuilder<ButtonBuilder>().addComponents(retryBtn);
return interaction.reply({ embeds: [embed], components: [row] }); return interaction.editReply({ embeds: [embed], components: [row] });
} catch (err: any) { } 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') { if (subcommand === 'battle') {
const targetUser = interaction.options.getUser('target', true); const targetUser = interaction.options.getUser('target', true);
if (targetUser.id === interaction.user.id) { if (targetUser.id === interaction.user.id) {
return interaction.reply({ content: '❌ 자신을 공격할 수 없습니다.', ephemeral: true }); return interaction.editReply({ content: '❌ 자신을 공격할 수 없습니다.' });
} }
if (targetUser.bot) { if (targetUser.bot) {
return interaction.reply({ content: '❌ 봇과 전투할 수 없습니다.', ephemeral: true }); return interaction.editReply({ content: '❌ 봇과 전투할 수 없습니다.' });
} }
try { try {
@ -170,9 +170,9 @@ export default {
if (result.attackerDurability <= 0) embed.setFooter({ text: '⚠️ [경고] 무기가 전투 불능이 되었습니다. 내구도 0에서 다시 공격하면 파괴됩니다!' }); if (result.attackerDurability <= 0) embed.setFooter({ text: '⚠️ [경고] 무기가 전투 불능이 되었습니다. 내구도 0에서 다시 공격하면 파괴됩니다!' });
} }
return interaction.reply({ embeds: [embed] }); return interaction.editReply({ embeds: [embed] });
} catch (err: any) { } 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') { if (subcommand === 'checkin') {
try { try {
const res = await RefinementService.checkIn(interaction.user.id, interaction.guildId); 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) { } 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 } { name: '전투', value: `승리: ${profile.battleWin} | 패배: ${profile.battleLoss}`, inline: false }
); );
return interaction.reply({ embeds: [embed] }); return interaction.editReply({ embeds: [embed] });
} }
// --- RANKING --- // --- RANKING ---
@ -243,16 +243,16 @@ export default {
}).join('\n') || '데이터가 없습니다.'; }).join('\n') || '데이터가 없습니다.';
embed.setDescription(listStr); embed.setDescription(listStr);
return interaction.reply({ embeds: [embed] }); return interaction.editReply({ embeds: [embed] });
} }
// --- SELL --- // --- SELL ---
if (subcommand === 'sell') { if (subcommand === 'sell') {
try { try {
const res = await RefinementService.sellWeapon(interaction.user.id, interaction.guildId); 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) { } 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% 증가합니다!' } { name: '🔥 피버 타임', value: '서버가 가장 활발한 시간대에 재련 성공 확률이 10% 증가합니다!' }
); );
return interaction.reply({ embeds: [embed] }); return interaction.editReply({ embeds: [embed] });
} }
}, },
}; };

View File

@ -19,10 +19,9 @@ export default {
if (!interaction.guildId) return; if (!interaction.guildId) return;
const { embed, components } = await SetupWizardRenderer.renderStep(0, interaction, locale); const { embed, components } = await SetupWizardRenderer.renderStep(0, interaction, locale);
return interaction.reply({ return interaction.editReply({
embeds: [embed], embeds: [embed],
components, components,
ephemeral: true,
}); });
}, },
}; };

View File

@ -88,22 +88,21 @@ export default {
if (action === 'set') { if (action === 'set') {
const channel = interaction.options.getChannel('channel'); 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({ await prisma.voiceGenerator.upsert({
where: { channelId: channel.id }, where: { channelId: channel.id },
update: { categoryId: category?.id || null, guildId }, update: { categoryId: category?.id || null, guildId },
create: { channelId: channel.id, guildId, categoryId: category?.id || null } create: { channelId: channel.id, guildId, categoryId: category?.id || null }
}); });
return interaction.reply({ return interaction.editReply({
content: t(locale, 'commands.voiceSetup.setSuccess', { channel: `${channel}` }), content: t(locale, 'commands.voiceSetup.setSuccess', { channel: `${channel}` }),
ephemeral: true
}); });
} }
if (action === 'create') { if (action === 'create') {
const name = interaction.options.getString('name'); 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({ const newChannel = await interaction.guild!.channels.create({
name, name,
@ -113,9 +112,8 @@ export default {
await prisma.voiceGenerator.create({ await prisma.voiceGenerator.create({
data: { channelId: newChannel.id, guildId, categoryId: category?.id || null } data: { channelId: newChannel.id, guildId, categoryId: category?.id || null }
}); });
return interaction.reply({ return interaction.editReply({
content: t(locale, 'commands.voiceSetup.createSuccess', { channel: `${newChannel}` }), content: t(locale, 'commands.voiceSetup.createSuccess', { channel: `${newChannel}` }),
ephemeral: true
}); });
} }
} }
@ -126,31 +124,29 @@ export default {
if (action === 'name') { if (action === 'name') {
const template = interaction.options.getString('template'); 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({ await prisma.voiceGuildConfig.upsert({
where: { guildId }, where: { guildId },
update: { defaultNameTemplate: template }, update: { defaultNameTemplate: template },
create: { guildId, defaultNameTemplate: template } create: { guildId, defaultNameTemplate: template }
}); });
return interaction.reply({ return interaction.editReply({
content: t(locale, 'commands.voiceConfig.setSuccess'), content: t(locale, 'commands.voiceConfig.setSuccess'),
ephemeral: true
}); });
} }
if (action === 'limit') { if (action === 'limit') {
const limit = interaction.options.getInteger('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({ await prisma.voiceGuildConfig.upsert({
where: { guildId }, where: { guildId },
update: { defaultUserLimit: limit }, update: { defaultUserLimit: limit },
create: { guildId, defaultUserLimit: limit } create: { guildId, defaultUserLimit: limit }
}); });
return interaction.reply({ return interaction.editReply({
content: t(locale, 'commands.voiceConfig.setSuccess'), content: t(locale, 'commands.voiceConfig.setSuccess'),
ephemeral: true
}); });
} }
@ -171,14 +167,13 @@ export default {
inline: true inline: true
} }
); );
return interaction.reply({ embeds: [embed], ephemeral: true }); return interaction.editReply({ embeds: [embed] });
} }
} }
} catch (error) { } catch (error) {
logger.error('Error in voice command', error); logger.error('Error in voice command', error);
return interaction.reply({ return interaction.editReply({
content: t(locale, 'errors.E3003.userMessage'), content: t(locale, 'errors.E3003.userMessage'),
ephemeral: true
}); });
} }
}, },

View File

@ -18,6 +18,8 @@ export default {
const command = client.commands.get(interaction.commandName); const command = client.commands.get(interaction.commandName);
if (!command) return; 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); const locale = await getInteractionLocale(interaction);
await withErrorHandler(interaction, async () => { await withErrorHandler(interaction, async () => {
await command.execute(interaction, locale); await command.execute(interaction, locale);

View File

@ -23,21 +23,20 @@ export async function getInteractionLocale(interaction: Interaction): Promise<Su
let guildLocale: string | null = null; let guildLocale: string | null = null;
try { try {
// Fetch user locale preference const [userPref, guildConfig] = await Promise.all([
const userPref = await prisma.userLocale.findUnique({ prisma.userLocale.findUnique({
where: { userId: interaction.user.id }, where: { userId: interaction.user.id },
select: { locale: true },
});
userLocale = userPref?.locale ?? null;
// Fetch guild locale preference
if (interaction.guildId) {
const guildConfig = await prisma.guildConfig.findUnique({
where: { guildId: interaction.guildId },
select: { locale: true }, select: { locale: true },
}); }),
guildLocale = guildConfig?.locale ?? null; interaction.guildId
} ? prisma.guildConfig.findUnique({
where: { guildId: interaction.guildId },
select: { locale: true },
})
: Promise.resolve(null),
]);
userLocale = userPref?.locale ?? null;
guildLocale = guildConfig?.locale ?? null;
} catch { } catch {
// If DB lookup fails, fall through to Discord locale / default // If DB lookup fails, fall through to Discord locale / default
} }
@ -61,21 +60,22 @@ export async function getContextLocale(
let guildLocale: string | null = null; let guildLocale: string | null = null;
try { try {
if (userId) { const [userPref, guildConfig] = await Promise.all([
const userPref = await prisma.userLocale.findUnique({ userId
where: { userId }, ? prisma.userLocale.findUnique({
select: { locale: true }, where: { userId },
}); select: { locale: true },
userLocale = userPref?.locale ?? null; })
} : Promise.resolve(null),
guildId
if (guildId) { ? prisma.guildConfig.findUnique({
const guildConfig = await prisma.guildConfig.findUnique({ where: { guildId },
where: { guildId }, select: { locale: true },
select: { locale: true }, })
}); : Promise.resolve(null),
guildLocale = guildConfig?.locale ?? null; ]);
} userLocale = userPref?.locale ?? null;
guildLocale = guildConfig?.locale ?? null;
} catch { } catch {
// Fall through to default // Fall through to default
} }