#3 항해 sw camp 지도 기능 제작

김재우·2023년 3월 17일
0

geolocation

목록 보기
1/1

항해 sw camp #3

기능을 맡게 된 배경

pm분들이 기획 해 주신 수리업체 찾기 페이지를 담당하게 되었다 . 평소에 새로운 기술에 대해서 학습하고자 하는 욕구가 강하기도 하여 같이 하는 프론트엔드 분에게 이 기능을 제가 구현해봐도 되는지 여쭤보고 승낙을 해주셔서 너무나 감사했다.

내가 맡은 기능 페이지의 wire frame 은 이렇게 되어있다.

이렇게 현재위치를 받아서 주변의 수리업체 혹은 서비스 센터를 찾아주는 기능이 담긴 페이지를 구현해야한다. 이 페이지를 구현하기 위해서는 단계별로 정리를 해보았다.
1. 현재위치를 가져온다.
2. 검색을 했을 시 해당 업체의 마커가 지도에 찍히게 한다.
3. 현재위치를 기반으로 가까운 업체의 마커가 검색 리스트로 표시되어야 한다.

처음에는 react 프로젝트이다 보니 react-kakao-sdk 라는 라이브러리를 사용해서 구현을 하려고 했다. 충분히 라이브러리를 사용해서도 구현을 할 수 있을것 같았는데 해당 라이브러리에는 내가 원하는 정보들이 많이 부족하여 구현에 어려움이 있었다.
그렇지만 다행히도 굉장히 많은 레퍼런스들과 kakao developer 에서 제공해주는 docs 와 sample code 가 큰 도움이 되었다.
카카오톡에서 페이지네이션을 제공해주는데 이것을 화살표로 바꾸는 방법을 찾아보는데 나오지 않고 있다 .. 어디가야 정보를 찾을 수 있을지 ...😢😢😢

트러블 슈팅한 코드 정리!

1-1 현재위치를 찾는 함수를 만들자.

const getCurrentLocation = async () => {
  return new Promise((res, rej) => {
    if (navigator.geolocation) {
      // GeoLocation을 이용해서 접속 위치를 얻어옵니다
      navigator.geolocation.getCurrentPosition((position) => {
        const lat = position.coords.latitude, // 위도
          lon = position.coords.longitude; // 경도

        const locPosition = new kakao.maps.LatLng(lat, lon);
        res(locPosition);
      });
      // 마커가 표시될 위치를 geolocation으로 얻어온 좌표로 생성합니다
    } else {
      // HTML5의 GeoLocation을 사용할 수 없을때 마커 표시 위치와 인포윈도우 내용을 설정합니다

      rej(new Error('현재 위치를 불러 올 수 없습니다.'));
    }
  });
};

1-2 키워드를 통해 장소를 검색하는 함수

const searchPlaces = async () => {
      const currentLocation = await getCurrentLocation();
      let options = {
        location: currentLocation,
        radius: 10000,
        sort: kakao.maps.services.SortBy.DISTANCE, //여기가 현재위치 기반으로 거리순으로 검색 리스트를 표시해주는 부분이다.
      };
      let keyword = props.searchKeyword;
      ps.keywordSearch(keyword, placesSearchCB, options);
    };

이번에 kakao geolocation 을 이용하면서 트러블 슈팅한 부분이기도 하다. 블로그들이 대부분 kakao developer에서 제공해주는 코드가 대부분이였고 , 우리 프로젝트에선 현재 위치를 기반으로 해서 가까운 업체를 리스트에 표시 해야했기 때문에 이 부분을 구현하려고 많은 시간을 들였다.
kakao developer 에 docs 문서가 정말 많은 도움이 됐다.

문서의 내용에 option에 sort라는 부분이 있다. sort 를 보면 정렬 옵션으로 DISTANCE일땐 지정한 좌표값에 기반하여 동작한다고 되어있고 기본값은 ACCURACY 이다 . 이 부분을 이용하면 쉽게 현재 위치 기반해서 카카오 지도를 이용할 수 있다.!

나머지 코드는 kakao devloper에 샘플 코드를 이용해서 구현 했기 때문에 공식 홈페이지의 문서를 참고하는걸 추천한다!

1-3 전체코드

import React, { useEffect } from 'react';
import styled from 'styled-components';
import { propsType } from '../Fix';
import { useRecoilState } from 'recoil';
import { useFixState } from '../../recoil/fix';
import markerimg from '../../assets/icon/marker.svg';

interface placeType {
  place_name: string;
  road_address_name: string;
  address_name: string;
  phone: string;
  place_url: string;
  totalCount: number;
}

