이번에는 캐릭터의 공격대 진행 상황을 나타내는 컴포넌트를 만들것이다.

우선 캐릭터의 던전 진척도를 api를 통해 가져온다.

데이터를 가져오면서 내가 쓰기 편한 형태로 가공해주었다.

비교적 최근 확장팩의 레이드와 그 이전의 확장팩 레이드가 난이도가 다르게 표기되는 부분이 있었다.

현재는 LFR(공격대 찾기), NORMAL(일반), HEROIC(영웅), MYTHIC(신화)로 구분되는데, 이전 일부 확장팩에서는 LEGACY_10_MAN 이런식으로 표기되어있어 이부분은 현재 확장팩의 난이도에 맞게 바꾸어주었다.

진척도 카드에 들어갈 이미지도 같이 불러올것이다.

이제 v-card를 이용해 공격대 진행도를 나타내는 카드를 만들것이다

RaidProgressCard 컴포넌트로 v-card 틀을 만들고, ExpansionSort 컴포넌트로 확장팩별로 정리를 해주고, RaidProgressIter 컴포넌트로 확장팩별로 카드들을 출력했다. 결과는 다음과 같다.

각 컴포넌트간에 데이터를 넘겨줄때는 props를 이용하면 된다.

이 부분을 작업하면서 신경쓰였던 부분이 이미지가 전부 불러와지기 전에 컴포넌트가 구성되어서 그런지 이미지가 누락되는 문제가 있었다. 그래서 페이지가 구성될 때 미리 이미지를 불러오는 식으로 순서를 바꾸어주었다.

async 모듈의 auto를 이용해 교통정리를 좀 해줬다.

auto를 사용하면서 잘 안돼서 골머리를 좀 썩혔는데, 알고보니 action에서 사용된 IO함수때문에 문제가 발생했던것이다. 떄문에 accessToken을 dispatch할떄 promise를 리턴하게 하고 .then()을 붙여주었다. 

이번엔 네임 플레이트에 들어갈 캐릭터 썸네일을 가져올것이다.

근데 이전에 만들어둔 썸네일을 가져오는 함수가 잘 작동하다가 오류가 발생해 자세히 보니 최근 확장팩 업데이트를 하면서 api의 형식이 조금 바뀌어서 최근 플레이를 한 캐릭터를 검색할 경우 썸네일이 제대로 불러와지지 않는 문제가 있었다.

따라서 이 부분에 대해서 예외처리를 해줄 필요가 생겼다.

export function getCharacterMedia(store){
  this.$axios.$get(`https://kr.api.blizzard.com/profile/wow/character/${store.state.realmSlug}/${store.state.characterName}/character-media`,{
    params:{
      region:'kr',
      namespace:'profile-kr',
      locale:'en_US',
      access_token: store.state.accessToken
    }
  })
    .then(function (response) {
      console.log(response)
      if(response.hasOwnProperty('assets')){
        store.commit('setCharacterMedia',getCharacterMediaUrl_New(response))
      }
      else{
        store.commit('setCharacterMedia',getCharacterMediaUrl_Old(response))
      }
    })
    .catch(function (error) {
      console.log(error)
    })
}

function getCharacterMediaUrl_Old(rawData){
  let mediaUrl ={
    bust: rawData['bust_url'],
    avatar: rawData['avatar_url'],
    render: rawData['render_url']
  }
  return mediaUrl
}

function getCharacterMediaUrl_New(rawData){
  let mediaUrl ={}
  rawData['assets'].forEach(img=>{
    mediaUrl[img['key']] = img['value']
  })
  return mediaUrl
}

 

이후 nameplate 컴포넌트를 수정해준다

