1탄에서 이어지는 내용입니다.
[04 사용자의 현재 위치 정보 사용하기]
(1) 현재 위치 가져오기
- 이 기능을 추가하면서 가장 애를 먹었던 부분이다. 현재 위치 정보를 가져오는 코드는 금방 찾았는데 아무리 구글링을 해도 이걸 기반으로 검색 기능을 구현한 블로그가 없었기 때문...ㅎ 블로그에 의존하던 내 실력의 한계를 느꼈다.
- 일단 웹에서 내 위치를 가져오는 기능은 Geolocation API를 사용하면 된다. 구글링해서 얻은 코드에 Promise 객체를 return 하도록 함수를 수정하고 KakaoMap에서 사용하는 위치 정보 객체로 변경해서 응답값에 넣어주었다.
const getCurrentCoordinate = async () => {
console.log("getCurrentCoordinate 함수 실행!!!");
return new Promise((res, rej) => {
// HTML5의 geolocaiton으로 사용할 수 있는지 확인합니다.
if (navigator.geolocation) {
// GeoLocation을 이용해서 접속 위치를 얻어옵니다.
navigator.geolocation.getCurrentPosition(function (position) {
console.log(position);
const lat = position.coords.latitude; // 위도
const lon = position.coords.longitude; // 경도
const coordinate = new kakao.maps.LatLng(lat, lon);
res(coordinate);
});
} else {
rej(new Error("현재 위치를 불러올 수 없습니다."));
}
});
};
- 여기까지 구현하고 위에서 말한대로 이걸 Kakao Map에 적용하는 방법이 막혀서 그제서야 지푸라기라도 잡는 심정으로 공식문서를 정독했다!
(2) 현위치를 기준으로 설정하기
- 먼저 메인 기능을 수행하는 keywordSearch( ) 함수 설명을 봤다. 현재는 인자값으로 검색 키워드와 콜백 함수만 받는데 여기에 options를 추가할 수 있었다! 추가로 어떤 타입이어야 하는지도 상세하게 나와있는...
- 이걸 보고 위의 getCurrentCoordinate( ) 함수를 만들 때 응답값이 LatLng 객체가 되도록 한 것이다.
- 나는 많은 옵션 중 아래 세 개를 사용했다. 이걸 또 어떻게 옵션으로 할당하지? 했는데 Object라고 써져있는..!
- 또 sort 옵션을 쓰기 위해 카카오가 만들어놓은 SortBy라는 객체를 써야하는 거 같길래 눌러봤다. 그럼 아래와 같이 두 가지 입력값을 확인 가능~ 그대로 써주었다.
- 따라서 아래와 같이 코드를 수정했다.
async function searchPlaces() {
console.log("searchPlaces 실행!!!");
var keyword = "피씨방";
const currentCoordinate = await getCurrentCoordinate();
console.log(currentCoordinate);
var options = {
location: currentCoordinate,
radius: 10000,
sort: kakao.maps.services.SortBy.DISTANCE,
};
// 장소검색 객체를 통해 키워드로 장소검색을 요청합니다
ps.keywordSearch(keyword, placesSearchCB, options);
}
- 작성을 하고 보니 별 거 없는데 이걸 알아내기까지 시간이 너무 오래걸렸다..! ㅋㅋㅋㅋㅋ
(3) 전체 코드
- KakaoMap에서 현 위치 기반으로 키워드 검색한 장소 목록을 front에 보여주기 위해 필요한 코드 전체이다.
더보기
MyPageMap.css
.map_wrap {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.map_wrap,
.map_wrap * {
margin: 0;
padding: 0;
font-family: "Malgun Gothic", dotum, "돋움", sans-serif;
font-size: 12px;
}
.map_wrap a,
.map_wrap a:hover,
.map_wrap a:active {
color: #000;
text-decoration: none;
}
.map_wrap {
position: relative;
width: 100%;
height: 500px;
}
#menu_wrap {
position: absolute;
top: 0;
bottom: 0;
width: 250px;
margin: 10px 560px 30px 10px;
padding: 5px;
overflow-y: auto;
background: rgba(255, 255, 255, 0.7);
z-index: 1;
font-size: 12px;
border-radius: 10px;
}
.bg_white {
background: #fff;
}
#menu_wrap hr {
display: block;
height: 1px;
border: 0;
border-top: 2px solid #5f5f5f;
margin: 3px 0;
}
#menu_wrap .option {
text-align: center;
}
#menu_wrap .option p {
margin: 10px 0;
}
#menu_wrap .option button {
margin-left: 5px;
}
#placesList li {
list-style: none;
}
#placesList .item {
position: relative;
border-bottom: 1px solid #888;
overflow: hidden;
cursor: pointer;
min-height: 65px;
}
#placesList .item span {
display: block;
margin-top: 4px;
}
#placesList .item h5,
#placesList .item .info {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
#placesList .item .info {
padding: 10px 0 10px 55px;
}
#placesList .info {
color: black;
}
#placesList .info .gray {
color: #8a8a8a;
}
#placesList .info .jibun {
padding-left: 26px;
background: url(https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/places_jibun.png)
no-repeat;
}
#placesList .info .tel {
color: #009900;
}
#placesList .item .markerbg {
float: left;
position: absolute;
width: 36px;
height: 37px;
margin: 10px 0 0 10px;
background: url(https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/marker_number_blue.png)
no-repeat;
}
#placesList .item .marker_1 {
background-position: 0 -10px;
}
#placesList .item .marker_2 {
background-position: 0 -56px;
}
#placesList .item .marker_3 {
background-position: 0 -102px;
}
#placesList .item .marker_4 {
background-position: 0 -148px;
}
#placesList .item .marker_5 {
background-position: 0 -194px;
}
#placesList .item .marker_6 {
background-position: 0 -240px;
}
#placesList .item .marker_7 {
background-position: 0 -286px;
}
#placesList .item .marker_8 {
background-position: 0 -332px;
}
#placesList .item .marker_9 {
background-position: 0 -378px;
}
#placesList .item .marker_10 {
background-position: 0 -423px;
}
#placesList .item .marker_11 {
background-position: 0 -470px;
}
#placesList .item .marker_12 {
background-position: 0 -516px;
}
#placesList .item .marker_13 {
background-position: 0 -562px;
}
#placesList .item .marker_14 {
background-position: 0 -608px;
}
#placesList .item .marker_15 {
background-position: 0 -654px;
}
#pagination {
margin: 10px auto;
text-align: center;
}
#pagination a {
display: inline-block;
margin-right: 10px;
}
#pagination .on {
font-weight: bold;
cursor: default;
color: #777;
}
MyPageMap.js
/*global kakao*/
import "../styles/Mypage/MypageMap.css";
import React, { useEffect } from "react";
import Swal from "sweetalert2";
const getCurrentCoordinate = async () => {
console.log("getCurrentCoordinate 함수 실행!!!");
console.log("navigator.geolocation", navigator.geolocation);
return new Promise((res, rej) => {
// HTML5의 geolocaiton으로 사용할 수 있는지 확인합니다.
if (navigator.geolocation) {
// GeoLocation을 이용해서 접속 위치를 얻어옵니다.
navigator.geolocation.getCurrentPosition(function (position) {
console.log(position);
const lat = position.coords.latitude; // 위도
const lon = position.coords.longitude; // 경도
const coordinate = new kakao.maps.LatLng(lat, lon);
res(coordinate);
});
} else {
rej(new Error("현재 위치를 불러올 수 없습니다."));
}
});
};
const MypageMap = () => {
useEffect(() => {
// 마커를 담을 배열입니다
try {
var markers = [];
var mapContainer = document.getElementById("map"); // 지도를 표시할 div
var mapOption = {
center: new kakao.maps.LatLng(33.450701, 126.570667), // 지도의 중심좌표
level: 3, // 지도의 확대 레벨
};
// 지도를 생성합니다
var map = new kakao.maps.Map(mapContainer, mapOption);
// 일반 지도와 스카이뷰로 지도 타입을 전환할 수 있는 지도타입 컨트롤을 생성합니다
var mapTypeControl = new kakao.maps.MapTypeControl();
// 지도에 컨트롤을 추가해야 지도위에 표시됩니다
// kakao.maps.ControlPosition은 컨트롤이 표시될 위치를 정의하는데 TOPRIGHT는 오른쪽 위를 의미합니다
map.addControl(mapTypeControl, kakao.maps.ControlPosition.TOPRIGHT);
// 지도 확대 축소를 제어할 수 있는 줌 컨트롤을 생성합니다
var zoomControl = new kakao.maps.ZoomControl();
map.addControl(zoomControl, kakao.maps.ControlPosition.RIGHT);
// 장소 검색 객체를 생성합니다
var ps = new kakao.maps.services.Places();
console.log("ps:", ps);
// 검색 결과 목록이나 마커를 클릭했을 때 장소명을 표출할 인포윈도우를 생성합니다
var infowindow = new kakao.maps.InfoWindow({ zIndex: 1 });
console.log("infowindow:", infowindow);
// 키워드로 장소를 검색합니다
searchPlaces();
// 키워드 검색을 요청하는 함수입니다
async function searchPlaces() {
console.log("searchPlaces 실행!!!");
var keyword = "피씨방";
const currentCoordinate = await getCurrentCoordinate();
console.log(currentCoordinate);
var options = {
location: currentCoordinate,
radius: 10000,
sort: kakao.maps.services.SortBy.DISTANCE,
};
// 장소검색 객체를 통해 키워드로 장소검색을 요청합니다
ps.keywordSearch(keyword, placesSearchCB, options);
}
// 장소검색이 완료됐을 때 호출되는 콜백함수 입니다
function placesSearchCB(data, status, pagination) {
if (status === kakao.maps.services.Status.OK) {
// 정상적으로 검색이 완료됐으면
// 검색 목록과 마커를 표출합니다
console.log(data);
displayPlaces(data);
// 페이지 번호를 표출합니다
displayPagination(pagination);
} else if (status === kakao.maps.services.Status.ZERO_RESULT) {
Swal.fire("검색 결과가 존재하지 않습니다.");
return;
} else if (status === kakao.maps.services.Status.ERROR) {
Swal.fire("검색 결과 중 오류가 발생했습니다.");
return;
}
}
// 검색 결과 목록과 마커를 표출하는 함수입니다
function displayPlaces(places) {
var listEl = document.getElementById("placesList"),
menuEl = document.getElementById("menu_wrap"),
fragment = document.createDocumentFragment(),
bounds = new kakao.maps.LatLngBounds(),
listStr = "";
// 검색 결과 목록에 추가된 항목들을 제거합니다
removeAllChildNods(listEl);
// 지도에 표시되고 있는 마커를 제거합니다
removeMarker();
for (var i = 0; i < places.length; i++) {
// 마커를 생성하고 지도에 표시합니다
var placePosition = new kakao.maps.LatLng(places[i].y, places[i].x),
marker = addMarker(placePosition, i),
itemEl = getListItem(i, places[i]); // 검색 결과 항목 Element를 생성합니다
// 검색된 장소 위치를 기준으로 지도 범위를 재설정하기위해
// LatLngBounds 객체에 좌표를 추가합니다
bounds.extend(placePosition);
// 마커와 검색결과 항목에 mouseover 했을때
// 해당 장소에 인포윈도우에 장소명을 표시합니다
// mouseout 했을 때는 인포윈도우를 닫습니다
(function (marker, title) {
kakao.maps.event.addListener(marker, "mouseover", function () {
displayInfowindow(marker, title);
});
kakao.maps.event.addListener(marker, "mouseout", function () {
infowindow.close();
});
itemEl.onmouseover = function () {
displayInfowindow(marker, title);
};
itemEl.onmouseout = function () {
infowindow.close();
};
})(marker, places[i].place_name);
fragment.appendChild(itemEl);
}
// 검색결과 항목들을 검색결과 목록 Element에 추가합니다
listEl.appendChild(fragment);
menuEl.scrollTop = 0;
// 검색된 장소 위치를 기준으로 지도 범위를 재설정합니다
map.setBounds(bounds);
}
// 검색결과 항목을 Element로 반환하는 함수입니다
function getListItem(index, places) {
var el = document.createElement("li"),
itemStr =
'<span class="markerbg marker_' +
(index + 1) +
'"></span>' +
'<div class="info">' +
" <h5>" +
places.place_name +
"</h5>";
if (places.road_address_name) {
itemStr +=
" <span>" +
places.road_address_name +
"</span>" +
' <span class="jibun gray">' +
places.address_name +
"</span>";
} else {
itemStr += " <span>" + places.address_name + "</span>";
}
itemStr += ' <span class="tel">' + places.phone + "</span>" + "</div>";
el.innerHTML = itemStr;
el.className = "item";
return el;
}
// 마커를 생성하고 지도 위에 마커를 표시하는 함수입니다
function addMarker(position, idx, title) {
var imageSrc =
"https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/marker_number_blue.png", // 마커 이미지 url, 스프라이트 이미지를 씁니다
imageSize = new kakao.maps.Size(36, 37), // 마커 이미지의 크기
imgOptions = {
spriteSize: new kakao.maps.Size(36, 691), // 스프라이트 이미지의 크기
spriteOrigin: new kakao.maps.Point(0, idx * 46 + 10), // 스프라이트 이미지 중 사용할 영역의 좌상단 좌표
offset: new kakao.maps.Point(13, 37), // 마커 좌표에 일치시킬 이미지 내에서의 좌표
},
markerImage = new kakao.maps.MarkerImage(
imageSrc,
imageSize,
imgOptions
),
marker = new kakao.maps.Marker({
position: position, // 마커의 위치
image: markerImage,
});
marker.setMap(map); // 지도 위에 마커를 표출합니다
markers.push(marker); // 배열에 생성된 마커를 추가합니다
return marker;
}
// 지도 위에 표시되고 있는 마커를 모두 제거합니다
function removeMarker() {
for (var i = 0; i < markers.length; i++) {
markers[i].setMap(null);
}
markers = [];
}
// 검색결과 목록 하단에 페이지번호를 표시는 함수입니다
function displayPagination(pagination) {
var paginationEl = document.getElementById("pagination"),
fragment = document.createDocumentFragment(),
i;
// 기존에 추가된 페이지번호를 삭제합니다
while (paginationEl.hasChildNodes()) {
paginationEl.removeChild(paginationEl.lastChild);
}
for (i = 1; i <= pagination.last; i++) {
var el = document.createElement("a");
el.href = "#";
el.innerHTML = i;
if (i === pagination.current) {
el.className = "on";
} else {
el.onclick = (function (i) {
return function () {
pagination.gotoPage(i);
};
})(i);
}
fragment.appendChild(el);
}
paginationEl.appendChild(fragment);
}
// 검색결과 목록 또는 마커를 클릭했을 때 호출되는 함수입니다
// 인포윈도우에 장소명을 표시합니다
function displayInfowindow(marker, title) {
var content =
'<div style="padding:10px;z-index:1;color:black;font-weight:bold">' +
title +
"</div>";
infowindow.setContent(content);
infowindow.open(map, marker);
}
// 검색결과 목록의 자식 Element를 제거하는 함수입니다
function removeAllChildNods(el) {
while (el.hasChildNodes()) {
el.removeChild(el.lastChild);
}
}
} catch (err) {
console.log(err);
}
}, []);
return (
<div className="map_wrap">
<div
id="map"
style={{
width: "800px",
height: "500px",
position: "relative",
overflow: "hidden",
}}
></div>
<div id="menu_wrap" className="bg_white">
<hr />
<ul id="placesList"></ul>
<div id="pagination"></div>
</div>
</div>
);
};
export default MypageMap;
보여줄_페이지.js (필요 없는 부분은 삭제한 코드)
import styled from "styled-components";
import MypageMap from "../components/mypage/MypageMap";
const Mypage = () => {
return (
<>
<Header></Header>
<Main imgUrl={myPageBg}>
<div className="kakaoMap middle2">
<div className="topic common">내 주변 PC방 찾기</div>
<div className="common">
<MypageMap />
</div>
</div>
</Main>
</>
);
};
const Header = styled.div`
height: 50px;
`;
const Main = styled.div`
background-size: 100%;
background-image:
${(props) => `url(${props.imgUrl})`};
min-height: 100vh;
padding: 15% 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.middle2 {
width: 80%;
margin-bottom: 80px;
text-align: basis;
// width: 80%;
background: rgba(255, 255, 255, 0.6);
box-shadow: 0 8px 32px #6869d0;
backdrop-filter: blur(2.5px);
border-radius: 10px;
text-transform: uppercase;
letter-spacing: 0.4rem;
padding: 20px;
}
`;
export default Mypage;
(4) 로컬 서버에서 동작 확인
- 사는 곳 보호(?)를 위해 팀원분의 동네에서 잘 동작하는 사진이다.
- 사실 geolocation API가 웹에서는 엄청난 정확도를 보이는 건 아니지만, 반경 10km 거리순으로 45개의 피씨방이 뜨기 때문에 괜찮을 것 같다.
- 정확도를 높이는 방법도 찾아봤는데 IP 타겟팅 기술이 어쩌고 해서 관뒀다...
(5) 배포 서버에서는 작동하지 않는 모습
- 그런데 여기서 문제!!! 배포 서버로 가면 검색이 제대로 동작하지 않는다. 지도가 안뜨면 카카오와 통신 오류인가 할거고 오류 메세지라도 뜨면 덜 답답했을텐데 아래처럼만 뜨고 오류도 안 떠서 모든 함수에 콘솔을 찍었다.
- 아래와 같이 제주도 카카오 본사 위치에 장소 목록 배경만 뜬다...
- 결론은 Geolocation API가 HTTP 환경에서 동작하지 않는다는 것. 여기까지만 해도 아이참 원래 SSL 적용하려 했는데~ 이유가 하나 늘었군~ 하면서 신났다.
- 따라서 HTTPS로 변경하기 위해 아래와 같이 LetsEncrypt 인증서를 발급받고 nginx 설정 파일을 열심히 수정했는데... 그랬는데! 엘리스가 443 포트 사용할 수 있다고 해놓고 실제로는 안열어놨었던거다. ARE YOU DEVIL..?
- 난 그것도 모르고 내가 nginx 설정 잘못 했을거라 생각해서 열심히 삽질했는데 ㅎㅎ
- 엘리스가 443 쓸 수 있다고 했던거 박제~
- 내가 이렇게까지 집착한 이유는 1차 프로젝트 기간에도 이틀은 SSL 적용하겠다고 자물쇠 채우겠다고 공을 들였기 때문.... 덕분에 standalone, webroot, DNS 방식 등 인증서 종류도 알고 공부는 했지만 억울해!!!😤
- 그래서 발표 이틀 전부터 443 포트 열어달라고 요청했는데 시간상(?) 해줄 수 없다며 포기하라고 했다...😥
[05 느낀점]
- 공식 문서를 가장 처음에 봐야 했는데(심지어 한글로 되어 있으면서) 미루고 미루다 읽었을 때 거기서 답을 찾았기에... 큰 깨달음을 얻고 반성을 했다. 아무리 친절한 블로그도 공식 문서를 먼저 읽고 읽어보자는 거~😉
- 배포 서버에서 적용되지 않았던 게 너무 아쉽지만 이제 AWS 서버로 옮겨갔으니 거기서 SSL을 적용해 볼 것이다. 적용 후 잘 되는 모습을 추가해야지!
- 사실 프로젝트 끝나고 한 달이 지나서 이 포스팅을 작성하려니까 분명 시행착오 엄청 많았는데 다 기억이 나지 않았다...ㅎㅎ 그래도 결국 포스팅을 작성했으니 됐다.
- 이제 3차 프로젝트 시작인데 엄청난 분들 사이에서 내가 팀장을 맡게 되었다 두둥😱
- 3차 프로젝트에도 역시 포지션은 백엔드를 맡게 되었는데 포스팅할 만한 기능을 맡게 되면 꼭 캡처라도 열심히 해둬야겠다. 2차 때는 구현에 성공하고 나서 캡처하려니 캡처할게 별로 없었고 포스팅할 때 다시 들어가 환경 세팅부터 캡처를 일일이 해야했다..! (물론 복습은 된다)
- 어쨌든 확실히 책임감이 좀 느껴지는데 더더 열심히 참여하고 9시 스크럼에 지각하지 않기 위해 일찍 자봐야겠다... 제일 어려운 것...😫
- 엘리스에서의 마지막 프로젝트 화이팅!!!
'엘리스 AI트랙 4기 > 프로젝트' 카테고리의 다른 글
Oracle Cloud 인스턴스에 MySQL 서버 구축_1탄 (0) | 2022.06.04 |
---|---|
Gitlab과 Discord Webhook 연동하기 (0) | 2022.06.02 |
KakaoMap API로 "현위치 주변" 장소 검색 목록 보여주기_1탄 (0) | 2022.06.01 |
AWS EC2에 프로젝트 배포하기_2탄 (0) | 2022.05.30 |
AWS EC2에 프로젝트 배포하기_1탄 (0) | 2022.05.30 |