diff --git a/Docs/Plans/Setup_Wizard_Plan.md b/Docs/Plans/Setup_Wizard_Plan.md index dffe20b..4f9a189 100644 --- a/Docs/Plans/Setup_Wizard_Plan.md +++ b/Docs/Plans/Setup_Wizard_Plan.md @@ -35,26 +35,34 @@ - **컴포넌트**: 점검 통과 상태 (✅ 모두 정상 / ⚠️ 일부 부족). 부족한 경우 권한 부여를 안내합니다. (권한 검사 모듈 `/audit-permissions`의 축소판) - **액션**: `[다시 검사]` / `[다음]` 버튼. -### Step 3: 감사 채널 설정 (Audit Channel) +### Step 3: 감사 로그 채널 설정 (Audit Channel) - **내용**: 봇의 주요 이벤트와 에러 로그를 남길 시스템 통보 채널을 지정합니다. - **컴포넌트**: - 텍스트 채널을 선택하는 `ChannelSelectMenu` (ChannelType.GuildText 채널만). - `[사용 안함(비활성화)]` 버튼. -- **액션**: 채널 선택 시 DB 갱신 후, `[다음]` 버튼을 눌러 이동 (혹은 선택 즉시 자동 다음 이동 고려). +- **액션**: 채널 선택 시 DB 갱신 후, Step 4로 이동 (사용 안함 선택 시 Step 5로 이동). -### Step 4: 임시 음성 채널 설정 (Voice Generator) +### Step 4: 감사 로그 카테고리 설정 (Audit Categories) + +- **내용**: 수신할 감사 로그의 종류(음성, 권한, 시스템 등)를 필터링합니다. +- **컴포넌트**: + - 각 카테고리(SYSTEM, VOICE, PERMISSION, INVITE, MIMIC)를 토글할 수 있는 버튼 5개. + - 활성화 상태는 **초록색(Success)**, 비활성화는 **빨간색(Danger)**으로 표시. +- **액션**: 버튼 클릭 시 DB의 `disabledCategories` 필드 업데이트 후 현재 뷰 갱신. `[다음 단계]` 버튼으로 이동. + +### Step 5: 임시 음성 채널 설정 (Voice Generator) - **내용**: 임시 음성 채널 생성 시스템의 진입점이 될 '생성기 채널'을 지정하거나 새로 생성합니다. - **컴포넌트**: - 기존 생성할 채널을 고르는 `ChannelSelectMenu` (ChannelType.GuildVoice). - `[자동 생성]` 버튼: 봇이 새 음성 카테고리와 "➕ 음성 채널 생성" 채널을 자동으로 만들어 줌. - `[건너뛰기]` 버튼. -- **액션**: 설정 시 즉각 시스템 구동. 이후 `[다음(완료)]` 클릭. +- **액션**: 설정 시 즉각 시스템 구동. 이후 Step 6(완료 요약)으로 이동. -### Step 5: 설정 요약 (Summary) +### Step 6: 설정 요약 (Summary) -- **내용**: 지금까지 설정된 모든 항목의 최종 상태를 요약하여 보여줍니다. +- **내용**: 지금까지 설정된 모든 항목(내용, 감사 채널/카테고리, 음성 채널)의 최종 상태를 요약하여 보여줍니다. - **컴포넌트**: 설정 결과 요약 Embed. - **액션**: `[설정 마치기]` 버튼 (누르면 "설정이 완료되었습니다"로 메시지 변경 후 버튼 비활성화). diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index b280f87..831aefe 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -191,7 +191,12 @@ export const en: TranslationSchema = { disableBtn: 'Disable Audit Logs', nextBtn: 'Next Step' }, - step4: { + step4: { + title: '3-1️⃣ Audit Log Categories', + desc: 'Select which log categories to receive. **Green** buttons are enabled, **Red** buttons are disabled.', + nextBtn: 'Next Step', + }, + step5: { title: '4️⃣ Temporary Voice Channel Setup', desc: 'Select the "Generator Channel" for temporary voice channels.\nYou can choose an existing channel or have the bot **auto-create** a new category and channel.', placeholder: 'Select Generator Channel', @@ -199,15 +204,22 @@ export const en: TranslationSchema = { skipBtn: 'Disable Temp Voice', nextBtn: 'Finish Setup' }, - step5: { + step6: { title: '🎉 Setup Summary', - desc: '**1. Language**: {{lang}}\n**2. Audit Channel**: {{audit}}\n**3. Temp Voice**: {{voice}}', + desc: '**1. Language**: {{lang}}\n**2. Audit Channel**: {{audit}}\n**3. Audit Categories**: {{categories}}\n**4. Temp Voice**: {{voice}}', finishBtn: 'Done' }, finished: '✅ The setup wizard has been finished.', expired: '⏳ The session has expired. Please run `/setup` again.', defaultCategoryName: 'Voice Channels', defaultGeneratorName: '➕ Create Channel', + auditCategories: { + SYSTEM: 'System', + VOICE: 'Voice', + PERMISSION: 'Permission', + INVITE: 'Invite', + MIMIC: 'Mimic', + }, }, }, diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index 771e13f..275eb1b 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -191,7 +191,12 @@ export const ko: TranslationSchema = { disableBtn: '감사 채널 끄기/해제', nextBtn: '다음 단계' }, - step4: { + step4: { + title: '3-1️⃣ 감사 로그 카테고리', + desc: '수신할 로그 카테고리를 선택하세요. 버튼 색상이 **초록색**이면 수신, **빨간색**이면 차단 상태입니다.', + nextBtn: '다음 단계', + }, + step5: { title: '4️⃣ 임시 음성 채널 설정', desc: '임시 음성 채널을 생성할 "생성기 채널"을 선택해주세요.\n기존의 채널을 고르거나 카테고리/채널을 봇이 **자동 생성**하게 할 수도 있습니다.', placeholder: '생성기로 쓸 음성 채널 선택', @@ -199,15 +204,22 @@ export const ko: TranslationSchema = { skipBtn: '임시 음성 사용 안함', nextBtn: '설정 완료' }, - step5: { + step6: { title: '🎉 설정 완료 요약', - desc: '**1. 언어**: {{lang}}\n**2. 감사 채널**: {{audit}}\n**3. 임시 음성 채널**: {{voice}}', + desc: '**1. 언어**: {{lang}}\n**2. 감사 채널**: {{audit}}\n**3. 감사 카테고리**: {{categories}}\n**4. 임시 음성 채널**: {{voice}}', finishBtn: '마치기' }, finished: '✅ 설정 마법사를 종료했습니다.', expired: '⏳ 시간이 만료되었습니다. `/setup`을 다시 실행해주세요.', defaultCategoryName: '음성 채널', defaultGeneratorName: '➕ 채널 생성하기', + auditCategories: { + SYSTEM: '시스템', + VOICE: '음성', + PERMISSION: '권한', + INVITE: '초대', + MIMIC: '흉내', + }, }, }, diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 42f6a7c..2e09ab1 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -123,12 +123,20 @@ export interface TranslationSchema { step1: { title: string; desc: string; placeholder: string; nextBtn: string; skipBtn: string; }; step2: { title: string; descOk: string; descFail: string; recheckBtn: string; nextBtn: string; }; step3: { title: string; desc: string; placeholder: string; disableBtn: string; nextBtn: string; }; - step4: { title: string; desc: string; placeholder: string; autoBtn: string; skipBtn: string; nextBtn: string; }; - step5: { title: string; desc: string; finishBtn: string; }; + step4: { title: string; desc: string; nextBtn: string; }; + step5: { title: string; desc: string; placeholder: string; autoBtn: string; skipBtn: string; nextBtn: string; }; + step6: { title: string; desc: string; finishBtn: string; }; finished: string; expired: string; defaultCategoryName: string; defaultGeneratorName: string; + auditCategories: { + SYSTEM: string; + VOICE: string; + PERMISSION: string; + INVITE: string; + MIMIC: string; + }; }; }; diff --git a/src/interactions/handlers/setupWizardHandler.ts b/src/interactions/handlers/setupWizardHandler.ts index 265ccc4..0b9672b 100644 --- a/src/interactions/handlers/setupWizardHandler.ts +++ b/src/interactions/handlers/setupWizardHandler.ts @@ -33,6 +33,29 @@ export async function handleSetupWizardInteraction(interaction: MessageComponent return; } + // Step 4 Toggle: Audit Category + if (customId.startsWith('setup_audit_toggle_')) { + const category = customId.replace('setup_audit_toggle_', ''); + const audit = await prisma.auditChannel.findUnique({ where: { guildId: interaction.guildId! } }); + if (!audit) return; + + let disabled = [...audit.disabledCategories]; + if (disabled.includes(category)) { + disabled = disabled.filter(c => c !== category); + } else { + disabled.push(category); + } + + await prisma.auditChannel.update({ + where: { guildId: interaction.guildId! }, + data: { disabledCategories: disabled } + }); + + const { embed, components } = await SetupWizardRenderer.renderStep(4, interaction, locale); + await interaction.update({ embeds: [embed], components }); + return; + } + // Step 1: Language Select if (customId === 'setup_lang_select' && interaction.isStringSelectMenu()) { const selectedLocale = interaction.values[0] as SupportedLocale; @@ -57,14 +80,15 @@ export async function handleSetupWizardInteraction(interaction: MessageComponent create: { guildId: interaction.guildId!, channelId } }); - // Auto proceed to next step + // Auto proceed to next step (Step 4: Categories) const { embed, components } = await SetupWizardRenderer.renderStep(4, interaction, locale); await interaction.update({ embeds: [embed], components }); return; } if (customId === 'setup_audit_disable') { await prisma.auditChannel.delete({ where: { guildId: interaction.guildId! } }).catch(() => {}); - const { embed, components } = await SetupWizardRenderer.renderStep(4, interaction, locale); + // Skip categories if disabled, go to Step 5: Voice Setup + const { embed, components } = await SetupWizardRenderer.renderStep(5, interaction, locale); await interaction.update({ embeds: [embed], components }); return; } @@ -89,7 +113,7 @@ export async function handleSetupWizardInteraction(interaction: MessageComponent create: { channelId: channel.id, guildId: interaction.guildId!, categoryId: channel.parentId } }); - const { embed, components } = await SetupWizardRenderer.renderStep(5, interaction, locale); + const { embed, components } = await SetupWizardRenderer.renderStep(6, interaction, locale); await interaction.update({ embeds: [embed], components }); return; } @@ -123,7 +147,7 @@ export async function handleSetupWizardInteraction(interaction: MessageComponent } }); - const { embed, components } = await SetupWizardRenderer.renderStep(5, interaction, locale); + const { embed, components } = await SetupWizardRenderer.renderStep(6, interaction, locale); await interaction.editReply({ embeds: [embed], components }); } catch (e) { if ((e as Error).message.includes('Missing Permissions')) { @@ -136,7 +160,7 @@ export async function handleSetupWizardInteraction(interaction: MessageComponent if (customId === 'setup_voice_disable') { await prisma.voiceGenerator.deleteMany({ where: { guildId: interaction.guildId! } }); - const { embed, components } = await SetupWizardRenderer.renderStep(5, interaction, locale); + const { embed, components } = await SetupWizardRenderer.renderStep(6, interaction, locale); await interaction.update({ embeds: [embed], components }); return; } diff --git a/src/services/SetupWizardRenderer.ts b/src/services/SetupWizardRenderer.ts index 1a5201d..29c91e0 100644 --- a/src/services/SetupWizardRenderer.ts +++ b/src/services/SetupWizardRenderer.ts @@ -124,26 +124,57 @@ export class SetupWizardRenderer { } 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' | 'MIMIC')[] = ['SYSTEM', 'VOICE', 'PERMISSION', 'INVITE', 'MIMIC']; + 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.step4.placeholder')) + .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.step4.autoBtn')) + .setLabel(t(locale, 'commands.setup.step5.autoBtn')) .setStyle(ButtonStyle.Success), new ButtonBuilder() .setCustomId('setup_voice_disable') - .setLabel(t(locale, 'commands.setup.step4.skipBtn')) + .setLabel(t(locale, 'commands.setup.step5.skipBtn')) .setStyle(ButtonStyle.Danger), new ButtonBuilder() - .setCustomId('setup_next_5') - .setLabel(t(locale, 'commands.setup.step4.nextBtn')) + .setCustomId('setup_next_6') + .setLabel(t(locale, 'commands.setup.step5.nextBtn')) .setStyle(ButtonStyle.Secondary) ); @@ -152,30 +183,39 @@ export class SetupWizardRenderer { break; } - case 5: { + 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.step5.title')) + 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'; - embed.setDescription(t(locale, 'commands.setup.step5.desc', { + // 감사 로그 카테고리 요약 + let catStr = 'None'; + if (audit?.channelId) { + const allCats: ('SYSTEM' | 'VOICE' | 'PERMISSION' | 'INVITE' | 'MIMIC')[] = ['SYSTEM', 'VOICE', 'PERMISSION', 'INVITE', 'MIMIC']; + 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.step5.finishBtn')) + .setLabel(t(locale, 'commands.setup.step6.finishBtn')) .setStyle(ButtonStyle.Success) );