<template>
  <v-layout
    row
  >
    <v-flex
      class="px-6"
      xs12>
      <v-card>
        <v-layout>
          <v-img
            xs2
            min-height="150px"
            min-width="150px"
            max-width="180px"
            max-height="180px"
            :src=imgSource
          ></v-img>
          <v-flex xs12>
            <v-card-title primary-title>
              <v-col
                xs="12"
                md="5"
                lg="2"
                style="min-width: 400px;"
              >
                <h5>{{this.$store.state.characterProfile['title']}}</h5>
                <h1 :style="characterNameStyle">
                  {{this.$store.state.characterName}}
                </h1>
                <!--<h1 style="color: #d59012">color test</h1>-->
              </v-col>
              <v-col xs="12" md="6">
                <v-row>
                  <h3 v-if="this.$store.state.characterProfile['level']">
                    {{this.$store.state.characterProfile['equipped_item_level']}} LV
                  </h3>
                </v-row>
                <v-row>
                  {{this.$store.state.characterProfile['level']}}
                  {{this.$store.state.characterProfile['race']}}&nbsp
                  <span :style="characterNameStyle">{{this.$store.state.characterProfile['active_spec']}}</span>&nbsp
                  <span :style="characterNameStyle">{{this.$store.state.characterProfile['character_class']}}</span>
                </v-row>
                <v-row>
                  <span style="color: #ffa500;" v-if="this.$store.state.characterProfile['guild']">
                    <{{this.$store.state.characterProfile['guild']}}>
                  </span>
                  &nbsp
                  {{this.$store.state.characterProfile['realm']}}
                </v-row>
              </v-col>
              <v-col>

              </v-col>
            </v-card-title>
          </v-flex>
        </v-layout>
      </v-card>
    </v-flex>


  </v-layout>
</template>

<script>
  export default {
    name: "NamePlate",
    data(){
      return{
      }
    },
    computed: {
      imgSource:function(){
        return this.$store.state.characterMedia['avatar']
      },
      characterNameStyle: function () {
        let red = 255
        let green = 255
        let blue = 255
        let alpha = 255
        let characterClass = this.$store.state.characterProfile['character_class']

        let characterRGBA = {
          none: [255, 255, 255, 255], // RGBA
          Warrior: [175, 145, 100, 255],
          Mage: [140, 200, 255, 255],
          'Death Knight': [205, 0, 0, 255],
          Hunter: [130, 210, 90, 255],
          Priest: [255, 255, 255, 255],
          'Demon Hunter': [130, 60, 120, 255],
          Monk: [70, 210, 150, 255],
          Paladin: [255, 170, 220, 255],
          Rogue: [240, 255, 150, 255],
          Shaman: [40, 60, 255, 255],
          Warlock: [100, 70, 200, 255],
          Druid: [210, 145, 20, 255]
        }
        if (characterRGBA.hasOwnProperty(characterClass)) {
          return {
            color: `rgba(
            ${characterRGBA[characterClass][0]},
            ${characterRGBA[characterClass][1]},
            ${characterRGBA[characterClass][2]},
            ${characterRGBA[characterClass][3]})`
          }
        } else {
          return {
            color: `rgba(
            ${characterRGBA['none'][0]},
            ${characterRGBA['none'][1]},
            ${characterRGBA['none'][2]},
            ${characterRGBA['none'][3]})`
          }
        }
      }
    }
  }
</script>

<style scoped>

</style>

 

이미지 소스가 실시간으로 변경되면 적용될 수 있도록 computed에 imgSource를 넣어주고 img태그의 src에 바인딩해준다 

처음엔 모르고 data부분에 넣어주었는데, 이렇게 하면 다른 캐릭터를 검색할 때에 이미지가 변하지 않더라. 

원래 이번 포스팅에 auto나 waterfall을 이용해 콜백 교통정리를 해줄 생각이었는데... 검색한대로 시도해보아도 잘 되지 않아 여기서 포스팅을 끊어 가겠다

 

지난번에 api서버와의 통신으로 토큰을 가져오는데에 성공했다.

이번엔 그 토큰으로 내가 필요한 데이터를 받아올것이다.

그 전에, 작업을 하면서 시각적으로 변화를 확인할 수 있도록 네임플레이트를 만들어보겠다.

