이제 보석의 현 시세가를 갱신하고 전달하는 로직을 구현하면 서버측의 보석검색 기능은 완료된것이다.
해당 기능은 프론트에서는 클라이언트의 로스트아크 api키를 입력받아 로스트아크측에 원하는 직업을 검색하여, 서버측으로부터 받은 현재 시세와 비교하여 보석을 변환하여 판매하는 것이 얼마 만큼의 이득인지를 확인가능하게 되는 방식으로 구동한다.
서버에서 보석시세를 갱신하고 저장하는 방식은 앞선 과정과 매우 유사하다.
스킬 별이 아닌 5~10레벨의 겁화, 작열, 멸화, 홍염 보석의 보석의 최저가만을 갱신하면 되기에 24개의 요청을 보내고 받으면된다.
포인트는 다음과 같다.
- 받은 응답의 Items 항목의 맨위의 내용이 최저가이다.
- 간혹 10레벨 겁화나 작열의 경우 매물이 없을 경우가 있다. 이는 최저가를 0골드로 저장해야한다.
- json파일로 저장시 저장 시간을 넣어야한다.
createJsonInputString을 통해 보내야할 보석 이름만을 수정한다.
//보석 가격 정보를 업데이트 (갱신해두는거)
@GetMapping("/gemPrice")
public ResponseEntity<?> getGemPrice(
) {
gemApiService.getGemPrice();
return ResponseEntity.status(HttpStatus.OK).body("보석 가격 정보를 업데이트하였습니다");
}
private final String[] gemCategory = {"5레벨 멸", "6레벨 멸", "7레벨 멸", "8레벨 멸", "9레벨 멸", "10레벨 멸",
"5레벨 홍", "6레벨 홍", "7레벨 홍", "8레벨 홍", "9레벨 홍", "10레벨 홍","5레벨 겁", "6레벨 겁", "7레벨 겁", "8레벨 겁", "9레벨 겁", "10레벨 겁",
"5레벨 작", "6레벨 작", "7레벨 작", "8레벨 작", "9레벨 작", "10레벨 작"};
/*
* 보석 시세를 가져오는 메서드
*/
@Transactional
public void getGemPrice() {
String reqURL = "https://developer-lostark.game.onstove.com/auctions/items";
try {
for (String gemName : gemCategory) {
HttpURLConnection conn = (HttpURLConnection) new URL(reqURL).openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Authorization", "bearer " + craftApi[1]);
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);
String jsonInputString = createJsonInputString(gemName);
// JSON 데이터 전송
try (OutputStream os = conn.getOutputStream()) {
byte[] input = jsonInputString.getBytes("utf-8");
os.write(input, 0, input.length);
}
int responseCode = conn.getResponseCode();
InputStreamReader streamReader = (responseCode == 200) ?
new InputStreamReader(conn.getInputStream()) : new InputStreamReader(conn.getErrorStream());
BufferedReader br = new BufferedReader(streamReader);
StringBuilder result = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
result.append(line);
}
br.close();
saveGemPrice(result);
// 연결을 명시적으로 닫음
conn.disconnect();
}
List<GemPriceEntity> gemPriceEntities = gemPriceRepository.findAll();
if(gemPriceEntities.isEmpty()) {
log.error("GemPriceEntity가 비어있습니다.");
throw new GemDataException("GemPriceEntity가 비어있습니다.");
}
String jsonResult = objectMapper.writeValueAsString(gemPriceEntities);
saveJsonToFile(jsonResult);
} catch (IOException e) {
throw new GemPriceApiException("로아 api 요청으로 보석 시세를 가져오는 중 오류가 발생했습니다");
}
}
@Transactional
protected void saveGemPrice(StringBuilder result){
String responseString = result.toString();
// JSON 응답 파싱
JSONObject responseJson = new JSONObject(responseString);
JSONArray itemsArray = responseJson.getJSONArray("Items");
if (!itemsArray.isEmpty()) {
JSONObject cheapestItem = itemsArray.getJSONObject(0);
String gemName = cheapestItem.getString("Name");
int tier = cheapestItem.getInt("Tier");
int buyPrice = cheapestItem.getJSONObject("AuctionInfo").getInt("BuyPrice");
GemPriceEntity gemPriceEntity = gemPriceRepository.findByGemName(gemName);
if (gemPriceEntity != null) {
gemPriceEntity.setBuyPrice(buyPrice);
gemPriceRepository.save(gemPriceEntity);
} else {
GemPriceEntity newGemPriceEntity = new GemPriceEntity();
newGemPriceEntity.setGemTier(tier);
newGemPriceEntity.setGemName(gemName);
newGemPriceEntity.setBuyPrice(buyPrice);
gemPriceRepository.save(newGemPriceEntity);
}
} else {
log.warn("로아 api 요청은 정상 처리되었으나, 보석 데이터가 없습니다. 아마 해당 보석 매물이 없는 것으로 보입니다.");
}
}
private String createJsonInputString(String gemName) {
return "{"
+ "\"ItemLevelMin\": 0,"
+ "\"ItemLevelMax\": 0,"
+ "\"ItemGradeQuality\": null,"
+ "\"ItemUpgradeLevel\": null,"
+ "\"ItemTradeAllowCount\": null,"
+ "\"SkillOptions\": ["
+ " {"
+ " \"FirstOption\": null,"
+ " \"SecondOption\": null,"
+ " \"MinValue\": null,"
+ " \"MaxValue\": null"
+ " }"
+ "],"
+ "\"EtcOptions\": ["
+ " {"
+ " \"FirstOption\": null,"
+ " \"SecondOption\": null,"
+ " \"MinValue\": null,"
+ " \"MaxValue\": null"
+ " }"
+ "],"
+ "\"Sort\": \"BUY_PRICE\","
+ "\"CategoryCode\": 210000,"
+ "\"CharacterClass\": null,"
+ "\"ItemTier\": null,"
+ "\"ItemGrade\": null,"
+ "\"ItemName\": \"" + gemName + "\","
+ "\"PageNo\": 0,"
+ "\"SortCondition\": \"ASC\""
+ "}";
}
private void saveJsonToFile(String jsonData) {
try {
// 현재 날짜와 시간을 얻음
String currentDateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// 전체 데이터를 JSON 객체로 저장
JSONObject jsonObject = new JSONObject();
jsonObject.put("갱신 시간", currentDateTime); // 날짜 및 시간 추가
// 데이터 변환
JSONArray jsonArray = new JSONArray(jsonData);
JSONObject Object = new JSONObject();
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject gem = jsonArray.getJSONObject(i);
String gemName = gem.getString("gemName");
JSONObject gemData = new JSONObject();
gemData.put("buyPrice", gem.getInt("buyPrice"));
gemData.put("gemTier", gem.getInt("gemTier"));
Object.put(gemName, gemData);
}
// JSON 데이터 완성
jsonObject.put("시세", Object);
// JSON 파일로 저장
Files.write(Paths.get(filePath), jsonObject.toString(4).getBytes());
} catch (IOException e) {
throw new GemDataException("보석 시세 JSON 파일 저장 중 오류가 발생했습니다");
}
}
저장된 형태
"갱신 시간": "2024-12-14 20:43:05",
"시세": {
"9레벨 겁화의 보석": {
"buyPrice": 889000,
"gemTier": 4
},
"5레벨 겁화의 보석": {
"buyPrice": 11500,
"gemTier": 4
},
"9레벨 작열의 보석": {
"buyPrice": 886500,
"gemTier": 4
}, ...
//현재 보석 가격 정보를 전달함 (갱신한거 전달)
@GetMapping("/nowGemPrice")
public ResponseEntity<?> getNowGemPrice() {
String jsonData = gemApiService.readJsonFromFile();
Object data = gemApiService.parseJsonToObject(jsonData);
return ResponseEntity.ok(data);
}
public String readJsonFromFile() {
try {
return Files.readString(Paths.get(filePath));
} catch (IOException e) {
throw new GemDataException("보석 시세 JSON 파일 읽기 중 오류가 발생했습니다");
}
}
public Object parseJsonToObject(String jsonData){
try {
return objectMapper.readValue(jsonData, Object.class);
} catch (JsonProcessingException e) {
throw new GemDataException("보석 시세 JSON 데이터를 객체로 변환 중 오류가 발생했습니다");
}
}
해당 기능에 관하여 전체 흐름은 다음과같다.
- 서버는 주에 1회 혹은 달에 1회 크롤링 및 사전작업을 통해 보석 채용률을 계산하여 json파일로 저장한다.
- 서버는 30분마다 실시간 보석 시세를 갱신하여 json파일을 저장한다.
- 프론트는 해당 페이지 접근시, 서버측에 보석 시세와 채용률을 요청한다.
- 프론트는 클라이언트의 개인 api키를 받아놓고 원하는 검색옵션을 토대로 해당 직업의 스킬 최저가를 가져온다.
- 모아로아 서버로 부터 받은 보석시세와 api키를 이용하여 받은 스킬별 보석시세의 차액을 계산하여 정렬하여 보여준다.
아래 링크로 이동하면 해당 페이지를 볼 수 있다.
moaloa 링크 : https://moaloa.org/gemsearch
