지난 시간에는 길드컨텐츠 참여 내역 정보를 생성하고 수정하는 명령어를 만들어봤다.
Discord.js로 메이플스토리 디스코드 봇 만들기 (5)
지난 시간에 길드원 정보를 mongoDB 데이터베이스에 생성하고 삭제하는 명령어를 만들어봤다. Discord.js로 메이플스토리 디스코드 봇 만들기 (4) 저번 시간에 캐릭터 정보를 불러오는 명령어를 구현
huzan2.tistory.com
이번에는 입력한 정보를 이용해서 직위조정을 편하게 해보자.
먼저 길드에 가입된 캐릭터들은 본캐와 부캐로 나뉜다. 노블스킬 사용 권한이 없는 직위 또한 보기 편하도록 본캐와 부캐 각각 하나씩 만들어 두었다.
(메이플스토리 리부트 서버 길드는 역시?! "햇봄" 화이팅~!~!)
편의상 본캐의 직위 이름은 "본캐", 부캐는 "부캐", 그리고 각각 길드컨텐츠 미참여시 조정할 직위 이름은 "본캐압수"와 "부캐압수"로 칭하고 구현하도록 하겠다.
직위조정 도우미
먼저 지난 시간에 생성한 guildContents 폴더 안에 gradeHelper.js를 생성하고 NumberOption으로 월(month)과 주차(week) 정보를 각각 받도록 구성했다. (명령어 구성 코드는 이제 생력할 예정이니, 이전 포스팅들을 참고해주길 바란다)
마찬가지로 데이터베이스에 저장된 정보를 이용해야 하기 때문에 생성해둔 mongoose model을 불러와주고,
const memberDB = require('../../models/memberSchema');
지난 시간에 길컨 명령어를 구현할 때와 마찬가지로 targetTime 문자열을 생성 후 해당 주차 정보가 존재하는지 확인을 거쳐주도록 하자.
run: ({ interaction }) => {
const inputMonth = interaction.options.get("month").value;
const inputWeek = interaction.options.get("week").value;
const targetTime = `${inputMonth}-${inputWeek}`;
const ifexist = memberDB.exists({
guildContents: {
$elemMatch: { time: targetTime },
},
});
if (!ifexist) {
interaction.reply("해당 주차 길드컨텐츠 참여 정보가 존재하지 않습니다.");
return;
}
}
이제 직위조정이 필요한 각각의 경우만큼 배열을 생성해 해당 배열에 닉네임을 push해줄 것이다. 먼저 배열을 선언해주자.
let mainFarr = []; // 본캐 중 길컨 미참여
let subFarr = []; // 부캐 중 길컨 미참여
let mainTarr = []; // 본캐압수 직위에서 길컨 참여
let subTarr = []; // 부캐압수 직위에서 길컨 참여
let elseArr = []; // 나머지(2회 이상 연속으로 길컨 미참여)
지난 시간에 이용했던 for 문과 find()를 이용해서 db에 저장된 전체 문서를 하나씩 불러와주고, target Index를 찾아주자.
for await (const doc of memberDB.find()) {
const targetIndex = doc.guildContents.findIndex(
(e) => e.time === targetTime
);
}
이제 이 for문 안에서 각 멤버별로 길드컨텐츠 참여 내역과 직위를 확인하고, 직위조정이 필요할 경우 해당 케이스의 배열로 push해주자.
if (!doc.guildContents[targetIndex].participated) {
// 길컨 미참여
if (doc.grade === "본캐") {
mainFarr.push(doc.nickName); //본캐 -> 본캐압수
} else if (doc.grade === "부캐") {
subFarr.push(doc.nickName); // 부캐 -> 부캐압수
} else {
elseArr.push(doc.nickName); // 본캐압수 or 부캐압수에서 다시 길컨 미참여
}
} else {
// 길컨 참여
if (doc.grade === "본캐압수") {
mainTarr.push(doc.nickName); // 본캐압수 -> 본캐
} else if (doc.grade === "부캐압수") {
subTarr.push(doc.nickName); // 부캐압수 -> 부캐
}
}
해당하는
이제 출력 부분을 만들 차례인데, embed로 깔끔하게 출력해보도록 하자. discord.js에서 EmbedBuilder를 불러와주고,
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
embed를 구성한 후 테스트 출력을 해보도록 하겠다.
어라? 오류가 발생한다.
로그를 자세히 확인해봤더니 빈 배열을 embed Field의 value로 전달한 것이 문제가 되는 것 같아 다음과 같이 빈 배열에 "-"를 push해주도록 했다.
if (mainTarr.length === 0) {
mainTarr.push("-");
}
if (subTarr.length === 0) {
subTarr.push("-");
}
if (mainFarr.length === 0) {
mainFarr.push("-");
}
if (subFarr.length === 0) {
subFarr.push("-");
}
if (elseArr.length === 0) {
elseArr.push("-");
}
이제 다시 테스트 출력을 해보면...
잘 출력되는 것을 확인할 수 있다.
DB에 자동 반영
조정이 필요한 캐릭터의 목록만 출력해도 되지만, 다음 주에도 다시 직위조정을 해 줘야 하고, 이번 주에 조정한 내역을 일일히 명령어로 입력해주기엔 너무 번거롭다. 때문에 조정 내역을 DB에 자동으로 반영해주는 기능을 추가해 줄 것이다.
명령어 사용시 즉시 반영하기보다는 한 번의 확인 절차를 거치고 반영하는 편이 안정성 측면에서 좋을 것이다.때문에 버튼을 추가해서 db에 반영할지 말지 정하도록 해 주도록 하자.
먼저 discord.js에서 ButtonBuilder와 ActionRowBuilder, ButtonStyle을 추가로 불러와주자.
const {
SlashCommandBuilder,
EmbedBuilder,
ButtonBuilder,
ActionRowBuilder,
ButtonStyle,
} = require("discord.js");
다음으로 choices라는 배열 안에 각 버튼의 내용을 먼저 저장해둘 것이다. 파일 상단에서 module.exports 밖에 만들어주자.
const choices = [
{ name: "yes", emoji: "⭕️", beats: "반영" },
{ name: "no", emoji: "❌", beats: "미반영" },
];
이제 embed에 버튼을 추가해줄 것인데, embed를 생성한 코드 바로 아래에 다음과 같이 버튼을 구성해주자.
const buttons = choices.map((choice) => {
return new ButtonBuilder()
.setCustomId(choice.name)
.setLabel(choice.name)
.setStyle(ButtonStyle.Primary)
.setEmoji(choice.emoji)
});
이제 아래 코드와 같이 ActionRow를 하나 생성해주고, component로 버튼을 넣어준 다음, 답장 내용을 구성해주면 된다.
const row = new ActionRowBuilder().addComponents(buttons);
const reply = await interaction.reply({
content: `${inputMonth}월 ${inputWeek}주차 직위조정 필요 캐릭터 목록은 다음과 같습니다.\n데이터베이스에 조정내용을 자동으로 반영하려면 "반영"버튼을 눌러주세요.`,
embeds: [embed],
components: [row],
});
이제 봇을 실행하고 명령어를 사용해보면..
잘 출력된다! 근데 이모지가 조금 안 어울리는 거 같아서 바꿔주었다.
위의 코드에서 interaction.reply()로 답장을 그냥 보내지 않고 reply라는 변수에 담은 것은 이후에 사용자가 버튼을 눌렀을 때 추가적인 상호작용을 이어가기 위함이다.
먼저 아래 코드와 같이 사용자가 누른 버튼을 불러와보자.
const userInteraction = await reply.awaitMessageComponent();
const userChoice = choices.find(
(choice) => choice.name === userInteraction.customId
);
이렇게 하면 userChoice에는 위에서 생성한 choices배열 안의 요소 중 사용자가 선택한 반응에 해당하는 객체를 값으로 가지게 된다.
즉 아래 내용과 같이 조건을 달아 답장 내용을 수정할 수 있게 된다.
if (userChoice.name === "반영") {
await reply.edit({
content: `[DB 반영 완료]\n${inputMonth}월 ${inputWeek}일차 자동 직위조정 내용은 다음과 같습니다.`,
embeds: [embed],
components: [],
});
} else if (userChoice.name === "미반영") {
await reply.edit({
content: `[DB 반영 취소됨]\n${inputMonth}월 ${inputWeek}일차 자동 직위조정 내용은 다음과 같습니다.`,
embeds: [embed],
components: [],
});
}
봇을 실행하고 명령어 상호작용을 테스트해보도록 하자.
성공적으로 답장 내용이 수정되었고, 버튼들도 응답이 끝나면 잘 사라진 것을 확인할 수 있다.
이제 "반영"을 선택했을 경우 조정된 직위를 db에 반영하도록 하는 코드만 작성하면 된다.
배열 안에 담긴 닉네임을 findOne()으로 하나씩 불러온 후 직위변경을 적용하려 했지만... 계속해서 grade를 수정하는 과정에서 null 값을 가져와버려서 어쩔 수 없이 위에서 작성한 for문을 그대로 가져와준 후 직위를 변경해주었다.
for await (const doc of memberDB.find()) {
const targetIndex = doc.guildContents.findIndex(
(e) => e.time === targetTime
);
if (!doc.guildContents[targetIndex].participated) {
if (doc.grade === "본캐") {
doc.grade = "본캐압수";
doc.save();
} else if (doc.grade === "부캐") {
doc.grade = "부캐압수";
doc.save();
}
} else {
if (doc.grade === "본캐압수") {
doc.grade = "본캐";
doc.save();
} else if (doc.grade === "부캐압수") {
doc.grade = "부캐";
doc.save();
}
}
}
일단 테스트를 진행해보자.
일단 테스트 결과 자체는 성공적이다. 코드가 조금 마음에 들지 않긴 하지만(...) 천천히 오류를 찾아보고 효율적인 코드로 수정하는 것은 조금만 미뤄두도록 하겠다...............
이쯤에서 잊고 있던 한 가지가 떠올랐는데, 바로 연속 길드컨텐츠 미참 횟수를 저장하기로 해두고 코드에 구현하지 않았다는 것이다. 코드를 수정해 연속 길컨 미참여시 warned가 1씩 증가하도록, 길드컨텐츠 참여시 0으로 초기화되도록 구현해주자. 위에서 짠 for문을 다음과 같이 수정해주었다.
for await (const doc of memberDB.find()) {
const targetIndex = doc.guildContents.findIndex(
(e) => e.time === targetTime
);
if (!doc.guildContents[targetIndex].participated) {
if (doc.grade === "본캐") {
doc.grade = "본캐압수";
doc.warned++;
doc.save();
} else if (doc.grade === "부캐") {
doc.grade = "부캐압수";
doc.warned++;
doc.save();
} else {
doc.warned++;
doc.save();
}
} else {
if (doc.grade === "본캐압수") {
doc.grade = "본캐";
doc.warned = 0;
doc.save();
} else if (doc.grade === "부캐압수") {
doc.grade = "부캐";
doc.warned = 0;
doc.save();
}
}
}
다시 테스트를 진행해보면...
반대로 참여 시 상황도 가정해서 다시 테스트를 진행해보자.
성공이다~!~!
이번 포스팅에서는 입력한 길드컨텐츠 참여 정보를 바탕으로 직위조정이 필요한 캐릭터 목록을 출력하고, 버튼을 활용한 추가 상호작용으로 이를 데이터베이스에 자동으로 반영하는 명령어를 만들어봤다. 아래에 오늘 작성한 gradeHelper.js의 코드 전문과 현재까지의 파일트리를 첨부하고 마무리하겠다.
const {
SlashCommandBuilder,
EmbedBuilder,
ButtonBuilder,
ActionRowBuilder,
ButtonStyle,
} = require("discord.js");
const memberDB = require("../../models/memberSchema");
const choices = [
{ name: "반영", emoji: "✅", beats: "반영" },
{ name: "미반영", emoji: "❎", beats: "미반영" },
];
module.exports = {
managerOnly: true,
run: async ({ interaction }) => {
const inputMonth = interaction.options.get("month").value;
const inputWeek = interaction.options.get("week").value;
const targetTime = `${inputMonth}-${inputWeek}`;
const ifexist = await memberDB.exists({
guildContents: {
$elemMatch: { time: targetTime },
},
});
if (!ifexist) {
interaction.reply("해당 주차 길드컨텐츠 참여 정보가 존재하지 않습니다.");
return;
}
let mainFarr = []; // 본캐 중 길컨 미참여
let subFarr = []; // 부캐 중 길컨 미참여
let mainTarr = []; // 본캐압수 직위에서 길컨 참여
let subTarr = []; // 부캐압수 직위에서 길컨 참여
let elseArr = []; // 나머지(2회 이상 연속으로 길컨 미참여)
for await (const doc of memberDB.find()) {
const targetIndex = doc.guildContents.findIndex(
(e) => e.time === targetTime
);
if (!doc.guildContents[targetIndex].participated) {
// 길컨 미참여
if (doc.grade === "본캐") {
mainFarr.push(doc.nickName); //본캐 -> 본캐압수
} else if (doc.grade === "부캐") {
subFarr.push(doc.nickName); // 부캐 -> 부캐압수
} else {
elseArr.push(doc.nickName); // 본캐압수 or 부캐압수에서 다시 길컨 미참여
}
} else {
// 길컨 참여
if (doc.grade === "본캐압수") {
mainTarr.push(doc.nickName); // 본캐압수 -> 본캐
} else if (doc.grade === "부캐압수") {
subTarr.push(doc.nickName); // 부캐압수 -> 부캐
}
}
}
if (mainTarr.length === 0) {
mainTarr.push("-");
}
if (subTarr.length === 0) {
subTarr.push("-");
}
if (mainFarr.length === 0) {
mainFarr.push("-");
}
if (subFarr.length === 0) {
subFarr.push("-");
}
if (elseArr.length === 0) {
elseArr.push("-");
}
const embed = new EmbedBuilder().setColor("White").addFields(
{
name: "본캐 -> 본캐압수",
value: `${mainFarr}`,
},
{
name: "부캐 -> 부캐압수",
value: `${subFarr}`,
},
{
name: "본캐압수 -> 본캐",
value: `${mainTarr}`,
},
{
name: "부캐압수 -> 부캐",
value: `${subTarr}`,
},
{
name: "연속 길드컨텐츠 미참여",
value: `${elseArr}`,
}
);
const buttons = choices.map((choice) => {
return new ButtonBuilder()
.setCustomId(choice.name)
.setLabel(choice.name)
.setStyle(ButtonStyle.Primary)
.setEmoji(choice.emoji);
});
const row = new ActionRowBuilder().addComponents(buttons);
const reply = await interaction.reply({
content: `${inputMonth}월 ${inputWeek}주차 직위조정 필요 캐릭터 목록은 다음과 같습니다.\n데이터베이스에 조정내용을 자동으로 반영하려면 "반영"버튼을 눌러주세요.`,
embeds: [embed],
components: [row],
});
const userInteraction = await reply.awaitMessageComponent();
const userChoice = choices.find(
(choice) => choice.name === userInteraction.customId
);
if (userChoice.name === "반영") {
for await (const doc of memberDB.find()) {
const targetIndex = doc.guildContents.findIndex(
(e) => e.time === targetTime
);
if (!doc.guildContents[targetIndex].participated) {
if (doc.grade === "본캐") {
doc.grade = "본캐압수";
doc.warned++;
doc.save();
} else if (doc.grade === "부캐") {
doc.grade = "부캐압수";
doc.warned++;
doc.save();
} else {
doc.warned++;
doc.save();
}
} else {
if (doc.grade === "본캐압수") {
doc.grade = "본캐";
doc.warned = 0;
doc.save();
} else if (doc.grade === "부캐압수") {
doc.grade = "부캐";
doc.warned = 0;
doc.save();
}
}
}
await reply.edit({
content: `[DB 반영 완료]\n${inputMonth}월 ${inputWeek}일차 자동 직위조정 내용은 다음과 같습니다.`,
embeds: [embed],
components: [],
});
} else if (userChoice.name === "미반영") {
await reply.edit({
content: `[DB 반영 취소됨]\n${inputMonth}월 ${inputWeek}일차 자동 직위조정 내용은 다음과 같습니다.`,
embeds: [embed],
components: [],
});
}
},
data: new SlashCommandBuilder()
.setName("직위조정도우미")
.setDescription("직위조정이 필요한 캐릭터 목록을 출력합니다.")
.addNumberOption((option) =>
option
.setName("month")
.setDescription("정보를 확인할 월 정보 | ex) 8월 -> 8")
.setRequired(true)
)
.addNumberOption((option) =>
option
.setName("week")
.setDescription("정보를 확인할 주차 정보 | ex) 2주차 -> 2")
.setRequired(true)
),
};
'Node.js > Discord.js' 카테고리의 다른 글
Discord.js로 메이플스토리 디스코드 봇 만들기 외전(1) (1) | 2023.08.30 |
---|---|
Discord.js로 메이플스토리 디스코드 봇 만들기 (7) -완- (0) | 2023.08.28 |
Discord.js로 메이플스토리 디스코드 봇 만들기 (5) (0) | 2023.08.27 |
Discord.js로 메이플스토리 디스코드 봇 만들기 (4) (0) | 2023.08.26 |
Discord.js로 메이플스토리 디스코드 봇 만들기 (3) (0) | 2023.08.25 |