<template>
  <v-layout row>
    <v-flex
      class="px-6"
      xs12>
      <v-card>
        <v-layout>
          <v-img
            xs2
            min-height="150px"
            min-width="150px"
            max-width="180px"
            max-height="180px"
            src="https://render-kr.worldofwarcraft.com/character/azshara/0/119314432-avatar.jpg"
          ></v-img>
          <v-flex xs12>
            <v-card-title primary-title>
              <v-col
                xs="12"
                md="5"
                lg="2"
                style="min-width: 400px;"
              >
                <h5>TITLE</h5>
                <h1>
                  캐릭터 닉네임
                </h1>
                <!--<h1 style="color: #d59012">color test</h1>-->
              </v-col>
              <v-col xs="12" md="6">
                <v-row>
                  <h3>
                    100LV
                  </h3>
                </v-row>
                <v-row>
                  60레벨
                  종족&nbsp
                  <span>특성</span>&nbsp
                  <span>직업</span>
                </v-row>
                <v-row>
                  <span style="color: #ffa500;">
                      길드명
                  </span>
                  &nbsp
                  서버
                </v-row>
              </v-col>
              <v-col>

              </v-col>
            </v-card-title>
          </v-flex>
        </v-layout>
      </v-card>
    </v-flex>


  </v-layout>
</template>

<script>
  export default {
    name: "NamePlate"
  }
</script>

<style scoped>

</style>
//네임플레이트

이제 이 네임플레이트에 사용자가 캐릭터 이름을 검색하면 해당 캐릭터의 정보가 출력되도록 해볼것이다.

이에 필요한 검색창을 하나 만들것이다.

한 캐릭터를 특정하기 위해서는 두가지 정보 - 서버와 캐릭터명이 필요하다.

현재 게임에 존재하는 서버의 리스트를 서버에서 가져온다.

export function getRealmSlug(store){
  this.$axios.$get('https://kr.api.blizzard.com/data/wow/realm/index',{
    params:{
      region:'kr',
      namespace:'dynamic-kr',
      locale:'ko-KR',
      access_token:store.state.accessToken
    }
  })
    .then(function (result) {
      store.commit('setRealmSlugList',getRealmSlugList(result))
    })
    .catch(function (error) {
      console.log(error)
    })
}

function getRealmSlugList(rawData){
  let data = []
  rawData.realms.forEach(realm => {
    let tmp = {
      slug: realm['slug'],
      ko_KR: realm['name']['ko_KR'],
      en_US: realm['name']['en_US']
    }
    data.push(tmp)
  })
  return data
}

//서버 리스트 통신
import secret from '../apiAccount'


export function getAccessToken(store) {
  let data = `client_id=${secret.clientID}&client_secret=${secret.clientSecret}&grant_type=client_credentials`
  return this.$axios.$post('https://us.battle.net/oauth/token', data)
    .then(function (response) {
      console.log(response)
      store.commit('setAccessToken', response.access_token)
      return true
    })
    .catch(function (error) {
      console.log(error)
      return false
    })
}
//토큰 통신
async mounted() {
    await this.$store.dispatch('getAccessToken')
    if(this.$store.state.accessToken !== 'default Token String'){
      this.$store.dispatch('getRealmSlug')
    }else{
        //
    }
  }
  //layout의 mounted hook
export default {
  setAccessToken(state, payload){
    state.accessToken = payload
  },
  setRealmSlugList(state,payload){
    state.realmSlugList = payload
  },
}
//mutation

서버 리스트를 요청하기 전에 반드시 getAccessToken 함수가 선행되어야하기 때문에 async/await를 사용해 토큰을 받아온 후에 getRealmSlug를 실행한다.

서버 목록이 정상적으로 state에 저장된것을 확인할 수 있다.

이 다음으로는 네임플레이트에 들어갈 정보들을 불러온다.

export function getCharacterProfile(store){
  this.$axios.$get(`https://kr.api.blizzard.com/profile/wow/character/${store.state.realmSlug}/${store.state.characterName}`,{
    params:{
      region:'kr',
      namespace:'profile-kr',
      locale:'en_US',
      access_token: store.state.accessToken
    }
  })
    .then(function (response) {
      store.commit('setCharacterProfile',getProfileData(response))
    })
    .catch(function (error) {
      console.log(error)
    })
}

