|
| 1 | +import { |
| 2 | + SlashCommandBuilder, |
| 3 | + ChatInputCommandInteraction, |
| 4 | + MessageContextMenuCommandInteraction, |
| 5 | + UserContextMenuCommandInteraction, |
| 6 | + CacheType, |
| 7 | + TextChannel, |
| 8 | + EmbedBuilder, |
| 9 | + ButtonBuilder, |
| 10 | + ActionRowBuilder, |
| 11 | + ButtonStyle, |
| 12 | + PermissionsBitField, ChannelType, |
| 13 | +} from "discord.js"; |
| 14 | +import StoreManager from "../util/manange-store"; |
| 15 | +import { VoteData } from "../types/vote"; |
| 16 | +import {AddExecute, CommandData, InteractionHandler} from "../util/interaction-handler"; |
| 17 | + |
| 18 | +@CommandData( |
| 19 | + new SlashCommandBuilder() |
| 20 | + .setName("vote") // 커맨드 이름 설정 |
| 21 | + .setDescription("투표 관련 조작을 할 수 있습니다.") // 커맨드 설명 |
| 22 | + // 투표 시작 커맨드 |
| 23 | + .addSubcommand((option) => |
| 24 | + option |
| 25 | + .setName("start") |
| 26 | + .setDescription("새로운 투표를 시작합니다.") |
| 27 | + .addStringOption((option) => |
| 28 | + option |
| 29 | + .setName("title") |
| 30 | + .setDescription("투표의 제목을 입력합니다.") |
| 31 | + .setRequired(true) |
| 32 | + ) |
| 33 | + // 투표 설명 옵션 |
| 34 | + .addStringOption((option) => |
| 35 | + option |
| 36 | + .setName("description") |
| 37 | + .setDescription("투표의 설명을 입력합니다.") |
| 38 | + .setRequired(true) |
| 39 | + ) |
| 40 | + // 투표를 보낼 채널 옵션 |
| 41 | + .addChannelOption((option) => |
| 42 | + option |
| 43 | + .setName("channel") |
| 44 | + .setDescription("투표를 보낼 채널을 선택합니다.") |
| 45 | + .setRequired(true) |
| 46 | + ) |
| 47 | + // 투표 항목 개수 옵션 (2-10개) |
| 48 | + .addIntegerOption((option) => |
| 49 | + option |
| 50 | + .setName("options_count") |
| 51 | + .setDescription("투표 항목의 개수를 입력합니다 (최대 10개)") |
| 52 | + .setRequired(true) |
| 53 | + .setMinValue(2) |
| 54 | + .setMaxValue(10) |
| 55 | + ) |
| 56 | + // 투표 항목 1-10 옵션들 |
| 57 | + .addStringOption((option) => |
| 58 | + option |
| 59 | + .setName("option1") |
| 60 | + .setDescription("첫 번째 투표 항목") |
| 61 | + .setRequired(true) |
| 62 | + ) |
| 63 | + .addStringOption((option) => |
| 64 | + option |
| 65 | + .setName("option2") |
| 66 | + .setDescription("두 번째 투표 항목") |
| 67 | + .setRequired(true) |
| 68 | + ) |
| 69 | + .addStringOption((option) => |
| 70 | + option |
| 71 | + .setName("option3") |
| 72 | + .setDescription("세 번째 투표 항목") |
| 73 | + .setRequired(false) |
| 74 | + ) |
| 75 | + .addStringOption((option) => |
| 76 | + option |
| 77 | + .setName("option4") |
| 78 | + .setDescription("네 번째 투표 항목") |
| 79 | + .setRequired(false) |
| 80 | + ) |
| 81 | + .addStringOption((option) => |
| 82 | + option |
| 83 | + .setName("option5") |
| 84 | + .setDescription("다섯 번째 투표 항목") |
| 85 | + .setRequired(false) |
| 86 | + ) |
| 87 | + .addStringOption((option) => |
| 88 | + option |
| 89 | + .setName("option6") |
| 90 | + .setDescription("여섯 번째 투표 항목") |
| 91 | + .setRequired(false) |
| 92 | + ) |
| 93 | + .addStringOption((option) => |
| 94 | + option |
| 95 | + .setName("option7") |
| 96 | + .setDescription("일곱 번째 투표 항목") |
| 97 | + .setRequired(false) |
| 98 | + ) |
| 99 | + .addStringOption((option) => |
| 100 | + option |
| 101 | + .setName("option8") |
| 102 | + .setDescription("여덟 번째 투표 항목") |
| 103 | + .setRequired(false) |
| 104 | + ) |
| 105 | + .addStringOption((option) => |
| 106 | + option |
| 107 | + .setName("option9") |
| 108 | + .setDescription("아홉 번째 투표 항목") |
| 109 | + .setRequired(false) |
| 110 | + ) |
| 111 | + .addStringOption((option) => |
| 112 | + option |
| 113 | + .setName("option10") |
| 114 | + .setDescription("열 번째 투표 항목") |
| 115 | + .setRequired(false) |
| 116 | + ) |
| 117 | + ) |
| 118 | + // 투표 종료 커맨드 |
| 119 | + .addSubcommand((option) => |
| 120 | + option |
| 121 | + .setName("close") |
| 122 | + .setDescription("투표를 종료합니다.") |
| 123 | + .addStringOption( |
| 124 | + (option) => |
| 125 | + option |
| 126 | + .setName("vote_id") // 투표 ID 옵션 |
| 127 | + .setDescription("종료할 투표의 아이디를 입력합니다.") |
| 128 | + .setRequired(true) // 필수 옵션 |
| 129 | + ) |
| 130 | + .addBooleanOption( |
| 131 | + (option) => |
| 132 | + option |
| 133 | + .setName("mention_everyone") // @everyone 멘션 옵션 |
| 134 | + .setDescription("모든 사람에게 투표 결과 공개 알림을 보냅니다.") |
| 135 | + .setRequired(false) // 선택 옵션 |
| 136 | + ) |
| 137 | + .addChannelOption( |
| 138 | + (option) => |
| 139 | + option |
| 140 | + .setName("channel") // 결과 공개 채널 옵션 |
| 141 | + .setDescription("투표 결과 공개 알림을 보낼 채널을 선택합니다.") |
| 142 | + .setRequired(false) // 선택 옵션 |
| 143 | + ) |
| 144 | + ) |
| 145 | + .toJSON() |
| 146 | +) |
| 147 | +@InteractionHandler() |
| 148 | +export default class VoteCommand { |
| 149 | + |
| 150 | + // 투표 시작 커맨드 |
| 151 | + @AddExecute("vote/start") |
| 152 | + async startVote(interaction: ChatInputCommandInteraction<CacheType>) { |
| 153 | + if ( |
| 154 | + !interaction.memberPermissions?.has( |
| 155 | + PermissionsBitField.Flags.Administrator |
| 156 | + ) |
| 157 | + ) { |
| 158 | + interaction.reply({ |
| 159 | + content: "권한이 없습니다.", |
| 160 | + ephemeral: true, |
| 161 | + }); |
| 162 | + return; |
| 163 | + } |
| 164 | + // 투표 데이터 저장소 초기화 |
| 165 | + const store = new StoreManager("votes"); |
| 166 | + const channel = interaction.channel as TextChannel; |
| 167 | + |
| 168 | + // 채널 유효성 검사 |
| 169 | + if (!channel) { |
| 170 | + interaction.reply({ |
| 171 | + content: "채널을 선택해주세요.", |
| 172 | + ephemeral: true, |
| 173 | + }); |
| 174 | + return; |
| 175 | + } |
| 176 | + if (!("options" in interaction)) return; |
| 177 | + |
| 178 | + // 투표 제목과 설명 가져오기 |
| 179 | + const title = ( |
| 180 | + interaction as ChatInputCommandInteraction |
| 181 | + ).options.getString("title"); |
| 182 | + const description = ( |
| 183 | + interaction as ChatInputCommandInteraction |
| 184 | + ).options.getString("description"); |
| 185 | + |
| 186 | + // 고유한 투표 ID 생성 |
| 187 | + const voteId = `${Date.now()}`; |
| 188 | + |
| 189 | + // 임베드 메시지 생성 |
| 190 | + const embed = new EmbedBuilder() |
| 191 | + .setTitle(title) |
| 192 | + .setDescription(description) |
| 193 | + .setColor("#eb7723") |
| 194 | + .setFooter({ |
| 195 | + text: `vote id: ${voteId}`, |
| 196 | + }) |
| 197 | + .setTimestamp(); |
| 198 | + |
| 199 | + // 투표 항목 개수 가져오기 |
| 200 | + const optionsCount = ( |
| 201 | + interaction as ChatInputCommandInteraction |
| 202 | + ).options.getInteger("options_count"); |
| 203 | + if (!optionsCount) return; |
| 204 | + |
| 205 | + // 버튼 컴포넌트 생성 |
| 206 | + const row = new ActionRowBuilder<ButtonBuilder>(); |
| 207 | + |
| 208 | + // 투표 데이터 구조 초기화 |
| 209 | + let voteData: VoteData = { |
| 210 | + title: title || "", |
| 211 | + votedUser: [], |
| 212 | + options: {}, |
| 213 | + closed: false, |
| 214 | + }; |
| 215 | + |
| 216 | + // 투표 항목별 버튼 생성 및 데이터 구조화 |
| 217 | + for (let i = 1; i <= optionsCount; i++) { |
| 218 | + const buttonLabel = ( |
| 219 | + interaction as ChatInputCommandInteraction |
| 220 | + ).options.getString(`option${i}`); |
| 221 | + if (!buttonLabel) return; |
| 222 | + const button = new ButtonBuilder() |
| 223 | + .setCustomId(`vote_${voteId}_${buttonLabel}`) |
| 224 | + .setLabel(buttonLabel) |
| 225 | + .setStyle(ButtonStyle.Primary); |
| 226 | + row.addComponents(button); |
| 227 | + voteData.options[buttonLabel] = { count: 0 }; |
| 228 | + } |
| 229 | + |
| 230 | + // 투표 데이터 저장 |
| 231 | + store.set(voteId, voteData); |
| 232 | + |
| 233 | + // 지정된 채널에 투표 메시지 전송 |
| 234 | + const voteChannel = interaction.options.getChannel("channel"); |
| 235 | + if (!voteChannel) return; |
| 236 | + if (voteChannel.type !== ChannelType.GuildText) return; |
| 237 | + await (voteChannel as TextChannel).send({ |
| 238 | + embeds: [embed], |
| 239 | + components: [row], |
| 240 | + }); |
| 241 | + |
| 242 | + // 투표 생성 완료 메시지 전송 |
| 243 | + interaction.reply({ |
| 244 | + content: "투표가 생성되었습니다.", |
| 245 | + ephemeral: true, |
| 246 | + }); |
| 247 | + } |
| 248 | + |
| 249 | + // 투표 종료 커맨드 |
| 250 | + @AddExecute("vote/close") |
| 251 | + async closeVote(interaction: any) { |
| 252 | + if ( |
| 253 | + !interaction.memberPermissions?.has( |
| 254 | + PermissionsBitField.Flags.Administrator |
| 255 | + ) |
| 256 | + ) { |
| 257 | + interaction.reply({ |
| 258 | + content: "권한이 없습니다.", |
| 259 | + ephemeral: true, |
| 260 | + }); |
| 261 | + return; |
| 262 | + } |
| 263 | + |
| 264 | + // 투표 ID 가져오기 |
| 265 | + const voteId = interaction.options.getString("vote_id"); |
| 266 | + if (!voteId) { |
| 267 | + interaction.reply({ |
| 268 | + content: "투표 아이디를 입력해주세요.", |
| 269 | + ephemeral: true, |
| 270 | + }); |
| 271 | + return; |
| 272 | + } |
| 273 | + |
| 274 | + // 투표 데이터 스토어에서 투표 정보 가져오기 |
| 275 | + const store = new StoreManager("votes"); |
| 276 | + const vote = store.get(voteId) as VoteData; |
| 277 | + |
| 278 | + // 투표가 존재하지 않는 경우 |
| 279 | + if (!vote) { |
| 280 | + interaction.reply({ |
| 281 | + content: "존재하지 않는 투표입니다.", |
| 282 | + ephemeral: true, |
| 283 | + }); |
| 284 | + return; |
| 285 | + } |
| 286 | + |
| 287 | + // 이미 종료된 투표인 경우 |
| 288 | + if (vote.closed) { |
| 289 | + interaction.reply({ |
| 290 | + content: "이미 종료된 투표입니다.", |
| 291 | + ephemeral: true, |
| 292 | + }); |
| 293 | + return; |
| 294 | + } |
| 295 | + |
| 296 | + // 투표 종료 처리 |
| 297 | + vote.closed = true; |
| 298 | + store.set(voteId, vote); |
| 299 | + interaction.reply({ |
| 300 | + content: "투표가 종료되었습니다.", |
| 301 | + ephemeral: true, |
| 302 | + }); |
| 303 | + |
| 304 | + // 옵션 값 가져오기 |
| 305 | + const mentionEveryone = interaction.options.getBoolean("mention_everyone"); |
| 306 | + const channel = interaction.options.getChannel("channel"); |
| 307 | + |
| 308 | + // 지정된 채널이 있는 경우 |
| 309 | + if (channel) { |
| 310 | + // @everyone 멘션이 활성화된 경우 |
| 311 | + if (mentionEveryone) { |
| 312 | + await channel?.send({ |
| 313 | + content: "@everyone", |
| 314 | + }); |
| 315 | + } |
| 316 | + |
| 317 | + // 투표 결과 임베드 생성 |
| 318 | + const embed = new EmbedBuilder() |
| 319 | + .setColor("#eb7723") |
| 320 | + .setTitle("투표가 종료되었습니다.") |
| 321 | + .setDescription( |
| 322 | + `투표 타이틀:${ |
| 323 | + vote.title |
| 324 | + } \n투표 id: ${voteId}\n \n **가장 많은 표를 받은 항목: ${ |
| 325 | + // 가장 많은 표를 받은 항목 찾기 |
| 326 | + Object.entries(vote.options).reduce( |
| 327 | + (max, [key, value]) => |
| 328 | + value.count > max[1].count ? [key, value] : max, |
| 329 | + ["", { count: -1 }] |
| 330 | + )[0] |
| 331 | + }**\n\n` |
| 332 | + ); |
| 333 | + await channel?.send({ |
| 334 | + embeds: [embed], |
| 335 | + }); |
| 336 | + } else { |
| 337 | + // 채널이 지정되지 않은 경우 현재 채널에 결과 표시 |
| 338 | + if (mentionEveryone) { |
| 339 | + await interaction.channel?.send({ |
| 340 | + content: "@everyone", |
| 341 | + }); |
| 342 | + } |
| 343 | + |
| 344 | + // 투표 결과 임베드 생성 |
| 345 | + const embed = new EmbedBuilder() |
| 346 | + .setColor("#eb7723") |
| 347 | + .setTitle("투표가 종료되었습니다.") |
| 348 | + .setDescription( |
| 349 | + `투표 타이틀:${ |
| 350 | + vote.title |
| 351 | + } \n투표 id: ${voteId}\n \n **가장 많은 표를 받은 항목: ${ |
| 352 | + // 가장 많은 표를 받은 항목 찾기 |
| 353 | + Object.entries(vote.options).reduce( |
| 354 | + (max, [key, value]) => |
| 355 | + value.count > max[1].count ? [key, value] : max, |
| 356 | + ["", { count: -1 }] |
| 357 | + )[0] |
| 358 | + }**\n\n` |
| 359 | + ); |
| 360 | + |
| 361 | + // 각 투표 옵션별 결과 추가 |
| 362 | + for (const option in vote.options) { |
| 363 | + embed.addFields({ |
| 364 | + name: option, |
| 365 | + value: `${vote.options[option].count}회 클릭됨.`, |
| 366 | + }); |
| 367 | + } |
| 368 | + |
| 369 | + store.delete(voteId); |
| 370 | + |
| 371 | + await interaction.channel?.send({ |
| 372 | + embeds: [embed], |
| 373 | + }); |
| 374 | + } |
| 375 | + } |
| 376 | +} |
0 commit comments