-
Naver MAP API를 이용해 지도 구현취준/Project 2024. 6. 16. 16:04
다른 아르바이트 사이트들의 단점을 보완하기 위해 위치 기반의 아르바이트 웹 페이지를 구현하고자 하였다.
그래서 지도를 기반으로 내 주변 아르바이트 일자리를 쉽게 찾아볼 수 있도록 하였고 이 기능을 위해서는 지도 API가 필수적이었다.
Naver MAP API 호출
네이버 클라우드 콘솔 회원가입
https://www.ncloud.com/product/applicationService/maps
NAVER CLOUD PLATFORM
cloud computing services for corporations, IaaS, PaaS, SaaS, with Global region and Security Technology Certification
www.ncloud.com
- 네이버 클라우드 플랫폼에 들어가 회원가입을 하고 결제정보를 등록한다
기술 문서
https://navermaps.github.io/maps.js.ncp/docs/NAVER Maps API v3
NAVER Maps API v3로 여러분의 지도를 만들어 보세요. 유용한 기술문서와 다양한 예제 코드를 제공합니다.
navermaps.github.io
- 기술 문서가 상당히 잘 정리되어 있다.
- Examples에 다양한 예제와 소스코드들이 구현되어 있어 쉽게 문서를 보고 구현할 수 있다.
프로젝트 정보 등록, client-id 발급
- maps에서 사용하고자 하는 서비스를 선택하고 Web서비스이기 때문에 프로젝트 URL을 등록하여 준다.
- 성공적으로 발급된 Client-ID를 복사하여 저장해 둔다.
naver.map.client-id= {Cilent-ID}
@Controller public class MainController { @Value("${naver.map.client-id}") private String naverMapClientId; private final Logger logger = LoggerFactory.getLogger(MainController.class); @GetMapping("/") public String home(Model model){ model.addAttribute("naverMapClientId", naverMapClientId); return "home"; } }
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layouts/default_layout}"> <th:block layout:fragment="content"> <div class="container text-center" style="width: 100%;"> <div class="row"> <!-- 검색창과 상점 정보가 나오는 부분 --> <div class="col-3 d-flex align-items-center justify-content-start"> <div id="store-list"></div> </div> <!-- 지도 부분 --> <div class = "col d-flex align-items-center justify-content-center" id="map" style="width: 100%; height: 800px;"></div> </div> </div> <!-- 네이버 지도 API 스크립트 --> <script type="text/javascript" th:src="@{|https://oapi.map.naver.com/openapi/v3/maps.js?ncpClientId=${naverMapClientId}&callback=initMap|}"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> <script type="text/javascript" th:src="@{/user/map.js}"></script> <link rel="stylesheet" href="/user/home.css"/> </th:block> </html>
- properties에 Client-ID를 넣어주고 Controller에서 Map이 호출되는 화면에 Client-ID의 변숫값을 model에 담아 넘겨준다.
- <div id = "map"></div>에 실제 지도를 넣어줄 것이고 지도의 설정은 js 파일에서 해줘야 한다.
map.js
function initMap() { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(showPosition, showError); } else { defaultPosition(); } } // 위치 가져오기 성공 함수 function showPosition(position) { var lat = position.coords.latitude; var lng = position.coords.longitude; var map = new naver.maps.Map('map', { center: new naver.maps.LatLng(lat, lng), zoom: 15, maxZoom: 17, minZoom: 8, zoomControl: true, zoomControlOptions: { position: naver.maps.Position.TOP_RIGHT } }); } // 기본 위치 function defaultPosition() { var lat = 37.3595704; var lng = 127.105399; var map = new naver.maps.Map('map', { center: new naver.maps.LatLng(lat, lng), zoom: 15, maxZoom: 18, minZoom: 8, zoomControl: true, zoomControlOptions: { position: naver.maps.Position.TOP_RIGHT } }); }
- 사용자의 현재 위치를 받아오기 위해 navigator.geolocation.getCurrentPosition(showPosition, showError);을 이용한다. 사용자의 현재 위치를 받아오는 것을 성공하면 showPosition 함수를, 실패하면 showError 함수를 호출한다.
- showError 함수는 임의의 좌표를 기반으로 기본값으로 세팅되어 있는 defaultPosition 함수를 사용하도록 하였다.
지도 범위 내에 있는 데이터 비동기 호출
getStore 함수 정의
function getStore(map) { var bounds = map.getBounds(); var ne = bounds.getNE(); var sw = bounds.getSW(); $.ajax({ url: '/getStore', method: 'GET', contentType: 'application/json', data: { northEastLat: ne.lat(), northEastLng: ne.lng(), southWestLat: sw.lat(), southWestLng: sw.lng() }, success: function(response) { console.log('get Store from backend :', response); clearMarkers(); clearStoreList(); response.forEach((store, index) => { var marker = new naver.maps.Marker({ position: new naver.maps.LatLng(store.area_lat, store.area_lng), map: map }); markers.push({marker: marker, storeId: store.store_id}); appendStoreToList(store, index); }); }, error: function(xhr, status, error) { console.error('백엔드로 좌표 전달 실패:', error); } }); }
- map.getBounds 함수를 이용하면 현재 지도에서 표시하고 있는 북동쪽 위도 경도, 남서쪽 위도 경도를 받아올 수 있다. AJAX를 이용하여 4가지 값을 Server에 전달한다.
- js코드에서는 전달받은 response를 반복문을 돌면서 marker 객체를 생성하고 marker에 데이터의 위도, 경도, map을 할당하여 준다.
Controller
@Data @AllArgsConstructor @Builder @ToString public class CoordinateDto { public double northEastLat; public double northEastLng; public double southWestLat; public double southWestLng; }
@GetMapping("/getStore") @ResponseBody public ResponseEntity<List<StoreFindResultDto>> getStore(@RequestParam("northEastLat") double northEastLat, @RequestParam("northEastLng") double northEastLng, @RequestParam("southWestLat") double southWestLat, @RequestParam("southWestLng") double southWestLng) { CoordinateDto coordinateDto = new CoordinateDto(northEastLat, northEastLng, southWestLat, southWestLng); List<StoreFindResultDto> storeList = storeService.findStoresWithinBounds(coordinateDto); return ResponseEntity.ok(storeList); }
- AJAX에서 호출되는 Controller이다. 4가지 위도, 경도를 이용하여 CoordidateDto를 생성한다.
- Service단에서는 CoordanateDto의 4가지 값을 이용하여 데이터를 DB에서 조회할 것이다.
Serivce
@Data @AllArgsConstructor @NoArgsConstructor @Builder @ToString public class StoreFindResultDto { public Long store_id; public String store_name; public double area_lat; public double area_lng; public int store_salary; public String image_url; public String work_days; public LocalDateTime start_time; public LocalDateTime end_time; public WorkType type; public StoreFindResultDto toDto(Store store){ return StoreFindResultDto.builder() .store_id(store.getStore_id()) .store_name(store.getStore_name()) .area_lat(store.getArea_lat()) .area_lng(store.getArea_lng()) .store_salary(store.getStore_salary()) .image_url(store.getStore_picture_url()) .work_days(store.getWork_days()) .start_time(store.getStart_time()) .end_time(store.getEnd_time()) .type(store.getType()) .build(); } }
@Service @RequiredArgsConstructor public class StoreService { private final StoreRepository storeRepository; private final Logger logger = LoggerFactory.getLogger(StoreService.class); @Transactional public List<StoreFindResultDto> findStoresWithinBounds(CoordinateDto coordinateDto) { logger.info("find store in " + coordinateDto); List<Store> storeList = storeRepository.findStoresWithinBounds( coordinateDto.getNorthEastLat(), coordinateDto.getNorthEastLng(), coordinateDto.getSouthWestLat(), coordinateDto.getSouthWestLng() ); List<StoreFindResultDto> storeFindResultDtoList = new ArrayList<>(); for (Store store : storeList){ storeFindResultDtoList.add(new StoreFindResultDto().toDto(store)); } logger.info("result counting : " + storeFindResultDtoList.size()); return storeFindResultDtoList; } }
- Service에서는 CooridanateDto를 이용하여 Repository에 파라미터를 전달한다.
- 결괏값을 담을 StoreFindResultDto를 생성하고 toDto 함수를 정의하여 Store의 결과 DTO를 생성하여 Controller에 전달할 수 있도록 해준다.
Repository
public interface StoreRepository extends JpaRepository<Store, Long> { @Query("SELECT s FROM Store s " + "WHERE s.area_lat BETWEEN :southWestLat AND :northEastLat " + "AND s.area_lng BETWEEN :southWestLng AND :northEastLng") List<Store> findStoresWithinBounds( @Param("northEastLat") double northEastLat, @Param("northEastLng") double northEastLng, @Param("southWestLat") double southWestLat, @Param("southWestLng") double southWestLng ); }
- 4가지 매개변수를 이용하여 쿼리를 작성한다. 남서쪽 위도 <= 위도 <= 북동쪽 위도 AND 북동쪽 경도 <= 경도 <= 남서쪽 경도를 만족하는 Store를 모두 조회하여 List에 담아낸다.
- 추후에 성능 개선을 위해서는 store에 지역과 현재 Map 바운더리 내에 지역을 특정하여 1차 필터링을 하고 데이터를 조회하면 성능 튜닝이 가능할 것 같다.
Param for query method parameters, or when on Java 8+ use the javac flag -parameters
- java8 이상일 경우 -parameters를 선언해 주면 Repository에서 쿼리에 파라미터를 전달해 줄 경우 매개변수를 따로 명시하지 않아도 된다고 하였지만 나의 경우 -parameters를 선언해도 잘 되지 않았다.
- 그래서 -parameters 세팅과 @Param을 동시에 선언하다 보니 해결되었다.
그밖에 JS 코드들
function addMapEventListeners(map) { naver.maps.Event.addListener(map, "dragend", () => { getStore(map); }); naver.maps.Event.addListener(map, "zoom_changed", () => { getStore(map); }); } function clearMarkers() { markers.forEach(markerObj => markerObj.marker.setMap(null)); markers = []; } function clearStoreList() { $('#store-list').empty(); } function appendStoreToList(store, index) { const storeItem = ` <div class="row store-item" style="cursor: pointer;" onmouseover="highlightMarker(${index}, true)" onmouseout="highlightMarker(${index}, false)" onclick="goToDetail(${store.store_id})"> <div class="col-6 store-image-container"> <img src="/images/${store.image_url}" alt="${store.store_name}" style="width: 100%; height: auto;" class="store-image"> </div> <div class="col store-details"> <h4 class="store-name">${store.store_name}</h4> <p class="store-salary">시급: ${store.store_salary}</p> <p class="store-work-days">근무요일: ${store.work_days}</p> </div> </div> `; $('#store-list').append(storeItem); } function highlightMarker(index, onHover) { const markerObj = markers[index]; if (onHover) { markerObj.marker.setAnimation(naver.maps.Animation.BOUNCE); } else { markerObj.marker.setAnimation(null); } } function goToDetail(storeId) { window.location.href = `/detailStore/${storeId}`; }
addMapEventListeners- 지도가 움직이거나, 줌의 범위가 변경되면 getStore함수를 호출하도록 2가지 이벤트 리스너를 달아주었다.
clearMarkers
- getStore함수가 호출되면 새로운 데이터들을 성공적으로 전달받으면 기존에 있던 marker 삭제를 위한 함수이다.
- 마커가 200개 이상 맵에 표현되면 성능에 심각한 영향을 끼친다고 하기 때문에 성능을 위해 필요하다고 생각되는 함수이다.
clearStoreList
- 지도에 포함되는 데이터의 정보를 보여주는 storeList를 삭제하는 함수이다.
appendStoreToList
- store들이 표현될 div를 정의한다. 전달받은 데이터를 store-list에 append 한다.
highlightMarker
- UX 향상을 위해 데이터 정보를 보여주는 storeList에 마우스를 올리면 지도에 포함되는 marker가 반응을 보이도록 하는 함수이다.
- 네이버에서 제공하는 Animation을 달아주어 해당 데이터의 위치가 어디인지 지도에서 보기 쉽게 표시한다.
goToDetail
- store의 세부 정보 페이지로 들어가도록 하는 함수이다.
Test Code
package com.example.albaya; import com.example.albaya.enums.WorkType; import com.example.albaya.store.dto.CoordinateDto; import com.example.albaya.store.dto.StoreFindResultDto; import com.example.albaya.store.dto.StoreSaveDto; import com.example.albaya.store.entity.Store; import com.example.albaya.store.service.StoreService; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; @SpringBootTest @Transactional public class StoreTest { @Autowired private StoreService storeService; @Test @DisplayName("Find store test within bound") public void findStoreTestWithinBounder(){ StoreSaveDto storeSaveDto_1 = StoreSaveDto.builder() .store_name("store_test1") .area_lat(37.0600000) .area_lng(127.0500000) .store_salary(2000) .work_days("월-화") .start_time(LocalDateTime.now()) .end_time(LocalDateTime.now()) .type(WorkType.PART) .build(); StoreSaveDto storeSaveDto_2 = StoreSaveDto.builder() .store_name("store_test2") .area_lat(37.0700000) .area_lng(127.0700000) .store_salary(2000) .work_days("월-화") .start_time(LocalDateTime.now()) .end_time(LocalDateTime.now()) .type(WorkType.PART) .build(); StoreSaveDto storeSaveDto_3 = StoreSaveDto.builder() .store_name("store_test3") .area_lat(37.0792107) .area_lng(127.0700000) .store_salary(2000) .work_days("월-화") .start_time(LocalDateTime.now()) .end_time(LocalDateTime.now()) .type(WorkType.PART) .build(); CoordinateDto coordinateDto = CoordinateDto.builder() .northEastLat(37.0792105) .northEastLng(127.0835125) .southWestLat(37.0586649) .southWestLng(127.0416701) .build(); storeService.saveStore(storeSaveDto_1); storeService.saveStore(storeSaveDto_2); storeService.saveStore(storeSaveDto_3); List<StoreFindResultDto>storeFindResultDtoList = storeService.findStoresWithinBounds(coordinateDto); Assertions.assertEquals(storeFindResultDtoList.size(), 2); } }
'취준 > Project' 카테고리의 다른 글
EC2에 Redis 설치후 연동하기 (0) 2024.06.08 Spring Boot, Redis를 이용하여 RefreshToken 발급하기 (0) 2024.05.31 Spring Boot LazyInitializationException (0) 2024.05.29 Spring Boot 개발 환경 분리하기(properties File) (1) 2024.05.23 Reason: Failed to determine a suitable driver class (2) 2024.05.01