데이터 베이스 설계가 굉장히 머리가 아프다. 계산과 조회의 중첩을 최대한 줄이기 위해 개인 전적에 들어갈 분석은 처음 분석에서 모두 끝마치고 데이터베이스에 넣어두어야겠다고 생각했다. 그리고 필요할때마다 꺼내쓰고 업데이트되는 전적들은 그 위에 약간의 계산으로 추가되게끔 유도해야한다.
고민되는 지점
기존 서비스에서 파악되는 매치 객체들마다 팀원들의 당시 티어들을 기록하고, 평균티어 추출이 가능한데, 이는 매치안에서 10명의 평균 실력을 알 수 있는 중요한 지표이다. 만약, 어떠한 플레이어가 순수한 본인 실력이 챌린저인데 새로운 아이디로 하여금 실버부터 챌린저까지 티어를 올린다면, 초반 구간에서는 엄청난 고승률과 좋은 개인 데이터를 가질 것이며 후반 구간에서는 그렇지 않을 것이다. 물론 20~50경기정도로 끊게 되면 이런 것을 예방할 수 있겠지만, 내가 플레이한 구간별로 데이터 통계를 제공하는 것도 고려하고 싶은데 API DOCs를 뒤져봐도 현재기준으로의 티어만 제공한다.
일단 1개의 매치 데이터 레코드에는 1:N 구조로 연결된 10개의 매치 플레이어 데이터가 존재한다. 그리고 이 매치 플레이어 테이블에 1:1로 연결된 매치 Compare 테이블이 다시 존재한다. 이는 모두 매치가 테이블에 생성될 때 자동 생성된다.
class CreateStatsCompared:
def __init__(self, par_entry):
self.par_entry = par_entry
self.user_match_id = par_entry.user_match_id
self.get_oppo()
self.get_with_match_participant()
self.DamagePerMinute()
self.BountyGold()
self.Dpg()
self.Dpt()
self.TurretPlatesTaken()
self.KillAfterHiddenWithAlly()
self.TakedownsFirstXMinutes()
self.LaneMinionsFirst10Minutes()
self.Ddpt()
self.Dgpt()
self.KillsNearTurret()
self.OutnumberedKills()
self.MoreEnemyJungleThanOpponent()
self.VisionScorePerMinute()
self.Pings()
각 메서드들은 생성된 Compare테이블에 들어갈 레코드들을 생성하게 된다.
## 비교 스텟 추가
for participant in participants:
par_entry = Participant.objects.get(user_match=match, puuid=participant.get('puuid', ''))
compare_ins = CreateStatsCompared(par_entry)
par_compare_entry = ParticipantCompare.objects.create(
participant = par_entry,
damagePerMinuteOppo = compare_ins.damagePerMinuteOppo,
damagePerMinuteRank = compare_ins.damagePerMinuteRank,
bountyGoldOppo = compare_ins.bountyGoldOppo,
bountyGoldRank = compare_ins.bountyGoldRank,
dptOppo = compare_ins.dptOppo,
dptRank = compare_ins.dptRank,
dpgOppo = compare_ins.dpgOppo,
dpgRank = compare_ins.dpgRank,
ddptOppo = compare_ins.ddptOppo,
ddptRank = compare_ins.ddptRank,
dgptOppo = compare_ins.dgptOppo,
dgptRank = compare_ins.dgptRank,
turretPlatesTakenOppo = compare_ins.turretPlatesTakenOppo,
turretPlatesTakenRank = compare_ins.turretPlatesTakenRank,
killAfterHiddenWithAllyOppo = compare_ins.killAfterHiddenWithAllyOppo,
killAfterHiddenWithAllyRank = compare_ins.killAfterHiddenWithAllyRank,
takedownsFirstXMinutesOppo = compare_ins.takedownsFirstXMinutesOppo,
takedownsFirstXMinutesRank = compare_ins.takedownsFirstXMinutesRank,
laneMinionsFirst10MinutesOppo = compare_ins.laneMinionsFirst10MinutesOppo,
laneMinionsFirst10MinutesRank = compare_ins.laneMinionsFirst10MinutesRank,
killsNearTurret = compare_ins.killsNearTurret,
killsNearTurretOppo = compare_ins.killsNearTurretOppo,
killsNearTurretRank = compare_ins.killsNearTurretRank,
outnumberedKillsOppo = compare_ins.outnumberedKillsOppo,
outnumberedKillsRank = compare_ins.outnumberedKillsRank,
moreEnemyJungleThanOpponentOppo = compare_ins.moreEnemyJungleThanOpponentOppo,
moreEnemyJungleThanOpponentRank = compare_ins.moreEnemyJungleThanOpponentRank,
visionScorePerMinuteOppo = compare_ins.visionScorePerMinuteOppo,
visionScorePerMinuteRank = compare_ins.visionScorePerMinuteRank,
totalPingsOppo = compare_ins.totalPingsOppo,
totalPingsRank = compare_ins.totalPingsRank,
myTeamPings = compare_ins.myTeamPings,
myTeamPingsOppo = compare_ins.myTeamPingsOppo,
)
par_compare_entry.save()
print("ParticipantCompare saved successfully {}".format(participant.get('puuid', '')))
일단 일부 중요 지표만 담았고 대부분 Oppo, Rank로 나뉜다. Oppo는 자신의 매치 안에서의 맞라이너에 비해 얼마나 높거나 낮은 지표를 보이는 편차를 백분위나 단순 차이로 나타내었으며 Rank는 말그대로 10명중 순위이다.
데이터 설계에 대한 초안은 다 만들어 두었으니 이제 남은 것은 데이터 생성과 데이터 응답이다. 연결구조에 머리가 터질 것 같아 식힐 겸 프로젝트 디자인이나 프론트엔드쪽 디테일을 살려볼까 한다.
update버튼에만 의존하던 구조를 바꾸어 메인페이지로 들어왔을 때 mount시 바로 GET작업을 통해 프로필관련 정보들을 가져오기 위해 몇 가지 코드를 추가했다.
mounted() {
axiosInstance
.get("http://localhost:8000/api/update/", {
params: {
userId: this.userId,
},
})
.then((res) => {
console.log(res.data);
this.name = res.data.name;
this.level = res.data.summonerLevel;
this.icon = res.data.profileIconId;
this.iconUrl = require("../assets/profileicon/" + this.icon + ".png");
this.tagName = this.userSummoner;
this.tier = res.data.tier;
this.rank = res.data.rank;
this.leaguePoints = res.data.leaguePoints;
this.wins = res.data.wins;
this.losses = res.data.losses;
this.winrate = (this.wins / (this.wins + this.losses)) * 100;
this.winrate = this.winrate.toFixed(1);
this.$store.commit("setSummonerProfile", {
name: this.name,
level: this.level,
icon: this.icon,
iconUrl: this.iconUrl,
tagName: this.tagName,
tier: this.tier,
rank: this.rank,
leaguePoints: this.leaguePoints,
wins: this.wins,
losses: this.losses,
winrate: this.winrate,
});
})
.catch((err) => {
console.log(err);
});
},
POST VS GET
GET은 POST와 달리 ONLY 데이터 조회이다. 그렇기에 Update버튼은 수정 및 추가 작업을, Get은 mount시에 바로 얻을 데이터들을 얻게끔 구분할 예정이다. get은 request에 헤더 파라미터로 담는 것이 아니라, param에 담아서 보낸다.
def get(self, request):
userId = request.query_params.get('userId')
user_instance = User.objects.get(username=userId)
ra = RiotAPI(RIOTAPIKEY)
try:
# 이미 존재하는 레코드를 가져옵니다.
entry = UserRiot.objects.get(user=user_instance)
profile = ra.get_user_profile(entry.riot_puuid)
summoner_id = ra.get_summonerId_from_summonerName(entry.riot_id)
obj_solorank_summoner = ra.get_league(summoner_id)[0]
except UserRiot.DoesNotExist:
return Response({'error': '소환사 정보가 업데이트 되지 않았습니다.'}, status=status.HTTP_400_BAD_REQUEST)
else:
tier = obj_solorank_summoner['tier']
rank = obj_solorank_summoner['rank']
leaguePoints = obj_solorank_summoner['leaguePoints']
wins = obj_solorank_summoner['wins']
losses = obj_solorank_summoner['losses']
return Response({'message': '소환사 정보 업데이트 성공',
'summonerLevel': profile['summonerLevel'],
'profileIconId': profile['profileIconId'],
'name': profile['name'],
'tier': tier,
'rank': rank,
'leaguePoints': leaguePoints,
'wins': wins,
'losses': losses}, status=status.HTTP_201_CREATED)
들어온 데이터는 모두 Vuex store에 담는다.
현재 mount시에 보여지는 화면단이다. 티어뱃지와 티어 텍스트 색은 티어에 따라 자동적으로 나뉘도록 설계했다.
티어뱃지의 경우 커뮤니티 드래곤이라는 롤 데이터 드래곤에 관련한 url을 제공하는 사이트에서 url data를 구성했다.
컴포넌트 하나하나 디자인적으로 배치하는 것이 꽤나 어려웠지만 배운 부분이 많았다. vuetify에서 제공하는 그들의 문법을 통해 padding과 margin을 조정하는데, m(a,t,b,r,l)-(0,1,2,3,4..)와 같이 예로 mt-0 윗 방향의 마진을 0으로 하는 것이다. 만약 class="mt-0 ml-1 mr-2 mb-3 pa-2"를 준다면 마진 모든 방향의 간격을 다르게 주면서 모든 방향의 패딩을 2 줄 수 있다.