import { client } from "../index"; import { Command } from "../types/Command"; import { DB } from "../utils/Database"; import { DEFAULT_VOICE } from "../classes/TTSClient"; import { VoiceType } from "../utils/tts/Chzzk"; import { ActionRowBuilder, CacheType, ChannelType, ChatInputApplicationCommandData, ChatInputCommandInteraction, Message, StringSelectMenuBuilder, StringSelectMenuInteraction, } from "discord.js"; // VoiceType 은 enum: 키 = 한글 표시명, 값 = 슬러그. // Object.entries 의 키 순서는 enum 선언 순서를 따른다. const VOICE_ENTRIES = Object.entries(VoiceType) as [string, VoiceType][]; const nameOf = (slug: VoiceType): string => VOICE_ENTRIES.find(([_, v]) => v === slug)?.[0] ?? slug; /** /목소리 명령어: 유저 개인별 TTS 목소리를 DB에 저장한다. */ export default class implements Command { name = "목소리"; visible = true; aliases: string[] = ["voice"]; description: string = "내 TTS 목소리를 선택합니다."; metaData: ChatInputApplicationCommandData = { name: this.name, description: this.description, }; async slashRun(interaction: ChatInputCommandInteraction) { if (!interaction.guildId) { await interaction.editReply({ embeds: [ client.mkembed({ title: "서버 안에서만 사용 가능합니다.", color: "DarkRed", }) ] }); return; } const row = DB.user.get(interaction.guildId, interaction.user.id); const current = (row?.voice_type && Object.values(VoiceType).includes(row.voice_type as VoiceType)) ? row.voice_type as VoiceType : DEFAULT_VOICE; const select = new StringSelectMenuBuilder() .setCustomId(this.name) .setPlaceholder("사용할 목소리를 선택해주세요.") .addOptions( VOICE_ENTRIES.map(([label, value]) => ({ label, value, default: value === current, })) ); await interaction.editReply({ embeds: [ client.mkembed({ title: "내 TTS 목소리 설정", description: [ `현재 목소리: **${nameOf(current)}**`, "", "아래에서 원하는 목소리를 선택해주세요.", "선택은 본인에게만 적용되며, 봇을 재시작해도 유지됩니다.", ].join("\n"), }) ], components: [ new ActionRowBuilder().addComponents(select), ], }); } async menuRun(interaction: StringSelectMenuInteraction, args: string[]) { if (!interaction.guildId) { await interaction.editReply({ embeds: [ client.mkembed({ title: "서버 안에서만 사용 가능합니다.", color: "DarkRed", }) ] }); return; } const value = args[0]; if (!value || !Object.values(VoiceType).includes(value as VoiceType)) { await interaction.editReply({ embeds: [ client.mkembed({ title: "알 수 없는 목소리입니다.", description: "선택지를 다시 확인해주세요.", color: "DarkRed", }) ] }); return; } // 외래키 제약 대비: 길드 행이 없으면 먼저 생성. // (/tts channel register 를 한 번도 안 돌린 서버에서도 /목소리 자체는 동작하도록.) if (!DB.guild.get(interaction.guildId)) { DB.guild.set({ id: interaction.guildId, name: interaction.guild?.name ?? "", channel_id: "", }); } const ok = DB.user.setVoice( interaction.guildId, interaction.user.id, interaction.user.username, value, ); if (!ok) { await interaction.editReply({ embeds: [ client.mkembed({ title: "저장 실패", description: "DB 저장 중 오류가 발생했습니다.", color: "DarkRed", }) ] }); return; } await interaction.editReply({ embeds: [ client.mkembed({ title: "목소리 설정 완료", description: `이제부터 **${nameOf(value as VoiceType)}** 목소리로 읽어드립니다.`, }) ] }); } async messageRun(message: Message) { if (message.channel?.type !== ChannelType.GuildText) return; await message.channel.send({ content: `\`/${this.name}\` (슬래시) 명령어로만 사용할 수 있습니다.` }) .then(m => client.msgDelete(m, 5)); } }