function getProfileData(rawData){
  let profile ={
    faction: rawData['faction']['type'],
    title:'',
    realm: rawData['realm']['name'],
    active_spec: rawData['active_spec']['name'],
    race: rawData['race']['name'],
    character_class: rawData['character_class']['name'],
    equipped_item_level: rawData['equipped_item_level'],
    guild:'',
    level: rawData['level'],

  }
  if(rawData.hasOwnProperty('active_title')){
    profile['title'] = rawData['active_title']['name']
  }
  if(rawData['guild'].hasOwnProperty('name')){
    profile['guild'] = rawData['guild']['name']
  }
  return profile
}
//캐릭터 정보

profile에 들어갈 정보중 타이틀, 길드 등 캐릭터가 가지고 있지 않을 수 있는 정보들은 예외처리를 해주어야한다. api를 불러올떄 해당 정보가 없으면 에러가 발생하기때문이다.

export function getCharacterData(store,payload){
  store.commit('setSearchInfo',payload)
  store.dispatch('getCharacterProfile')
}
export default {
  setAccessToken(state, payload) {
    state.accessToken = payload
  },
  setRealmSlugList(state, payload) {
    state.realmSlugList = payload
  },
  setCharacterProfile(state, payload) {
    state.characterProfile = payload
  },
  setSearchInfo(state,payload){
    state.characterName = payload['characterName']
    state.realmSlug = payload['realmSlug']
  }
}
//mutation

이제 내가 검색한 캐릭터에 대한 정보를 성공적으로 받아온것을 확인할 수 있다.

이제 만들어둔 네임플레이트에 state에 저장된 정보를 알맞게 넣어주면 된다.

<template>
  <v-layout
    row
  >
    <v-flex
      class="px-6"
      xs12>
      <v-card>
        <v-layout>
          <v-img
            xs2
            min-height="150px"
            min-width="150px"
            max-width="180px"
            max-height="180px"
            src="https://render-kr.worldofwarcraft.com/character/azshara/0/119314432-avatar.jpg"
          ></v-img>
          <v-flex xs12>
            <v-card-title primary-title>
              <v-col
                xs="12"
                md="5"
                lg="2"
                style="min-width: 400px;"
              >
                <h5>{{this.$store.state.characterProfile['title']}}</h5>
                <h1 :style="characterNameStyle">
                  {{this.$store.state.characterName}}
                </h1>
                <!--<h1 style="color: #d59012">color test</h1>-->
              </v-col>
              <v-col xs="12" md="6">
                <v-row>
                  <h3 v-if="this.$store.state.characterProfile['level']">
                    {{this.$store.state.characterProfile['equipped_item_level']}} LV
                  </h3>
                </v-row>
                <v-row>
                  {{this.$store.state.characterProfile['level']}}
                  {{this.$store.state.characterProfile['race']}}&nbsp
                  <span :style="characterNameStyle">{{this.$store.state.characterProfile['active_spec']}}</span>&nbsp
                  <span :style="characterNameStyle">{{this.$store.state.characterProfile['character_class']}}</span>
                </v-row>
                <v-row>
                  <span style="color: #ffa500;" v-if="this.$store.state.characterProfile['guild']">
                    <{{this.$store.state.characterProfile['guild']}}>
                  </span>
                  &nbsp
                  {{this.$store.state.characterProfile['realm']}}
                </v-row>
              </v-col>
              <v-col>

              </v-col>
            </v-card-title>
          </v-flex>
        </v-layout>
      </v-card>
    </v-flex>


  </v-layout>
</template>

<script>
  export default {
    name: "NamePlate",
    computed: {
      characterNameStyle: function () {
        let red = 255
        let green = 255
        let blue = 255
        let alpha = 255
        let characterClass = this.$store.state.characterProfile['character_class']

        let characterRGBA = {
          none: [255, 255, 255, 255], // RGBA
          Warrior: [175, 145, 100, 255],
          Mage: [140, 200, 255, 255],
          'Death Knight': [205, 0, 0, 255],
          Hunter: [130, 210, 90, 255],
          Priest: [255, 255, 255, 255],
          'Demon Hunter': [130, 60, 120, 255],
          Monk: [70, 210, 150, 255],
          Paladin: [255, 170, 220, 255],
          Rogue: [240, 255, 150, 255],
          Shaman: [40, 60, 255, 255],
          Warlock: [100, 70, 200, 255],
          Druid: [210, 145, 20, 255]
        }
        if (characterRGBA.hasOwnProperty(characterClass)) {
          return {
            color: `rgba(
            ${characterRGBA[characterClass][0]},
            ${characterRGBA[characterClass][1]},
            ${characterRGBA[characterClass][2]},
            ${characterRGBA[characterClass][3]})`
          }
        } else {
          return {
            color: `rgba(
            ${characterRGBA['none'][0]},
            ${characterRGBA['none'][1]},
            ${characterRGBA['none'][2]},
            ${characterRGBA['none'][3]})`
          }
        }
      }
    }
  }
