개요
이번 글은 저번 시리즈에 이어서 FIFA ONLINE 4 API를 이용한 게임 통계 웹사이트 제작 4편을 작성하려한다.
이번 시리즈에서는 유저 거래 기록 조회 API와 선수 고유 식별자(spid) 메타데이터 조회 API를 이용해서 검색한 유저의 이적시장 판매/구매 거래 기록 조회 데이터를 가져오고 그 값을 보여주는 단계를 진행 할 예정이다.
유저 거래 기록 조회 API 분석
우리가 유저 거래 기록 조회 API를 사용하기 위해서는 유저가 가지고 있는 고유한 accessid와 판매/구매 내역을 구분하기 위한 tradetype, 페이징 처리를 위한 offset과 limit이 필요하다
이 accessid는 시리즈 2편에서 유저 정보를 조회할 때 받아올 수 있었기 때문에 따로 accessid를 조회하기 위한 API 호출은 필요 없고, 유저 정보 조회할 때 해당 accessid 값을 받아오면 된다.
tradetype 같은 경우는 판매/구매 버튼을 각각 만들어서 버튼을 누를때 tradetype을 같이 전달해주고, 페이징 처리를 위한 offset과 limit은 임시로 0과 10으로 맞춰놓고 진행할 것이다.
본인이 페이징 처리까지 해주고싶으면 offset과 limit값을 파라미터로 받을 수 있도록 코드를 짜주면 된다.
우리가 받을 수 있는 데이터는 크게 5가지로 tradeDate, saleSn, spid, grade, value가 있다. 이번 시리즈에서는 거래 고유식별자는 사용하지 않을 것이고 spid를 제외한 나머지 데이터들은 쌩으로 뿌려줘도 문제 없지만 spid 같은 경우에는 쌩으로 뿌려주었을 때 사용자가 무엇을 의미하는지 알 수 없으므로 아래에서 설명할 선수 고유 식별자 조회 API를 통해 해당 spid를 replace 해주는 방향으로 진행할 것이다.
선수 고유 식별자(spid) 메타데이터 조회 API 분석
선수 고유 식별자 조회 API는 모든 선수의 데이터가 JSON 배열로 날라오기 때문에 파라미터 값이 필요가 없다.
위 예시처럼 모든 선수의 데이터가 통으로 날라오기 때문에 저 데이터들을 <id, name> 형태의 Map으로 만들고 우리가 거래 기록 조회 API로 받아온 spid를 이용해서 Map의 Key(id)값을 통한 Value(name)를 찾아주면 선수의 이름을 찾아낼 수 있다.
build.gradle 의존성 추가
implementation 'com.google.code.gson:gson:2.9.0'
implementation group: 'org.json', name: 'json', version: '20090211'
JSON 데이터를 Spring에서 편하게 가공하기 위해 의존성을 추가해준다.
UserTradeResponseDto 구현
package com.FIFAONLINE4.GG.user;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
@Data
@Getter
@Setter
public class UserTradeResponseDto {
private String tradeDate;
private String saleSn;
private Object spid;
private Integer grade;
private Long value;
}
거래 조회 API를 분석한 것 처럼 거래 관련 Dto가 tradeDate, saleSn, spid, grade, value를 가질 수 있도록 한다.
이때 추후 선수 고유 식별자 API를 통해 Interger -> String 형태로 변환해주어야 하기 때문에 spid를 Object로 사용하고, Lombok의 Setter를 사용해주었다.
UserApiClient 구현
private final String userTradeUrl = "https://api.nexon.co.kr/fifaonline4/v1.0/users/{accessid}/markets?tradetype={tradetype}&offset=0&limit=10";
public UserTradeResponseDto[] requestUserTrade(String accessId, String tradeType) {
final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("Authorization", API_KEY);
final HttpEntity<String> entity = new HttpEntity<>(httpHeaders);
return restTemplate.exchange(userTradeUrl, HttpMethod.GET, entity, UserTradeResponseDto[].class, accessId, tradeType).getBody();
}
마찬가지로 거래 목록 또한 JSON 배열 형태로 return 되기 때문에 UserTradeResponseDto가 배열 형태로 데이터를 받을 수 있도록 해주고 위에서 페이징 처리를 위한 offset과 limit을 0과 10으로 설정해주었기 때문에 accessId와 tradeType을 매개변수로 받을 수 있도록 해준다.
UserService구현
public UserTradeResponseDto[] searchUserTrade(String accessid, String tradeType) {
UserTradeResponseDto[] userTradeInfos = null;
userTradeInfos = userApiClient.requestUserTrade(accessid, tradeType);
for(int i = 0; i < userTradeInfos.length; i++) {
String[] dateArr = userTradeInfos[i].getTradeDate().split("T");
userTradeInfos[i].setTradeDate(dateArr[0]);
}
return userTradeInfos;
}
Service에서는 UserTradeResponseDto의 거래일자를 split 해주어 년도/월/일 형태로 사용자에게 제공될 수 있도록 해주었다.
본인은 시간/분/초까지 나타내고 싶다면 위 내용을 안넣어주면 된다.
UserController 구현
@GetMapping("/user")
public String user(Model model) {
if(userInfoDto != null) {
model.addAttribute("nickName", userInfoDto.getNickname());
model.addAttribute("level", userInfoDto.getLevel());
if(userDivisionDtoArr != null) {
model.addAttribute("maxDivision", userService.replaceDivision(userDivisionDtoArr[0].getDivision()));
model.addAttribute("matchType", "순위 경기");
model.addAttribute("regDate", userDivisionDtoArr[0].getAchievementDate());
}
model.addAttribute("curAccessId", curAccessId);
if(userTradeDtoArr != null) {
Map playerInfoMap = playerService.getMapFromPlayerJson();
model.addAttribute("tradeList", playerService.replacePlayerName(playerInfoMap, userTradeDtoArr));
}
}
return "user";
}
@GetMapping("api/v1/trade/{accessid}/{tradeType}")
@ResponseBody
public UserTradeResponseDto[] getUserTradeInfo(@PathVariable("accessid") String accessid, @PathVariable String tradeType) {
accessid = curAccessId;
userTradeDtoArr = userService.searchUserTrade(accessid, tradeType);
return userTradeDtoArr;
}
Cotroller에서는 PathVariable을 통해 accessid와 tradetype 값을 넘겨 받을 수 있도록 해준다.
PlayerApiClient 구현
package com.FIFAONLINE4.GG.player;
import lombok.RequiredArgsConstructor;
import org.json.JSONArray;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
@RequiredArgsConstructor
@Service
public class PlayerApiClient {
public JSONArray requestPlayerInfo() {
try {
URL playerInfoUrl = new URL("https://static.api.nexon.co.kr/fifaonline4/latest/spid.json");
HttpURLConnection conn = (HttpURLConnection) playerInfoUrl.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Content-type", "application/json");
conn.setDoOutput(true);
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder sb = new StringBuilder();
String line = null;
while((line = br.readLine()) != null) {
sb.append(line);
}
conn.disconnect();
String unSerialJson = sb.toString();
JSONArray jsonArray = new JSONArray(unSerialJson);
return jsonArray;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
선수 고유 식별자 API를 통해 받아온 데이터를 우리가 Spring 환경에서 사용할 수 있도록 JSONArray로 만들어 준다.
PlayerService 구현
package com.FIFAONLINE4.GG.player;
import com.FIFAONLINE4.GG.user.UserTradeResponseDto;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@RequiredArgsConstructor
@Service
public class PlayerService {
private final PlayerApiClient playerApiClient;
public Map<Integer, String> getMapFromPlayerJson() {
Map<Integer, String> mapList = new HashMap<>();
try {
JSONArray jsonArray = playerApiClient.requestPlayerInfo();
for(int i = 0; i < jsonArray.length(); i++) {
JSONObject playerInfoJson = (JSONObject) jsonArray.get(i);
String playerName = playerInfoJson.getString("name");
String playerId = playerInfoJson.getString("id");
mapList.put(Integer.valueOf(playerId),playerName);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
return mapList;
}
}
public UserTradeResponseDto[] replacePlayerName(Map<Integer, String> playerInfoMap, UserTradeResponseDto[] userTradeResponseDtos) {
for(int i = 0; i < userTradeResponseDtos.length; i++) {
userTradeResponseDtos[i].setSpid(playerInfoMap.get(userTradeResponseDtos[i].getSpid()));
}
return userTradeResponseDtos;
}
}
PlayerService에서 getMapFromPlayerJson은 PlayerApiClient를 통해 얻어온 JSONArray를 Key: id, Value: name 형태의 Map으로 변환해주는 작업을 해주고, replacePlayerName에서는 변환된 Map을 통해 spid를 key로 두고 value 즉, 선수의 이름을 찾아내어 userTradeResponseDto를 spid -> name으로 replace 해준다.
user.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<style>
footer {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
padding: 15px 0;
text-align: center;
color: white;
background: blue;
}
table {
margin-left: auto;
margin-right: auto;
border-style: dashed;
}
td{
border-style: solid;
}
.right-box{
float: right;
position: absolute;
left: 50%;
transform: translate(50%, 50%);
}
.search-user{
float: left;
position: absolute;
right: 50%;
transform: translate(-30%, -50%);
transform: translateY(-30%);
}
</style>
<script type="text/javascript">
function searchInfo(searchMaxDivision) {
const nickname = $('#nickname').val();
$.ajax({
type: 'GET',
url: 'api/v1/user/'+nickname,
dataType: 'json',
contentType: 'application/json; charset=utf-8',
}).done(function (res) {
var strRes = JSON.stringify(res);
searchUserDivision(strRes.split('"')[3]);
window.location.reload();
}).fail(function (error){
alert('없는 유저입니다. 다시 입력해주세요.');
});
}
function searchUserDivision(accessid) {
$.ajax({
type: 'GET',
url: 'api/v1/divisioninfo/'+accessid,
traditional: true,
dataType: 'json',
contentType: 'application/json; charset=utf-8',
}).done(function (res) {
location.href = location.href;
}).fail(function (error){
alert('최고순위 조회 실패');
});
window.location.reload();
}
function searchUserTrade(accessId, tradeType) {
$.ajax({
type: 'GET',
url: 'api/v1/trade/'+accessId+'/'+tradeType,
traditional: true,
dataType: 'json',
contentType: 'application/json; charset=utf-8',
}).done(function (res) {
window.location.reload();
}).fail(function (error){
alert('유저의 이름을 입력해주세요.');
});
}
</script>
<head>
<meta charset="UTF-8">
<title>FIFA4-UserInfo</title>
<script src="https://code.jquery.com/jquery-3.3.1.js"></script>
</head>
<body>
<h1 style="text-align: center">FIFA Online 4 - GG</h1>
<div class="search-user">
<div class="container" style="margin-top: 150px;">
<h3 style="text-align: center">구단주 검색</h3>
<form style="margin: auto; width: 230px">
<div class="input-group">
<input type="text" class="form-control" id="nickname" placeholder="구단주를 입력하세요">
<button class="btn btn-primary" type="button" id="search-info" onclick="searchInfo(searchUserDivision)">검색</button>
</div>
</form>
</div>
<p>
<table class="table table-horizontal table-bordered">
<thead class="thead-strong">
<tr>
<th>유저 닉네임</th>
<th>레벨</th>
</tr>
</thead>
<tbody id="tbody1">
<tr>
<td th:text="${nickName}"></td>
<td th:text="${level}"></td>
</tr>
</tbody>
</table>
</p>
<p>
<table class="table table-horizontal table-bordered">
<thead class="thead-strong">
<tr>
<th>순위 경기 매치 최고 점수</th>
<th>경기 타입</th>
<th>최고등급 달성일자</th>
</tr>
</thead>
<tbody id="tbody2">
<tr>
<td th:text="${maxDivision}"></td>
<td th:text="${matchType}"></td>
<td th:text="${regDate}"></td>
</tr>
</tbody>
</table>
</p>
</div>
<div class="right-box">
<h3 style="text-align: center;">이적시장 거래 정보</h3>
<ul>
<button class="btn btn-primary" type="button" id="search-info" onclick="searchUserTrade('${curAccessId}','buy')">구매 내역</button>
<button class="btn btn-primary" type="button" id="search-info" onclick="searchUserTrade('${curAccessId}','sell')">판매 내역</button>
</ul>
<div>
<table class="table table-horizontal table-bordered">
<thead class="thead-strong">
<tr>
<th>거래일자</th>
<th>선수 식별자</th>
<th>거래 선수 강화 등급</th>
<th>거래 선수 가치(BP)</th>
</tr>
</thead>
<tbody id="tbody2">
<tr th:each="item : ${tradeList}">
<td th:text="${item.tradeDate}"></td>
<td th:text="${item.spid}"></td>
<td th:text="${item.grade}"></td>
<td th:text="${item.value}"></td>
</tr>
</tbody>
</table>
</div>
</div>
<footer>
<h4>Data based on NEXON DEVELOPERS</h4>
</footer>
</body>
</html>
아까 언급한 것 처럼 구매 내역을 누르면 tradetype으로 buy가 판매 내역을 누르면 tradetype으로 sell이 파라미터로 전달될 수 있도록 구현해주면 된다.
View
소스
https://github.com/Camelllia/FIFAONLINE4-GG
GitHub - Camelllia/FIFAONLINE4-GG: 피파온라인4 API를 사용한 게임 통계 사이트
피파온라인4 API를 사용한 게임 통계 사이트. Contribute to Camelllia/FIFAONLINE4-GG development by creating an account on GitHub.
github.com