Kord/src/services/SetupWizardRenderer.ts

230 lines
8.8 KiB
TypeScript

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<any>[] }> {
const embed = new EmbedBuilder().setColor(Colors.Blurple);
const components: ActionRowBuilder<any>[] = [];
switch (step) {
case 0: {
embed.setTitle(t(locale, 'commands.setup.step0.title'))
.setDescription(t(locale, 'commands.setup.step0.desc'));
const row = new ActionRowBuilder<ButtonBuilder>().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<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId('setup_next_2')
.setLabel(t(locale, 'commands.setup.step1.nextBtn'))
.setStyle(ButtonStyle.Secondary)
);
components.push(new ActionRowBuilder<StringSelectMenuBuilder>().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<ButtonBuilder>().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<ButtonBuilder>().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<ChannelSelectMenuBuilder>().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<ButtonBuilder>();
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<ButtonBuilder>().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<ButtonBuilder>().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<ChannelSelectMenuBuilder>().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<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId('setup_finish')
.setLabel(t(locale, 'commands.setup.step6.finishBtn'))
.setStyle(ButtonStyle.Success)
);
components.push(btnRow);
break;
}
}
return { embed, components };
}
}