</script>

<style scoped>

</style>

직업 구분을 위해 폰트에 직업에 맞는 색상을 지정하고 넣어주었다.

프로필 정보를 받아올때와 마찬가지로, 길드와 타이틀은 예외처리를 해주어야한다.

blizzard open api와 nuxt를 이용한 캐릭터 정보 검색 어플리케이션을 만들었다.

버전업과 코드 정리를 겸하며 github에 올리려고 한다.

일단 먼저 api와 통신하려면 토큰이 필요하기 때문에 가장 먼저 토큰을 받아 오는 기능을 만들어준다.

우선 store/action에 getAccessToken 함수를 만든다.

import secret from '../apiAccount'

export function getAccessToken(store) {
  let data = `client_id=${secret.clientID}&client_secret=${secret.clientSecret}&grant_type=client_credentials`
  this.$axios.$post('https://us.battle.net/oauth/token', data)
    .then(function (response) {
      store.commit('setAccessToken', response.access_token)
    })
    .catch(function (error) {
      console.log(error)
    })
}

아래는 setAccessToken 코드이다. mutation/index에 작성되었다.

export default {
  setAccessToken(state, payload){
    state.accessToken = payload
  }
}

아래는 store/state/index에 작성되었다.

export default {
  accessToken:''
}

위 코드는 블리자드 api와 통신해 토큰을 받아와 store/state에 저장하는 함수이다.

그런데 토큰을 받아오기 위해서는 내 블리자드 계정의 clientID와 clientSecret이 필요하다.

해당 키는 블리자드 api 사이트에서 발급받을 수 있다.

github에 업로드 해야하기 때문에 내 ID와 Secret은 올라가지 않게 해야한다.

아래 코드는 apiAccount이다.

export default {
  clientID: 'input your blizzard api client ID',
  clientSecret : 'input your blizzard api client secret'
}

이렇게 github에 업로드 해두고, 내가 작업할 때에는 내 ID와 secret이 들어가도록 수정한다.

그리고 내가 수정한 코드가 다시 github에 업로드 되지 않게 설정해둔다.

프로젝트의 파일 중 .gitignore 파일에 업로드 하고싶지 않은 파일을 넣어주면 된다.

store/action/index에서 앞으로 생성될 action들을 관리해줄것이다.

import {getAccessToken} from './getAccessToken'

export default {
  getAccessToken: getAccessToken
}

store/index에서 state/mutation/action을 관리한다

import myState from './state/index'
import myMutation from "./mutation/index"
import myActions from "./action/index"

export const state = () => (myState)
export const mutations = myMutation
export const actions = myActions

 

이제 api서버에서 토큰을 받아올 준비가 끝났다.

토큰을 불러오는 action을 dispatch해주기만 하면 된다.

<script>
  import routeInfo from './routeInfo'

  export default {
    data() {
      return {
        clipped: false,
        drawer: false,
        fixed: false,
        items: routeInfo,
        miniVariant: false,
        right: true,
        rightDrawer: false,
        title: 'Vuetify.js'
      }
    },
    mounted() {
      this.$store.dispatch('getAccessToken')
    }
  }
</script>

layouts/defalut 파일의 script에 mounted 훅을 걸어서 해당 페이지 레이아웃이 생성될 때 토큰을 받아오는 action이 실행되도록 한다.

정상적으로 토큰을 가져오는데에 성공했다.

Vuex는 한 어플레케이션의 모든 컴포넌트에 대한 중앙 저장소 역할을 한다.

스토어는 애플리케이션의 상태를 저장/관리하는 역할을 수행한다.