//------------------자기 위치 찾기 ----------------
const getCurrentLocation = async () => {
  return new Promise((res, rej) => {
    if (navigator.geolocation) {
      // GeoLocation을 이용해서 접속 위치를 얻어옵니다
      navigator.geolocation.getCurrentPosition((position) => {
        const lat = position.coords.latitude, // 위도
          lon = position.coords.longitude; // 경도

        const locPosition = new kakao.maps.LatLng(lat, lon);
        res(locPosition);
      });
      // 마커가 표시될 위치를 geolocation으로 얻어온 좌표로 생성합니다
    } else {
      // HTML5의 GeoLocation을 사용할 수 없을때 마커 표시 위치와 인포윈도우 내용을 설정합니다

      rej(new Error('현재 위치를 불러 올 수 없습니다.'));
    }
  });
};

//KaKao API 불러오기
const { kakao } = window as any;
const KaKao = (props: propsType) => {
  const [count, setCount] = useRecoilState(useFixState);

  // 지도를 표시할 div
  let markers: any[] = [];
  useEffect(() => {
    //지도 생성

    const container = document.getElementById('map');
    const option = {
      center: new kakao.maps.LatLng(37.566826, 126.9786567),
      level: 1,
    };
    //지도 생성
    const map = new kakao.maps.Map(container, option);
    const zoomControl = new kakao.maps.ZoomControl();
    map.addControl(zoomControl, kakao.maps.ControlPosition.RIGHT);
    const markerPosition = new kakao.maps.LatLng(37.566826, 126.9786567);

    const marker = new kakao.maps.Marker({
      position: markerPosition,
    });

    marker.setMap(map);

    //장소 검색 객체를 생성
    const ps = new kakao.maps.services.Places();

    // 검색 결과 목록 마커 클릭 때 장소명 표출할 인포 윈도우
    const infowindow = new kakao.maps.InfoWindow({ zIndex: 1 });
    //

    //지도에 마커와 인포윈도우를 표시하는 함수
    function displayMarker(locPosition: any, message: any) {
      // 마커를 생성합니다
      const marker = new kakao.maps.Marker({
        map: map,
        position: locPosition,
      });

      const iwContent = message, // 인포윈도우에 표시할 내용
        iwRemoveable = true;

      // 인포윈도우를 생성합니다
      const infowindow = new kakao.maps.InfoWindow({
        content: iwContent,
        removable: iwRemoveable,
      });

      // 인포윈도우를 마커위에 표시합니다
      infowindow.open(map, marker);

      // 지도 중심좌표를 접속위치로 변경합니다
      map.setCenter(locPosition);
    }
    // -----------키워드로 장소 검색 --------------
    if (!map) return;

    //장소 검색 객체를 통해 키워드로 장소 검색을 요청
    const searchPlaces = async () => {
      const currentLocation = await getCurrentLocation();
      const options = {
        location: currentLocation,
        radius: 10000,
        size: 6,
        sort: kakao.maps.services.SortBy.DISTANCE,
      };
      const keyword = props.searchKeyword;
      ps.keywordSearch(keyword, placesSearchCB, options);
    };

    const placesSearchCB = (data: any, status: any, pagination: any) => {
      if (status === kakao.maps.services.Status.OK) {
        // 정상적으로 검색이 완료됐으면
        // 검색 목록과 마커를 표출
        displayPlaces(data);
        setCount(pagination?.totalCount);
        //페이지 번호를 표출
        displayPagination(pagination);
      } else if (status === kakao.maps.services.Status.ZERO_RESULT) {
        alert('주변 5km 내에 매장이 없습니다.');
        return;
      } else if (status === kakao.maps.services.Status.ERROR) {
        alert('검색 결과 중 오류가 발생했습니다.');
        return;
      }
      console.log(data, status, pagination);
    };
    // 검색 결과 목록과 마커를 표출하는 함수
    const displayPlaces = (places: string | any[]) => {
      const listEl = document.getElementById('places-list'),
        resultEl = document.getElementById('search-result'),
        fragment = document.createDocumentFragment(),
        bounds = new kakao.maps.LatLngBounds();

      //검색 결과 목록에 추가된 항목들 제거
      listEl && removeAllChildNods(listEl);
      //지도에 표시되고 있는 마커를 제거
      removeMarker();

      for (let i = 0; i < places.length; i++) {
        // 마커를 생성하고 지도에 표시
        const placePosition = new kakao.maps.LatLng(places[i].y, places[i].x),
          marker = addMarker(placePosition, i, undefined),
          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 && listEl.appendChild(fragment);
      if (resultEl) {
        resultEl.scrollTop = 0;
      }
      // 검색된 장소 위치를 기준으로 지도 범위를 재설정
      map.setBounds(bounds);
    };

    // 검색결과 항목을 Element로 반환하는 함수
    function getListItem(index: number, places: placeType) {
      const el = document.createElement('li');
      const itemStr = `
      
          <div style="padding:5px;
          z-index:1;
          border: 1px solid #E4CCFF;
          box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
          border-radius: 3px; 
          margin-top:2%;
          margin-left:-10%;
          width: 100%;
          max-width: 610px;
          min-width:610px;
          height:70px;
          display:flex;
          align-items: center;
          " class="info">
          <img style="width:29px;
            height:29px;" src="${markerimg}"/>
            <div style="display:flex;
            flex-direction:column;
            margin-left:4px;">
            
            <a href="${places.place_url}" target="_blank">
              <h5 style="font-size:11px" class="info-item place-name">${places.place_name}</h5>
              ${
                places.road_address_name
                  ? `<span style="font-size:9px;font-weight:400;" class="info-item road-address-name">
                    ${places.road_address_name}
                   </span>
                  `
                  : `<span style="font-size:9px;" class="info-item address-name">
             	     ${places.address_name}
                  </span>`
              }
              <br>
              <span style="font-size:9px;" class="info-item tel">
                ${places.phone}
              </span>
            </a>
            </div>
            
            <div style="display:flex;flex-direction:column;align-items:end;gap:10px;position:absolute;left:47%;">
            <input style="width:20px;
            height:20px;
            color: #5A3092;
            border:1px solid;
            accent-color:#5A3092;
            " type="checkbox"/>
            <button style="width:63px;
            height:23px;color:#5A3092;
            border:1px solid #5A3092;
            border-radius:11.5px;font-size:10px">위치보기</button>
            </div>
          </div>
          `;

      el.innerHTML = itemStr;
      el.className = 'item';

      return el;
    }

    // 마커를 생성하고 지도 위에 마커를 표시하는 함수
    function addMarker(position: any, idx: number, title: undefined) {
      const 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 (let i = 0; i < markers.length; i++) {
        markers[i].setMap(null);
      }
      markers = [];
    }

    //검색 결과 목록 하단에 페이지 번호 표시 함수
    function displayPagination(pagination: {
      last: number;
      totalCount: number;
      current: number;
      gotoPage: (arg0: number) => void;
    }) {
      const paginationEl = document.getElementById('pagination') as HTMLElement;
      const fragment = document.createDocumentFragment();
      let i;

      // 기존에 추가된 페이지번호를 삭제
      while (paginationEl.hasChildNodes()) {
        paginationEl.lastChild && paginationEl.removeChild(paginationEl.lastChild);
      }

      for (i = 1; i <= pagination.last; i++) {
        const el = document.createElement('a') as HTMLAnchorElement;
        el.href = '#';
        el.innerHTML = i.toString();

        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: any, title: string) {
      const content = '<div style="padding:5px;z-index:1;" class="marker-title">' + title + '</div>';

      infowindow.setContent(content);
      infowindow.open(map, marker);
    }
    //검색결과 목록의 자식 element 제거 함수
    function removeAllChildNods(el: HTMLElement) {
      while (el.hasChildNodes()) {
        el.lastChild && el.removeChild(el.lastChild);
      }
    }
    searchPlaces();
  }, [props.searchKeyword]);
  return (
    <Mapcontainer className="map-container">
      <ResultList>
        {/* <span>컴퓨터수리노트북수리</span>
        <span>서울 강남구 테헤란로 20길 18 6층</span>
        <span>02-6953-8153</span> */}

        <div id="search-result">
          <p className="result-text">{/* <span className="result-keyword">{props.searchKeyword}</span> */}</p>
          <div className="scroll-wrapper">
            <PlaceList id="places-list"></PlaceList>
          </div>
          <Pagenation id="pagination"></Pagenation>
        </div>
      </ResultList>
      <MapDiv>
        <div
          id="map"
          className="map"
          style={{
            width: '400px',
            height: '500px',
          }}
        ></div>
      </MapDiv>
    </Mapcontainer>
  );
};

export default KaKao;
const Mapcontainer = styled.div`
  display: flex;
  flex-direction: row;
  margin-left: 59px;
  width: 1029px;
  height: 514px;
  margin-top: 10px;
`;
const ResultList = styled.div``;

const PlaceList = styled.ul`
  width: 610px;
  height: 70px;

  background-color: #ffffff;
`;

//Item Box

const MapDiv = styled.div`
  margin-left: -3%;
  margin-top: 1%;
`;
const Pagenation = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  margin-top: 20px;
  position: relative;

  top: 410px;
  left: 200px;
`;

숫자로 출력되는 페이지네이션을 와이어프레임 대로 구현을 해야하는데 너무 어렵다 ... 정보가 부족해 흑흑 ㅠㅠ

profile
프론트엔드 꾸준개발자입니다.

0개의 댓글