import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, ChannelSelectMenuBuilder, ChannelType, MessageComponentInteraction, ChatInputCommandInteraction, Colors, } from 'discord.js'; import { SupportedLocale, t } from '../i18n'; import { PermissionAuditService } from './PermissionAuditService'; import { prisma } from '../database'; export class SetupWizardRenderer { static async renderStep( step: number, interaction: MessageComponentInteraction | ChatInputCommandInteraction, locale: SupportedLocale ): Promise<{ embed: EmbedBuilder; components: ActionRowBuilder[] }> { const embed = new EmbedBuilder().setColor(Colors.Blurple); const components: ActionRowBuilder[] = []; switch (step) { case 0: { embed.setTitle(t(locale, 'commands.setup.step0.title')) .setDescription(t(locale, 'commands.setup.step0.desc')); const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId('setup_next_1') .setLabel(t(locale, 'commands.setup.step0.startBtn')) .setStyle(ButtonStyle.Primary) ); components.push(row); break; } case 1: { embed.setTitle(t(locale, 'commands.setup.step1.title')) .setDescription(t(locale, 'commands.setup.step1.desc', { locale: locale === 'ko' ? 'Korean' : 'English' })); const select = new StringSelectMenuBuilder() .setCustomId('setup_lang_select') .setPlaceholder(t(locale, 'commands.setup.step1.placeholder')) .addOptions([ { label: 'Korean', value: 'ko', description: '한국어로 봇을 설정합니다.' }, { label: 'English', value: 'en', description: 'Set bot to English.' } ]); const btnRow = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId('setup_next_2') .setLabel(t(locale, 'commands.setup.step1.nextBtn')) .setStyle(ButtonStyle.Secondary) ); components.push(new ActionRowBuilder().addComponents(select)); components.push(btnRow); break; } case 2: { if (!interaction.guild) throw new Error('Guild not found'); const results = await PermissionAuditService.auditGuild(interaction.guild); const hasFails = results.some(r => r.status === 'FAIL'); embed.setTitle(t(locale, 'commands.setup.step2.title')); if (hasFails) { embed.setDescription(t(locale, 'commands.setup.step2.descFail')) .setColor(Colors.Red); // ⚠️ 권한 부족 목록 렌더링 const failLines = results .filter(r => r.status === 'FAIL') .map(r => `- **${t(locale, `commands.permissionAudit.features.${r.featureKey as any}`) || r.featureKey}**\n > \`${r.missingPermissions.join('`, `')}\``); if (failLines.length > 0) { embed.addFields({ name: 'Missing Permissions', value: failLines.join('\n') }); } } else { embed.setDescription(t(locale, 'commands.setup.step2.descOk')) .setColor(Colors.Green); } const btnRow = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId('setup_refresh_2') .setLabel(t(locale, 'commands.setup.step2.recheckBtn')) .setStyle(ButtonStyle.Secondary), new ButtonBuilder() .setCustomId('setup_next_3') .setLabel(t(locale, 'commands.setup.step2.nextBtn')) .setStyle(hasFails ? ButtonStyle.Danger : ButtonStyle.Primary) ); components.push(btnRow); break; } case 3: { embed.setTitle(t(locale, 'commands.setup.step3.title')) .setDescription(t(locale, 'commands.setup.step3.desc')); const select = new ChannelSelectMenuBuilder() .setCustomId('setup_audit_select') .setPlaceholder(t(locale, 'commands.setup.step3.placeholder')) .setChannelTypes(ChannelType.GuildText); const btnRow = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId('setup_audit_disable') .setLabel(t(locale, 'commands.setup.step3.disableBtn')) .setStyle(ButtonStyle.Danger), new ButtonBuilder() .setCustomId('setup_next_4') .setLabel(t(locale, 'commands.setup.step3.nextBtn')) .setStyle(ButtonStyle.Secondary) ); components.push(new ActionRowBuilder().addComponents(select)); components.push(btnRow); break; } case 4: { const audit = await prisma.auditChannel.findUnique({ where: { guildId: interaction.guildId! } }); const disabled = audit?.disabledCategories || []; embed.setTitle(t(locale, 'commands.setup.step4.title')) .setDescription(t(locale, 'commands.setup.step4.desc')); const categories: ('SYSTEM' | 'VOICE' | 'PERMISSION' | 'INVITE')[] = ['SYSTEM', 'VOICE', 'PERMISSION', 'INVITE']; const row1 = new ActionRowBuilder(); categories.forEach(cat => { const isEnabled = !disabled.includes(cat); row1.addComponents( new ButtonBuilder() .setCustomId(`setup_audit_toggle_${cat}`) .setLabel(t(locale, `commands.setup.auditCategories.${cat}`)) .setStyle(isEnabled ? ButtonStyle.Success : ButtonStyle.Danger) ); }); const row2 = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId('setup_next_5') .setLabel(t(locale, 'commands.setup.step4.nextBtn')) .setStyle(ButtonStyle.Primary) ); components.push(row1, row2); break; } case 5: { embed.setTitle(t(locale, 'commands.setup.step5.title')) .setDescription(t(locale, 'commands.setup.step5.desc')); const select = new ChannelSelectMenuBuilder() .setCustomId('setup_voice_select') .setPlaceholder(t(locale, 'commands.setup.step5.placeholder')) .setChannelTypes(ChannelType.GuildVoice); const btnRow = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId('setup_voice_auto') .setLabel(t(locale, 'commands.setup.step5.autoBtn')) .setStyle(ButtonStyle.Success), new ButtonBuilder() .setCustomId('setup_voice_disable') .setLabel(t(locale, 'commands.setup.step5.skipBtn')) .setStyle(ButtonStyle.Danger), new ButtonBuilder() .setCustomId('setup_next_6') .setLabel(t(locale, 'commands.setup.step5.nextBtn')) .setStyle(ButtonStyle.Secondary) ); components.push(new ActionRowBuilder().addComponents(select)); components.push(btnRow); break; } case 6: { if (!interaction.guild) throw new Error('Guild not found'); const config = await prisma.guildConfig.findUnique({ where: { guildId: interaction.guild.id } }); const audit = await prisma.auditChannel.findUnique({ where: { guildId: interaction.guild.id } }); const voice = await prisma.voiceGenerator.findFirst({ where: { guildId: interaction.guild.id } }); embed.setTitle(t(locale, 'commands.setup.step6.title')) .setColor(Colors.Green); const langStr = config?.locale === 'ko' ? 'Korean' : 'English'; const auditStr = audit?.channelId ? `<#${audit.channelId}>` : 'Disabled'; const voiceStr = voice?.channelId ? `<#${voice.channelId}>` : 'Disabled'; // 감사 로그 카테고리 요약 let catStr = 'None'; if (audit?.channelId) { const allCats: ('SYSTEM' | 'VOICE' | 'PERMISSION' | 'INVITE')[] = ['SYSTEM', 'VOICE', 'PERMISSION', 'INVITE']; const enabled = allCats.filter(c => !audit.disabledCategories.includes(c)); catStr = enabled.map(c => t(locale, `commands.setup.auditCategories.${c}`)).join(', '); } embed.setDescription(t(locale, 'commands.setup.step6.desc', { lang: langStr, audit: auditStr, categories: catStr, voice: voiceStr })); const btnRow = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId('setup_finish') .setLabel(t(locale, 'commands.setup.step6.finishBtn')) .setStyle(ButtonStyle.Success) ); components.push(btnRow); break; } } return { embed, components }; } }