Vuex는 신뢰할 수 있는 유일 정보원 패턴을 사용하는 것을 전제로 구현되었고, 여기서 사용되는 유일 정보원이 스토어다.

스토어를 구성하는 개념으로 스테이트(state),  게터(getter), 뮤테이션(mutation), 액션(action)이 있다.

 

스테이트(state)

스테이트는 애플리케이션의 상태를 저장하는 객체다. 

애플리케이션의 상태를 스테이트에 저장해 유일 정보원으로서의 역할을 수행할 수 있게 한다.

하지만 애플리케이션의 모든 정보를 스테이트에서 관리할 필요는 없다. 컴포넌트 안에서만 사용되는 데이터는 컴포넌트에 두는 것이 적합하다.

스테이트에 저장하는것이 적합한 데이터로는 서버에서 데이터를 받아오는 중인지 나타내는 플래그, 사용자 정보, 여러 컴포넌트에서 사용될 가능성이 있는 데이터들이 있다.

 

게터(getter)

게터는 스테이트로부터 다른 값을 계산해 얻어낼 때 사용한다.

컴포넌트의 computed와 같은 기능을 한다.

게터는 계산한 값을 캐싱해두고, 이 값은 해당 값을 계산하는데에 쓰인 스테이트가 바뀌지 않는 한 유지된다.

이를 통해 자주 사용되는 계산 로직을 게터로 생성해두면 퍼포먼스를 향상시킬 수 있다.

 

뮤테이션(mutation)

뮤테이션은 스테이트를 업데이트 할 때 사용된다.

뮤테이션만이 스테이트를 수정할 수 있기 때문에 스테이트의 상태 수정이 발생했을 때 추적이 쉽다.

뮤테이션은 동기 처리만을 담당해야한다. 만약 비동기 처리를 사용하면 스테이트 수정이 어떤 뮤테이션 호출에서 일어났는지 알기 어려워진다.

 

액션(action)

액션은 바로 이전에 설명한 뮤테이션과 달리 비동기 처리를 담당한다.

액션은 비동기 처리 이외에도 외부 API 통신 수행과 뮤테이션을 호출하기 위해 사용된다.

store.dispatch('액션 함수명', 전달인자) 형태로 사용한다.

액션의 함수들은 비동기처리이기 때문에 주로 콜백함수로 작성된다.

메뉴얼을 전부 읽어보고 직접 실사용된 예를 보는것이 이해에 도움이 될거란 생각이 들어 평소에 자주 들르던 사이트에 들어가보았다.

https://ko.warcraftlogs.com/

 

Warcraft Logs - Combat Analysis for Warcraft

블리자드사의 MMO "월드 오브 워크래프트"의 전투 분석을 제공하는 웹 사이트, 와우로그에 오신걸 환영합니다. 전투 기록, 업로드, 정보 분석을 실시간으로 보고 무엇이 문제였는지 그리고 어떻게 고칠수 있는지 확인 하십시오! 업로드. 분석. 개선.

ko.warcraftlogs.com

이 사이트는 검색된 플레이어의 던전/레이드 로그와 현재 착용중인 아이템, 아이템레벨 등을 표기해주는 사이트이다.

많은 공대장들이 이 사이트를 공대원 모집시 해당 플레이어의 역량을 확인하는데에 사용한다.

해당 기능을 제공하는 사이트는 여러 개 있고, 제공하는 정보에는 조금씩 차이가 있다.

(참고: 플레이어의 점수를 계산하는데에 사용되는 레이드/던전 로그는 blizzard api에서 제공하는 정보가 아닌 외부 프로그램에 의해 기록 및 업로드 된다.)

생각보다 많은 소스들이 Blizzard API에서 가져온것이 아니라 wowhead에서 지원하는 리소스를 사용하고 있었다.

페이지 소스를 한참 읽은 후에야 일부에서 blizzard api를 이용한 곳을 찾을 수 있었다.

1339번 라인의 스크립트는 현재 검색된 캐릭터의 초상화를 가져오는 부분이다.

이 초상화는 profile api에서 가져올 수 있다.

다른 사이트에서 제공하는 오픈 api도 많이 있는것같다... 한번 훑어봐야지

+ Recent posts