2024.09.08 - [Java/공중화장실 찾기] - 공중화장실 찾기 - 2 <공공데이터 가공>
지난 포스팅에서는 공공데이터를 가져와 csv 파일로 변경한 뒤 csv 데이터를 DB에 Insert 해보았다.
이번에는 DB에 들어있는 좌표 데이터를 가지고 네이버 지도 API를 이용하여 화면에 마킹해 보는 시간을 갖는다.
✅ 지도 화면 만들기
가장 먼저 아주 간단한 html과 js를 이용하여 네이버 지도를 불러온다.
1️⃣ 비즈니스 로직
🟠 GlobalController.java
package toy.project.suddenPoo.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
@RequiredArgsConstructor
public class GlobalController {
@Value("${map.id}")
private String clientId;
@GetMapping("/")
public String home(Model model) {
model.addAttribute("clientId", clientId);
return "home";
}
}
네이버 지도를 사용하기 위해서는 네이버 클라우드 플랫폼에서 생성한 클라이언트 아이디가 필요하기 때문에
해당 값을 컨트롤러를 통해 html로 넘겨준다.
🟠 ToiletControllerAPI.java
package toy.project.suddenPoo.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import toy.project.suddenPoo.csv.CsvDTO;
import toy.project.suddenPoo.dto.MapRangeDTO;
import toy.project.suddenPoo.service.ToiletService;
import java.util.List;
@Slf4j
@RestController
@RequiredArgsConstructor
public class ToiletControllerAPI {
private final ToiletService toiletService;
@PostMapping("/api/toilets")
public List<CsvDTO> findToiletsByRange(@RequestBody MapRangeDTO mapRangeDTO) {
return toiletService.findToiletsByRange(mapRangeDTO);
}
}
지도를 움직였을 때 화면 범위에 있는 화장실 정보를 가져올 수 있도록 컨트롤러를 생성한다.
🟠 ToiletService.java
package toy.project.suddenPoo.service;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import toy.project.suddenPoo.csv.CsvDTO;
import toy.project.suddenPoo.dto.MapRangeDTO;
import toy.project.suddenPoo.entity.Csv;
import toy.project.suddenPoo.repository.ToiletRepository;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ToiletService {
private final ToiletRepository toiletRepository;
/**
* 범위 내 화장실 찾기
* @param mapRangeDTO 범위 값 DTO
* @return
*/
public List<CsvDTO> findToiletsByRange(MapRangeDTO mapRangeDTO) {
List<Csv> toiletsByRange = toiletRepository.findToiletsByRange(mapRangeDTO.getSwLat(), mapRangeDTO.getNeLat(), mapRangeDTO.getSwLng(), mapRangeDTO.getNeLng());
return toiletsByRange.stream().map(CsvDTO::new).toList();
}
}
🟠 ToiletRepositoryImpl.java
package toy.project.suddenPoo.repository;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Repository;
import toy.project.suddenPoo.entity.Csv;
import java.util.List;
import static toy.project.suddenPoo.entity.QCsv.csv;
@Repository
public class ToiletRepositoryImpl implements ToiletRepositoryCustom{
private final JPAQueryFactory queryFactory;
public ToiletRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public List<Csv> findToiletsByRange(String swLat, String neLat, String swLng, String neLng) {
return queryFactory
.selectFrom(csv)
.where(
csv.latitude.between(swLat, neLat)
.and(csv.longitude.between(swLng, neLng))
)
.fetch();
}
}
이전 포스팅에서 ToiletRepositoryCustom을 얘기했었는데 QueryDSL을 통해 위와 같이 범위에 따른
화장실 정보를 가져온다.
2️⃣ 화면
🟠 home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.ofg">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<title>급똥</title>
<style>
#searchBtn {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
background-color: #2b8cbe;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
z-index: 1000;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
</style>
</head>
<body>
<div id="map" style="width:100%;height:100vh;"></div>
<button id="searchBtn" style="display:none;">현 지도에서 검색</button>
<script type="text/javascript" th:src="'https://oapi.map.naver.com/openapi/v3/maps.js?ncpClientId=' + ${clientId}"></script>
<script src="/js/library/jquery-3.7.1.min.js"></script>
<script src="/js/map.js"></script>
</body>
</html>
지도만 화면에 표출할 것이기 때문에 css도 html에 함께 넣었다.
🟠 map.js
const mapDiv = document.getElementById('map');
const zoom = 16;
const searchBtn = $('#searchBtn');
let map;
let markers = new Array();
let infoWindows = new Array();
let currentBounds;
// 현재 위치 파악
navigator.geolocation.getCurrentPosition(success, fail);
// 위치 정보 수락 시
function success(pos) {
const lat = pos.coords.latitude;
const lng = pos.coords.longitude;
loadNaverMap(lat, lng, zoom);
updateToilet();
}
// 위치 정보 거부 시
function fail(err) {
loadNaverMap(37.3595704, 127.105399, zoom);
updateToilet();
}
function loadNaverMap(lat, lng, zoom) {
const position = new naver.maps.LatLng(lat, lng);
const mapOptions = {
center: position,
zoom: zoom,
minZoom: 7, //지도의 최소 줌 레벨
zoomControl: true, //줌 컨트롤의 표시 여부
zoomControlOptions: { //줌 컨트롤의 옵션
position: naver.maps.Position.TOP_RIGHT
},
tileTransition: true, // 타일 fadeIn 효과
scaleControl: false,
logoControl: true,
mapDataControl: false,
mapTypeControl: true
}
map = new naver.maps.Map(mapDiv, mapOptions);
// 지도를 움직이면 '현 위치에서 찾기' 표출
naver.maps.Event.addListener(map, 'idle', function() {
const newBounds = map.getBounds();
if (!currentBounds.equals(newBounds)) {
searchBtn.show(); // 범위가 변경되면 버튼 표시
} else {
searchBtn.hide(); // 범위가 동일하면 버튼 숨김
}
});
}
function updateToilet() {
const bounds = map.getBounds();
currentBounds = bounds;
const range = {
swLat: bounds._sw._lat,
neLat: bounds._ne._lat,
swLng: bounds._sw._lng,
neLng: bounds._ne._lng
}
// 기존 마커 제거
removeAllMarkers();
// 서버에서 데이터 가져오기
$.ajax({
url: "/api/toilets",
method: "post",
contentType: "application/json",
data: JSON.stringify(range),
success: function(res) {
for (let i=0; i<res.length; i++) {
const marker = new naver.maps.Marker({
map: map,
title: res[i].toiletName,
position: new naver.maps.LatLng(res[i].latitude, res[i].longitude)
});
const infoWindow = new naver.maps.InfoWindow({
content: '<div style="width:200px;text-align: center;padding: 10px"><b>' + res[i].toiletName +
'</b></br>' + res[i].roadName + '</div>'
});
markers.push(marker);
infoWindows.push(infoWindow);
}
for (let i=0; i<markers.length; i++) {
naver.maps.Event.addListener(markers[i], 'click', getClickHandler(i));
}
},
error: function(req, status, error) {
console.log(error);
}
});
}
// 해당 마커의 인덱스를 seq라는 클로저 변수로 저장하는 이벤트 핸들러를 반환합니다.
function getClickHandler(seq) {
return function (e) {
const marker = markers[seq],
infoWindow = infoWindows[seq];
if (infoWindow.getMap()) {
infoWindow.close();
} else {
infoWindow.open(map, marker);
}
}
}
// 모든 마커 제거 함수
function removeAllMarkers() {
for (let i = 0; i < markers.length; i++) {
markers[i].setMap(null); // 지도에서 마커 제거
}
markers = [];
for (let i = 0; i < infoWindows.length; i++) {
infoWindows[i].close();
}
infoWindows = [];
}
searchBtn.on('click', function() {
updateToilet();
searchBtn.hide();
});
네이버 지도 API 활용에 대한 자세한 내용은 이전 포스팅에서 안내해준 예제 사이트에서 살펴볼 수 있다.
https://navermaps.github.io/maps.js.ncp/docs/tutorial-2-Getting-Started.html
✅ 테스트
로컬에서 테스트를 해보면 현재 위치를 보여주며 화장실 위치가 마킹되는 것을 알 수 있다.
✅ 마무리
정리 해놓고 보니 별거 없지만 사실 가져온 데이터를 지도에 마킹하는 부분에서 꽤 애를 먹었다.
예제에서는 분명 쉽게 쉽게 해 둔 것 같은데 다중 마킹에 대한 정보가 없어서 아마 못찾은걸지도..;;
아무튼 금방 해결할 수 있었다.
https://github.com/Kimmingki/suddenPoo
'Java > 공중화장실 찾기' 카테고리의 다른 글
공중화장실 찾기 - 4 <Spring Boot GGP 배포> (2) | 2024.09.11 |
---|---|
공중화장실 찾기 - 2 <공공데이터 가공> (3) | 2024.09.08 |
공중화장실 찾기 - 1 <프로젝트 생성 및 네이버 지도 API 적용> (1) | 2024.09.06 |