랜덤 네비게이션
랜덤 네비게이션은 두가지 방식으로 설계되었다.
일단 빠르게 API 기능을 만드는 것을 목적으로 만들었기에 실용성은 좀 부족하다.
하나는 출발지와 목적지, 반경을 입력하면 경유지만 랜덤으로 추천해주는 기능이고,
다른 하나는 출발지와 반경을 입력하면 목적지와 경유지를 랜덤으로 추천해주는 기능이다.
두 API 모두 출발지 기준 최대 20km이내의 관광명소중 랜덤한 장소를 골라 추천해줘서 아직은 범위가 좀 좁다.
앞으로 경유지와 목적지를 적절하게 잘 골라주는 알고리즘을 설계해서 적용시킬 것이다.
0. 출발지 기준 특정 범위 이내의 관광명소 리스트 구하기
카카오의 주변의 특정 시설을 알려주는 API를 참고했다.
출발지 좌표와 특정 범위(최대 20)을 입력하면 자동으로 특정 범위 이내 관광명소 리스트를 반환해준다.
@Slf4j
@Service
@RequiredArgsConstructor
public class RandomKakaoCategorySearchService { // 특정 카테고리 -> 관광명소
private final KakaoUriBuilderService kakaoUriBuilderService;
private final RestTemplate restTemplate;
private static final String PARK_CATEGORY = "AT4";
@Value("${kakao.rest.api.key}")
private String kakaoRestApiKey;
// KakaoAddressSearch -> 주소 -> 위도,경도 -> 값을 매핑
public KakaoApiResponseDto requestPharmacyCategorySearch(double y, double x, double radius){
URI uri = kakaoUriBuilderService.buildUriByCategorySearch(y, x, radius, PARK_CATEGORY);
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.AUTHORIZATION, "KakaoAK " + kakaoRestApiKey);
HttpEntity httpEntity = new HttpEntity(headers);
// kakao api 호출
return restTemplate.exchange(uri, HttpMethod.GET, httpEntity, KakaoApiResponseDto.class).getBody();
}
}
1. 랜덤 길찾기 기능 구현
앞서 소개한 주소를 위도, 경도로 바꿔주는 API를 이용했다.
https://hwk99.tistory.com/115
자세히 보면 두 기능에 큰 차이점은 없다. 단지 몇개의 랜덤 장소를 이용하냐 뿐이다.
하지만 랜덤 길찾기에서는 기존 방식과 다르게 URI를 따로 만들지 않고, 조금 다른 형식을 사용했다.
이유는 다중 경유지 길찾기 API를 사용했기에, Request와 Response의 형태가 다르기 때문이다.
https://developers.kakaomobility.com/docs/navi-api/waypoints/
@Slf4j(topic = "KakaoRouteSearchService")
@Service
@RequiredArgsConstructor
public class RandomKakaoRouteSearchService {
private final RestTemplate restTemplate;
private final KakaoAddressSearchService kakaoAddressSearchService;
private final RandomKakaoCategorySearchService kakaoCategorySearchService;
@Value("${kakao.rest.api.key}")
private String kakaoRestApiKey;
public KakaoRouteAllResponseDto requestRandomWays(String originAddress, Integer redius) {
if (ObjectUtils.isEmpty(originAddress) || ObjectUtils.isEmpty(redius)) return null;
// 출발지와 도착지 주소를 각각 좌표로 변환
DocumentDto origin = kakaoAddressSearchService.requestAddressSearch(originAddress).getDocumentDtoList().get(0);
/***
* 목적지와 경유지 값을 반경으로 계산해서 가져오는 메소드
***/
KakaoApiResponseDto responses = kakaoCategorySearchService.requestPharmacyCategorySearch(origin.getLatitude(), origin.getLongitude(), redius);
/***
랜덤으로 다중 목적지와 경유지 만들기 알고리즘
***/
int RandomLength = responses.getDocumentDtoList().size();
Random rd = new Random();
int destinationCnt = rd.nextInt(RandomLength);
int waypointsCnt = rd.nextInt(RandomLength);
if(destinationCnt == waypointsCnt){
waypointsCnt = destinationCnt - 1;
}
DocumentDto destination = responses.getDocumentDtoList().get(destinationCnt);
DocumentDto waypoints = responses.getDocumentDtoList().get(waypointsCnt);
/***
* 요청 헤더 만드는 공식
***/
return makeRequestForm(origin,destination,waypoints);
}
public KakaoRouteAllResponseDto requestRamdomWay(String originAddress, String destinationAddress,Integer redius) {
if (ObjectUtils.isEmpty(originAddress) || ObjectUtils.isEmpty(redius) || ObjectUtils.isEmpty(destinationAddress)) return null;
// 출발지와 도착지 주소를 각각 좌표로 변환
DocumentDto origin = kakaoAddressSearchService.requestAddressSearch(originAddress).getDocumentDtoList().get(0);
DocumentDto destination = kakaoAddressSearchService.requestAddressSearch(destinationAddress).getDocumentDtoList().get(0);
/***
* 목적지와 경유지 값을 반경으로 계산해서 가져오는 메소드
***/
KakaoApiResponseDto responses = kakaoCategorySearchService.requestPharmacyCategorySearch(origin.getLatitude(), origin.getLongitude(), redius);
/***
랜덤으로 경유지 만들기 알고리즘
***/
int RandomLength = responses.getDocumentDtoList().size();
Random rd = new Random();
int waypointsCnt = rd.nextInt(RandomLength);
DocumentDto waypoints = responses.getDocumentDtoList().get(waypointsCnt);
/***
* 요청 헤더 만드는 공식
***/
return makeRequestForm(origin,destination,waypoints);
}
private KakaoRouteAllResponseDto makeRequestForm(DocumentDto origin, DocumentDto destination, DocumentDto waypoints){
Map<String,Object> uriData = new TreeMap<>();
uriData.put("origin",new DocumentDto(origin.getAddressName(),origin.getLatitude(), origin.getLongitude()));
uriData.put("destination",new DocumentDto(destination.getAddressName(),destination.getLatitude(),destination.getLongitude()));
uriData.put("waypoints",new ArrayList<>(Arrays.asList(
new DocumentDto(waypoints.getAddressName(), waypoints.getLatitude(), waypoints.getLongitude())
)));
uriData.put("priority","RECOMMEND");
URI uri = URI.create("https://apis-navi.kakaomobility.com/v1/waypoints/directions");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set(HttpHeaders.AUTHORIZATION, "KakaoAK " + kakaoRestApiKey);
HttpEntity <Map<String,Object>> httpEntity = new HttpEntity<>(uriData,headers);
return restTemplate.postForEntity(uri,httpEntity,KakaoRouteAllResponseDto.class).getBody();
}
}
2. 결과
다음과 같은 Controller를 만들어 주었다.
@Controller
@RequiredArgsConstructor
@Slf4j(topic = "RouteController")
public class RandomRouteController {
private final RandomKakaoRouteSearchService kakaoRouteSearchService;
@Value("${kakao.rest.api.key}")
private String kakaoRestApiKey;
@GetMapping("/all-random-route")
public ResponseEntity<KakaoRouteAllResponseDto> getRandomWays(@RequestParam String originAddress, @RequestParam Integer redius) {
KakaoRouteAllResponseDto response = kakaoRouteSearchService.requestRandomWays(originAddress,redius);
if (response == null) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT); // 적절한 HTTP 상태 코드로 응답
}
return new ResponseEntity<>(response, HttpStatus.OK);
}
@GetMapping("/random-route")
public ResponseEntity<KakaoRouteAllResponseDto> getRandomWay(@RequestParam String originAddress,@RequestParam String destinationAddress, @RequestParam Integer redius) {
KakaoRouteAllResponseDto response = kakaoRouteSearchService.requestRamdomWay(originAddress,destinationAddress,redius);
if (response == null) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT); // 적절한 HTTP 상태 코드로 응답
}
return new ResponseEntity<>(response, HttpStatus.OK);
}
}
프론트엔드 코드는 아래와 같다.
html코
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Kakao Route Search</title>
<link rel="stylesheet" href="/css.css">
</head>
<body>
<h3>반경 기반 랜덤 길찾기</h3>
<form id="all-random-search-form">
<label for="all-random-originAddress">출발지:</label><br>
<input type="text" id="all-random-originAddress" name="all-random-originAddress"><button type='button' id='all-random-search-origin'>주소 검색</button><br>
<button type='button' id='all-random-current-location'>현재 위치를 추가하기</button><br>
<label for="all-random-redius">반경:</label><br>
<input type="text" id="all-random-redius" name="all-random-redius"><br>
<button type='submit'>길 찾기</button>
</form>
<br>
<h3>목적지, 반경 기반 랜덤 길찾기</h3>
<form id="random-search-form">
<label for="random-originAddress">출발지:</label><br>
<input type="text" id="random-originAddress" name="random-originAddress"><button type='button' id='random-search-origin'>주소 검색</button><br>
<button type='button' id='random-current-location'>현재 위치를 추가하기</button><br>
<label for="random-destinationAddress">목적지:</label><br>
<input type="text" id="random-destinationAddress" name="random-destinationAddress"><button type='button' id='random-search-destination'>주소 검색</button><br>
<label for="random-redius">반경:</label><br>
<input type="text" id="random-redius" name="random-redius"><br>
<button type='submit'>길 찾기</button>
</form>
<script src="/navigation.js"></script> <!-- 여기에 실제 자바스크립트 파일 경로를 입력하세요 -->
<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script> <!-- 카카오 우편번호 서비스 -->
</body>
</html>
js 코드는 아래와 같다.
// 반경 기반 랜덤 길찾기 출발지 주소 검색 버튼----------------------------------------------------------------------------------------------------//
document.getElementById('all-random-search-origin').addEventListener('click', function() {
new daum.Postcode({
oncomplete: function(data) {
document.getElementById('all-random-originAddress').value = data.address;
}
}).open();
});
// 목적지,반경 기반 랜덤 길찾기 출발지 주소 검색 버튼----------------------------------------------------------------------------------------------------//
document.getElementById('random-search-origin').addEventListener('click', function() {
new daum.Postcode({
oncomplete: function(data) {
document.getElementById('random-originAddress').value = data.address;
}
}).open();
});
// 목적지,반경 기반 랜덤 길찾기 목적지 주소 검색 버튼----------------------------------------------------------------------------------------------------//
document.getElementById('random-search-destination').addEventListener('click', function() {
new daum.Postcode({
oncomplete: function(data) {
document.getElementById('random-destinationAddress').value = data.address;
}
}).open();
});
// 사용자가 반경기반 랜덤 길 찾기 버튼을 눌렀을 때의 동작----------------------------------------------------------------------------------------------------//
document.getElementById('all-random-search-form').addEventListener('submit', function(e) {
e.preventDefault(); // 기본 submit 동작을 막습니다.
var originAddress = document.getElementById('all-random-originAddress').value;
var redius = document.getElementById('all-random-redius').value;
fetch('/all-random-route?originAddress=' + originAddress + '&redius=' + redius)
.then(response => response.json())
.then(data => {
// data는 KakaoRouteAllResponseDto 객체
clearPolylines(); // 기존의 선들을 모두 제거
if (!map) {
map = new kakao.maps.Map(document.getElementById('map'), {
level: 3
});
}
// 경로 정보(routes)의 각 섹션(section)별로 반복하여 처리합니다.
for (let route of data.routes) {
for (let section of route.sections) {
// 각 섹션의 경계 상자(bound) 정보를 가져옵니다.
let bound = section.bound;
// 카카오 지도에 섹션을 표시합니다.
var bounds = new kakao.maps.LatLngBounds(
new kakao.maps.LatLng(bound.min_y, bound.min_x),
new kakao.maps.LatLng(bound.max_y, bound.max_x)
);
map.setBounds(bounds);
// polyline 생성
for(let road of section.roads){
let path = [];
for(let i=0; i<road.vertexes.length; i+=2){
console.log("vertexes: ", road.vertexes[i], road.vertexes[i+1]);
path.push(new kakao.maps.LatLng(road.vertexes[i+1], road.vertexes[i]));
}
let polyline = new kakao.maps.Polyline({
path: path,
strokeWeight: 5,
strokeColor: '#007bff',
strokeOpacity: 0.7,
strokeStyle: 'solid'
});
polyline.setMap(map);
polylines.push(polyline); // 선을 배열에 추가
}
}
}
});
});
// 사용자가 목적지기반 랜덤 길 찾기 버튼을 눌렀을 때의 동작----------------------------------------------------------------------------------------------------//
document.getElementById('random-search-form').addEventListener('submit', function(e) {
e.preventDefault(); // 기본 submit 동작을 막습니다.
var originAddress = document.getElementById('random-originAddress').value;
var destinationAddress = document.getElementById('random-destinationAddress').value;
var redius = document.getElementById('random-redius').value;
fetch('/random-route?originAddress=' + originAddress + '&destinationAddress=' + destinationAddress + '&redius=' + redius)
.then(response => response.json())
.then(data => {
// data는 KakaoRouteAllResponseDto 객체
clearPolylines(); // 기존의 선들을 모두 제거
if (!map) {
map = new kakao.maps.Map(document.getElementById('map'), {
level: 3
});
}
// 경로 정보(routes)의 각 섹션(section)별로 반복하여 처리합니다.
for (let route of data.routes) {
for (let section of route.sections) {
// 각 섹션의 경계 상자(bound) 정보를 가져옵니다.
let bound = section.bound;
// 카카오 지도에 섹션을 표시합니다.
var bounds = new kakao.maps.LatLngBounds(
new kakao.maps.LatLng(bound.min_y, bound.min_x),
new kakao.maps.LatLng(bound.max_y, bound.max_x)
);
map.setBounds(bounds);
// polyline 생성
for(let road of section.roads){
let path = [];
for(let i=0; i<road.vertexes.length; i+=2){
console.log("vertexes: ", road.vertexes[i], road.vertexes[i+1]);
path.push(new kakao.maps.LatLng(road.vertexes[i+1], road.vertexes[i]));
}
let polyline = new kakao.maps.Polyline({
path: path,
strokeWeight: 5,
strokeColor: '#007bff',
strokeOpacity: 0.7,
strokeStyle: 'solid'
});
polyline.setMap(map);
polylines.push(polyline); // 선을 배열에 추가
}
}
}
});
});
// kakaomap 표시 해주는 곳-----------------------------------------------------------------------------------------------------//
var container = document.getElementById('map');
var options = {
center: new kakao.maps.LatLng(33.450701, 126.570667),
level: 3
};
var map = new kakao.maps.Map(container, options);
// 경로 안내 polyline ----------------------------------------------------------------------------------------------------------//
var polylines = [];
function clearPolylines() {
for (let i = 0; i < polylines.length; i++) {
polylines[i].setMap(null);
}
polylines = [];
}
프론트에 큰 힘을 안쓰기도 했고, js 코드에 대해 미숙해, 함수를 제대로 활용하지 못했다.
함수를 좀 활용해서, 더 깔끔한 코드를 만들 수 있을 것이다.
'프로젝트 > RanDrive' 카테고리의 다른 글
Random-Drive-Project 기본 MVC 구현(경로 조회, 상세조회) (1) | 2023.10.18 |
---|---|
Random-Drive-Project 기본 MVC 구현(경로 저장) (1) | 2023.10.18 |
Random-Drive-Project 기본 MVC 구현(기본 네비게이션) (1) | 2023.10.14 |
팀 Repo에 Docker및 CI/CD 구축 (1) | 2023.10.11 |
Github Actions를 통한 CI/CD 구축(Docker + EC2) (0) | 2023.10.10 |