Compare commits
No commits in common. "ef15273a021aa5309f8581ffdfe211d1bb2aaff1" and "429953306c74f7c006a02a94a2b58986d7e9363d" have entirely different histories.
ef15273a02
...
429953306c
|
|
@ -1,27 +0,0 @@
|
|||
---
|
||||
description: Discord Bot UI/UX Design Philosophy
|
||||
---
|
||||
|
||||
# Discord Bot UI/UX Design Philosophy
|
||||
|
||||
When designing or updating Discord command interfaces (Embeds, Components), adhere to the following UI/UX philosophy to ensure a clean, intuitive, and modern user experience.
|
||||
|
||||
## 1. Minimal and Non-redundant Information (중복 정보 최소화)
|
||||
- Do not display information in the Embed that is already visually apparent in the UI components.
|
||||
- For example, if a `RoleSelectMenuBuilder` allows the user to select roles, use `.addDefaultRoles(ids)` (available in discord.js 14.14+) to display the currently selected roles natively inside the dropdown menu.
|
||||
- Do NOT list those same roles redundantly as text inside the Embed fields. The Embed should remain concise, showing only titles and essential descriptions or instructions.
|
||||
|
||||
## 2. Implicit State (명시적 토글 지양 및 상태 직관화)
|
||||
- Avoid creating manual On/Off toggle buttons unless absolutely necessary.
|
||||
- Derive the "Enabled/Disabled" state directly from the user's data naturally. For instance, if the user has selected at least one role (`roleIds.length > 0`), the feature is automatically considered "Active/Enabled". If they clear the selection, the feature is "Disabled".
|
||||
- This reduces UI clutter (removing unnecessary toggle ActionRows) and aligns with modern design patterns where state implicitly follows the presence of data.
|
||||
|
||||
## 3. Persistent and Seamless Interaction (매끄러운 대시보드 유지)
|
||||
- Component interactions should feel fast and seamless without fragmenting the chat history.
|
||||
- Always immediately call `await interaction.deferUpdate();` (or equivalent) when handling components (buttons, select menus) to prevent "Unknown interaction" timeout errors.
|
||||
- Use `await interaction.editReply(...)` with the newly generated UI components to seamlessly update the dashboard frame in place.
|
||||
- Do NOT generate new follow-up messages or close the menu unilaterally when the user still expects to tweak settings.
|
||||
|
||||
## 4. Safe Response Timings (타임아웃 방지)
|
||||
- When processing `ChatInputCommandInteraction` that might involve a database cold-start connection or external API calls, proactively call `await interaction.deferReply({ ephemeral: true });` right at the start.
|
||||
- Update the UI with `await interaction.editReply(...)` once business logic resolves, bypassing Discord's strict 3-second timeout limitation and preventing crashes during initial boot load.
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "ExcludeType" AS ENUM ('ROLE', 'USER');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AutoRoleConfig" (
|
||||
"guildId" TEXT NOT NULL,
|
||||
"userRoleId" TEXT,
|
||||
"botRoleId" TEXT,
|
||||
"isEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"botEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "AutoRoleConfig_pkey" PRIMARY KEY ("guildId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AutoRoleExclude" (
|
||||
"id" TEXT NOT NULL,
|
||||
"guildId" TEXT NOT NULL,
|
||||
"targetId" TEXT NOT NULL,
|
||||
"type" "ExcludeType" NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AutoRoleExclude_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AutoRoleExclude_guildId_targetId_type_key" ON "AutoRoleExclude"("guildId", "targetId", "type");
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `botRoleId` on the `AutoRoleConfig` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `userRoleId` on the `AutoRoleConfig` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "AutoRoleConfig" DROP COLUMN "botRoleId",
|
||||
DROP COLUMN "userRoleId",
|
||||
ADD COLUMN "botRoleIds" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
ADD COLUMN "userRoleIds" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `AutoRoleExclude` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropTable
|
||||
DROP TABLE "AutoRoleExclude";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "ExcludeType";
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `InviteRole` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropTable
|
||||
DROP TABLE "InviteRole";
|
||||
|
|
@ -17,6 +17,16 @@ model GuildConfig {
|
|||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model InviteRole {
|
||||
id String @id @default(uuid())
|
||||
guildId String
|
||||
inviteCode String
|
||||
roleId String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([guildId, inviteCode])
|
||||
}
|
||||
|
||||
model UserSubscription {
|
||||
userId String @id
|
||||
tier SubscriptionTier @default(FREE)
|
||||
|
|
@ -198,18 +208,6 @@ model FeverState {
|
|||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model AutoRoleConfig {
|
||||
guildId String @id
|
||||
userRoleIds String[] @default([])
|
||||
botRoleIds String[] @default([])
|
||||
isEnabled Boolean @default(false)
|
||||
botEnabled Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
|
||||
|
||||
model RefinementLevelConfig {
|
||||
level Int @id
|
||||
successRate Float
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export class KordClient extends Client {
|
|||
GatewayIntentBits.GuildVoiceStates,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.GuildInvites,
|
||||
],
|
||||
partials: [Partials.Message, Partials.Channel, Partials.GuildMember],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
import {
|
||||
SlashCommandBuilder,
|
||||
ChatInputCommandInteraction,
|
||||
PermissionFlagsBits,
|
||||
EmbedBuilder,
|
||||
Colors,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
RoleSelectMenuBuilder,
|
||||
ComponentType
|
||||
} from 'discord.js';
|
||||
import { Command, CommandTrait } from '../core/command';
|
||||
import { autoRoleService } from '../services/AutoRoleService';
|
||||
import { t, SupportedLocale } from '../i18n';
|
||||
|
||||
class AutoRoleCommand extends Command {
|
||||
protected override readonly trait = CommandTrait.General;
|
||||
protected override guildOnly = true;
|
||||
|
||||
protected override define() {
|
||||
return new SlashCommandBuilder()
|
||||
.setName('autorole')
|
||||
.setDescription('Configure automatic role assignment upon joining.')
|
||||
.setDescriptionLocalizations({
|
||||
ko: '입장 시 역할을 자동으로 부여하는 기능을 설정합니다.',
|
||||
})
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator);
|
||||
}
|
||||
|
||||
protected override async handle(interaction: ChatInputCommandInteraction, locale: SupportedLocale) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
const guild = interaction.guild!;
|
||||
const dashboard = await generateAutoRoleDashboard(guild, locale);
|
||||
await interaction.editReply({
|
||||
...dashboard
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateAutoRoleDashboard(guild: import('discord.js').Guild, locale: SupportedLocale) {
|
||||
const config = await autoRoleService.getConfig(guild.id);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(t(locale, 'commands.autorole.statusTitle'))
|
||||
.setColor(Colors.Blue)
|
||||
.setDescription(t(locale, 'commands.autorole.description') || '유저 및 봇이 서버에 접속할 때 자동으로 부여할 기본 역할을 선택하세요. 역할을 선택하면 즉시 활성화됩니다.');
|
||||
|
||||
const userSelect = new RoleSelectMenuBuilder()
|
||||
.setCustomId('autorole_select_user')
|
||||
.setPlaceholder(t(locale, 'commands.autorole.userRolePlaceholder'))
|
||||
.setMaxValues(10);
|
||||
if (config?.userRoleIds && config.userRoleIds.length > 0) {
|
||||
userSelect.addDefaultRoles(config.userRoleIds);
|
||||
}
|
||||
|
||||
const rowUserRole = new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(userSelect);
|
||||
|
||||
const botSelect = new RoleSelectMenuBuilder()
|
||||
.setCustomId('autorole_select_bot')
|
||||
.setPlaceholder(t(locale, 'commands.autorole.botRolePlaceholder'))
|
||||
.setMaxValues(10);
|
||||
if (config?.botRoleIds && config.botRoleIds.length > 0) {
|
||||
botSelect.addDefaultRoles(config.botRoleIds);
|
||||
}
|
||||
|
||||
const rowBotRole = new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(botSelect);
|
||||
|
||||
return {
|
||||
embeds: [embed],
|
||||
components: [rowUserRole, rowBotRole]
|
||||
};
|
||||
}
|
||||
|
||||
export default new AutoRoleCommand().toModule();
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import { Events, Guild } from 'discord.js';
|
||||
import { InviteService } from '../services/InviteService';
|
||||
import { PresenceService } from '../services/PresenceService';
|
||||
|
||||
export default {
|
||||
name: Events.GuildCreate,
|
||||
once: false,
|
||||
async execute(guild: Guild) {
|
||||
await InviteService.cacheGuildInvites(guild);
|
||||
PresenceService.updatePresence(guild.client);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Events, GuildMember } from 'discord.js';
|
||||
import { autoRoleService } from '../services/AutoRoleService';
|
||||
import { InviteService } from '../services/InviteService';
|
||||
|
||||
export default {
|
||||
name: Events.GuildMemberAdd,
|
||||
once: false,
|
||||
async execute(member: GuildMember) {
|
||||
await autoRoleService.handleMemberJoin(member);
|
||||
await InviteService.handleMemberAdd(member);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -143,40 +143,6 @@ export default {
|
|||
}, locale);
|
||||
}
|
||||
}
|
||||
else if (interaction.isButton() && interaction.customId.startsWith('autorole_')) {
|
||||
const locale = await getInteractionLocale(interaction);
|
||||
const { autoRoleService } = require('../services/AutoRoleService');
|
||||
await withErrorHandler(interaction, async () => {
|
||||
// 타임아웃 방지를 위해 즉시 승인
|
||||
await interaction.deferUpdate();
|
||||
|
||||
// 나머지 버튼 처리 (현재 사용 안함)
|
||||
}, locale);
|
||||
}
|
||||
else if (interaction.isRoleSelectMenu() && interaction.customId.startsWith('autorole_select_')) {
|
||||
const locale = await getInteractionLocale(interaction);
|
||||
const { autoRoleService } = require('../services/AutoRoleService');
|
||||
await withErrorHandler(interaction, async () => {
|
||||
// 타임아웃 방지를 위해 즉시 승인
|
||||
await interaction.deferUpdate();
|
||||
|
||||
const guild = interaction.guild!;
|
||||
const isBot = interaction.customId.includes('bot');
|
||||
const roleIds = interaction.values;
|
||||
|
||||
await autoRoleService.updateConfig(guild.id, {
|
||||
[isBot ? 'botRoleIds' : 'userRoleIds']: roleIds
|
||||
});
|
||||
|
||||
const { generateAutoRoleDashboard } = require('../commands/autorole');
|
||||
const dashboard = await generateAutoRoleDashboard(guild, locale);
|
||||
|
||||
await interaction.editReply({
|
||||
content: '',
|
||||
...dashboard
|
||||
});
|
||||
}, locale);
|
||||
}
|
||||
else if (interaction.isModalSubmit()) {
|
||||
const customId = interaction.customId;
|
||||
if (customId.startsWith('modal_vc_')) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import { Events, Invite } from 'discord.js';
|
||||
import { InviteService } from '../services/InviteService';
|
||||
|
||||
export default {
|
||||
name: Events.InviteCreate,
|
||||
once: false,
|
||||
async execute(invite: Invite) {
|
||||
await InviteService.handleInviteCreate(invite);
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { Events, Invite } from 'discord.js';
|
||||
import { InviteService } from '../services/InviteService';
|
||||
|
||||
export default {
|
||||
name: Events.InviteDelete,
|
||||
once: false,
|
||||
async execute(invite: Invite) {
|
||||
await InviteService.handleInviteDelete(invite);
|
||||
},
|
||||
};
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { Events } from 'discord.js';
|
||||
import { KordClient } from '../client/KordClient';
|
||||
import { logger } from '../utils/logger';
|
||||
import { InviteService } from '../services/InviteService';
|
||||
import { VoiceService } from '../services/VoiceService';
|
||||
import { PresenceService } from '../services/PresenceService';
|
||||
import { EventService } from '../services/EventService';
|
||||
|
|
@ -13,6 +14,7 @@ export default {
|
|||
once: true,
|
||||
async execute(client: KordClient) {
|
||||
logger.info(`Ready! Logged in as ${client.user?.tag}`);
|
||||
await InviteService.cacheAllInvites(client);
|
||||
await VoiceService.syncChannels(client);
|
||||
PresenceService.startActivePresence(client);
|
||||
EventService.startReminderLoop(client);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TranslationSchema } from '../types';
|
||||
import { TranslationSchema } from '../types';
|
||||
|
||||
/**
|
||||
* English translations ??the DEFAULT and FALLBACK locale.
|
||||
|
|
@ -191,26 +191,6 @@ export const en: TranslationSchema = {
|
|||
status: 'Status',
|
||||
},
|
||||
},
|
||||
autorole: {
|
||||
description: 'Configure automatic role assignment upon joining.',
|
||||
statusTitle: 'Auto Role Configuration Status',
|
||||
userRoleLabel: 'User Role',
|
||||
botRoleLabel: 'Bot Role',
|
||||
statusLabel: 'User Auto Assignment',
|
||||
botStatusLabel: 'Bot Auto Assignment',
|
||||
userRolePlaceholder: 'Select default user role',
|
||||
botRolePlaceholder: 'Select default bot role',
|
||||
toggleUserEnable: '🟢 Enable User AutoRole',
|
||||
toggleUserDisable: '🔴 Disable User AutoRole',
|
||||
toggleBotEnable: '🟢 Enable Bot AutoRole',
|
||||
toggleBotDisable: '🔴 Disable Bot AutoRole',
|
||||
notSet: 'Not Set',
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
updateSuccess: 'Auto role settings have been updated.',
|
||||
permissionsError: 'Failed to assign role due to low bot hierarchy or missing permissions.',
|
||||
suspendNotice: 'Auto role assignment has been suspended due to insufficient permissions. Please check the bot\'s permissions and role hierarchy.',
|
||||
},
|
||||
music: {
|
||||
description: 'Play YouTube audio in voice channels.',
|
||||
addDescription: 'Search YouTube or add a video URL to the queue.',
|
||||
|
|
@ -348,6 +328,8 @@ export const en: TranslationSchema = {
|
|||
VOICE_GLOBAL: 'Voice Channels (Global)',
|
||||
VOICE_GENERATOR_CHANNEL: 'Voice Generator Channel',
|
||||
VOICE_GENERATOR_CATEGORY: 'Voice Generator Category',
|
||||
INVITE_TRACKING: 'Invite Tracking',
|
||||
INVITE_ROLE_HIERARCHY: 'Invite Role Assignment (Hierarchy)',
|
||||
MIMIC_WEBHOOK: 'Message Mimic (Webhook)',
|
||||
},
|
||||
},
|
||||
|
|
@ -406,6 +388,7 @@ export const en: TranslationSchema = {
|
|||
BOOT: 'Boot',
|
||||
VOICE: 'Voice',
|
||||
PERMISSION: 'Permission',
|
||||
INVITE: 'Invite',
|
||||
},
|
||||
},
|
||||
config: {
|
||||
|
|
|
|||
|
|
@ -1,216 +1,196 @@
|
|||
import { TranslationSchema } from '../types';
|
||||
import { TranslationSchema } from '../types';
|
||||
|
||||
/**
|
||||
* 한국어 번역 파일.
|
||||
* 모든 키가 en.ts와 1:1 대응되어야 합니다.
|
||||
* ?쒓뎅??踰덉뿭 ?뚯씪.
|
||||
* 紐⑤뱺 ?ㅺ? en.ts? 1:1 ??묐릺?댁빞 ?⑸땲??
|
||||
*/
|
||||
export const ko: TranslationSchema = {
|
||||
// ── 에러 메시지 ─────────────────────────────────────────
|
||||
// ?? ?먮윭 硫붿떆吏 ?????????????????????????????????????????
|
||||
errors: {
|
||||
E1001: {
|
||||
userMessage: '사용자 제한 값이 올바르지 않습니다.',
|
||||
resolution: '0에서 99 사이의 숫자를 입력해주세요. (0 = 무제한)',
|
||||
resolution: '0?먯꽌 99 ?ъ씠???レ옄瑜??낅젰?댁<?몄슂. (0 = 臾댁젣??',
|
||||
},
|
||||
E1002: {
|
||||
userMessage: '채널 이름 형식이 올바르지 않습니다.',
|
||||
resolution: '유효한 채널 이름을 입력해주세요. (최대 100자)',
|
||||
userMessage: '梨꾨꼸 ?대쫫 ?뺤떇???щ컮瑜댁? ?딆뒿?덈떎.',
|
||||
resolution: '?좏슚??梨꾨꼸 ?대쫫???낅젰?댁<?몄슂. (理쒕? 100??',
|
||||
},
|
||||
E1003: {
|
||||
userMessage: '자기 자신에게는 이 작업을 수행할 수 없습니다.',
|
||||
userMessage: '?먭린 ?먯떊?먭쾶?????묒뾽???섑뻾?????놁뒿?덈떎.',
|
||||
},
|
||||
E1004: {
|
||||
userMessage: '선택한 유저가 음성 채널에 없습니다.',
|
||||
resolution: '작업을 수행하기 전에 해당 유저가 채널에 있는지 확인해주세요.',
|
||||
userMessage: '?좏깮???좎?媛 ?뚯꽦 梨꾨꼸???놁뒿?덈떎.',
|
||||
resolution: '?묒뾽???섑뻾?섍린 ?꾩뿉 ?대떦 ?좎?媛 梨꾨꼸???덈뒗吏 ?뺤씤?댁<?몄슂.',
|
||||
},
|
||||
E2001: {
|
||||
userMessage: '봇에 채널을 관리할 권한이 부족합니다.',
|
||||
resolution: '서버 관리자에게 봇의 "채널 관리" 권한을 확인해달라고 요청하세요.',
|
||||
userMessage: '遊뉗뿉 梨꾨꼸??愿由ы븷 沅뚰븳??遺議깊빀?덈떎.',
|
||||
resolution: '?쒕쾭 愿由ъ옄?먭쾶 遊뉗쓽 "梨꾨꼸 愿由? 沅뚰븳???뺤씤?대떖?쇨퀬 ?붿껌?섏꽭??',
|
||||
},
|
||||
E2002: {
|
||||
userMessage: '봇에 음성 채널 관련 권한이 부족합니다.',
|
||||
resolution: '서버 관리자에게 봇의 "채널 관리", "역할 관리", "멤버 이동" 권한을 확인해달라고 요청하세요.',
|
||||
userMessage: '遊뉗뿉 ?뚯꽦 梨꾨꼸 愿??沅뚰븳??遺議깊빀?덈떎.',
|
||||
resolution: '?쒕쾭 愿由ъ옄?먭쾶 遊뉗쓽 "梨꾨꼸 愿由?, "??븷 愿由?, "硫ㅻ쾭 ?대룞" 沅뚰븳???뺤씤?대떖?쇨퀬 ?붿껌?섏꽭??',
|
||||
},
|
||||
E2003: {
|
||||
userMessage: '이 명령어를 사용할 권한이 없습니다.',
|
||||
resolution: '이 명령어는 관리자 권한이 필요합니다.',
|
||||
userMessage: '??紐낅졊?대? ?ъ슜??沅뚰븳???놁뒿?덈떎.',
|
||||
resolution: '??紐낅졊?대뒗 愿由ъ옄 沅뚰븳???꾩슂?⑸땲??',
|
||||
},
|
||||
E2004: {
|
||||
userMessage: '채널 소유자만 이 기능을 사용할 수 있습니다.',
|
||||
userMessage: '梨꾨꼸 ?뚯쑀?먮쭔 ??湲곕뒫???ъ슜?????덉뒿?덈떎.',
|
||||
},
|
||||
E2005: {
|
||||
userMessage: '활성된 임시 음성 채널에 참여 중이어야 사용할 수 있습니다.',
|
||||
resolution: '임시 음성 채널에 참여한 뒤 다시 시도해주세요.',
|
||||
userMessage: '?쒖꽦???꾩떆 ?뚯꽦 梨꾨꼸??李몄뿬 以묒씠?댁빞 ?ъ슜?????덉뒿?덈떎.',
|
||||
resolution: '?꾩떆 ?뚯꽦 梨꾨꼸??李몄뿬?????ㅼ떆 ?쒕룄?댁<?몄슂.',
|
||||
},
|
||||
E3001: {
|
||||
userMessage: '요청을 처리하는 중 내부 오류가 발생했습니다.',
|
||||
resolution: '잠시 후 다시 시도해주세요. 문제가 지속되면 봇 관리자에게 문의하세요.',
|
||||
userMessage: '?붿껌??泥섎━?섎뒗 以??대? ?ㅻ쪟媛 諛쒖깮?덉뒿?덈떎.',
|
||||
resolution: '?좎떆 ???ㅼ떆 ?쒕룄?댁<?몄슂. 臾몄젣媛 吏?띾릺硫?遊?愿由ъ옄?먭쾶 臾몄쓽?섏꽭??',
|
||||
},
|
||||
E3002: {
|
||||
userMessage: '요청을 처리하는 중 내부 오류가 발생했습니다.',
|
||||
resolution: '잠시 후 다시 시도해주세요.',
|
||||
userMessage: '?붿껌??泥섎━?섎뒗 以??대? ?ㅻ쪟媛 諛쒖깮?덉뒿?덈떎.',
|
||||
resolution: '?좎떆 ???ㅼ떆 ?쒕룄?댁<?몄슂.',
|
||||
},
|
||||
E3003: {
|
||||
userMessage: '명령어를 실행하는 중 오류가 발생했습니다.',
|
||||
resolution: '다시 시도해주세요. 문제가 지속되면 봇 관리자에게 문의하세요.',
|
||||
userMessage: '紐낅졊?대? ?ㅽ뻾?섎뒗 以??ㅻ쪟媛 諛쒖깮?덉뒿?덈떎.',
|
||||
resolution: '?ㅼ떆 ?쒕룄?댁<?몄슂. 臾몄젣媛 吏?띾릺硫?遊?愿由ъ옄?먭쾶 臾몄쓽?섏꽭??',
|
||||
},
|
||||
E3999: {
|
||||
userMessage: '예상치 못한 오류가 발생했습니다.',
|
||||
resolution: '나중에 다시 시도해주세요. 문제가 계속되면 봇 관리자에게 문의하세요.',
|
||||
userMessage: '?덉긽移?紐삵븳 ?ㅻ쪟媛 諛쒖깮?덉뒿?덈떎.',
|
||||
resolution: '?섏쨷???ㅼ떆 ?쒕룄?댁<?몄슂. 臾몄젣媛 怨꾩냽?섎㈃ 遊?愿由ъ옄?먭쾶 臾몄쓽?섏꽭??',
|
||||
},
|
||||
E4001: {
|
||||
userMessage: 'Discord에 의해 요청이 제한되었습니다.',
|
||||
resolution: '잠시 기다린 후 다시 시도해주세요.',
|
||||
userMessage: 'Discord???섑빐 ?붿껌???쒗븳?섏뿀?듬땲??',
|
||||
resolution: '?좎떆 湲곕떎由????ㅼ떆 ?쒕룄?댁<?몄슂.',
|
||||
},
|
||||
E4002: {
|
||||
userMessage: '권한 부족으로 Discord가 작업을 거부했습니다.',
|
||||
resolution: '서버 관리자에게 봇의 역할 및 채널 권한을 확인해달라고 요청하세요.',
|
||||
userMessage: '沅뚰븳 遺議깆쑝濡?Discord媛 ?묒뾽??嫄곕??덉뒿?덈떎.',
|
||||
resolution: '?쒕쾭 愿由ъ옄?먭쾶 遊뉗쓽 ??븷 諛?梨꾨꼸 沅뚰븳???뺤씤?대떖?쇨퀬 ?붿껌?섏꽭??',
|
||||
},
|
||||
E4003: {
|
||||
userMessage: 'Discord에 일시적인 문제가 발생했습니다.',
|
||||
resolution: '잠시 후 다시 시도해주세요. 문제가 지속되면 https://discordstatus.com 에서 상태를 확인해주세요.',
|
||||
userMessage: 'Discord???쇱떆?곸씤 臾몄젣媛 諛쒖깮?덉뒿?덈떎.',
|
||||
resolution: '?좎떆 ???ㅼ떆 ?쒕룄?댁<?몄슂. 臾몄젣媛 吏?띾릺硫?https://discordstatus.com ?먯꽌 ?곹깭瑜??뺤씤?댁<?몄슂.',
|
||||
},
|
||||
},
|
||||
|
||||
// ── 에러 카테고리 타이틀 ────────────────────────────────
|
||||
// ?? ?먮윭 移댄뀒怨좊━ ??댄? ????????????????????????????????
|
||||
errorTitles: {
|
||||
USER_INPUT: '입력을 확인해주세요',
|
||||
PERMISSION: '권한이 부족합니다',
|
||||
BOT_INTERNAL: '문제가 발생했습니다',
|
||||
DISCORD_API: '일시적인 문제입니다',
|
||||
PERMISSION: '沅뚰븳??遺議깊빀?덈떎',
|
||||
BOT_INTERNAL: '臾몄젣媛 諛쒖깮?덉뒿?덈떎',
|
||||
DISCORD_API: '일시적인 문제입니다.',
|
||||
},
|
||||
|
||||
// ── 에러 Embed 필드 라벨 ────────────────────────────────
|
||||
// ?? ?먮윭 Embed ?꾨뱶 ?쇰꺼 ????????????????????????????????
|
||||
errorFields: {
|
||||
resolution: '💡 해결 방법',
|
||||
},
|
||||
|
||||
// ── 음성 채널 ───────────────────────────────────────────
|
||||
// ?? ?뚯꽦 梨꾨꼸 ???????????????????????????????????????????
|
||||
voice: {
|
||||
channelReady: '{{owner}}, 임시 채널이 준비되었습니다! 아래 드롭다운 메뉴로 관리하세요.',
|
||||
defaultRoomName: '{{username}}의 방',
|
||||
controlPanel: {
|
||||
placeholder: '⚙️ 채널 설정 관리',
|
||||
placeholder: '채널 설정 관리',
|
||||
rename: '채널 이름 변경',
|
||||
limit: '인원 제한 설정',
|
||||
lock: '채널 잠금 / 해제',
|
||||
kick: '유저 추방',
|
||||
limit: '?몄썝 ?쒗븳 ?ㅼ젙',
|
||||
lock: '梨꾨꼸 ?좉툑 / ?댁젣',
|
||||
kick: '?좎? 異붾갑',
|
||||
ban: '유저 차단 / 숨기기',
|
||||
transfer: '소유권 이전',
|
||||
transfer: '?뚯쑀沅??댁쟾',
|
||||
},
|
||||
responses: {
|
||||
channelLocked: '채널이 잠겼습니다! 초대된 멤버만 참여할 수 있습니다.',
|
||||
channelUnlocked: '채널이 해제되었습니다! 누구나 참여할 수 있습니다.',
|
||||
channelRenamed: '채널 이름이 **{{name}}**(으)로 변경되었습니다!',
|
||||
limitSet: '인원 제한이 **{{limit}}**명으로 설정되었습니다!',
|
||||
channelUnlocked: '梨꾨꼸???댁젣?섏뿀?듬땲?? ?꾧뎄??李몄뿬?????덉뒿?덈떎.',
|
||||
channelRenamed: '梨꾨꼸 ?대쫫??**{{name}}**(??濡?蹂寃쎈릺?덉뒿?덈떎!',
|
||||
limitSet: '?몄썝 ?쒗븳??**{{limit}}**紐낆쑝濡??ㅼ젙?섏뿀?듬땲??',
|
||||
limitUnlimited: '무제한',
|
||||
kicked: '{{user}}을(를) 채널에서 추방했습니다.',
|
||||
banned: '{{user}}에게 채널을 숨기고 차단했습니다.',
|
||||
transferPrompt: '채널의 새 소유자를 선택하세요.',
|
||||
transferDone: '소유권이 {{user}}에게 이전되었습니다.',
|
||||
banPrompt: '차단하면 해당 유저에게 채널이 보이지 않게 됩니다.',
|
||||
kicked: '{{user}}??瑜? 梨꾨꼸?먯꽌 異붾갑?덉뒿?덈떎.',
|
||||
banned: '{{user}}?먭쾶 梨꾨꼸???④린怨?李⑤떒?덉뒿?덈떎.',
|
||||
transferPrompt: '梨꾨꼸?????뚯쑀?먮? ?좏깮?섏꽭??',
|
||||
transferDone: '?뚯쑀沅뚯씠 {{user}}?먭쾶 ?댁쟾?섏뿀?듬땲??',
|
||||
banPrompt: '李⑤떒?섎㈃ ?대떦 ?좎??먭쾶 梨꾨꼸??蹂댁씠吏 ?딄쾶 ?⑸땲??',
|
||||
},
|
||||
},
|
||||
|
||||
// ── 명령어 ──────────────────────────────────────────────
|
||||
// ?? 紐낅졊????????????????????????????????????????????????
|
||||
commands: {
|
||||
voiceSetup: {
|
||||
description: '임시 음성 채널을 위한 생성기 채널을 설정합니다.',
|
||||
setDescription: '기존 음성 채널을 생성기로 설정합니다',
|
||||
createDescription: '새 음성 채널을 만들고 생성기로 설정합니다',
|
||||
channelOptionDescription: '생성기로 사용할 음성 채널',
|
||||
categoryOptionDescription: '(선택) 임시 채널이 생성될 카테고리',
|
||||
nameOptionDescription: '새 생성기 음성 채널의 이름',
|
||||
setSuccess: '{{channel}}을(를) 음성 생성기 채널로 설정했습니다!',
|
||||
createSuccess: '{{channel}}을(를) 음성 생성기 채널로 생성 및 설정했습니다!',
|
||||
description: '?꾩떆 ?뚯꽦 梨꾨꼸???꾪븳 ?앹꽦湲?梨꾨꼸???ㅼ젙?⑸땲??',
|
||||
setDescription: '기존 음성 채널을 생성기로 설정합니다.',
|
||||
createDescription: '새 음성 채널을 만들고 생성기로 설정합니다.',
|
||||
channelOptionDescription: '?앹꽦湲곕줈 ?ъ슜???뚯꽦 梨꾨꼸',
|
||||
categoryOptionDescription: '(?좏깮) ?꾩떆 梨꾨꼸???앹꽦??移댄뀒怨좊━',
|
||||
nameOptionDescription: '???앹꽦湲??뚯꽦 梨꾨꼸???대쫫',
|
||||
setSuccess: '{{channel}}??瑜? ?뚯꽦 ?앹꽦湲?梨꾨꼸濡??ㅼ젙?덉뒿?덈떎!',
|
||||
createSuccess: '{{channel}}??瑜? ?뚯꽦 ?앹꽦湲?梨꾨꼸濡??앹꽦 諛??ㅼ젙?덉뒿?덈떎!',
|
||||
},
|
||||
voiceConfig: {
|
||||
description: '서버의 임시 음성 채널 설정을 관리합니다.',
|
||||
setNameTitle: '기본 이름 템플릿 설정',
|
||||
setNameDesc: '임시 채널 생성 시 사용할 기본 이름 형식을 설정합니다. (사용자명: {{username}})',
|
||||
setLimitTitle: '기본 인원 제한 설정',
|
||||
setLimitDesc: '임시 채널 생성 시 적용할 기본 인원 제한을 설정합니다.',
|
||||
statusTitle: '현재 서버 음성 설정',
|
||||
description: '?쒕쾭???꾩떆 ?뚯꽦 梨꾨꼸 ?ㅼ젙??愿由ы빀?덈떎.',
|
||||
setNameTitle: '湲곕낯 ?대쫫 ?쒗뵆由??ㅼ젙',
|
||||
setNameDesc: '?꾩떆 梨꾨꼸 ?앹꽦 ???ъ슜??湲곕낯 ?대쫫 ?뺤떇???ㅼ젙?⑸땲?? (?ъ슜?먮챸: {{username}})',
|
||||
setLimitTitle: '湲곕낯 ?몄썝 ?쒗븳 ?ㅼ젙',
|
||||
setLimitDesc: '?꾩떆 梨꾨꼸 ?앹꽦 ???곸슜??湲곕낯 ?몄썝 ?쒗븳???ㅼ젙?⑸땲??',
|
||||
statusTitle: '?꾩옱 ?쒕쾭 ?뚯꽦 ?ㅼ젙',
|
||||
templateLabel: '이름 템플릿',
|
||||
limitLabel: '기본 인원 제한',
|
||||
setSuccess: '서버의 임시 채널 설정이 업데이트되었습니다.',
|
||||
limitValue: '{{limit}}명 (0 = 무제한)',
|
||||
limitLabel: '湲곕낯 ?몄썝 ?쒗븳',
|
||||
setSuccess: '?쒕쾭???꾩떆 梨꾨꼸 ?ㅼ젙???낅뜲?댄듃?섏뿀?듬땲??',
|
||||
limitValue: '{{limit}}紐?(0 = 臾댁젣??',
|
||||
},
|
||||
language: {
|
||||
description: '봇의 언어를 설정합니다.',
|
||||
scopeDescription: '본인에게만 또는 서버 전체에 적용',
|
||||
localeDescription: '사용할 언어',
|
||||
scopeUser: '나만 적용',
|
||||
scopeServer: '서버 전체 (관리자 전용)',
|
||||
userSet: '개인 언어가 **{{locale}}**(으)로 설정되었습니다.',
|
||||
serverSet: '서버 언어가 **{{locale}}**(으)로 설정되었습니다.',
|
||||
serverPermissionDenied: '서버 언어 변경은 서버 관리자만 가능합니다.',
|
||||
description: '遊뉗쓽 ?몄뼱瑜??ㅼ젙?⑸땲??',
|
||||
scopeDescription: '蹂몄씤?먭쾶留??먮뒗 ?쒕쾭 ?꾩껜???곸슜',
|
||||
localeDescription: '?ъ슜???몄뼱',
|
||||
scopeUser: '?섎쭔 ?곸슜',
|
||||
scopeServer: '?쒕쾭 ?꾩껜 (愿由ъ옄 ?꾩슜)',
|
||||
userSet: '媛쒖씤 ?몄뼱媛 **{{locale}}**(??濡??ㅼ젙?섏뿀?듬땲??',
|
||||
serverSet: '?쒕쾭 ?몄뼱媛 **{{locale}}**(??濡??ㅼ젙?섏뿀?듬땲??',
|
||||
serverPermissionDenied: '?쒕쾭 ?몄뼱 蹂寃쎌? ?쒕쾭 愿由ъ옄留?媛?ν빀?덈떎.',
|
||||
},
|
||||
event: {
|
||||
description: '서버 이벤트 일정을 관리합니다.',
|
||||
createDescription: '새 서버 이벤트를 생성합니다.',
|
||||
listDescription: '예정된 서버 이벤트 목록을 조회합니다.',
|
||||
cancelDescription: '예약된 서버 이벤트를 취소합니다.',
|
||||
announceDescription: '이벤트 공지 Embed를 다시 게시합니다.',
|
||||
titleDescription: '이벤트 제목',
|
||||
dateDescription: 'YYYY-MM-DD 형식의 날짜',
|
||||
timeDescription: 'HH:mm 형식의 시간 (24시간제, Asia/Seoul 기준)',
|
||||
descriptionOptionDescription: '선택 사항인 이벤트 설명',
|
||||
channelDescription: '선택 사항인 공지 채널',
|
||||
reminderDescription: '리마인더 메시지 사용 여부',
|
||||
remindersDescription: '분 단위 리마인더 목록, 예: 0,10,60',
|
||||
idDescription: '취소할 이벤트 ID',
|
||||
createSuccessTitle: '이벤트 생성 완료',
|
||||
createSuccessBody: '**{{title}}** 이벤트가 예약되었습니다.',
|
||||
listTitle: '예정된 이벤트 목록',
|
||||
listEmpty: '예정된 이벤트가 없습니다.',
|
||||
listItemValue: '**시작 시각:** {{startsAt}}\n**남은 시간:** {{relative}}\n**상태:** {{status}}\n**리마인더:** {{reminder}}\n**채널:** {{channel}}',
|
||||
cancelSuccess: '`{{id}}` 이벤트를 취소했습니다.',
|
||||
cancelNotFound: 'ID가 `{{id}}`인 예약 이벤트를 찾지 못했습니다.',
|
||||
announceSuccess: '`{{id}}` 이벤트를 {{channel}} 채널에 공지했습니다.',
|
||||
announceNotAvailable: '이 이벤트에는 사용할 수 있는 공지 채널이 설정되어 있지 않습니다.',
|
||||
startAnnouncementTitle: '이벤트 시작',
|
||||
startAnnouncementLead: '이 이벤트가 지금 시작됩니다.',
|
||||
invalidDateTime: '이벤트 날짜 또는 시간 형식이 올바르지 않습니다.',
|
||||
invalidDateTimeResolution: '날짜는 `YYYY-MM-DD`, 시간은 `HH:mm` 24시간 형식으로 입력해주세요.',
|
||||
invalidReminderOffsets: '리마인더 분 입력 형식이 올바르지 않습니다.',
|
||||
invalidReminderOffsetsResolution: '`0,10,60`처럼 0 이상의 분을 쉼표로 구분해 입력해주세요. 비워두면 자동 공지는 하지 않습니다.',
|
||||
invalidPastDateTime: '과거 시각으로 이벤트를 예약할 수 없습니다.',
|
||||
invalidPastDateTimeResolution: '미래 시각을 선택한 뒤 다시 시도해주세요.',
|
||||
description: '?쒕쾭 ?대깽???쇱젙??愿由ы빀?덈떎.',
|
||||
createDescription: '???쒕쾭 ?대깽?몃? ?앹꽦?⑸땲??',
|
||||
listDescription: '?덉젙???쒕쾭 ?대깽??紐⑸줉??議고쉶?⑸땲??',
|
||||
cancelDescription: '?덉빟???쒕쾭 ?대깽?몃? 痍⑥냼?⑸땲??',
|
||||
announceDescription: '?대깽??怨듭? Embed瑜??ㅼ떆 寃뚯떆?⑸땲??',
|
||||
titleDescription: '?대깽???쒕ぉ',
|
||||
dateDescription: 'YYYY-MM-DD ?뺤떇???좎쭨',
|
||||
timeDescription: 'HH:mm ?뺤떇???쒓컙 (24?쒓컙?? Asia/Seoul 湲곗?)',
|
||||
descriptionOptionDescription: '?좏깮 ?ы빆???대깽???ㅻ챸',
|
||||
channelDescription: '?좏깮 ?ы빆??怨듭? 梨꾨꼸',
|
||||
reminderDescription: '由щ쭏?몃뜑 硫붿떆吏 ?ъ슜 ?щ?',
|
||||
remindersDescription: '遺??⑥쐞 由щ쭏?몃뜑 紐⑸줉, ?? 0,10,60',
|
||||
idDescription: '痍⑥냼???대깽??ID',
|
||||
createSuccessTitle: '?대깽???앹꽦 ?꾨즺',
|
||||
createSuccessBody: '**{{title}}** ?대깽?멸? ?덉빟?섏뿀?듬땲??',
|
||||
listTitle: '?덉젙???대깽??紐⑸줉',
|
||||
listEmpty: '?덉젙???대깽?멸? ?놁뒿?덈떎.',
|
||||
listItemValue: '**?쒖옉 ?쒓컖:** {{startsAt}}\n**?⑥? ?쒓컙:** {{relative}}\n**?곹깭:** {{status}}\n**由щ쭏?몃뜑:** {{reminder}}\n**梨꾨꼸:** {{channel}}',
|
||||
cancelSuccess: '`{{id}}` ?대깽?몃? 痍⑥냼?덉뒿?덈떎.',
|
||||
cancelNotFound: 'ID媛 `{{id}}`???덉빟 ?대깽?몃? 李얠? 紐삵뻽?듬땲??',
|
||||
announceSuccess: '`{{id}}` ?대깽?몃? {{channel}} 梨꾨꼸??怨듭??덉뒿?덈떎.',
|
||||
announceNotAvailable: '???대깽?몄뿉???ъ슜?????덈뒗 怨듭? 梨꾨꼸???ㅼ젙?섏뼱 ?덉? ?딆뒿?덈떎.',
|
||||
startAnnouncementTitle: '?대깽???쒖옉',
|
||||
startAnnouncementLead: '???대깽?멸? 吏湲??쒖옉?⑸땲??',
|
||||
invalidDateTime: '?대깽???좎쭨 ?먮뒗 ?쒓컙 ?뺤떇???щ컮瑜댁? ?딆뒿?덈떎.',
|
||||
invalidDateTimeResolution: '?좎쭨??`YYYY-MM-DD`, ?쒓컙? `HH:mm` 24?쒓컙 ?뺤떇?쇰줈 ?낅젰?댁<?몄슂.',
|
||||
invalidReminderOffsets: '由щ쭏?몃뜑 遺??낅젰 ?뺤떇???щ컮瑜댁? ?딆뒿?덈떎.',
|
||||
invalidReminderOffsetsResolution: '`0,10,60`泥섎읆 0 ?댁긽??遺꾩쓣 ?쇳몴濡?援щ텇???낅젰?댁<?몄슂. 鍮꾩썙?먮㈃ ?먮룞 怨듭????섏? ?딆뒿?덈떎.',
|
||||
invalidPastDateTime: '怨쇨굅 ?쒓컖?쇰줈 ?대깽?몃? ?덉빟?????놁뒿?덈떎.',
|
||||
invalidPastDateTimeResolution: '誘몃옒 ?쒓컖???좏깮?????ㅼ떆 ?쒕룄?댁<?몄슂.',
|
||||
statusScheduled: '예약됨',
|
||||
statusCancelled: '취소됨',
|
||||
statusCompleted: '완료됨',
|
||||
reminderOn: '사용',
|
||||
reminderOn: '?ъ슜',
|
||||
reminderOff: '사용 안 함',
|
||||
reminderNone: '자동 공지 없음',
|
||||
reminderNone: '?먮룞 怨듭? ?놁쓬',
|
||||
announcementChannelNone: '미설정',
|
||||
fields: {
|
||||
eventId: '이벤트 ID',
|
||||
startsAt: '시작 시각',
|
||||
reminder: '리마인더',
|
||||
announcementChannel: '공지 채널',
|
||||
status: '상태',
|
||||
eventId: '?대깽??ID',
|
||||
startsAt: '?쒖옉 ?쒓컖',
|
||||
reminder: '由щ쭏?몃뜑',
|
||||
announcementChannel: '怨듭? 梨꾨꼸',
|
||||
status: '?곹깭',
|
||||
},
|
||||
},
|
||||
autorole: {
|
||||
description: '입장 시 역할을 자동으로 부여하는 기능을 설정합니다.',
|
||||
statusTitle: '자동 역할 부여 설정 상태',
|
||||
userRoleLabel: '일반 유저 역할',
|
||||
botRoleLabel: '봇 역할',
|
||||
statusLabel: '유저 자동 부여',
|
||||
botStatusLabel: '봇 자동 부여',
|
||||
userRolePlaceholder: '유저 기본 역할을 선택하세요',
|
||||
botRolePlaceholder: '봇 기본 역할을 선택하세요',
|
||||
toggleUserEnable: '🟢 유저 자동부여 켜기',
|
||||
toggleUserDisable: '🔴 유저 자동부여 끄기',
|
||||
toggleBotEnable: '🟢 봇 자동부여 켜기',
|
||||
toggleBotDisable: '🔴 봇 자동부여 끄기',
|
||||
notSet: '미설정',
|
||||
enabled: '활성',
|
||||
disabled: '비활성',
|
||||
updateSuccess: '자동 역할 설정이 업데이트되었습니다.',
|
||||
permissionsError: '봇의 역할 순위가 낮거나 권한이 부족하여 역할을 부여할 수 없습니다.',
|
||||
suspendNotice: '권한 부족으로 인해 자동 역할 부여 기능이 일시 중지되었습니다. 봇의 권한과 역할 순위를 확인해 주세요.',
|
||||
},
|
||||
music: {
|
||||
description: 'Play YouTube audio in voice channels.',
|
||||
addDescription: 'Search YouTube or add a video URL to the queue.',
|
||||
|
|
@ -337,109 +317,112 @@ export const ko: TranslationSchema = {
|
|||
},
|
||||
permissionAudit: {
|
||||
title: '봇 권한 진단 보고서',
|
||||
channel: '채널',
|
||||
noResults: '진단할 기능이 없습니다. 봇이 아직 설정되지 않았을 수 있습니다.',
|
||||
summaryLabel: '진단 결과 요약',
|
||||
summaryOk: '✅ 모든 항목 정상. 문제가 없습니다.',
|
||||
summaryIssue: '❌ {{fail}}개 실패 · ⚠️ {{warn}}개 경고 감지됨.',
|
||||
hierarchyWarning: "봇 역할(순위: {{botPos}})이 '{{role}}'(순위: {{targetPos}})보다 위에 있어야 관리할 수 있습니다.",
|
||||
channel: '梨꾨꼸',
|
||||
noResults: '吏꾨떒??湲곕뒫???놁뒿?덈떎. 遊뉗씠 ?꾩쭅 ?ㅼ젙?섏? ?딆븯?????덉뒿?덈떎.',
|
||||
summaryLabel: '吏꾨떒 寃곌낵 ?붿빟',
|
||||
summaryOk: '??紐⑤뱺 ??ぉ ?뺤긽. 臾몄젣媛 ?놁뒿?덈떎.',
|
||||
summaryIssue: '??{{fail}}媛??ㅽ뙣 쨌 ?좑툘 {{warn}}媛?寃쎄퀬 媛먯???',
|
||||
hierarchyWarning: "遊???븷(?쒖쐞: {{botPos}})??'{{role}}'(?쒖쐞: {{targetPos}})蹂대떎 ?꾩뿉 ?덉뼱??愿由ы븷 ???덉뒿?덈떎.",
|
||||
features: {
|
||||
BASIC: '기본 봇 기능',
|
||||
VOICE_GLOBAL: '임시 음성 채널 (전역)',
|
||||
VOICE_GENERATOR_CHANNEL: '음성 생성기 채널',
|
||||
VOICE_GENERATOR_CATEGORY: '음성 생성기 카테고리',
|
||||
MIMIC_WEBHOOK: '메시지 흉내 (Webhook)',
|
||||
BASIC: '湲곕낯 遊?湲곕뒫',
|
||||
VOICE_GLOBAL: '?꾩떆 ?뚯꽦 梨꾨꼸 (?꾩뿭)',
|
||||
VOICE_GENERATOR_CHANNEL: '?뚯꽦 ?앹꽦湲?梨꾨꼸',
|
||||
VOICE_GENERATOR_CATEGORY: '?뚯꽦 ?앹꽦湲?移댄뀒怨좊━',
|
||||
INVITE_TRACKING: '珥덈? 異붿쟻',
|
||||
INVITE_ROLE_HIERARCHY: '珥덈? ??븷 遺??(怨꾩링 寃??',
|
||||
MIMIC_WEBHOOK: '硫붿떆吏 ?됰궡 (Webhook)',
|
||||
},
|
||||
},
|
||||
setup: {
|
||||
description: '설정 마법사를 실행하여 봇의 필수 기능들을 단계별로 설정합니다.',
|
||||
description: '?ㅼ젙 留덈쾿?щ? ?ㅽ뻾?섏뿬 遊뉗쓽 ?꾩닔 湲곕뒫?ㅼ쓣 ?④퀎蹂꾨줈 ?ㅼ젙?⑸땲??',
|
||||
step0: {
|
||||
title: '✨ 봇 설정 마법사 시작',
|
||||
desc: '환영합니다! 이 마법사를 통해 아래 4가지 항목을 설정합니다.\n\n1️⃣ **언어 설정**\n2️⃣ **필수 권한 점검**\n3️⃣ **감사 채널 설정**\n4️⃣ **임시 음성 채널 설정**',
|
||||
startBtn: '설정 시작하기'
|
||||
title: '??遊??ㅼ젙 留덈쾿???쒖옉',
|
||||
desc: '?섏쁺?⑸땲?? ??留덈쾿?щ? ?듯빐 ?꾨옒 4媛吏 ??ぉ???ㅼ젙?⑸땲??\n\n1截뤴깵 **?몄뼱 ?ㅼ젙**\n2截뤴깵 **?꾩닔 沅뚰븳 ?먭?**\n3截뤴깵 **媛먯궗 梨꾨꼸 ?ㅼ젙**\n4截뤴깵 **?꾩떆 ?뚯꽦 梨꾨꼸 ?ㅼ젙**',
|
||||
startBtn: '?ㅼ젙 ?쒖옉?섍린'
|
||||
},
|
||||
step1: {
|
||||
title: '1️⃣ 언어 설정',
|
||||
desc: '서버 전체에 적용될 봇의 기본 언어를 선택하세요. (현재: **{{locale}}**)',
|
||||
title: '1截뤴깵 ?몄뼱 ?ㅼ젙',
|
||||
desc: '?쒕쾭 ?꾩껜???곸슜??遊뉗쓽 湲곕낯 ?몄뼱瑜??좏깮?섏꽭?? (?꾩옱: **{{locale}}**)',
|
||||
placeholder: '언어를 선택하세요',
|
||||
nextBtn: '다음 단계',
|
||||
skipBtn: '건너뛰기'
|
||||
nextBtn: '?ㅼ쓬 ?④퀎',
|
||||
skipBtn: '嫄대꼫?곌린'
|
||||
},
|
||||
step2: {
|
||||
title: '2️⃣ 필수 권한 점검',
|
||||
descOk: '✅ **모든 필수 권한이 정상적으로 부여되어 있습니다.**',
|
||||
descFail: '⚠️ **일부 권한이 부족합니다.**\n결과를 확인하고 봇 역할에 필요한 기능 권한을 부여해주세요.',
|
||||
title: '2截뤴깵 ?꾩닔 沅뚰븳 ?먭?',
|
||||
descOk: '??**紐⑤뱺 ?꾩닔 沅뚰븳???뺤긽?곸쑝濡?遺?щ릺???덉뒿?덈떎.**',
|
||||
descFail: '?좑툘 **?쇰? 沅뚰븳??遺議깊빀?덈떎.**\n寃곌낵瑜??뺤씤?섍퀬 遊???븷???꾩슂??湲곕뒫 沅뚰븳??遺?ы빐二쇱꽭??',
|
||||
recheckBtn: '다시 검사하기',
|
||||
nextBtn: '다음 단계'
|
||||
nextBtn: '?ㅼ쓬 ?④퀎'
|
||||
},
|
||||
step3: {
|
||||
title: '3️⃣ 감사 채널 설정',
|
||||
desc: '봇의 주요 이벤트와 에러 통보를 받을 채널을 선택해주세요.',
|
||||
placeholder: '감사 통보 채널 선택',
|
||||
disableBtn: '감사 채널 끄기/해제',
|
||||
nextBtn: '다음 단계'
|
||||
title: '3截뤴깵 媛먯궗 梨꾨꼸 ?ㅼ젙',
|
||||
desc: '遊뉗쓽 二쇱슂 ?대깽?몄? ?먮윭 ?듬낫瑜?諛쏆쓣 梨꾨꼸???좏깮?댁<?몄슂.',
|
||||
placeholder: '媛먯궗 ?듬낫 梨꾨꼸 ?좏깮',
|
||||
disableBtn: '媛먯궗 梨꾨꼸 ?꾧린/?댁젣',
|
||||
nextBtn: '?ㅼ쓬 ?④퀎'
|
||||
},
|
||||
step4: {
|
||||
title: '감사 로그 카테고리 설정',
|
||||
desc: '로그를 수신할 카테고리를 선택해주세요.',
|
||||
nextBtn: '다음 단계',
|
||||
title: '媛먯궗 濡쒓렇 移댄뀒怨좊━ ?ㅼ젙',
|
||||
desc: '濡쒓렇瑜??섏떊??移댄뀒怨좊━瑜??좏깮?댁<?몄슂.',
|
||||
nextBtn: '?ㅼ쓬 ?④퀎',
|
||||
},
|
||||
step5: {
|
||||
title: '4️⃣ 임시 음성 채널 설정',
|
||||
desc: '임시 음성 채널을 생성할 "생성기 채널"을 선택해주세요.\n기존의 채널을 고르거나 카테고리/채널을 봇이 **자동 생성**하게 할 수도 있습니다.',
|
||||
placeholder: '생성기로 쓸 음성 채널 선택',
|
||||
autoBtn: '🚀 자동 생성하기',
|
||||
skipBtn: '임시 음성 사용 안함',
|
||||
nextBtn: '설정 완료'
|
||||
title: '4截뤴깵 ?꾩떆 ?뚯꽦 梨꾨꼸 ?ㅼ젙',
|
||||
desc: '?꾩떆 ?뚯꽦 梨꾨꼸???앹꽦??"?앹꽦湲?梨꾨꼸"???좏깮?댁<?몄슂.\n湲곗〈??梨꾨꼸??怨좊Ⅴ嫄곕굹 移댄뀒怨좊━/梨꾨꼸??遊뉗씠 **?먮룞 ?앹꽦**?섍쾶 ???섎룄 ?덉뒿?덈떎.',
|
||||
placeholder: '?앹꽦湲곕줈 ???뚯꽦 梨꾨꼸 ?좏깮',
|
||||
autoBtn: '?? ?먮룞 ?앹꽦?섍린',
|
||||
skipBtn: '?꾩떆 ?뚯꽦 ?ъ슜 ?덊븿',
|
||||
nextBtn: '?ㅼ젙 ?꾨즺'
|
||||
},
|
||||
step6: {
|
||||
title: '🎉 설정 완료 요약',
|
||||
desc: '**1. 언어**: {{lang}}\n**2. 감사 채널**: {{audit}}\n**3. 감사 카테고리**: {{categories}}\n**4. 임시 음성 채널**: {{voice}}',
|
||||
title: '?럦 ?ㅼ젙 ?꾨즺 ?붿빟',
|
||||
desc: '**1. ?몄뼱**: {{lang}}\n**2. 媛먯궗 梨꾨꼸**: {{audit}}\n**3. 媛먯궗 移댄뀒怨좊━**: {{categories}}\n**4. ?꾩떆 ?뚯꽦 梨꾨꼸**: {{voice}}',
|
||||
finishBtn: '마치기'
|
||||
},
|
||||
finished: '✅ 설정 마법사를 종료했습니다.',
|
||||
expired: '⏳ 시간이 만료되었습니다. `/setup`을 다시 실행해주세요.',
|
||||
defaultCategoryName: '음성 채널',
|
||||
defaultGeneratorName: '➕ 채널 생성하기',
|
||||
finished: '???ㅼ젙 留덈쾿?щ? 醫낅즺?덉뒿?덈떎.',
|
||||
expired: '???쒓컙??留뚮즺?섏뿀?듬땲?? `/setup`???ㅼ떆 ?ㅽ뻾?댁<?몄슂.',
|
||||
defaultCategoryName: '?뚯꽦 梨꾨꼸',
|
||||
defaultGeneratorName: '??梨꾨꼸 ?앹꽦?섍린',
|
||||
auditCategories: {
|
||||
SYSTEM: '시스템',
|
||||
BOOT: '부팅',
|
||||
VOICE: '음성',
|
||||
PERMISSION: '권한',
|
||||
VOICE: '?뚯꽦',
|
||||
PERMISSION: '沅뚰븳',
|
||||
INVITE: '珥덈?',
|
||||
},
|
||||
},
|
||||
config: {
|
||||
title: '기능 설정 변경 결과',
|
||||
noOptions: '변경할 옵션을 하나 이상 선택해주세요.',
|
||||
title: '湲곕뒫 ?ㅼ젙 蹂寃?寃곌낵',
|
||||
noOptions: '蹂寃쏀븷 ?듭뀡???섎굹 ?댁긽 ?좏깮?댁<?몄슂.',
|
||||
mimic: {
|
||||
label: '미믹(Mimic)',
|
||||
enabled: '활성',
|
||||
disabled: '비활성',
|
||||
label: '誘몃?(Mimic)',
|
||||
enabled: '활성화',
|
||||
disabled: '鍮꾪솢?깊솕',
|
||||
},
|
||||
emoji: {
|
||||
label: '이모지 확대(Big Emoji)',
|
||||
enabled: '활성',
|
||||
disabled: '비활성',
|
||||
label: '?대え吏 ?뺣?(Big Emoji)',
|
||||
enabled: '활성화',
|
||||
disabled: '鍮꾪솢?깊솕',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ── 모달 ────────────────────────────────────────────────
|
||||
// ?? 紐⑤떖 ????????????????????????????????????????????????
|
||||
modals: {
|
||||
renameTitle: '음성 채널 이름 변경',
|
||||
renameLabel: '새 채널 이름',
|
||||
limitTitle: '인원 제한 설정',
|
||||
limitLabel: '인원 제한 (0 = 무제한, 1-99)',
|
||||
renameLabel: '??梨꾨꼸 ?대쫫',
|
||||
limitTitle: '?몄썝 ?쒗븳 ?ㅼ젙',
|
||||
limitLabel: '?몄썝 ?쒗븳 (0 = 臾댁젣?? 1-99)',
|
||||
},
|
||||
|
||||
// ── 셀렉트 메뉴 플레이스홀더 ────────────────────────────
|
||||
// ?? ??됲듃 硫붾돱 ?뚮젅?댁뒪???????????????????????????????
|
||||
selects: {
|
||||
kickUser: '추방할 유저를 선택하세요',
|
||||
banUser: '차단할 유저를 선택하세요',
|
||||
transferOwner: '소유권을 이전할 유저를 선택하세요',
|
||||
},
|
||||
|
||||
// ── 상태 메시지 ──────────────────────────────────────────
|
||||
// ?? ?곹깭 硫붿떆吏 ??????????????????????????????????????????
|
||||
presence: {
|
||||
servers: '{{guildCount}}개의 서버에서 작동 중',
|
||||
help: '/help 명령어를 확인하세요',
|
||||
|
|
@ -447,3 +430,6 @@ export const ko: TranslationSchema = {
|
|||
version: 'Kord v1.0.0',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -145,26 +145,6 @@ export interface TranslationSchema {
|
|||
status: string;
|
||||
};
|
||||
};
|
||||
autorole: {
|
||||
description: string;
|
||||
statusTitle: string;
|
||||
userRoleLabel: string;
|
||||
botRoleLabel: string;
|
||||
statusLabel: string;
|
||||
botStatusLabel: string;
|
||||
userRolePlaceholder: string;
|
||||
botRolePlaceholder: string;
|
||||
toggleUserEnable: string;
|
||||
toggleUserDisable: string;
|
||||
toggleBotEnable: string;
|
||||
toggleBotDisable: string;
|
||||
notSet: string;
|
||||
enabled: string;
|
||||
disabled: string;
|
||||
updateSuccess: string;
|
||||
permissionsError: string;
|
||||
suspendNotice: string;
|
||||
};
|
||||
music: {
|
||||
description: string;
|
||||
addDescription: string;
|
||||
|
|
@ -302,6 +282,8 @@ export interface TranslationSchema {
|
|||
VOICE_GLOBAL: string;
|
||||
VOICE_GENERATOR_CHANNEL: string;
|
||||
VOICE_GENERATOR_CATEGORY: string;
|
||||
INVITE_TRACKING: string;
|
||||
INVITE_ROLE_HIERARCHY: string;
|
||||
MIMIC_WEBHOOK: string;
|
||||
};
|
||||
};
|
||||
|
|
@ -323,6 +305,7 @@ export interface TranslationSchema {
|
|||
BOOT: string;
|
||||
VOICE: string;
|
||||
PERMISSION: string;
|
||||
INVITE: string;
|
||||
};
|
||||
};
|
||||
config: {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { prisma } from '../database';
|
|||
import { env } from '../config/env';
|
||||
|
||||
export type AuditSeverity = 'INFO' | 'WARN' | 'ERROR';
|
||||
export type AuditCategory = 'SYSTEM' | 'BOOT' | 'VOICE' | 'PERMISSION' | 'MIMIC';
|
||||
export type AuditCategory = 'SYSTEM' | 'BOOT' | 'VOICE' | 'PERMISSION' | 'INVITE' | 'MIMIC';
|
||||
|
||||
export interface AuditLogPayload {
|
||||
category: AuditCategory;
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
import { Guild, GuildMember, PermissionFlagsBits } from 'discord.js';
|
||||
import { prisma } from '../database';
|
||||
import { logger } from '../utils/logger';
|
||||
import { auditLogService } from './AuditLogService';
|
||||
|
||||
export class AutoRoleService {
|
||||
/**
|
||||
* 서버의 자동 역할 설정을 조회합니다.
|
||||
*/
|
||||
async getConfig(guildId: string) {
|
||||
return prisma.autoRoleConfig.findUnique({
|
||||
where: { guildId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 서버의 자동 역할 설정을 업데이트합니다.
|
||||
*/
|
||||
async updateConfig(guildId: string, data: {
|
||||
userRoleIds?: string[];
|
||||
botRoleIds?: string[];
|
||||
isEnabled?: boolean;
|
||||
botEnabled?: boolean;
|
||||
}) {
|
||||
return prisma.autoRoleConfig.upsert({
|
||||
where: { guildId },
|
||||
create: {
|
||||
guildId,
|
||||
...data,
|
||||
},
|
||||
update: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동 역할 부여 기능을 활성/비활성합니다.
|
||||
*/
|
||||
async setEnabled(guildId: string, enabled: boolean) {
|
||||
return this.updateConfig(guildId, { isEnabled: enabled });
|
||||
}
|
||||
|
||||
/**
|
||||
* 신규 멤버가 입장했을 때 자동으로 역할을 부여합니다.
|
||||
*/
|
||||
async handleMemberJoin(member: GuildMember) {
|
||||
const config = await this.getConfig(member.guild.id);
|
||||
if (!config) return;
|
||||
|
||||
const isBot = member.user.bot;
|
||||
const isEnabled = isBot ? config.botEnabled : config.isEnabled;
|
||||
const roleIds = isBot ? config.botRoleIds : config.userRoleIds;
|
||||
|
||||
if (!isEnabled || roleIds.length === 0) return;
|
||||
|
||||
try {
|
||||
await member.roles.add(roleIds, 'Kord Auto-Role');
|
||||
logger.info(`[AutoRole] Added roles to ${member.user.tag} in ${member.guild.name}`);
|
||||
} catch (error) {
|
||||
logger.error(`[AutoRole] Failed to add roles to ${member.user.tag} in ${member.guild.name}`, error);
|
||||
|
||||
// 권한 문제인 경우 감사 로그에 기록
|
||||
await auditLogService.log(member.guild, {
|
||||
category: 'PERMISSION',
|
||||
severity: 'WARN',
|
||||
title: 'Auto-Role Failure',
|
||||
description: `Failed to assign roles to ${member.user.toString()} automatically. Please check the bot's permission and role hierarchy.`
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const autoRoleService = new AutoRoleService();
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { Client, Guild, Invite, GuildMember } from 'discord.js';
|
||||
import { cache } from '../cache';
|
||||
import { prisma } from '../database';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export class InviteService {
|
||||
public static async cacheAllInvites(client: Client) {
|
||||
for (const [, guild] of client.guilds.cache) {
|
||||
await this.cacheGuildInvites(guild);
|
||||
}
|
||||
logger.info('InviteMaster: Finished caching all invites.');
|
||||
}
|
||||
|
||||
public static async cacheGuildInvites(guild: Guild) {
|
||||
try {
|
||||
const invites = await guild.invites.fetch();
|
||||
const inviteData = invites.map(inv => ({
|
||||
code: inv.code,
|
||||
uses: inv.uses || 0
|
||||
}));
|
||||
await cache.set(`invites:${guild.id}`, JSON.stringify(inviteData));
|
||||
} catch (error) {
|
||||
logger.error(`InviteMaster: Failed to cache invites for guild ${guild.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
public static async handleInviteCreate(invite: Invite) {
|
||||
if (!invite.guild) return;
|
||||
logger.debug(`InviteMaster: New invite created: ${invite.code}`);
|
||||
await this.cacheGuildInvites(invite.guild as Guild);
|
||||
}
|
||||
|
||||
public static async handleInviteDelete(invite: Invite) {
|
||||
if (!invite.guild) return;
|
||||
logger.debug(`InviteMaster: Invite deleted: ${invite.code}`);
|
||||
await this.cacheGuildInvites(invite.guild as Guild);
|
||||
}
|
||||
|
||||
public static async handleMemberAdd(member: GuildMember) {
|
||||
const guild = member.guild;
|
||||
try {
|
||||
// Fetch current active invites
|
||||
const newInvites = await guild.invites.fetch();
|
||||
const cachedData = await cache.get(`invites:${guild.id}`);
|
||||
|
||||
let usedInvite: Invite | undefined;
|
||||
|
||||
if (cachedData) {
|
||||
const cachedInvites: { code: string, uses: number }[] = JSON.parse(cachedData);
|
||||
|
||||
// Find the invite where 'uses' has increased
|
||||
usedInvite = newInvites.find(inv => {
|
||||
const cached = cachedInvites.find(c => c.code === inv.code);
|
||||
return cached ? (inv.uses || 0) > cached.uses : false;
|
||||
});
|
||||
}
|
||||
|
||||
// Update the cache immediately to account for this new join
|
||||
await this.cacheGuildInvites(guild);
|
||||
|
||||
if (usedInvite) {
|
||||
logger.info(`InviteMaster: ${member.user.tag} joined using invite ${usedInvite.code}`);
|
||||
|
||||
// Check DB for mapped role
|
||||
const inviteRole = await prisma.inviteRole.findFirst({
|
||||
where: {
|
||||
guildId: guild.id,
|
||||
inviteCode: usedInvite.code
|
||||
}
|
||||
});
|
||||
|
||||
if (inviteRole) {
|
||||
const role = guild.roles.cache.get(inviteRole.roleId);
|
||||
if (role) {
|
||||
await member.roles.add(role);
|
||||
logger.info(`InviteMaster: Assigned role ${role.name} to ${member.user.tag}`);
|
||||
} else {
|
||||
logger.warn(`InviteMaster: Role ${inviteRole.roleId} mapped to invite ${usedInvite.code} not found.`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info(`InviteMaster: ${member.user.tag} joined but invite could not be determined (ex: Vanity URL).`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`InviteMaster: Failed to handle member add tracking:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -109,7 +109,24 @@ const FEATURE_DEFINITIONS: FeatureDefinition[] = [
|
|||
},
|
||||
},
|
||||
|
||||
// ── 5. 메시지 흉내 (Mimic) ──
|
||||
// ── 5. 초대 추적 ──
|
||||
{
|
||||
featureKey: 'INVITE_TRACKING',
|
||||
scope: 'guild',
|
||||
permissions: [PermissionFlagsBits.ManageGuild],
|
||||
},
|
||||
|
||||
// ── 6. 역할 자동 부여 (초대 연동) - 계층 검사 ──
|
||||
{
|
||||
featureKey: 'INVITE_ROLE_HIERARCHY',
|
||||
scope: 'hierarchy',
|
||||
resolveTargetRoleIds: async (guildId) => {
|
||||
const inviteRoles = await prisma.inviteRole.findMany({ where: { guildId } });
|
||||
return inviteRoles.map((ir: { roleId: string }) => ir.roleId);
|
||||
},
|
||||
},
|
||||
|
||||
// ── 7. 메시지 흉내 (Mimic) ──
|
||||
{
|
||||
featureKey: 'MIMIC_WEBHOOK',
|
||||
scope: 'guild',
|
||||
|
|
|
|||
Loading…
Reference in New Issue