주니어 개발자 1호

사이퍼즈 디스코드 봇 [1] 본문

사이드 프로젝트 진행

사이퍼즈 디스코드 봇 [1]

No_1 2023. 2. 26. 22:09

2023/02/26일 기준 mvp 동작 단계 확인

목적: 사이퍼즈 API, discord.js 를 활용한 API 봇 생성 

  • 기존 code 미 백업으로 인한 재작업
  • 기존 동작 예시 

링크: https://github.com/2Ruk/cyphers-bot-discord

기능

  • /정보 닉네임
    • 유저에 해당하는 닉네임, 급수(LEVEL), 승률등을 Embed 형태로 제공

 

필요사항

  • discord bot setting ( message indent 설정등 )
  • cyphers api key 발급

설계

  • index.js
    • discord client 실행
    • discord event handle 설정
  • controller
    • 추후 확장성을 고려한 controller
    • router와 같은 역할 수행
  • service
    • api 통신 및 비지니스 수행
  • helper
    • 공통적으로 쓰이거나, 연산을 도와주는 클래스, 함수에 대한 모음
  • dto
    • data transfer object
    • 사용할 객체에 대한 설계

진행 순서

  • 프로젝트 세팅
  • repository layer 설계
  • axios 통신 및 api to dto 설계
  • msg.reply 요청에 대한 응답 반환
    • reply: message → embed 로 변경

2편에 계속 진행할 부분

  • 명확한 layer 분리
  • api query에 필요한 기능 세팅 ( helper function 등으로 정리 - 날짜 세팅, limit 등이 필요 )

간략 코드 설명

Index.js

import * as Discord from 'discord.js';
import dotenv from 'dotenv';
import {cyphersController} from "./cyphers/controller/cyphers.controller.js";
import {routeHelper} from "./common/helper/route.helper.js";
import {UserInfoDto} from "./cyphers/dto/user-info.dto.js";
import {getUserDetailUserInfo, getUserId} from "./common/api/test/test.js";
dotenv.config({
    path: '.env'
});

const discord_token = process.env.DISCORD_TOKEN
const client = new Discord.Client();

// ready시 출력
client.on('ready', () => {
    console.log(`Logged in as ${client.user.tag}!`);
});

// message handler
client.on('message', async msg => {
    const userInput = msg.content;
    const [command,param] = userInput.split(' ');

    if(command === '/전적'){
        msg.reply('전적')
    }

    if(command === '/정보'){
        const userId = await getUserId(param);
        if(!userId) return;

        const userDetailInfo = await getUserDetailUserInfo(userId);
        if(!userDetailInfo) return;

        const totalWinCount = userDetailInfo.records[0].winCount + userDetailInfo.records[1].winCount;
        const totalLoseCount = userDetailInfo.records[0].loseCount + userDetailInfo.records[1].loseCount;
        const userWinRate = totalWinCount / (totalWinCount + totalLoseCount) * 100 ;

        const userInfo = new UserInfoDto(userId,userDetailInfo.nickname, userDetailInfo.grade, userDetailInfo.clanName)
        userInfo.setWinLoseInfo(totalWinCount, totalLoseCount, userWinRate);

        const embed = new Discord.MessageEmbed()
            .setTitle('사이퍼즈 전적')
            .setAuthor('치킨봇', '<https://resource.cyphers.co.kr/ui/img/comm/logo.png>', '<https://discord.js.org>')
            .setDescription('사이퍼즈 전적입니다.')
            .setThumbnail('<https://resource.cyphers.co.kr/ui/img/comm/logo.png>')
            .addFields(
                { name: '닉네임', value: userInfo.userName },
                { name: '랭크', value: userInfo.userGrade },
                { name: '클랜', value: userInfo.userClanName },
                { name: '승률', value: userInfo.userWinRate },
            )
            .setTimestamp()
            .setFooter('치킨봇', '<https://i.imgur.com/wSTFkRM.png>');

        msg.reply(embed);

        // msg.reply(`닉네임: ${userInfo.userName} \\n 랭크: ${userInfo.userGrade} \\n 클랜: ${userInfo.userClanName} \\n 승률: ${userInfo.userWinRate}`);

    }

});

// 추후 아래 컨트롤러로 변경
// const {command,param,router} = routeHelper(msg);
// if (router==='cyphers') {
//     cyphersController(command, param,msg);
// }

client.login(discord_token);

UserInfo Dto

  • 필수적인 사항 → 생성자
  • 이외 세팅 사항 → 내부 setting function
export class UserInfoDto {
    constructor(userId,userName, userGrade, userClanName) {
        this.userId = userId;
        this.userName = userName;
        this.userGrade = userGrade;
        this.userClanName = userClanName;
    }

    userId='';
    userName = '';
    userGrade = 0;
    userClanName = '';

    userWinCount = 0;
    userLoseCount = 0;
    userWinRate = 0;

