diff --git a/Docs/Design_Principles.md b/Docs/Design_Principles.md new file mode 100644 index 0000000..939c5b8 --- /dev/null +++ b/Docs/Design_Principles.md @@ -0,0 +1,24 @@ +# Kord 디자인 원칙 (Design Principles) + +Kord 봇의 기능 설계 및 사용자 경험(UX) 고도화를 위한 핵심 원칙입니다. + +## 1. 부가 기능의 기본 비활성화 (Opt-in by Default) + +사용자의 서버 운영에 필수가 아닌 부가 기능(Fun features, 유틸리티 성격의 부가 기능 등)은 초기 도입 시 **기본적으로 비활성화(Disabled)** 상태여야 합니다. + +- **이유**: 서버 관리자가 의도하지 않은 봇의 반응(메시지 치환, 자동 이모지 확대 등)으로 인한 혼선을 방지하기 위함입니다. +- **예시**: 미믹(Mimic), 이모지 확대(Big Emoji) 등. + +## 2. 설정 도우미(Setup Wizard)의 간결성 유지 + +`/setup` 명령어를 통해 제공되는 설정 도우미는 서버 운영의 **핵심 필수 설정**에만 집중합니다. + +- **포함 대상**: 언어 설정, 보안/감사 로그 채널, 필수 권한 점검, 핵심 서비스(임시 음성 채널 등) 진입점 설정. +- **제외 대상**: 미믹 활성화 여부, 이모지 확대 여부 등 부가적인 환경 설정. +- **설정 방법**: 설정 도우미에서 제외된 기능은 별도의 관리 명령어(예: `/config`)를 통해 개별적으로 활성화할 수 있도록 제공합니다. + +## 3. 용어의 일관성 (Terminology) + +기능의 명칭은 기술적 정의와 사용자 친화도 사이의 균형을 맞추며, 한 번 정해진 고유 명사는 일관되게 사용합니다. + +- **예시**: 사용자의 메시지를 흉내 내는 기능은 '흉내'라는 일반 명사 대신 **'미믹(Mimic)'**이라는 고유 명사를 프로젝트 전반(코드, 번역, 문서)에서 사용합니다. diff --git a/prisma/migrations/20260327080309_refactor_mimic_emoji_defaults/migration.sql b/prisma/migrations/20260327080309_refactor_mimic_emoji_defaults/migration.sql new file mode 100644 index 0000000..d5f01c0 --- /dev/null +++ b/prisma/migrations/20260327080309_refactor_mimic_emoji_defaults/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "GuildConfig" ADD COLUMN "bigEmojiEnabled" BOOLEAN NOT NULL DEFAULT false, +ALTER COLUMN "mimicEnabled" SET DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 21bde11..458b6e2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,8 +10,9 @@ datasource db { model GuildConfig { guildId String @id prefix String @default("!") - mimicEnabled Boolean @default(true) - locale String? + mimicEnabled Boolean @default(false) + bigEmojiEnabled Boolean @default(false) + locale String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } diff --git a/src/client/KordClient.ts b/src/client/KordClient.ts index bedfb6b..837e7af 100644 --- a/src/client/KordClient.ts +++ b/src/client/KordClient.ts @@ -16,8 +16,7 @@ export class KordClient extends Client { GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMessages, - // GatewayIntentBits.MessageContent, // Privileged -> Disabled for testing - // GatewayIntentBits.GuildMembers, // Privileged -> Disabled for testing + GatewayIntentBits.MessageContent, GatewayIntentBits.GuildInvites, ], partials: [Partials.Message, Partials.Channel, Partials.GuildMember], diff --git a/src/commands/config.ts b/src/commands/config.ts new file mode 100644 index 0000000..599156e --- /dev/null +++ b/src/commands/config.ts @@ -0,0 +1,63 @@ +import { SlashCommandBuilder, ChatInputCommandInteraction, PermissionFlagsBits, EmbedBuilder } from 'discord.js'; +import { prisma } from '../database'; +import { t, resolveLocale } from '../i18n'; + +export default { + data: new SlashCommandBuilder() + .setName('config') + .setDescription('Configure bot features') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addBooleanOption(opt => opt.setName('mimic').setDescription('Enable or disable Mimic feature')) + .addBooleanOption(opt => opt.setName('emoji').setDescription('Enable or disable Big Emoji feature')), + + async execute(interaction: ChatInputCommandInteraction) { + if (!interaction.guildId) return; + + const mimic = interaction.options.getBoolean('mimic'); + const emoji = interaction.options.getBoolean('emoji'); + + // Resolve proper supported locale + const locale = resolveLocale({ + discordLocale: interaction.locale, + guildLocale: interaction.guildLocale?.toString(), + }); + + if (mimic === null && emoji === null) { + return interaction.reply({ + content: t(locale, 'commands.config.noOptions'), + ephemeral: true + }); + } + + const updateData: any = {}; + if (mimic !== null) updateData.mimicEnabled = mimic; + if (emoji !== null) updateData.bigEmojiEnabled = emoji; + + await prisma.guildConfig.upsert({ + where: { guildId: interaction.guildId }, + update: updateData, + create: { + guildId: interaction.guildId, + mimicEnabled: mimic ?? false, + bigEmojiEnabled: emoji ?? false, + }, + }); + + const results: string[] = []; + if (mimic !== null) { + const state = mimic ? t(locale, 'commands.config.mimic.enabled') : t(locale, 'commands.config.mimic.disabled'); + results.push(`${t(locale, 'commands.config.mimic.label')}: **${state}**`); + } + if (emoji !== null) { + const state = emoji ? t(locale, 'commands.config.emoji.enabled') : t(locale, 'commands.config.emoji.disabled'); + results.push(`${t(locale, 'commands.config.emoji.label')}: **${state}**`); + } + + const embed = new EmbedBuilder() + .setColor(0x00AE86) + .setTitle(t(locale, 'commands.config.title')) + .setDescription(results.join('\n')); + + await interaction.reply({ embeds: [embed], ephemeral: true }); + }, +}; diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index bb1f3f7..6bca730 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -1,10 +1,24 @@ import { Events, Message } from 'discord.js'; import { MimicService } from '../services/MimicService'; +import { BigEmojiService } from '../services/BigEmojiService'; +import { prisma } from '../database'; export default { name: Events.MessageCreate, once: false, async execute(message: Message) { - await MimicService.handleMessage(message); + if (!message.guildId || message.author.bot) return; + + const config = await prisma.guildConfig.findUnique({ + where: { guildId: message.guildId } + }); + + if (config?.bigEmojiEnabled) { + await BigEmojiService.handleMessage(message); + } + + if (config?.mimicEnabled) { + await MimicService.handleMessage(message); + } }, }; diff --git a/src/handlers/CommandLoader.ts b/src/handlers/CommandLoader.ts index 8fb1ccc..43ba85a 100644 --- a/src/handlers/CommandLoader.ts +++ b/src/handlers/CommandLoader.ts @@ -11,12 +11,18 @@ export const loadCommands = async (client: KordClient) => { for (const file of commandFiles) { const filePath = path.join(commandsPath, file); - const command = require(filePath).default; - if (command && 'data' in command && 'execute' in command) { - client.commands.set(command.data.name, command); - logger.debug(`Loaded command: ${command.data.name}`); - } else { - logger.warn(`The command at ${filePath} is missing a required "data" or "execute" property.`); + try { + const module = require(filePath); + const command = module.default || module; + + if (command && 'data' in command && 'execute' in command) { + client.commands.set(command.data.name, command); + logger.debug(`Loaded command: ${command.data.name}`); + } else { + logger.warn(`The command at ${filePath} is missing a required "data" or "execute" property.`); + } + } catch (err) { + logger.error(`Failed to load command at ${filePath}:`, err); } } }; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 831aefe..bda0400 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -218,7 +218,6 @@ export const en: TranslationSchema = { VOICE: 'Voice', PERMISSION: 'Permission', INVITE: 'Invite', - MIMIC: 'Mimic', }, }, }, @@ -245,4 +244,18 @@ export const en: TranslationSchema = { managing: 'Managing Temp Voice Channels', version: 'Kord v1.0.0', }, + config: { + title: 'Feature Configuration', + noOptions: 'Please provide at least one option to configure.', + mimic: { + label: 'Mimic', + enabled: 'enabled', + disabled: 'disabled', + }, + emoji: { + label: 'Big Emoji', + enabled: 'enabled', + disabled: 'disabled', + }, + }, }; diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index 275eb1b..4bad602 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -192,8 +192,8 @@ export const ko: TranslationSchema = { nextBtn: '다음 단계' }, step4: { - title: '3-1️⃣ 감사 로그 카테고리', - desc: '수신할 로그 카테고리를 선택하세요. 버튼 색상이 **초록색**이면 수신, **빨간색**이면 차단 상태입니다.', + title: '감사 로그 카테고리 설정', + desc: '로그를 수신할 카테고리를 선택해주세요.', nextBtn: '다음 단계', }, step5: { @@ -218,7 +218,6 @@ export const ko: TranslationSchema = { VOICE: '음성', PERMISSION: '권한', INVITE: '초대', - MIMIC: '흉내', }, }, }, @@ -245,4 +244,18 @@ export const ko: TranslationSchema = { managing: '임시 음성 채널 관리 중', version: 'Kord v1.0.0', }, + config: { + title: '기능 설정 변경 결과', + noOptions: '변경할 옵션을 하나 이상 선택해주세요.', + mimic: { + label: '미믹(Mimic)', + enabled: '활성화', + disabled: '비활성화', + }, + emoji: { + label: '이모지 확대(Big Emoji)', + enabled: '활성화', + disabled: '비활성화', + }, + }, }; diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 2e09ab1..82e2086 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -135,10 +135,23 @@ export interface TranslationSchema { VOICE: string; PERMISSION: string; INVITE: string; - MIMIC: string; }; }; }; + config: { + title: string; + noOptions: string; + mimic: { + label: string; + enabled: string; + disabled: string; + }; + emoji: { + label: string; + enabled: string; + disabled: string; + }; + }; // ── Modals ── modals: { diff --git a/src/services/BigEmojiService.ts b/src/services/BigEmojiService.ts new file mode 100644 index 0000000..00f509a --- /dev/null +++ b/src/services/BigEmojiService.ts @@ -0,0 +1,45 @@ +import { Message, TextChannel, PermissionFlagsBits } from 'discord.js'; +import { WebhookService } from './WebhookService'; +import { logger } from '../utils/logger'; + +export class BigEmojiService { + public static async handleMessage(message: Message) { + if (message.author.bot) return; + if (!(message.channel instanceof TextChannel)) return; + + const content = message.content; + + // Check if message is exactly one custom discord emoji + const customEmojiRegex = /^$/i; + const match = content.match(customEmojiRegex); + + if (match) { + const emojiId = match[1]; + const isAnimated = content.startsWith('$/i; - const match = content.match(customEmojiRegex); - - if (match) { - const emojiId = match[1]; - const isAnimated = content.startsWith('(); categories.forEach(cat => { @@ -200,7 +200,7 @@ export class SetupWizardRenderer { // 감사 로그 카테고리 요약 let catStr = 'None'; if (audit?.channelId) { - const allCats: ('SYSTEM' | 'VOICE' | 'PERMISSION' | 'INVITE' | 'MIMIC')[] = ['SYSTEM', 'VOICE', 'PERMISSION', 'INVITE', 'MIMIC']; + 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(', '); }