기본 구현을 하느냐 글쓸 시간이 없었다. 이전 3일동안의 일정을 소개하자면
첫날, 멘토링을 받고 프로젝트 방향을 수정했지만, 결국 바뀐 내용은 없었고, 일단 기본 기능 구현을 하기로 했다.
둘째날, 기본 기능을 구현한 후, 프로젝트에 대한 감을 잡아 멘토링 질문을 작성하고, 프로젝트 방향을 수정했다..
그리고 오늘, 멘토링을 받고, 멘토링을 기반으로 프로젝트 방향을 수정한 후, 기본 기능 구현을 병합하고,
다음주에 바로 수행 해야 할 작업과 와이어 프레임을 작성했다.
아무래도 많은 사례가 존재하지 않는 프로젝트를 기획함에 있어 시간이 가장 많이 걸리는 것 같다.
그럼 완성된 기능에 대해 소개하겠다.
기본 네비게이션
기본 동작 방식은 출발지와 목적지를 입력하면, 카카오 API가 가장 효율적인 길을 안내해준다.
그러기 위해서는 먼저 출발지와 목적지의 주소를 입력받아 좌표로 변환해줘야 한다.
좌표로 변환을 하든, 효율적인 길을 안내받든지 간에 먼저 카카오 API가 원하는 형식의 URI 설계를 해줘야 한다.
0. URI 설계
아래와 같은 URI를 설계 해줘야 한다. 이유는 아래 사이트를 참고하자.
https://developers.kakaomobility.com/docs/navi-api/waypoints/
짧게 설명하자면 카카오 API가 원하는 형식의 데이터를 넣어줘야 하기 때문이다.
@Slf4j
@Service
// 카카오 요청 uri 만드는 곳
public class KakaoUriBuilderService {
// 주소검색 api
private static final String KAKAO_LOCAL_SEARCH_ADDRESS_URL = "https://dapi.kakao.com/v2/local/search/address.json";
// 길찾기
private static final String KAKAO_ROUTE_SEARCH_URL = "https://apis-navi.kakaomobility.com/v1/directions";
// 카테고리 api
private static final String KAKAO_LOCAL_CATEGORY_SEARCH_URL = "https://dapi.kakao.com/v2/local/search/category.json";
// 주소검색
public URI buildUriByAddressSearch(String address) {
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(KAKAO_LOCAL_SEARCH_ADDRESS_URL);
uriBuilder.queryParam("query", address);
URI uri = uriBuilder.build().encode().toUri(); // encoding -> 브라우저에서 해석할 수 없는 것들-> UTF-8 로 인코딩해줌
log.info("*** 로그 [KakaoUriBuilderService buildUriByAddressSearch] address: {}, uri: {}", address, uri);
return uri;
}
// 길찾기
public URI buildUriByRouteSearch(String origin, String destination) {
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(KAKAO_ROUTE_SEARCH_URL);
uriBuilder.queryParam("origin", origin);
uriBuilder.queryParam("destination", destination);
URI routeUri = uriBuilder.build().encode().toUri();
log.info("*** 로그 [KakoaUriBuilerSerivce buildUrilByRoutreSerach] origin: {}, destination: {}, uri: {}", origin, destination ,routeUri);
return routeUri;
}
// 카테고리
public URI buildUriByCategorySearch(double y, double x, double radius, String category){
double meterRadius = radius * 1000;
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(KAKAO_LOCAL_CATEGORY_SEARCH_URL);
uriBuilder.queryParam("category_group_code", category);
uriBuilder.queryParam("x", x);
uriBuilder.queryParam("y", y);
uriBuilder.queryParam("radius", meterRadius); // 반경 순
uriBuilder.queryParam("sort", "popularity"); // 인기도 순 정렬
URI uri = uriBuilder.build().encode().toUri();
log.info("[KakaoAddressSearchService buildUriByCategorySearch] uri : {} ", uri);
return uri;
}
}
1. 좌표로 변환
좌표로 변환해주는 과정도 기본적으로 카카오 API가 해준다.
고로 API에 주소를 URI 형식으로 변환해서 넣어주면 된다.
@Slf4j
@Service
@RequiredArgsConstructor
public class KakaoAddressSearchService {
private final RestTemplate restTemplate;
private final KakaoUriBuilderService kakaoUriBuilderService;
@Value("${kakao.rest.api.key}")
private String kakaoRestApiKey;
// 해당 메서드를 호출 -> 입력한 주소값 -> 요청할 수 있는 uri + headers에 key값
// 요청한 것에 대한 응답 값 -> kakaoApiResponseDto 객체로 반환
public KakaoApiResponseDto requestAddressSearch(String address){ // 주소 -> 위도, 경도로 변환
if(ObjectUtils.isEmpty(address)) return null;
URI uri = kakaoUriBuilderService.buildUriByAddressSearch(address);
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();
}
}
2. 출발지와 목적지의 좌표를 카카오 API에 넣어줘 경로 추천
기본적인 길찾기는 URI를 맨 위에서 보여준 것과 같이 출발지와 목적지 좌표를 넣어주면 된다.
그 결과 API는 출발지와 목적지의 위도, 경도 뿐만아니라, 경로의 도로정보를 포함한 경로추천을 위한 Response를 준다.
@Service
@RequiredArgsConstructor
public class KakaoRouteSearchService {
private final RestTemplate restTemplate;
private final KakaoUriBuilderService kakaoUriBuilderService;
private final KakaoAddressSearchService kakaoAddressSearchService;
@Value("${kakao.rest.api.key}")
private String kakaoRestApiKey;
public KakaoRouteAllResponseDto requestRouteSearch(String originAddress, String destinationAddress) {
if (ObjectUtils.isEmpty(originAddress) || ObjectUtils.isEmpty(destinationAddress)) return null;
// 출발지와 도착지 주소를 각각 좌표로 변환
DocumentDto origin = kakaoAddressSearchService.requestAddressSearch(originAddress).getDocumentDtoList().get(0);
DocumentDto destination = kakaoAddressSearchService.requestAddressSearch(destinationAddress).getDocumentDtoList().get(0);
// "위도,경도" 형식의 문자열 생성
String originCoord = origin.getLongitude() + "," + origin.getLatitude();
String destinationCoord = destination.getLongitude() + "," + destination.getLatitude();
URI uri = kakaoUriBuilderService.buildUriByRouteSearch(originCoord, destinationCoord);
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.AUTHORIZATION, "KakaoAK " + kakaoRestApiKey);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity httpEntity = new HttpEntity(headers);
return restTemplate.exchange(uri, HttpMethod.GET, httpEntity, KakaoRouteAllResponseDto.class).getBody();
}
}
3. 결과
Controller는 다음과 같다.
@Controller
@RequiredArgsConstructor
public class RouteController {
private final KakaoRouteSearchService kakaoRouteSearchService;
@Value("${kakao.javascript.api.key}")
private String kakaoJavascriptApiKey;
@GetMapping("/map")
public String showMap(Model model) {
model.addAttribute("kakaoJavascriptApiKey", kakaoJavascriptApiKey);
return "navigation";
}
@GetMapping("/route")
public ResponseEntity<KakaoRouteAllResponseDto> getRoute(@RequestParam String originAddress,
@RequestParam String destinationAddress) {
KakaoRouteAllResponseDto response = kakaoRouteSearchService.requestRouteSearch(originAddress, destinationAddress);
if (response == null) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
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>
<h1>Kakao Route Search</h1>
<div id="map" style="width:1300px;height:900px;"></div> <!-- 지도가 표시될 영역 -->
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey="카카오 API 키""></script> <!-- 카카오 지도 API -->
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey="카카오 API 키""></script>
<h3>기본 길찾기</h3>
<form id="search-form">
<label for="originAddress">출발지:</label><br>
<input type="text" id="originAddress" name="originAddress"><button type='button' id='search-origin'>주소 검색</button><br>
<button type='button' id='current-location'>현재 위치를 추가하기</button><br>
<label for="destinationAddress">도착지:</label><br>
<input type="text" id="destinationAddress" name="destinationAddress"><button type='button' id='search-destination'>주소 검색</button><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 코드
function handleCurrentLocationClick() {
navigator.geolocation.getCurrentPosition(function(position) {
var lat = position.coords.latitude,
lon = position.coords.longitude;
fetch(
'https://dapi.kakao.com/v2/local/geo/coord2regioncode.json?x=' + lon + '&y=' + lat,
{
headers: { Authorization: 'KakaoAK 4752e5a5b955f574af7718613891f796' },
}
)
.then((response) => response.json())
.then((data) => {
if (data.documents && data.documents.length > 0) {
document.getElementById('originAddress').value = data.documents[0].address_name;
} else {
throw new Error('Could not find address for this coordinates.');
}
});
});
}
document.getElementById('current-location').addEventListener('click', handleCurrentLocationClick);
// 출발지 주소 검색 버튼----------------------------------------------------------------------------------------------------//
document.getElementById('search-origin').addEventListener('click', function() {
new daum.Postcode({
oncomplete: function(data) {
document.getElementById('originAddress').value = data.address;
}
}).open();
});
// 목적지 주소 검색 버튼----------------------------------------------------------------------------------------------------//
document.getElementById('search-destination').addEventListener('click', function() {
new daum.Postcode({
oncomplete: function(data) {
document.getElementById('destinationAddress').value = data.address;
}
}).open();
});
// 사용자가 길 찾기 버튼을 눌렀을 때의 동작----------------------------------------------------------------------------------------------------//
document.getElementById('search-form').addEventListener('submit', function(e) {
e.preventDefault(); // 기본 submit 동작을 막습니다.
var originAddress = document.getElementById('originAddress').value;
var destinationAddress = document.getElementById('destinationAddress').value;
fetch('/route?originAddress=' + originAddress + '&destinationAddress=' + destinationAddress)
.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); // 선을 배열에 추가
}
}
}
});
});
js 코드를 보면 Controller 주소의 형식에 맞게 값을 넣어주고, html의 카카오 지도가 길찾기에 필요한 정보를 넣어준다.
흠.. 위에를 보면 Section 정보가 길찾기에 가장 중요한 정보가 되는 것 같다.
경로 저장과 조회시 효율적인 Entity 설계에 고려해봐야 할 것 같다.
'프로젝트 > RanDrive' 카테고리의 다른 글
Random-Drive-Project 기본 MVC 구현(경로 저장) (1) | 2023.10.18 |
---|---|
Random-Drive-Project 기본 MVC 구현(랜덤 네비게이션) (0) | 2023.10.14 |
팀 Repo에 Docker및 CI/CD 구축 (1) | 2023.10.11 |
Github Actions를 통한 CI/CD 구축(Docker + EC2) (0) | 2023.10.10 |
Docker와 EC2 사용한 배포 (0) | 2023.10.08 |