728x90
LostArkAPI를 이용하여 거래소 검색사이트를 간단하게 구현하는 방법에 대해 알아보겠습니다.
SpringBoot와 Thymeleaf를 이용하여 구현하였습니다. 전체소스는 GitHub를 참고해 주세요.
1. 사이트 접속
- 로그인 후, API 키를 발급받습니다.
- https://developer-lostark.game.onstove.com/getting-started#login 사이트에서 가이드 문서를 확인할 수 있습니다.
2. build.gradle 구성
plugins {
id 'java'
id 'war'
id 'org.springframework.boot' version '3.3.1'
id 'io.spring.dependency-management' version '1.1.5'
}
apply plugin: 'war'
bootWar {
archiveBaseName = '-'
archiveFileName = 'ROOT.war'
archiveVersion = "1.0.0"
}
group = 'com.api'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
// basic
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
3. application.yml 구성
spring:
thymeleaf:
enabled: true
cache: true
prefix: classpath:templates/
suffix: .html
check-template-location: true
server:
port: 1234
servlet:
encoding:
charset: UTF-8
loa:
apiKey: #발급받은API키#
apiUrl: https://developer-lostark.game.onstove.com
---
spring:
config:
activate:
on-profile: "local"
---
spring:
config:
activate:
on-profile: "prod"
4. 컨트롤러 구현
package com.api.locah.controller;
import com.api.locah.controller.dto.MarketDetailReq;
import com.api.locah.controller.dto.MarketDetailRes;
import com.api.locah.controller.dto.MarketSearchReq;
import com.api.locah.controller.dto.MarketSearchRes;
import com.api.locah.service.ApiService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ApiController {
private final ApiService _apiService;
/**
* 거래소 검색
*/
@PostMapping("/market/search")
public ResponseEntity<MarketSearchRes> marketSearch(@RequestBody MarketSearchReq params) {
return _apiService.marketSearch(params);
}
/**
* 거래소 상세 검색
*/
@PostMapping("/market/detail")
public ResponseEntity<MarketDetailRes> marketDetail(@RequestBody MarketDetailReq params) {
return _apiService.marketDetail(params);
}
}
5. 서비스 구현
package com.api.locah.service;
import com.api.locah.controller.dto.MarketDetailReq;
import com.api.locah.controller.dto.MarketDetailRes;
import com.api.locah.controller.dto.MarketSearchReq;
import com.api.locah.controller.dto.MarketSearchRes;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.util.List;
@Service
public class ApiService {
@Value("${loa.apiKey}")
private String apiKey;
@Value("${loa.apiUrl}")
private String apiUrl;
HttpHeaders headers;
RestTemplate restTemplate;
@PostConstruct
public void init() {
headers = new HttpHeaders();
headers.set("Accept", "application/json");
headers.set("Authorization", "bearer " + apiKey);
restTemplate = new RestTemplate();
}
/**
* 거래소검색
*/
public ResponseEntity<MarketSearchRes> marketSearch(final MarketSearchReq params) {
HttpEntity<Object> entity = new HttpEntity<>(params, headers);
return restTemplate.exchange(
apiUrl + "/markets/items",
HttpMethod.POST,
entity,
MarketSearchRes.class);
}
/**
* 거래소 상세 검색
*/
public ResponseEntity<MarketDetailRes> marketDetail(final MarketDetailReq params) {
// HTTP 요청을 위한 HttpEntity 설정
HttpEntity<Object> entity = new HttpEntity<>(headers);
// API 호출 및 응답 받기
ResponseEntity<String> jsonResult = restTemplate.exchange(
apiUrl + "/markets/items/" + params.getItemId(),
HttpMethod.GET,
entity,
String.class);
// API 응답을 JSON 문자열로 받아옴
String jsonArray = jsonResult.getBody();
// ObjectMapper를 사용하여 JSON 문자열을 MarketDetailRes 객체로 변환
ObjectMapper objectMapper = new ObjectMapper();
MarketDetailRes marketDetailRes = null;
try {
List<MarketDetailRes> marketDetails = objectMapper.readValue(jsonArray, new TypeReference<List<MarketDetailRes>>() {
});
if (!marketDetails.isEmpty()) {
marketDetailRes = marketDetails.get(0); // 첫 번째 객체를 선택
}
} catch (IOException e) {
e.printStackTrace();
}
// 변환된 객체를 ResponseEntity에 담아 반환
if (marketDetailRes != null) {
return new ResponseEntity<>(marketDetailRes, HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
}
6. 공통 Javascript 구현
// POST 호출
function postApi(url, params, successCallback, errorCallback) {
$.ajax({
type: 'POST',
url: url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
data: JSON.stringify(params),
success: function (data) {
if (typeof successCallback === 'function') {
successCallback(data);
}
},
error: function (err) {
if (typeof errorCallback === 'function') {
errorCallback(err);
}
}
});
}
// 모달 열기
function openModal() {
document.querySelector('.modal').style.display = 'block';
}
// 모달 닫기
function closeModal() {
document.querySelector('.modal').style.display = 'none';
}
function onApiError(err) {
alert('오류');
}
7. 공통 헤더 구현 [templates > layout > fragments > head.html]
<!DOCTYPE html>
<html lagn="ko" xmlns:th="http://www.thymeleaf.org">
<th:block th:fragment="headFragment">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<link th:href="@{/css/styles.css}" rel="stylesheet"/>
<link rel="icon" th:href="@{/favicon.ico}" type="image/x-icon">
<script th:src="@{/js/jquery-3.7.1.min.js}"></script>
<script th:src="@{/js/common.js}"></script>
<script src="https://kit.fontawesome.com/a9eb1f10be.js" crossorigin="anonymous"></script>
<title>로차</title>
</head>
</th:block>
</html>
8. css 구성
.hide {
display: none;
}
.cursor {
cursor: pointer;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
header {
background-color: #333;
color: #fff;
padding: 10px;
text-align: center;
}
main {
padding: 10px;
}
label {
display: block;
margin-top: 10px;
font-weight: bold;
}
input[type="text"], select {
width: 100%;
padding: 10px;
margin-top: 5px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
width: 100%;
padding: 10px;
margin-top: 20px;
background-color: #007BFF;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f2f2f2;
}
/* 모달 스타일 */
.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
- 디자인은 챗GPT의 도움을 받아 작성하였습니다.
9. 화면 구현
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{layout/fragments/head.html :: headFragment}"/>
<body>
<main>
<div class="search-container">
<label for="sort">정렬:</label>
<select id="sort">
<option value="GRADE">등급</option>
<option value="CURRENT_MIN_PRICE">최저가</option>
<option value="RECENT_PRICE" selected>최근판매가</option>
<option value="YDAY_AVG_PRICE">전일평균가</option>
</select>
<label for="categoryCode">카테고리 선택:</label>
<select id="categoryCode">
<option value="">전체</option>
<option value="10100">장비 상자 전체</option>
<option value="20005">아바타 > 무기</option>
<option value="20010">아바타 > 머리</option>
<option value="20020">아바타 > 얼굴1</option>
<option value="20030">아바타 > 얼굴2</option>
<option value="20050">아바타 > 상의</option>
<option value="20060">아바타 > 하의</option>
<option value="20070">아바타 > 상하의 세트</option>
<option value="21400">아바타 > 악기</option>
<option value="21500">아바타 > 아바타 상자</option>
<option value="21600">아바타 > 이동 효과</option>
<option value="20000">아바타 전체</option>
<option value="40000">각인서 전체</option>
<option value="50010">강화 재료 > 재련 재료</option>
<option value="50020">강화 재료 > 재련 추가 재료</option>
<option value="51000">강화 재료 > 기타 재료</option>
<option value="51100">강화 재료 > 무기 진화 재료</option>
<option value="50000" selected>강화 재료 전체</option>
<option value="60200">전투 용품 > 배틀 아이템 -회복형</option>
<option value="60300">전투 용품 > 배틀 아이템 -공격형</option>
<option value="60400">전투 용품 > 배틀 아이템 -기능성</option>
<option value="60500">전투 용품 > 배틀 아이템 -버프형</option>
<option value="60000">전투 용품 전체</option>
<option value="70000">요리 전체</option>
<option value="90200">생활 > 식물채집 전리품</option>
<option value="90300">생활 > 벌목 전리품</option>
<option value="90400">생활 > 채광 전리품</option>
<option value="90500">생활 > 수렵 전리품</option>
<option value="90600">생활 > 낚시 전리품</option>
<option value="90700">생활 > 고고학 전리품</option>
<option value="90800">생활 > 기타</option>
<option value="90000">생활 전체</option>
<option value="100000">모험의 서</option>
<option value="110100">항해 > 선박 재료</option>
<option value="110110">항해 > 선박 스킨</option>
<option value="111900">항해 > 선박 재료 상자</option>
<option value="110000">항해 전체</option>
<option value="140100">펫 > 펫</option>
<option value="140200">펫 > 펫 상자</option>
<option value="140000">펫 전체</option>
<option value="160100">탈것 > 탈것</option>
<option value="160200">탈것 > 탈것 상자</option>
<option value="160000">탈것 전체</option>
<option value="170000">기타</option>
<option value="220000">보석 상자</option>
</select>
<label for="itemName">아이템 이름:</label>
<input type="text" id="itemName" placeholder="아이템 이름 입력" value="오레하">
<label for="sortCondition">정렬 조건:</label>
<select id="sortCondition">
<option value="ASC">오름차순</option>
<option value="DESC" selected>내림차순</option>
</select>
<button onclick="searchMarket()">검색</button>
</div>
<div id="pagination">
<span id="pageInfo">페이지 1 / 전체 페이지 0</span>
<button class="hide" id="prevBtn" onclick="prevPage()">이전</button>
<button class="hide" id="nextBtn" onclick="nextPage()">다음</button>
</div>
<table id="resultsTable">
<thead>
<tr>
<th>ID</th>
<th>이름</th>
<th>등급</th>
<th>아이콘</th>
<th>묶음 수</th>
<th>전일 평균 가격</th>
<th>최근 가격</th>
<th>현재 최소 가격</th>
</tr>
</thead>
<tbody id="results">
</tbody>
</table>
<!-- 모달 창 -->
<div id="modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal()">×</span>
<div id="modalResults">
<!-- 상세 정보 -->
</div>
</div>
</div>
</main>
<script th:inline="javascript">
let currentPage = 1;
let pageSize = 10; // 페이지당 보여주는 개수
function searchMarket() {
currentPage = 1; // 검색을 시작할 때 페이지 초기화
let data = {
sort: document.getElementById('sort').value,
categoryCode: document.getElementById('categoryCode').value,
itemName: document.getElementById('itemName').value,
pageNo: currentPage,
pageSize: pageSize,
sortCondition: document.getElementById('sortCondition').value
}
postApi('/api/market/search', data, onSearchMarketSuccess, onApiError);
}
function onSearchMarketSuccess(data) {
const results = document.getElementById('results');
results.innerHTML = ''; // 기존 결과 초기화
data.Items.forEach(item => {
const row = document.createElement('tr');
row.innerHTML = `
<td class="cursor" onclick="searchDetail('${item.Id}')">${item.Id}</td>
<td>${item.Name}</td>
<td>${item.Grade}</td>
<td><img src="${item.Icon}" alt="${item.Name} 아이콘"></td>
<td>${item.BundleCount}</td>
<td>${item.YDayAvgPrice}</td>
<td>${item.RecentPrice}</td>
<td>${item.CurrentMinPrice}</td>
`;
results.appendChild(row);
});
updatePage(data.PageNo, data.TotalCount);
}
function searchDetail(itemId) {
let data = {
itemId: itemId
}
postApi('/api/market/detail', data, onSearchDetailSuccess, onApiError);
}
function onSearchDetailSuccess(data) {
const modalResults = document.getElementById('modalResults');
modalResults.innerHTML = ''; // 기존 결과 초기화
const item = data;
const content = `
<h2>${item.Name}</h2>
<h3>Stats</h3>
<table>
<thead>
<tr>
<th>Date</th>
<th>Avg Price</th>
<th>Trade Count</th>
</tr>
</thead>
<tbody>
${item.Stats.map(stat => `
<tr>
<td>${stat.Date}</td>
<td>${stat.AvgPrice}</td>
<td>${stat.TradeCount}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
modalResults.innerHTML = content;
openModal();
}
/**
* 페이징 처리
*/
function updatePage(pageNo, totalCount) {
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
if (pageNo > 1) {
prevBtn.style.display = 'inline-block';
prevBtn.disabled = false;
} else {
prevBtn.style.display = 'none';
prevBtn.disabled = true;
}
if (pageNo * pageSize < totalCount) {
nextBtn.style.display = 'inline-block';
nextBtn.disabled = false;
} else {
nextBtn.style.display = 'none';
nextBtn.disabled = true;
}
const pageInfo = document.getElementById('pageInfo');
const totalPages = Math.ceil(totalCount / pageSize);
pageInfo.textContent = `페이지 ${pageNo} / 전체 페이지 ${totalPages}`;
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
fetchResults();
}
}
function nextPage() {
currentPage++;
fetchResults();
}
</script>
</body>
</html>
10. 실행화면
감사합니다.
728x90
'프로그래밍 언어 > API' 카테고리의 다른 글
[LostArkAPI] 캐릭터 정보 검색 추가 구현하기 (1) | 2024.07.17 |
---|---|
[ElasticSearch] 계정 생성하기 (0) | 2024.05.21 |
[ElasticSearch] Window 에 설치하기 (0) | 2024.05.21 |
[YoutubeAPI] SpringBoot + Kotlin 으로 구현하기 (0) | 2024.05.21 |
[YoutubeAPI] Youtube Data API v3 사용방법 (0) | 2024.05.21 |