    set setUserId(userId){
        this.userId = userId;
    }

    setAllUserInfo(userName, userGrade, clanName, userWinCount, userLoseCount, userWinRate) {
        this.userName = userName;
        this.userGrade = userGrade;
        this.userClanName = clanName;
        this.userWinCount = userWinCount;
        this.userLoseCount = userLoseCount;
        this.userWinRate = userWinRate;
    }

    setDefaultUserInfo(userName, userGrade, clanName){
        this.userName = userName;
        this.userGrade = userGrade;
        this.userClanName = clanName;
    }

    setWinLoseInfo(userWinCount, userLoseCount, userWinRate){
        this.userWinCount = userWinCount;
        this.userLoseCount = userLoseCount;
        this.userWinRate = userWinRate;
    }

    getUserInfo() {
        return {
            userName: this.userName,
            userGrade: this.userGrade,
            userClanName: this.userClanName,
            userWinCount: this.userWinCount,
            userLoseCount: this.userLoseCount,
            userWinRate: this.userWinRate
        }
    }
}

cyphers api helper

  • 기본적인 null check guard
  • api에 맞는 url 작성
import axios from "axios";
import * as dotenv from "dotenv";
import {UserInfoDto} from "../../../cyphers/dto/user-info.dto.js";

dotenv.config({
    path: '.env'
});
const api_key = process.env.CYPHERS_API_KEY;

export async function getUserId(userNickName){
    const { data } = await axios.get(`https://api.neople.co.kr/cy/players?nickname=${userNickName}&apikey=${api_key}`);
    if(!data){
        return null;
    }
    if(data.rows.length === 0){
        return null;
    }
    return data.rows[0].playerId;
}

export async function getUserDetailUserInfo(userId){
    const { data } = await axios.get(`https://api.neople.co.kr/cy/players/${userId}?apikey=${api_key}`);
    if(!data){
        return null;
    }
    return data;
}

export async function getMatchList(userId){
    const { data } = await axios.get(`https://api.neople.co.kr/cy/players/${userId}/matches?gameTypeId=normal&apikey=${api_key}`);
    if(!data){
        return null;
    }
    return data;
}
// (async()=>{
//     const userId = await getUserId("반ㅋ반ㅋ치ㅋ킨ㅋ");
//     if(!userId) return;
//
//     const userMatchInfo = await getMatchList(userId);
//     console.log(userMatchInfo.matches.rows)
//     console.log(userMatchInfo.matches.rows[0].playInfo.partyInfo)
// })()
// ((async () => {
//     const userId = await getUserId("반ㅋ반ㅋ치ㅋ킨ㅋ");
//     if(!userId) return;
//
//     const userDetailInfo = await getUserDetailUserInfo(userId);
//     if(!userDetailInfo) return;
//
//     // -- 일반, 공식을 나누어서 분석
//     // const normalWinCount = userDetailInfo.records.find(record => record.gameTypeId === 'normal').winCount;
//     // const normalLoseCount = userDetailInfo.records.find(record => record.gameTypeId === 'normal').loseCount;
//     // const normalWinRate = normalWinCount / (normalWinCount + normalLoseCount) * 100;
//     //
//     // const ratingWinCount = userDetailInfo.records.find(record => record.gameTypeId === 'rating').winCount;
//     // const ratingLoseCount = userDetailInfo.records.find(record => record.gameTypeId === 'rating').loseCount;
//     // const ratingWinRate = ratingWinCount / (ratingWinCount + ratingLoseCount) * 100;
//     //
//     // const totalWinCountExample2 = normalWinCount + ratingWinCount;
//     // const totalLoseCountExample2 = normalLoseCount + ratingLoseCount;
//     // const userWinRateExample2 = totalWinCountExample2 / (totalWinCountExample2 + totalLoseCountExample2) * 100;
//
//     // -- reduce를 이용한 분석
//     // const totalWinCountExample = userDetailInfo.records.reduce((acc,cur)=> acc += cur.winCount ,0)
//     // const totalLoseCountExample = userDetailInfo.records.reduce((acc,cur)=> acc += cur.loseCount ,0)
//     // const userWinRateExample = totalWinCountExample / (totalWinCountExample + totalLoseCountExample) * 100 ;
//
//     // -- 더 간단하게
//     const totalWinCount = userDetailInfo.records[0].winCount + userDetailInfo.records[1].winCount;
//     const totalLoseCount = userDetailInfo.records[0].loseCount + userDetailInfo.records[1].loseCount;
//     const userWinRate = totalWinCount / (totalWinCount + totalLoseCount) * 100 ;
//
//     const userInfo = new UserInfoDto(userId,userDetailInfo.nickname, userDetailInfo.grade, userDetailInfo.clanName)
//     userInfo.setWinLoseInfo(totalWinCount, totalLoseCount, userWinRate);
//
//
//
//
// })());