공중화장실 찾기 - 3 <네이버 지도 API 적용>

2024. 9. 9. 14:01·Java/공중화장실 찾기
목차
  1. ✅ 지도 화면 만들기
  2. 1️⃣ 비즈니스 로직
  3. 2️⃣ 화면
  4. ✅ 테스트
  5. ✅ 마무리
728x90

2024.09.08 - [Java/공중화장실 찾기] - 공중화장실 찾기 - 2 <공공데이터 가공>

 

공중화장실 찾기 - 2 <공공데이터 가공>

2024.09.06 - [Java/공중화장실 찾기] - 공중화장실 찾기 - 1 " data-og-description="최근 부쩍 단조로워진 나의 삶이 지루해져서 재미난 게 없을까 생각하다가 요 근래 속이 많이 안 좋아화장실을 찾는 일이

magicmk.tistory.com

 

지난 포스팅에서는 공공데이터를 가져와 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

 

NAVER Maps API v3

NAVER Maps API v3로 여러분의 지도를 만들어 보세요. 유용한 기술문서와 다양한 예제 코드를 제공합니다.

navermaps.github.io


✅ 테스트

로컬에서 테스트를 해보면 현재 위치를 보여주며 화장실 위치가 마킹되는 것을 알 수 있다.

localhost


✅ 마무리

정리 해놓고 보니 별거 없지만 사실 가져온 데이터를 지도에 마킹하는 부분에서 꽤 애를 먹었다.

예제에서는 분명 쉽게 쉽게 해 둔 것 같은데 다중 마킹에 대한 정보가 없어서 아마 못찾은걸지도..;;

아무튼 금방 해결할 수 있었다.

 

https://github.com/Kimmingki/suddenPoo

 

GitHub - Kimmingki/suddenPoo: 급똥이 마려울 때 얼른 화장실 찾아야지...

급똥이 마려울 때 얼른 화장실 찾아야지... Contribute to Kimmingki/suddenPoo development by creating an account on GitHub.

github.com

 

저작자표시 비영리 (새창열림)

'Java > 공중화장실 찾기' 카테고리의 다른 글

공중화장실 찾기 - 4 <Spring Boot GGP 배포>  (2) 2024.09.11
공중화장실 찾기 - 2 <공공데이터 가공>  (3) 2024.09.08
공중화장실 찾기 - 1 <프로젝트 생성 및 네이버 지도 API 적용>  (1) 2024.09.06
  1. ✅ 지도 화면 만들기
  2. 1️⃣ 비즈니스 로직
  3. 2️⃣ 화면
  4. ✅ 테스트
  5. ✅ 마무리
'Java/공중화장실 찾기' 카테고리의 다른 글
  • 공중화장실 찾기 - 4 <Spring Boot GGP 배포>
  • 공중화장실 찾기 - 2 <공공데이터 가공>
  • 공중화장실 찾기 - 1 <프로젝트 생성 및 네이버 지도 API 적용>
요술공주밍키
요술공주밍키
조금씩이라도 꾸준히..
  • 요술공주밍키
    삽질의흔적
    요술공주밍키
  • 전체
    오늘
    어제
    • 분류 전체보기 (132)
      • Java (42)
        • Spring Boot (14)
        • Spring Boot 게시판 (14)
        • 공중화장실 찾기 (4)
        • 쇼핑몰 (8)
      • JavaScript (8)
        • NodeJS (2)
      • Python (5)
        • Django (4)
      • Server (10)
        • Docker (4)
        • K8S (0)
        • Jenkins (1)
      • 알고리즘 (22)
        • 프로그래머스 (17)
        • 백준 (5)
      • Etc (21)
        • 개발 팁 (1)
      • 일상 (23)
        • 독서 포스트 (21)
        • 회고록 (2)
  • 인기 글

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
요술공주밍키
공중화장실 찾기 - 3 <네이버 지도 API 적용>

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.