The Debugging Chronicles : "코드의 미학"

[카카오 지도 API ] 1. 반경 값 구하기(Next.js, TypeScript) 본문

FrontEnd

[카카오 지도 API ] 1. 반경 값 구하기(Next.js, TypeScript)

sweetseonah1004 2025. 1. 14. 16:58

약 2.5주 동안 나를 괴롭혔던 카카오 지도에 대해서 포스트해보려고 한다.

커스터마이징 하는 게 쉽지 않았고 그리고 정말 많이 배운 카카오지도!

1.반경 값 구하기

카카오지도에서는

화면의 중간 좌표 그리고

화면의 우측 상단, 북동쪽 좌표와

화면의 좌측 하단, 남서쪽 좌표를 구하는 방법을 제공한다.

 

 

 

그래서 화면 중간 좌표 부터 북동쪽을 기점으로 하는 반지름을 중심으로 

반경의 값을 구하려고 했다.

 

 

먼저 하버사인 공식을 이용하여 두 좌표간의 거리를 구한 다음에 

반경을 구해주었다.

next.js와 typescript로 구현하였다.

/**
 *
 * @param lat1 점1 위도
 * @param lng1 점1 경도
 * @param lat2 점2 위도
 * @param lng2 점2 경도
 * @returns 위도 경도로 두 점 사이의 거리 구하기
 */
export const getDistance = (
  lat1: number,
  lng1: number,
  lat2: number,
  lng2: number,
): number => {
  const R = 6371;
  const dLat = (lat2 - lat1) * (Math.PI / 180);
  const dLng = (lng2 - lng1) * (Math.PI / 180);
  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(lat1 * (Math.PI / 180)) *
      Math.cos(lat2 * (Math.PI / 180)) *
      Math.sin(dLng / 2) *
      Math.sin(dLng / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return R * c;
};

 

하버사인 공식에 관한 내용은 검색하면 많이 나온다 자세한 내용은 검색하여 참고!!

사용할 때는 남서쪽과 북동쪽 좌표를 인수값에 넣어 절반을 해주면 반지름 값의 반경 값이 나온다

      const bounds = await getMapBounds();
      const center = map.getCenter();

      // 남서쪽(SouthWest)과 북동쪽(NorthEast) 좌표 가져오기
      const swLatLng = bounds.getSouthWest();  // 남서쪽 좌표
      const neLatLng = bounds.getNorthEast();  // 북동쪽 좌표
  
      // 지도 대각선 거리 계산 (반경)
      const distance = getDistance(
        swLatLng.getLat(),
        swLatLng.getLng(),
        neLatLng.getLat(),
        neLatLng.getLng()
      ) / 2;

 

getMapBounds함수는 카카오에서 제공하는 LatLngBounds객체를 이용하여 화면의 좌표를 가지고 올 수있다.

LatLngBounds의 더 자세한 내용 -> https://apis.map.kakao.com/web/sample/setBounds/

  const getMapBounds = (): Promise<kakao.maps.LatLngBounds> => {
    return new Promise((resolve, reject) => {
      const checkBoundsReady = () => {
        if (mapRef.current && mapRef.current.getBounds) {
          const bounds = mapRef.current.getBounds();
  
          // 유효성 검사 추가
          if (bounds.getSouthWest && bounds.getNorthEast) {
            resolve(bounds as kakao.maps.LatLngBounds);
          } else {
            reject(new Error("지도 범위를 가져올 수 없습니다."));
          }
        } else {
          setTimeout(checkBoundsReady, 100); // 준비될 때까지 재시도
        }
      };
      checkBoundsReady();
    });
  };

 

다음은 줌 이벤트에 적용한 handleZoom()함수에 적용한 반경의 예이다.

  const handleZoom = (
    bounds: kakao.maps.LatLng,
    swLatLng: kakao.maps.LatLng,
    neLatLng: kakao.maps.LatLng,
    center: kakao.maps.LatLng,
  ) => {
    setMapCenter({
      lat: center.getLat(),
      lng: center.getLng(),
    });
    // 중간 좌표 계산
    const centerPoint = center;

    const distance = getDistance(
      neLatLng.getLat(), // 동일한 위도 사용
      Number(centerPoint.getLng()), // 중간 좌표의 경도
      neLatLng.getLat(), // 동일한 위도 사용
      neLatLng.getLng(), // 북동쪽 좌표의 경도
    );

    setAllFranchiseParams((prev) => {
      const updatedParams = {
        ...prev,
        geo_lat: centerPoint.getLat(),
        geo_lng: centerPoint.getLng(),
        radius: distance < 1 ? 1 : setRadiusAccurate(distance),
      };

      setFillters((currentFillters) => {
        if (currentFillters.franchise && currentFillters.no_franchise) {
          // 가맹점, 비가맹점 모두 선택
          resetList('all');
          //getAllFranchises(updatedParams);
          getAllFranchisesAndFilter(updatedParams, swLatLng, neLatLng);
        } else if (currentFillters.franchise && !currentFillters.no_franchise) {
          // 가맹점만 선택
          resetList('all');
          //getAllFranchises(updatedParams);
          getAllFranchisesAndFilter(updatedParams, swLatLng, neLatLng);
        } else if (!currentFillters.franchise && currentFillters.no_franchise) {
          // 비가맹점만 선택
          resetList('all');
          //getAllFranchises(updatedParams);
          getAllFranchisesAndFilter(updatedParams, swLatLng, neLatLng);
        } else {
          // 가맹점, 비가맹점 둘 다 선택 안됨
          resetList('company');
          resetList('franchise');
          resetList('all');
        }
        return currentFillters; // fillters 상태 유지
      });

      return updatedParams; // Corrected return variable name
    });
  };

 

setAllFranchiseParams는 API 요청에 사용될 파라미터 값을 상태로 관리하며, 필요에 따라 동적으로 업데이트할 수 있도록 도와준다.

  const [allFranchiseParams, setAllFranchiseParams] =
    useState<AllFranchiseParam>({
      city: '',
      company_name: '',
      geo_lat: '37.5665',
      geo_lng: '126.9780',
      location: '',
      radius: '1',
      type: '3',
    });

 

그리고 getFranchiesAll API 로부터 응답값을 받아오고, 받아온 데이터를 **상태(setAllList)**로 업데이트하는 역할을 합니다.

 const getAllFranchises = async (params: AllFranchiseParam) => {
    try {
      const res = await getFranchiesAll(params);  // 데이터 요청 및 결과 저장
  
      setAllList({
        result: res.result,
        total: res.total,
      });
  
      return res;  // 결과 반환
    } catch (error) {
      return null;  // 에러 발생 시 null 반환
    }
  };
  const [allList, setAllList] = useState<AllFranchiseResult>({
    // 전체 리스트
    result: [],
    total: '0',
  });