Compare commits
4 Commits
6eb3826737
...
c459dfff02
| Author | SHA1 | Date |
|---|---|---|
|
|
c459dfff02 | |
|
|
122f20d031 | |
|
|
4cc14d8153 | |
|
|
63c9930fd9 |
|
|
@ -17,6 +17,7 @@ description: work routine
|
||||||
### 1단계: 기획 및 설계 (Planning Phase)
|
### 1단계: 기획 및 설계 (Planning Phase)
|
||||||
|
|
||||||
- 사용자가 새로운 기능이나 수정 사항을 요청하면, 필요한 스펙 및 예외 사항을 꼼꼼히 확인합니다.
|
- 사용자가 새로운 기능이나 수정 사항을 요청하면, 필요한 스펙 및 예외 사항을 꼼꼼히 확인합니다.
|
||||||
|
- 명령어를 파편화하지 말고, 관련 있는 기능들을 하나의 대표 명령어 아래 '서브 커맨드' 형태로 그룹화하여 설계할 것
|
||||||
- 변경 사항, 사용 스택, 아키텍처를 포함한 `implementation_plan.md`를 작성하거나 업데이트하여 사용자에게 검토 및 승인을 요청합니다.
|
- 변경 사항, 사용 스택, 아키텍처를 포함한 `implementation_plan.md`를 작성하거나 업데이트하여 사용자에게 검토 및 승인을 요청합니다.
|
||||||
|
|
||||||
### 2단계: 개발 및 구현 (Execution Phase)
|
### 2단계: 개발 및 구현 (Execution Phase)
|
||||||
|
|
@ -31,6 +32,7 @@ description: work routine
|
||||||
- 동시 실행 시 충돌을 방지하기 위해, 테스트 구동 시에는 반드시 **임의의 고유한 `INSTANCE_ID` 환경 변수를 할당**(`INSTANCE_ID=agent-test-xxxx yarn run dev`)하여 기존 인스턴스와 락(Lock)이나 로그가 겹치지 않게 합니다.
|
- 동시 실행 시 충돌을 방지하기 위해, 테스트 구동 시에는 반드시 **임의의 고유한 `INSTANCE_ID` 환경 변수를 할당**(`INSTANCE_ID=agent-test-xxxx yarn run dev`)하여 기존 인스턴스와 락(Lock)이나 로그가 겹치지 않게 합니다.
|
||||||
- 필요하다면 단위 테스트(`yarn test`)를 실행합니다.
|
- 필요하다면 단위 테스트(`yarn test`)를 실행합니다.
|
||||||
- 오류 발생 시 자체적으로 판단하고 디버깅하여 해결합니다.
|
- 오류 발생 시 자체적으로 판단하고 디버깅하여 해결합니다.
|
||||||
|
- 테스트가 완료되면 실행한 인스턴스를 종료합니다.
|
||||||
|
|
||||||
### 4단계: 사용자 최종 테스트 지원 (Final Manual Testing)
|
### 4단계: 사용자 최종 테스트 지원 (Final Manual Testing)
|
||||||
|
|
||||||
|
|
@ -43,3 +45,4 @@ description: work routine
|
||||||
- 3단계 구현 및 테스트가 성공적으로 완료되면, 사용자에게 최종 보고하기 **전에 반드시 먼저** `<PROJECT_ROOT>/Docs/` 디렉토리에 작업 완료(Work done), 트러블슈팅(Troubleshooting), 의사 결정(Decisions made) 내역을 문서화해야 합니다.
|
- 3단계 구현 및 테스트가 성공적으로 완료되면, 사용자에게 최종 보고하기 **전에 반드시 먼저** `<PROJECT_ROOT>/Docs/` 디렉토리에 작업 완료(Work done), 트러블슈팅(Troubleshooting), 의사 결정(Decisions made) 내역을 문서화해야 합니다.
|
||||||
- 새 문서가 생성되거나 수정되면 자동으로 `Docs/index.md`에 문서의 색인(링크)을 추가합니다.
|
- 새 문서가 생성되거나 수정되면 자동으로 `Docs/index.md`에 문서의 색인(링크)을 추가합니다.
|
||||||
- 모든 코드 작업 내역과 의사 결정이 완전히 로컬 `Docs/`에 기록 및 정리된 후에만 비로소 "작업을 완료했다"고 사용자에게 알립니다.
|
- 모든 코드 작업 내역과 의사 결정이 완전히 로컬 `Docs/`에 기록 및 정리된 후에만 비로소 "작업을 완료했다"고 사용자에게 알립니다.
|
||||||
|
- 설치, 테스트 방법, 구동, 기능, 명령어 등을 위한 변경사항을 <PROJECT_ROOT>/README.md에 최신화합니다.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Gathered Command UI Refactoring (2026-03-30)
|
||||||
|
|
||||||
|
## 작업 개요 (Work Summary)
|
||||||
|
|
||||||
|
디스코드 슬래시 명령어의 자동완성 목록이 파편화되는 문제를 해결하기 위해, 모든 명령어를 **2단계 계층 구조(Command -> Subcommand)**로 일원화하고 상세 동작은 **옵션(Option Choices)**으로 선택하는 '모아보기(Gathered UI)' 형태로 전면 리팩토링했습니다.
|
||||||
|
|
||||||
|
## 주요 변경 사항 (Key Changes)
|
||||||
|
|
||||||
|
### 1. 개발 규칙 업데이트
|
||||||
|
|
||||||
|
- **파일**: `.agents/rules/kord_routine.md`
|
||||||
|
- **변경**: `Subcommand Group` 대신 `Subcommand -> Option Choices` 패턴을 사용하여 자동완성 목록을 오염시키지 않도록 규칙 수정.
|
||||||
|
|
||||||
|
### 2. `/setup` 명령어 독립 (Standalone Wizard)
|
||||||
|
|
||||||
|
- **개요**: 봇의 초기 환경 설정을 위한 '마법사' 기능을 상징적인 중요도에 따라 별도의 최상위 명령어로 분리했습니다.
|
||||||
|
- **구조**: `/setup` (세부 옵션 없음)
|
||||||
|
|
||||||
|
### 3. `/config` 명령어 리팩토링 (Gathered UI)
|
||||||
|
|
||||||
|
- **구조**: `/config [subcommand] action:[choices] [options...]`
|
||||||
|
- **세부 사항**:
|
||||||
|
- `/config system action:language locale:[en/ko]` (서버 전체 언어 설정)
|
||||||
|
- `/config features target:[mimic/emoji] enable:[bool]` (각 기능 활성화 여부)
|
||||||
|
- *비고: 기존의 setup 과정은 독립된 `/setup` 명령어를 통해 제공됩니다.*
|
||||||
|
|
||||||
|
### 4. `/audit` 명령어 리팩토링 (Gathered)
|
||||||
|
|
||||||
|
- **구조**: `/audit [subcommand] action:[choices] [options...]`
|
||||||
|
- **세부 사항**:
|
||||||
|
- `/audit channel action:set channel:[#channel]` (채널 지정)
|
||||||
|
- `/audit channel action:clear` (채널 해제)
|
||||||
|
- `/audit channel action:status` (상태 확인)
|
||||||
|
- `/audit channel action:filter category:[...] enable:[bool]` (필터링)
|
||||||
|
- `/audit bot action:permissions` (권한 진단)
|
||||||
|
|
||||||
|
### 5. `/voice` 명령어 리팩토링 (Gathered)
|
||||||
|
|
||||||
|
- **구조**: `/voice [subcommand] action:[choices] [options...]`
|
||||||
|
- **세부 사항**:
|
||||||
|
- `/voice generator action:set/create [...]` (생성기 설정)
|
||||||
|
- `/voice settings action:name/limit/status [...]` (채널 설정)
|
||||||
|
|
||||||
|
## 의사 결정 (Decisions Made)
|
||||||
|
|
||||||
|
- **2단계 고정**: 디스코드 입력창에 `/voice`만 쳤을 때 `generator`와 `settings`만 딱 나타나게 하여 시각적 복잡도를 최소화함.
|
||||||
|
- **동적 옵션 처리**: 특정 `action`을 골랐을 때만 필요한 옵션(예: `locale`)이 뒤따라 수동으로 안내되도록 로직 구성 (필수 옵션 미입력 시 에러 메시지 처리).
|
||||||
|
|
||||||
|
## 결과 및 테스트 (Results & Testing)
|
||||||
|
|
||||||
|
- `yarn build` 성공 (Type safety 확인)
|
||||||
|
- 모든 `interaction.options.getSubcommand()` 분기 처리 완료.
|
||||||
|
- **주의**: 명령어 스키마가 변경되었으므로, 디스코드가 전역 명령어를 갱신(최대 1시간 소요)할 때까지 대기하거나 봇을 재구동해야 함.
|
||||||
|
|
@ -52,3 +52,4 @@
|
||||||
- [2026-03-30: 서버 이벤트 일정 관리 Phase 2 구현 (Event Schedule Phase 2 Implementation)](WorkDone/2026-03-30_Event_Schedule_Phase2_Implementation.md)
|
- [2026-03-30: 서버 이벤트 일정 관리 Phase 2 구현 (Event Schedule Phase 2 Implementation)](WorkDone/2026-03-30_Event_Schedule_Phase2_Implementation.md)
|
||||||
- [2026-03-30: 서버 이벤트 시작 시점 공지 구현 (Event Schedule Start Announcement Implementation)](WorkDone/2026-03-30_Event_Schedule_Start_Announcement_Implementation.md)
|
- [2026-03-30: 서버 이벤트 시작 시점 공지 구현 (Event Schedule Start Announcement Implementation)](WorkDone/2026-03-30_Event_Schedule_Start_Announcement_Implementation.md)
|
||||||
- [2026-03-30: 이벤트 리마인더 분 단위 옵션 구현 (Event Reminder Offsets Implementation)](WorkDone/2026-03-30_Event_Reminder_Offsets_Implementation.md)
|
- [2026-03-30: 이벤트 리마인더 분 단위 옵션 구현 (Event Reminder Offsets Implementation)](WorkDone/2026-03-30_Event_Reminder_Offsets_Implementation.md)
|
||||||
|
- [2026-03-30: 명령어 계층 구조 리팩토링 (Hierarchical Command Refactoring)](WorkDone/2026-03-30_HierarchicalRefactor.md)
|
||||||
|
|
|
||||||
70
README.md
70
README.md
|
|
@ -1 +1,71 @@
|
||||||
# Kord
|
# Kord
|
||||||
|
|
||||||
|
Kord는 Discord 서버 관리를 돕는 강력하고 유연한 다기능 봇입니다.
|
||||||
|
|
||||||
|
## 1. 개요 (Overview)
|
||||||
|
|
||||||
|
**Kord**는 효율적인 서버 운영을 위해 설계된 Discord 봇입니다. TypeScript와 Discord.js를 기반으로 구축되었으며, Prisma(PostgreSQL)와 Redis를 활용하여 안정적이고 확장 가능한 아키텍처를 제공합니다. 임시 음성 채널 관리, 상세 감사 로그, 권한 진단 등의 핵심 기능을 통해 서버 관리자의 부담을 줄여줍니다.
|
||||||
|
|
||||||
|
## 2. 요구사항 (Requirements)
|
||||||
|
|
||||||
|
- **Runtime**: Node.js v20 이상
|
||||||
|
- **Package Manager**: Yarn v4 (Berry)
|
||||||
|
- **Database**: PostgreSQL (Prisma 사용)
|
||||||
|
- **Cache**: Redis (다중 인스턴스 동기화 및 캐싱)
|
||||||
|
- **Discord**: Bot Token 및 Client ID (Slash Command 등록용)
|
||||||
|
|
||||||
|
## 3. 테스트 방법 (Test Methods)
|
||||||
|
|
||||||
|
본 프로젝트는 Jest를 사용하여 유닛 테스트 및 통합 테스트를 수행합니다.
|
||||||
|
|
||||||
|
- **전체 테스트 실행**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn test
|
||||||
|
```
|
||||||
|
|
||||||
|
- **i18n 번역 누락 확인**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn check-i18n
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 구동 방법 (Running Methods)
|
||||||
|
|
||||||
|
### 로컬 개발 환경
|
||||||
|
|
||||||
|
1. **의존성 설치**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **환경 변수 설정**: `.env.example` 파일을 복사하여 `.env` 파일을 생성하고 필수 값을 입력합니다.
|
||||||
|
|
||||||
|
3. **데이터베이스 초기화**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx prisma migrate dev
|
||||||
|
npx prisma generate
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **개발 서버 실행**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 프로덕션 환경
|
||||||
|
|
||||||
|
1. **빌드**: `yarn build`
|
||||||
|
2. **실행**: `yarn start`
|
||||||
|
3. **Docker**: `docker-compose up -d`를 통해 전체 스택(Bot, DB, Redis)을 실행할 수 있습니다.
|
||||||
|
|
||||||
|
## 5. 기능 목록 (Feature List)
|
||||||
|
|
||||||
|
- **임시 음성 채널 (Voice)**: 생성기(Generator) 채널을 통해 동적인 음성 채널 생성 및 관리 기능을 제공합니다.
|
||||||
|
- **감사 로그 (Audit Log)**: 서버 내 주요 이벤트를 카테고리별(VOICE, PERMISSION, SYSTEM 등)로 세분화하여 기록합니다.
|
||||||
|
- **서버 설정 (Config)**: 따라하기(Mimic), 큰 이모지(Big Emoji) 등 봇의 기능을 서버별 환경에 맞게 토글하거나 설정할 수 있습니다.
|
||||||
|
- **권한 감사 (Permission Audit)**: 봇의 정상 작동을 방해하는 권한 문제를 즉시 진단하고 해결 방법을 안내합니다.
|
||||||
|
- **초기 설정 마법사 (Setup Wizard)**: 봇 도입 초기 기능을 한눈에 설정할 수 있는 직관적인 UI를 제공합니다.
|
||||||
|
- **다국어 지원 (i18n)**: 한국어와 영어를 포함한 다국어 환경을 지원합니다.
|
||||||
|
|
|
||||||
|
|
@ -52,83 +52,75 @@ export default {
|
||||||
ko: '감사 로그 및 봇 권한을 관리합니다.',
|
ko: '감사 로그 및 봇 권한을 관리합니다.',
|
||||||
})
|
})
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||||
// --- Channel Subcommand Group ---
|
// --- Channel Subcommand ---
|
||||||
.addSubcommandGroup(group =>
|
.addSubcommand(subcommand =>
|
||||||
group
|
subcommand
|
||||||
.setName('channel')
|
.setName('channel')
|
||||||
.setDescription('Manage audit log channel settings.')
|
.setDescription('Manage audit log channel settings.')
|
||||||
.setDescriptionLocalizations({ ko: '감사 채널 설정을 관리합니다.' })
|
.setDescriptionLocalizations({ ko: '감사 채널 설정을 관리합니다.' })
|
||||||
.addSubcommand(subcommand =>
|
|
||||||
subcommand
|
|
||||||
.setName('set')
|
|
||||||
.setDescription('Set the audit log channel.')
|
|
||||||
.setDescriptionLocalizations({ ko: '감사 채널을 설정합니다.' })
|
|
||||||
.addChannelOption(option =>
|
|
||||||
option.setName('channel')
|
|
||||||
.setDescription('The text channel to use for audit logs.')
|
|
||||||
.setDescriptionLocalizations({ ko: '감사 로그를 기록할 텍스트 채널' })
|
|
||||||
.addChannelTypes(ChannelType.GuildText)
|
|
||||||
.setRequired(true)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.addSubcommand(subcommand =>
|
|
||||||
subcommand
|
|
||||||
.setName('clear')
|
|
||||||
.setDescription('Clear the audit log channel.')
|
|
||||||
.setDescriptionLocalizations({ ko: '감사 채널 설정을 해제합니다.' })
|
|
||||||
)
|
|
||||||
.addSubcommand(subcommand =>
|
|
||||||
subcommand
|
|
||||||
.setName('status')
|
|
||||||
.setDescription('Check current audit log channel status.')
|
|
||||||
.setDescriptionLocalizations({ ko: '현재 감사 채널 설정 상태를 확인합니다.' })
|
|
||||||
)
|
|
||||||
.addSubcommand(subcommand =>
|
|
||||||
subcommand
|
|
||||||
.setName('filter')
|
|
||||||
.setDescription('Enable or disable specific audit log categories.')
|
|
||||||
.setDescriptionLocalizations({ ko: '특정 종류의 감사 로그 수신 여부를 설정합니다.' })
|
|
||||||
.addStringOption(option =>
|
.addStringOption(option =>
|
||||||
option.setName('category')
|
option.setName('action')
|
||||||
.setDescription('The category to manage')
|
.setDescription('Action to perform')
|
||||||
.setDescriptionLocalizations({ ko: '설정할 카테고리' })
|
|
||||||
.setRequired(true)
|
.setRequired(true)
|
||||||
.addChoices(
|
.addChoices(
|
||||||
{ name: 'SYSTEM (System Errors)', value: 'SYSTEM' },
|
{ name: 'set (Set Channel)', value: 'set' },
|
||||||
{ name: 'BOOT (Bot Online Notifications)', value: 'BOOT' },
|
{ name: 'clear (Reset)', value: 'clear' },
|
||||||
{ name: 'VOICE (Voice Channels)', value: 'VOICE' },
|
{ name: 'status (Check)', value: 'status' },
|
||||||
{ name: 'PERMISSION (Permission Issues)', value: 'PERMISSION' },
|
{ name: 'filter (Toggle Categories)', value: 'filter' },
|
||||||
{ name: 'INVITE (Invite Tracking)', value: 'INVITE' },
|
)
|
||||||
{ name: 'MIMIC (Mimic Features)', value: 'MIMIC' }
|
)
|
||||||
|
.addChannelOption(option =>
|
||||||
|
option.setName('channel')
|
||||||
|
.setDescription('Channel to use (for set action)')
|
||||||
|
.addChannelTypes(ChannelType.GuildText)
|
||||||
|
)
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('category')
|
||||||
|
.setDescription('Category to toggle (for filter action)')
|
||||||
|
.addChoices(
|
||||||
|
{ name: 'SYSTEM', value: 'SYSTEM' },
|
||||||
|
{ name: 'BOOT', value: 'BOOT' },
|
||||||
|
{ name: 'VOICE', value: 'VOICE' },
|
||||||
|
{ name: 'PERMISSION', value: 'PERMISSION' },
|
||||||
|
{ name: 'INVITE', value: 'INVITE' },
|
||||||
|
{ name: 'MIMIC', value: 'MIMIC' }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.addBooleanOption(option =>
|
.addBooleanOption(option =>
|
||||||
option.setName('enable')
|
option.setName('enable')
|
||||||
.setDescription('True to receive logs for this category, False to ignore.')
|
.setDescription('Enable or disable (for filter action)')
|
||||||
.setDescriptionLocalizations({ ko: '해당 카테고리 수신 (true) 또는 차단 (false)' })
|
|
||||||
.setRequired(true)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
// --- Bot Subcommand ---
|
||||||
// --- Permissions Subcommand ---
|
|
||||||
.addSubcommand(subcommand =>
|
.addSubcommand(subcommand =>
|
||||||
subcommand
|
subcommand
|
||||||
.setName('permissions')
|
.setName('bot')
|
||||||
.setDescription('Check if the bot has all required permissions for its features.')
|
.setDescription('Diagnose and manage bot-specific status.')
|
||||||
.setDescriptionLocalizations({ ko: '봇이 필요한 권한을 가지고 있는지 진단합니다.' })
|
.setDescriptionLocalizations({ ko: '봇의 상태를 진단하고 관리합니다.' })
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('action')
|
||||||
|
.setDescription('Action to perform')
|
||||||
|
.setRequired(true)
|
||||||
|
.addChoices(
|
||||||
|
{ name: 'permissions (Diagnose)', value: 'permissions' },
|
||||||
|
)
|
||||||
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) {
|
async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) {
|
||||||
const group = interaction.options.getSubcommandGroup();
|
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
const guild = interaction.guild!;
|
const guild = interaction.guild!;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// --- CHANNEL GROUP ---
|
// --- CHANNEL ---
|
||||||
if (group === 'channel') {
|
if (subcommand === 'channel') {
|
||||||
if (subcommand === 'set') {
|
const action = interaction.options.getString('action', true);
|
||||||
|
|
||||||
|
if (action === 'set') {
|
||||||
|
const channel = interaction.options.getChannel('channel') as TextChannel;
|
||||||
|
if (!channel) return interaction.reply({ content: '❌ `channel` 옵션을 선택해주세요.', ephemeral: true });
|
||||||
|
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
const channel = interaction.options.getChannel('channel', true) as TextChannel;
|
|
||||||
const botMember = guild.members.me;
|
const botMember = guild.members.me;
|
||||||
if (!botMember) return;
|
if (!botMember) return;
|
||||||
const perms = channel.permissionsFor(botMember);
|
const perms = channel.permissionsFor(botMember);
|
||||||
|
|
@ -148,16 +140,16 @@ export default {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'clear') {
|
if (action === 'clear') {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
await auditLogService.clearChannel(guild.id);
|
await auditLogService.clearChannel(guild.id);
|
||||||
return interaction.editReply({ content: `✅ 감사 채널 설정이 해제되었습니다.` });
|
return interaction.editReply({ content: `✅ 감사 채널 설정이 해제되었습니다.` });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'status') {
|
if (action === 'status') {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
const config = await auditLogService.getChannel(guild.id);
|
const config = await auditLogService.getChannel(guild.id);
|
||||||
if (!config) return interaction.editReply({ content: `설정된 감사 채널이 없습니다. 먼저 \`/audit channel set\` 명령어로 채널을 설정해주세요.` });
|
if (!config) return interaction.editReply({ content: `설정된 감사 채널이 없습니다. 먼저 \`/audit channel action:set\` 명령어로 채널을 설정해주세요.` });
|
||||||
|
|
||||||
const disabled = config.disabledCategories.length > 0 ? config.disabledCategories.map((c: string) => `\`${c}\``).join(', ') : '없음 (모두 수신 중)';
|
const disabled = config.disabledCategories.length > 0 ? config.disabledCategories.map((c: string) => `\`${c}\``).join(', ') : '없음 (모두 수신 중)';
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
|
|
@ -171,21 +163,28 @@ export default {
|
||||||
return interaction.editReply({ embeds: [embed] });
|
return interaction.editReply({ embeds: [embed] });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'filter') {
|
if (action === 'filter') {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
const category = interaction.options.getString('category') as AuditCategory;
|
||||||
const category = interaction.options.getString('category', true) as AuditCategory;
|
const enable = interaction.options.getBoolean('enable');
|
||||||
const enable = interaction.options.getBoolean('enable', true);
|
|
||||||
|
|
||||||
|
if (!category || enable === null) {
|
||||||
|
return interaction.reply({ content: '❌ `category` 및 `enable` 옵션을 모두 입력해주세요.', ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
const config = await auditLogService.getChannel(guild.id);
|
const config = await auditLogService.getChannel(guild.id);
|
||||||
if (!config) return interaction.editReply({ content: `설정된 감사 채널이 없습니다. 먼저 \`/audit channel set\` 명령어로 채널을 설정해주세요.` });
|
if (!config) return interaction.editReply({ content: `설정된 감사 채널이 없습니다. 먼저 \`/audit channel action:set\` 명령어로 채널을 설정해주세요.` });
|
||||||
|
|
||||||
await auditLogService.setFilter(guild.id, category, enable);
|
await auditLogService.setFilter(guild.id, category, enable);
|
||||||
return interaction.editReply({ content: `✅ **${category}** 카테고리의 감사 로그 수신이 **${enable ? '활성화(ON)' : '비활성화(OFF)'}** 되었습니다.` });
|
return interaction.editReply({ content: `✅ **${category}** 카테고리의 감사 로그 수신이 **${enable ? '활성화(ON)' : '비활성화(OFF)'}** 되었습니다.` });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PERMISSIONS SUBCOMMAND ---
|
// --- BOT ---
|
||||||
if (subcommand === 'permissions') {
|
if (subcommand === 'bot') {
|
||||||
|
const action = interaction.options.getString('action', true);
|
||||||
|
|
||||||
|
if (action === 'permissions') {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
const results = await PermissionAuditService.auditGuild(guild);
|
const results = await PermissionAuditService.auditGuild(guild);
|
||||||
if (results.length === 0) return interaction.editReply({ content: t(locale, 'commands.permissionAudit.noResults') });
|
if (results.length === 0) return interaction.editReply({ content: t(locale, 'commands.permissionAudit.noResults') });
|
||||||
|
|
@ -223,11 +222,12 @@ export default {
|
||||||
category: 'PERMISSION',
|
category: 'PERMISSION',
|
||||||
severity: failCount > 0 ? 'ERROR' : 'WARN',
|
severity: failCount > 0 ? 'ERROR' : 'WARN',
|
||||||
title: '권한 감사에서 문제 감지',
|
title: '권한 감사에서 문제 감지',
|
||||||
description: `\`/audit permissions\` 실행 중 권한 문제가 확인되었습니다.\n\n${descLines.join('\n')}`
|
description: `\`/audit bot action:permissions\` 실행 중 권한 문제가 확인되었습니다.\n\n${descLines.join('\n')}`
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in audit command', error);
|
console.error('Error in audit command', error);
|
||||||
const reply = interaction.deferred ? interaction.editReply : interaction.reply;
|
const reply = interaction.deferred ? interaction.editReply : interaction.reply;
|
||||||
|
|
@ -235,3 +235,5 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,141 @@
|
||||||
import { SlashCommandBuilder, ChatInputCommandInteraction, PermissionFlagsBits, EmbedBuilder } from 'discord.js';
|
import {
|
||||||
|
SlashCommandBuilder,
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
PermissionFlagsBits,
|
||||||
|
EmbedBuilder,
|
||||||
|
Colors
|
||||||
|
} from 'discord.js';
|
||||||
import { prisma } from '../database';
|
import { prisma } from '../database';
|
||||||
import { t, resolveLocale } from '../i18n';
|
import { t, SupportedLocale, SUPPORTED_LOCALES } from '../i18n';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('config')
|
.setName('config')
|
||||||
.setDescription('Configure bot features')
|
.setDescription('Configure bot features and server settings.')
|
||||||
|
.setDescriptionLocalizations({
|
||||||
|
ko: '봇의 기능 및 서버 설정을 관리합니다.',
|
||||||
|
})
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||||
.addBooleanOption(opt => opt.setName('mimic').setDescription('Enable or disable Mimic feature'))
|
// --- System Subcommand ---
|
||||||
.addBooleanOption(opt => opt.setName('emoji').setDescription('Enable or disable Big Emoji feature')),
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('system')
|
||||||
|
.setDescription('Manage core system settings.')
|
||||||
|
.setDescriptionLocalizations({ ko: '시스템 핵심 설정을 관리합니다.' })
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('action')
|
||||||
|
.setDescription('System action to perform')
|
||||||
|
.setRequired(true)
|
||||||
|
.addChoices(
|
||||||
|
{ name: 'language (Server Locale)', value: 'language' },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('locale')
|
||||||
|
.setDescription('Language to use (for language action)')
|
||||||
|
.addChoices(
|
||||||
|
{ name: 'English', value: 'en' },
|
||||||
|
{ name: '한국어', value: 'ko' },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// --- Features Subcommand ---
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('features')
|
||||||
|
.setDescription('Enable or disable specific bot features.')
|
||||||
|
.setDescriptionLocalizations({
|
||||||
|
ko: '봇의 특정 기능들을 활성화 또는 비활성화합니다.',
|
||||||
|
})
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('target')
|
||||||
|
.setDescription('Feature to configure')
|
||||||
|
.setRequired(true)
|
||||||
|
.addChoices(
|
||||||
|
{ name: 'mimic', value: 'mimic' },
|
||||||
|
{ name: 'emoji', value: 'emoji' },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addBooleanOption(option =>
|
||||||
|
option.setName('enable')
|
||||||
|
.setDescription('Whether to enable this feature')
|
||||||
|
.setRequired(true)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
async execute(interaction: ChatInputCommandInteraction) {
|
async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) {
|
||||||
if (!interaction.guildId) return;
|
if (!interaction.guildId) return;
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
|
||||||
const mimic = interaction.options.getBoolean('mimic');
|
// --- SYSTEM ---
|
||||||
const emoji = interaction.options.getBoolean('emoji');
|
if (subcommand === 'system') {
|
||||||
|
const action = interaction.options.getString('action', true);
|
||||||
|
|
||||||
// Resolve proper supported locale
|
// Removed 'setup' action (now in standalone /setup command)
|
||||||
const locale = resolveLocale({
|
|
||||||
discordLocale: interaction.locale,
|
|
||||||
guildLocale: interaction.guildLocale?.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (mimic === null && emoji === null) {
|
if (action === 'language') {
|
||||||
|
const newLocale = interaction.options.getString('locale') as SupportedLocale;
|
||||||
|
if (!newLocale) {
|
||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
content: t(locale, 'commands.config.noOptions'),
|
content: '❌ `locale` 옵션을 선택해주세요.',
|
||||||
ephemeral: true
|
ephemeral: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!SUPPORTED_LOCALES.includes(newLocale)) {
|
||||||
|
return interaction.reply({ content: `Unsupported locale: ${newLocale}`, ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.guildConfig.upsert({
|
||||||
|
where: { guildId: interaction.guildId },
|
||||||
|
update: { locale: newLocale },
|
||||||
|
create: { guildId: interaction.guildId, locale: newLocale },
|
||||||
|
});
|
||||||
|
|
||||||
|
return interaction.reply({
|
||||||
|
content: t(newLocale, 'commands.language.serverSet', { locale: newLocale === 'ko' ? '한국어' : 'English' }),
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FEATURES ---
|
||||||
|
if (subcommand === 'features') {
|
||||||
|
const target = interaction.options.getString('target', true);
|
||||||
|
const enable = interaction.options.getBoolean('enable', true);
|
||||||
const updateData: any = {};
|
const updateData: any = {};
|
||||||
if (mimic !== null) updateData.mimicEnabled = mimic;
|
|
||||||
if (emoji !== null) updateData.bigEmojiEnabled = emoji;
|
let labelKey = '';
|
||||||
|
if (target === 'mimic') {
|
||||||
|
updateData.mimicEnabled = enable;
|
||||||
|
labelKey = 'commands.config.mimic.label';
|
||||||
|
} else if (target === 'emoji') {
|
||||||
|
updateData.bigEmojiEnabled = enable;
|
||||||
|
labelKey = 'commands.config.emoji.label';
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.guildConfig.upsert({
|
await prisma.guildConfig.upsert({
|
||||||
where: { guildId: interaction.guildId },
|
where: { guildId: interaction.guildId },
|
||||||
update: updateData,
|
update: updateData,
|
||||||
create: {
|
create: {
|
||||||
guildId: interaction.guildId,
|
guildId: interaction.guildId,
|
||||||
mimicEnabled: mimic ?? false,
|
mimicEnabled: target === 'mimic' ? enable : false,
|
||||||
bigEmojiEnabled: emoji ?? false,
|
bigEmojiEnabled: target === 'emoji' ? enable : false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const results: string[] = [];
|
const state = enable ? t(locale, 'commands.config.mimic.enabled') : t(locale, 'commands.config.mimic.disabled');
|
||||||
if (mimic !== null) {
|
const label = t(locale, labelKey);
|
||||||
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()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0x00AE86)
|
.setColor(enable ? Colors.Green : Colors.Grey)
|
||||||
.setTitle(t(locale, 'commands.config.title'))
|
.setTitle(t(locale, 'commands.config.title'))
|
||||||
.setDescription(results.join('\n'));
|
.setDescription(`${label}: **${state}**`);
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], ephemeral: true });
|
return interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,14 @@
|
||||||
import { SlashCommandBuilder, ChatInputCommandInteraction, PermissionFlagsBits } from 'discord.js';
|
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
|
||||||
import { prisma } from '../database';
|
import { prisma } from '../database';
|
||||||
import { t, SupportedLocale, SUPPORTED_LOCALES } from '../i18n';
|
import { t, SupportedLocale, SUPPORTED_LOCALES } from '../i18n';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('language')
|
.setName('language')
|
||||||
.setDescription('Set the language for the bot.')
|
.setDescription('Set your personal language for the bot.')
|
||||||
.setDescriptionLocalizations({
|
.setDescriptionLocalizations({
|
||||||
ko: '봇의 언어를 설정합니다.',
|
ko: '봇의 개인 사용 언어를 설정합니다.',
|
||||||
})
|
})
|
||||||
.addStringOption(option =>
|
|
||||||
option.setName('scope')
|
|
||||||
.setDescription('Apply to yourself or the entire server')
|
|
||||||
.setDescriptionLocalizations({
|
|
||||||
ko: '본인에게만 또는 서버 전체에 적용',
|
|
||||||
})
|
|
||||||
.setRequired(true)
|
|
||||||
.addChoices(
|
|
||||||
{ name: 'Just for me', name_localizations: { ko: '나만 적용' }, value: 'user' },
|
|
||||||
{ name: 'Entire server (Admin only)', name_localizations: { ko: '서버 전체 (관리자 전용)' }, value: 'server' },
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.addStringOption(option =>
|
.addStringOption(option =>
|
||||||
option.setName('locale')
|
option.setName('locale')
|
||||||
.setDescription('Language to use')
|
.setDescription('Language to use')
|
||||||
|
|
@ -35,7 +23,6 @@ export default {
|
||||||
),
|
),
|
||||||
|
|
||||||
async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) {
|
async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) {
|
||||||
const scope = interaction.options.getString('scope', true) as 'user' | 'server';
|
|
||||||
const newLocale = interaction.options.getString('locale', true) as SupportedLocale;
|
const newLocale = interaction.options.getString('locale', true) as SupportedLocale;
|
||||||
|
|
||||||
// Validate locale (safety check)
|
// Validate locale (safety check)
|
||||||
|
|
@ -44,7 +31,6 @@ export default {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scope === 'user') {
|
|
||||||
await prisma.userLocale.upsert({
|
await prisma.userLocale.upsert({
|
||||||
where: { userId: interaction.user.id },
|
where: { userId: interaction.user.id },
|
||||||
update: { locale: newLocale },
|
update: { locale: newLocale },
|
||||||
|
|
@ -56,27 +42,6 @@ export default {
|
||||||
content: t(newLocale, 'commands.language.userSet', { locale: newLocale === 'ko' ? '한국어' : 'English' }),
|
content: t(newLocale, 'commands.language.userSet', { locale: newLocale === 'ko' ? '한국어' : 'English' }),
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
});
|
});
|
||||||
} else if (scope === 'server') {
|
|
||||||
// Require Administrator permission
|
|
||||||
if (!interaction.memberPermissions?.has(PermissionFlagsBits.Administrator)) {
|
|
||||||
await interaction.reply({
|
|
||||||
content: t(locale, 'commands.language.serverPermissionDenied'),
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.guildConfig.upsert({
|
|
||||||
where: { guildId: interaction.guildId! },
|
|
||||||
update: { locale: newLocale },
|
|
||||||
create: { guildId: interaction.guildId!, locale: newLocale },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Respond in the NEWLY selected locale
|
|
||||||
await interaction.reply({
|
|
||||||
content: t(newLocale, 'commands.language.serverSet', { locale: newLocale === 'ko' ? '한국어' : 'English' }),
|
|
||||||
ephemeral: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import {
|
import {
|
||||||
SlashCommandBuilder,
|
SlashCommandBuilder,
|
||||||
PermissionFlagsBits,
|
|
||||||
ChatInputCommandInteraction,
|
ChatInputCommandInteraction,
|
||||||
|
PermissionFlagsBits
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { SetupWizardRenderer } from '../services/SetupWizardRenderer';
|
import { SetupWizardRenderer } from '../services/SetupWizardRenderer';
|
||||||
import { SupportedLocale } from '../i18n';
|
import { SupportedLocale } from '../i18n';
|
||||||
|
|
@ -9,16 +9,17 @@ import { SupportedLocale } from '../i18n';
|
||||||
export default {
|
export default {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('setup')
|
.setName('setup')
|
||||||
.setDescription('Run the setup wizard to configure the bot step by step.')
|
.setDescription('Initial setup wizard for the bot.')
|
||||||
.setDescriptionLocalizations({
|
.setDescriptionLocalizations({
|
||||||
ko: '설정 마법사를 실행하여 봇의 필수 기능들을 단계별로 설정합니다.',
|
ko: '봇의 초기 환경 설정을 위한 마법사를 실행합니다.',
|
||||||
})
|
})
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||||
|
|
||||||
async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) {
|
async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) {
|
||||||
// deferReply is not used because we can just reply directly with the first step View.
|
if (!interaction.guildId) return;
|
||||||
|
|
||||||
const { embed, components } = await SetupWizardRenderer.renderStep(0, interaction, locale);
|
const { embed, components } = await SetupWizardRenderer.renderStep(0, interaction, locale);
|
||||||
await interaction.reply({
|
return interaction.reply({
|
||||||
embeds: [embed],
|
embeds: [embed],
|
||||||
components,
|
components,
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
|
|
|
||||||
|
|
@ -18,104 +18,78 @@ export default {
|
||||||
ko: '임시 음성 채널 설정을 관리합니다.',
|
ko: '임시 음성 채널 설정을 관리합니다.',
|
||||||
})
|
})
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||||
// --- Setup Subcommands ---
|
// --- Generator Subcommand ---
|
||||||
.addSubcommandGroup(group =>
|
.addSubcommand(subcommand =>
|
||||||
group
|
subcommand
|
||||||
.setName('setup')
|
.setName('generator')
|
||||||
.setDescription('Configure the voice generator channel.')
|
.setDescription('Configure the voice generator channel.')
|
||||||
.setDescriptionLocalizations({ ko: '음성 생성기 채널을 설정합니다.' })
|
.setDescriptionLocalizations({ ko: '음성 생성기 채널을 설정합니다.' })
|
||||||
.addSubcommand(subcommand =>
|
.addStringOption(option =>
|
||||||
subcommand
|
option.setName('action')
|
||||||
.setName('set')
|
.setDescription('Action to perform')
|
||||||
.setDescription('Set an existing voice channel as a Generator')
|
.setRequired(true)
|
||||||
.setDescriptionLocalizations({ ko: '기존 음성 채널을 생성기로 설정합니다' })
|
.addChoices(
|
||||||
|
{ name: 'set (Set Existing)', value: 'set' },
|
||||||
|
{ name: 'create (Create New)', value: 'create' },
|
||||||
|
)
|
||||||
|
)
|
||||||
.addChannelOption(option =>
|
.addChannelOption(option =>
|
||||||
option.setName('channel')
|
option.setName('channel')
|
||||||
.setDescription('The voice channel to act as the Generator')
|
.setDescription('Voice channel (for set action)')
|
||||||
.setDescriptionLocalizations({ ko: '생성기로 사용할 음성 채널' })
|
|
||||||
.setRequired(true)
|
|
||||||
.addChannelTypes(ChannelType.GuildVoice)
|
.addChannelTypes(ChannelType.GuildVoice)
|
||||||
)
|
)
|
||||||
.addChannelOption(option =>
|
|
||||||
option.setName('category')
|
|
||||||
.setDescription('(Optional) The category where temp channels will be created')
|
|
||||||
.setDescriptionLocalizations({ ko: '(선택) 임시 채널이 생성될 카테고리' })
|
|
||||||
.setRequired(false)
|
|
||||||
.addChannelTypes(ChannelType.GuildCategory)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.addSubcommand(subcommand =>
|
|
||||||
subcommand
|
|
||||||
.setName('create')
|
|
||||||
.setDescription('Create a new voice channel and set it as a Generator')
|
|
||||||
.setDescriptionLocalizations({ ko: '새 음성 채널을 만들고 생성기로 설정합니다' })
|
|
||||||
.addStringOption(option =>
|
.addStringOption(option =>
|
||||||
option.setName('name')
|
option.setName('name')
|
||||||
.setDescription('The name of the new generator voice channel')
|
.setDescription('New channel name (for create action)')
|
||||||
.setDescriptionLocalizations({ ko: '새 생성기 음성 채널의 이름' })
|
|
||||||
.setRequired(true)
|
|
||||||
)
|
)
|
||||||
.addChannelOption(option =>
|
.addChannelOption(option =>
|
||||||
option.setName('category')
|
option.setName('category')
|
||||||
.setDescription('(Optional) The category where the new channel will be created')
|
.setDescription('Target category for temp channels')
|
||||||
.setDescriptionLocalizations({ ko: '(선택) 새 채널이 생성될 카테고리' })
|
|
||||||
.setRequired(false)
|
|
||||||
.addChannelTypes(ChannelType.GuildCategory)
|
.addChannelTypes(ChannelType.GuildCategory)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
// --- Settings Subcommand ---
|
||||||
// --- Config Subcommands ---
|
.addSubcommand(subcommand =>
|
||||||
.addSubcommandGroup(group =>
|
subcommand
|
||||||
group
|
.setName('settings')
|
||||||
.setName('config')
|
|
||||||
.setDescription('Manage default settings for temporary channels.')
|
.setDescription('Manage default settings for temporary channels.')
|
||||||
.setDescriptionLocalizations({ ko: '임시 채널의 기본 설정을 관리합니다.' })
|
.setDescriptionLocalizations({ ko: '임시 채널의 기본 설정을 관리합니다.' })
|
||||||
.addSubcommand(subcommand =>
|
.addStringOption(option =>
|
||||||
subcommand
|
option.setName('action')
|
||||||
.setName('name')
|
.setDescription('Action to perform')
|
||||||
.setDescription('Set the default naming template for new temp channels.')
|
.setRequired(true)
|
||||||
.setDescriptionLocalizations({ ko: '임시 채널의 기본 이름 템플릿을 설정합니다.' })
|
.addChoices(
|
||||||
|
{ name: 'name (Template)', value: 'name' },
|
||||||
|
{ name: 'limit (User Count)', value: 'limit' },
|
||||||
|
{ name: 'status (Check Config)', value: 'status' },
|
||||||
|
)
|
||||||
|
)
|
||||||
.addStringOption(option =>
|
.addStringOption(option =>
|
||||||
option.setName('template')
|
option.setName('template')
|
||||||
.setDescription('Template using {{username}} placeholder')
|
.setDescription('Naming template (for name action)')
|
||||||
.setDescriptionLocalizations({ ko: '{{username}}을 포함한 이름 템플릿' })
|
|
||||||
.setRequired(true)
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
.addSubcommand(subcommand =>
|
|
||||||
subcommand
|
|
||||||
.setName('limit')
|
|
||||||
.setDescription('Set the default user limit for new temp channels.')
|
|
||||||
.setDescriptionLocalizations({ ko: '임시 채널의 기본 인원 제한을 설정합니다.' })
|
|
||||||
.addIntegerOption(option =>
|
.addIntegerOption(option =>
|
||||||
option.setName('limit')
|
option.setName('limit')
|
||||||
.setDescription('User limit (0-99, 0 = unlimited)')
|
.setDescription('User limit (for limit action)')
|
||||||
.setDescriptionLocalizations({ ko: '인원 제한 (0-99, 0 = 무제한)' })
|
|
||||||
.setRequired(true)
|
|
||||||
.setMinValue(0)
|
.setMinValue(0)
|
||||||
.setMaxValue(99)
|
.setMaxValue(99)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
.addSubcommand(subcommand =>
|
|
||||||
subcommand
|
|
||||||
.setName('status')
|
|
||||||
.setDescription('View current guild voice settings.')
|
|
||||||
.setDescriptionLocalizations({ ko: '현재 서버의 음성 설정을 확인합니다.' })
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
|
|
||||||
async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) {
|
async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) {
|
||||||
const group = interaction.options.getSubcommandGroup();
|
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
const guildId = interaction.guildId!;
|
const guildId = interaction.guildId!;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// --- SETUP GROUP ---
|
// --- GENERATOR ---
|
||||||
if (group === 'setup') {
|
if (subcommand === 'generator') {
|
||||||
|
const action = interaction.options.getString('action', true);
|
||||||
const category = interaction.options.getChannel('category');
|
const category = interaction.options.getChannel('category');
|
||||||
|
|
||||||
if (subcommand === 'set') {
|
if (action === 'set') {
|
||||||
const channel = interaction.options.getChannel('channel', true);
|
const channel = interaction.options.getChannel('channel');
|
||||||
|
if (!channel) return interaction.reply({ content: '❌ `channel` 옵션을 선택해주세요.', ephemeral: true });
|
||||||
|
|
||||||
await prisma.voiceGenerator.upsert({
|
await prisma.voiceGenerator.upsert({
|
||||||
where: { channelId: channel.id },
|
where: { channelId: channel.id },
|
||||||
update: { categoryId: category?.id || null, guildId },
|
update: { categoryId: category?.id || null, guildId },
|
||||||
|
|
@ -127,8 +101,10 @@ export default {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'create') {
|
if (action === 'create') {
|
||||||
const name = interaction.options.getString('name', true);
|
const name = interaction.options.getString('name');
|
||||||
|
if (!name) return interaction.reply({ content: '❌ `name` 옵션을 입력해주세요.', ephemeral: true });
|
||||||
|
|
||||||
const newChannel = await interaction.guild!.channels.create({
|
const newChannel = await interaction.guild!.channels.create({
|
||||||
name,
|
name,
|
||||||
type: ChannelType.GuildVoice,
|
type: ChannelType.GuildVoice,
|
||||||
|
|
@ -144,10 +120,14 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- CONFIG GROUP ---
|
// --- SETTINGS ---
|
||||||
if (group === 'config') {
|
if (subcommand === 'settings') {
|
||||||
if (subcommand === 'name') {
|
const action = interaction.options.getString('action', true);
|
||||||
const template = interaction.options.getString('template', true);
|
|
||||||
|
if (action === 'name') {
|
||||||
|
const template = interaction.options.getString('template');
|
||||||
|
if (!template) return interaction.reply({ content: '❌ `template` 옵션을 입력해주세요.', ephemeral: true });
|
||||||
|
|
||||||
await prisma.voiceGuildConfig.upsert({
|
await prisma.voiceGuildConfig.upsert({
|
||||||
where: { guildId },
|
where: { guildId },
|
||||||
update: { defaultNameTemplate: template },
|
update: { defaultNameTemplate: template },
|
||||||
|
|
@ -159,8 +139,10 @@ export default {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'limit') {
|
if (action === 'limit') {
|
||||||
const limit = interaction.options.getInteger('limit', true);
|
const limit = interaction.options.getInteger('limit');
|
||||||
|
if (limit === null) return interaction.reply({ content: '❌ `limit` 옵션을 입력해주세요.', ephemeral: true });
|
||||||
|
|
||||||
await prisma.voiceGuildConfig.upsert({
|
await prisma.voiceGuildConfig.upsert({
|
||||||
where: { guildId },
|
where: { guildId },
|
||||||
update: { defaultUserLimit: limit },
|
update: { defaultUserLimit: limit },
|
||||||
|
|
@ -172,7 +154,7 @@ export default {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'status') {
|
if (action === 'status') {
|
||||||
const config = await prisma.voiceGuildConfig.findUnique({ where: { guildId } });
|
const config = await prisma.voiceGuildConfig.findUnique({ where: { guildId } });
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle(t(locale, 'commands.voiceConfig.statusTitle'))
|
.setTitle(t(locale, 'commands.voiceConfig.statusTitle'))
|
||||||
|
|
@ -201,3 +183,4 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue