ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [뉴스검색봇] 4. 사용자 키워드 입력받기, Select menus, Button 만들기
    DEVELOP/discord-bot 2022. 5. 6. 02:06

    이번에 구현한 기능들은 다음과 같다.

    - 사용자로부터 키워드를 입력받아서 검색결과 받아오기

    - select menu 추가

    - button 추가


    1. 사용자로부터 키워드 받아오기

     

    키워드 받아오는 것은 간단하다.

    우선 명령어에서 키워드를 입력하는 코드를 추가한다.

     

    data: new SlashCommandBuilder()
            .setName("뉴스검색")
            .setDescription("뉴스를 검색하는 새우.")
            .addStringOption((option) => option.setName("키워드").setDescription("키워드를 입력하새우.").setRequired(true)),

     

    setDescription까지가 기본골격이고, 여기에 추가로 옵션을 지정할 수 있다.

    옵션에 타입을 지정할 수 있는데, 문자열이니 addStringOption을 사용했다.

    setRequired값을 true로 지정하면 필수옵션이 된다. 값을 입력하지 않을 경우 명령어 실행 자체가 안되게 막아놓는 기능.

     

    const keyword = interaction.options.getString("키워드");

    execute함수 내에 해당코드를 추가하면 option에서 입력한 키워드를 받아올 수 있다.

     

    이 키워드로 api요청을 보내기 위해 이전에 만들어두었던 getNews.js 파일을 수정해야 한다.

    module.exports = {
        getNews : async function(keyword) {
            const fetch = require("node-fetch");
            const { Naver_Client_Id, Naver_Client_Secret } = require('../config.json');
    
            try {
                const header = new fetch.Headers({
                    "X-Naver-Client-Id": Naver_Client_Id,
                    "X-Naver-Client-Secret": Naver_Client_Secret,
                });
                
                let url = new URL(`https://openapi.naver.com/v1/search/news.json?query=${keyword}&display=100&start=1&sort=sim`);
                
                ...

    매개변수를 하나 추가하고, 표현언어를 사용해 쿼리에 해당 매개변수를 추가하면 된다.

     

    명령어 파일로 돌아와서, 매개변수로 아까 받아온 키워드를 넣어준다.

    let data = await gn.getNews(keyword); // api call

     


    2. Select Menus 추가

     

    discord.js v13에서 추가된 기능으로 select menus라는 것이 있다.

     

    from discord.js docs

     

    이렇게 선택가능한 메뉴를 추가할 수 있다.

    이걸 이용해서 가져온 뉴스들의 목록을 보여주고, 특정 메뉴를 선택하면 선택한 뉴스를 임베드에 보여주게 만들 것이다.

     

    기본 골격은 다음과 같다.

     

    from discord.js docs

     

    .setCustomId : 고유한 id, 해당 id로 각각의 메뉴들을 구별한다.

    .setPlaceholder : 사용자에게 보여지는 부분. 상단 사진에서 "Nothing Selected" 메시지가 보여지는 부분이다.

    .addOptions : 각각의 메뉴를 하나하나 지정할 수 있다.

        label : 사용자에게 보여지는 제목

        description : 사용자에게 보여지는 제목 및 부가설명

        value : 해당 메뉴를 선택하면 return되는 값 지정. String 타입만 지정할 수 있다.

     

    나는 따로 getSelectMenus.js파일에 메뉴 객체를 반환하는 함수를 만들었다.

    module.exports = {
        getSelectMenus : async function(data, pageNum) {
            const { MessageActionRow, MessageSelectMenu } = require("discord.js");
            const rt = require("./removeTags.js");
            const page = parseInt(pageNum/10) + 1
    
            return new MessageActionRow().addComponents(
                new MessageSelectMenu()
                    .setCustomId("select")
                    .setPlaceholder("뉴스기사를 선택해주세요.")
                    .addOptions([
                        {
                            label: rt.removeTags(data.items[pageNum + 0].title),
                            description: "1 of " + page + " Page",
                            value: "0",
                        },
                        {
                            label: rt.removeTags(data.items[pageNum + 1].title),
                            description: "2 of " + page + " Page",
                            value: "1",
                        },
                        {
                            label: rt.removeTags(data.items[pageNum + 2].title),
                            description: "3 of " + page + " Page",
                            value: "2",
                        },
                        {
                            label: rt.removeTags(data.items[pageNum + 3].title),
                            description: "4 of " + page + " Page",
                            value: "3",
                        },
                        {
                            label: rt.removeTags(data.items[pageNum + 4].title),
                            description: "5 of " + page + " Page",
                            value: "4",
                        },
                        {
                            label: rt.removeTags(data.items[pageNum + 5].title),
                            description: "6 of " + page + " Page",
                            value: "5",
                        },
                        {
                            label: rt.removeTags(data.items[pageNum + 6].title),
                            description: "7 of " + page + " Page",
                            value: "6",
                        },
                        {
                            label: rt.removeTags(data.items[pageNum + 7].title),
                            description: "8 of " + page + " Page",
                            value: "7",
                        },
                        {
                            label: rt.removeTags(data.items[pageNum + 8].title),
                            description: "9 of " + page + " Page",
                            value: "8",
                        },
                        {
                            label: rt.removeTags(data.items[pageNum + 9].title),
                            description: "10 of " + page + " Page",
                            value: "9",
                        },
                    ]) // MessageSelectMenu
            ); // end of row
        } // getSelectMenus
    } // module.exports

     

    기능을 구현하고 나서 블로그에 글을 작성하는거라 추가한 부분이 좀 많다.

    그리고 어쩌다보니 양이 꽤 많아서 블로그에 어떻게 작성해야할지 좀 막막하다..

    페이지네이션에 대한 설명은 다음 글에서 자세히 설명할 예정이다.

    네이버 뉴스 api 형식에서 data.items는 배열이기 때문에 인덱스를 이용하면 원하는 제목을 가져올 수 있다.

    라벨에 해당 뉴스의 제목을 보여주고, 설명에는 몇번 째 페이지의 몇번 째 뉴스기사인지 알려주는 내용을 적었다.

     

    이렇게 셀렉트 메뉴 객체를 반환하는 함수를 만들고, 명령어 커맨드의 excute함수 내에서 받아오면 된다.

    const gsm = require("../functions/getSelectMenus.js");
    
    let row = await gsm.getSelectMenus(data, 0);    // 메뉴생성

     

    이 셀렉트 메뉴를 임베드 밑에 추가할 수 있다.

     

    await interaction.editReply({
                    content: ":mag: `" + keyword + "` 로 검색한 결과입니다.",
                    embeds: [embed],
                    components: [row, row2],
                });

    디스코드에 메시지를 보낼 때 사용할 수 있는 property가 몇 가지 있는데,

    content : 단순 메시지

    embeds : 임베드

    components : 메뉴, 버튼 등등

    을 지정할 수 있다.

     

    아까 row 변수에 메뉴를 받아왔으니 이걸 components에 지정하면 임베드 밑에 메뉴가 출력된다.

     


     

    3. Buttons 추가

     

    버튼은 셀렉트 메뉴와 아주 유사하게 동작한다. 만드는 법도 비슷하다.

     

    from discord.js docs

     

    디스코드 내에 사진처럼 생긴 것이 버튼이다.

    이전페이지, 다음페이지 버튼을 만들어서 페이지네이션을 구현했다.

     

    우선, 기본 골격은 다음과 같다.

     

    from discord.js docs

     

    .setCustomId : 이 아이디로 객체를 식별할 수 있음.

    .setLabel : 사용자에게 보여지는 부분

    .setStyle : 버튼의 스타일 지정. PRIMARY(보라색), SECONDARY(회색), SUCCESS(초록색), DANGER(빨간색), LINK(링크), 총 5가지 스타일을 지정할 수 있다.

    * LINK 스타일의 경우, 링크"만"지정할 수 있다. 해당 버튼을 클릭해도 상호작용(interaction)이 발생하지 않으며, customId도 가질 수 없다.

     

    셀렉트 메뉴와 마찬가지로, 파일을 따로 만들어서 버튼 객체를 리턴하는 함수를 만들었다.

     

    module.exports = {
    
        getButton : async function() {
    
            const { MessageActionRow, MessageButton } = require("discord.js");
    
            const buttons = [
                {
                    customId: "previousPage",
                    label: "◀ 이전페이지",
                    style: "SUCCESS",
                },
                {
                    customId: "nextPage",
                    label: "▶ 다음페이지",
                    style: "SUCCESS",
                },
                // {
                //     customId: "save",
                //     label: "⭐ 찜하기",
                //     style: "PRIMARY",
                // },
                {
                    customId: "quit",
                    label: "❌ 검색종료",
                    style: "DANGER",
                },
            ];
    
            return new MessageActionRow().addComponents(
                buttons.map((button) => {
                    return new MessageButton()
                        .setCustomId(button.customId)
                        .setLabel(button.label)
                        .setStyle(button.style)
                })
            );
        }
    
    } // module.exports

     

    참고로 이모티콘은 저렇게 지정하면 안된다. setEmoji라는 옵션이 있다.

    값으로 이모지 id를 넣어주면 된다는데 이모지 아이디를 어디서 얻는지 몰라서.. 당장 급한건 아니니 일단 저렇게 해놨다.

     

    버튼 형식을 담은 배열을 만들고 map메소드로 배열을 순회하면서 하나하나 버튼객체로 리턴해준다.

     

    명령어 파일 내에서 함수를 호출해 row2 변수에 담아준다.

    const gb = require("../functions/getButton.js");
    
    const row2 = await gb.getButton();

     

     

    여기서 주의해야할 점이 한가지 있다. await 키워드를 꼭 넣어줘야한다. 그렇지 않으면 promise에러가 발생한다.

    +) promise <Pending> 에러

    처음 이 작업을 할 땐 await 키워드 없이 작성해서 해당 오류가 났다.

    함수로 만들지 않고 명령어 파일 안에서 쭉 작성했을 땐 안나던 오류였다.
    이는 작성한 함수의 return값이 promise객체로 반환되기 때문이다.

    마우스 호버링을 통해 간단하게 알 수 있다.
    함수를 호출해서 담은 변수와 함수로 따로 만들지 않고 생성한 객체를 담은 변수에 각각 마우스 호버링을 해보면




    이렇게 다르다는 것을 알 수 있다.
    해결방법은 await 키워드를 걸어주면 된다.

     

     


    4. 상호작용한 값 받아오기

     

    셀렉트 메뉴, 버튼의 상호작용은 createMessageComponentCollector()메소드로 받아올 수 있다.

     

    // in execute function
    // 메뉴, 버튼 클릭 하면 true 반환
    const filter = (interaction) => {
        if (interaction.customId === "select" || "previousPage" || "nextPage" || "quit") {
            return true;
        }
    };
                
    const collector = await interaction.channel.createMessageComponentCollector({
        filter,
        time: 60 * 1000,
    });

     

    필터를 통해 설정한 커스텀 아이디가 일치하면 true를 반환한다.

    createMessageComponentCollector 메소드를 만들고 그 안에 필터를 넣어주면 된다.

    time은 선택옵션으로, 메세지의 유효시간을 설정한다. 밀리세컨드 단위이므로 1분이 지나면 상호작용을 못하도록 시간제한을 걸어두었다.

     

    time을 지정하지 않으면 시간제한 없이 계속 상호작용 할 수 있다.

     

    그리고 collector.on함수 안에서 각 customId에 맞는 동작을 구현해 넣으면 된다.

     

    await collector.on("collect", async (interaction) => {
                    
                    if ((await interaction.customId) === "nextPage") {
                        if(pageNum >= 90) {
                            await interaction.reply( { content : "마지막 페이지 입니다.", ephemeral: true });
                            // await wait(2000);
                            // await interaction.deleteReply();
    
                            pageNum = 90;
    
                            return;
                        }
    
                        pageNum += 10;
                        selectedValue = pageNum;
    
                        console.log('pageNum = ', pageNum);
                        console.log('selectedValue = ', selectedValue);
                    }
    ...

    예시로 "다음페이지" 버튼을 눌렀을 때 페이지가 넘어가도록 하는 코드다.

    이런식으로 나머지 인터렉션들도 동일하게 만들면 된다.

     

    다음글에서는 페이지네이션 구현과 기능들을 함수로 분리해서 저장하는 법을 다룰 예정이다.

    댓글

Designed by Tistory.