React

 

1. 설치

npm install react-kakao-maps-sdk

 

 

 

2. <script> 태그 추가

이 라이브러리를 사용하기 위해서는 필수적으로 Kakao Maps API를 불러와야 한다.
API 키 발급받는 법

<script
  type="text/javascript"
  src="//dapi.kakao.com/v2/maps/sdk.js?appkey=발급받은 APP KEY&libraries=services,clusterer"
></script>

 

 

 

KakaoMap.jsx

import React, { useState, useEffect } from "react";
import { Map, MapMarker } from "react-kakao-maps-sdk";



const KakaoMap = ({ keyword, latitude, longitude }) => {
  const { kakao } = window;
  const [info, setInfo] = useState();
  const [markers, setMarkers] = useState([]);
  const [map, setMap] = useState();
  const [mapLength, setMapLength] = useState(1000);

  useEffect(() => {
    const overlayBox = document.querySelector(".overlaybox");
    if (overlayBox) {
      const grandParent = overlayBox.parentElement.parentElement;
      if (grandParent) {
        setBorder(grandParent.style.border); // border 값을 업데이트합니다.
        grandParent.style.border = "0"; // border 값을 0으로 설정합니다.
      }
    }


  }, []);

  useEffect( () => {
    if (!map) return;
  
    if(mapLength===1000){
      createMap(keyword);
    }
     
    if(mapLength===0){
      createMap(keyword);
      // 예 let address = "충청북도 청주시 상당구 미원면";
      //만약에 검색된 스터디카페 값이 없다면  충청북도 청주시 까지로 해서 검색
      let parts = keyword.split(" ");
      let result = parts[0] + " " + parts[1];
      console.log(result);  // 출력: "충청북도 청주시"
      createMap( result);
    }

  }, [map,mapLength]);

  async function createMap(keyword){   
    const ps = new kakao.maps.services.Places();
     ps.keywordSearch(`${keyword} 스터디카페`, async (data, status, _pagination) => {      
      setMapLength(data.length);
      
      if (status === kakao.maps.services.Status.OK) {
        // 검색된 장소 위치를 기준으로 지도 범위를 재설정하기위해
        // LatLngBounds 객체에 좌표를 추가합니다
        const bounds = new kakao.maps.LatLngBounds();
        let markers = [];
        
        for (var i = 0; i < data.length; i++) {
          // @ts-ignore
          markers.push({
            position: {
              lat: data[i].y,
              lng: data[i].x,
            },
            content: data[i].place_name,
          });
          // @ts-ignore
          bounds.extend(new kakao.maps.LatLng(data[i].y, data[i].x));
        }
        setMarkers(markers);

        // 검색된 장소 위치를 기준으로 지도 범위를 재설정합니다
        map.setBounds(bounds);

        //보더값 제거 배경색 투명
        const overlayBox = document.querySelector(".overlaybox");
        if (overlayBox) {
          const grandParent = overlayBox.parentElement.parentElement;
          if (grandParent) {
            setBorder(grandParent.style.border); // border 값을 업데이트합니다.
            setBackgroundColor(grandParent.style.background);
            grandParent.style.border = "0"; // border 값을 0으로 설정합니다.
            grandParent.style.background = "rgba(255,255,255,0)";
          }
        }
      }
    });

  }



  return (
    <div>
      <Map
        center={{ lat: latitude, lng: longitude }}
        style={{
          width: "100%",
          height: "700px",
          borderRadius: "20px",
        }}
        level={3}
        onCreate={setMap}
      >
        {/* //지도에 보여줄 위치 지정 (위도,경도)  */}

        {markers.map((marker) => (
          <MapMarker
            style={{ border: "tranparent" }}
            //position={{ lat: latitude, lng: longitude }}

            key={`marker-${marker.content}-${marker.position.lat},${marker.position.lng}`}
            position={marker.position}
            onClick={() => setInfo(marker)}
            image={{
                src: "https://cdn-icons-png.flaticon.com/512/5737/5737612.png",
                size: {
                  width: 50,
                  height: 50
              }
            }}        
           
          >
            {info && info.content === marker.content && (
              <>
                <div
                  style={{
                    color: "#000",
                    fontSize: "16px",
                    fontWeight: "600",
                    border: "2px solid #e30d7c",
                    borderRadius: "5px",
                    padding: "5px",
                    backgroundColor: "#ffffff",
                    boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)",
                    width: "300px",
                    textAlign: "center",
                  }}
                >
                  ☕???? {marker.content}
                </div>
              </>
            )}
          </MapMarker>
        ))}
      </Map>
    </div>
    //핀에 적힐 이름 (위치 이름)
  );
};

export default KakaoMap;

 

 

 

 

 

StudyCafeFinder

import Header from "../../components/Header";
import { useLoaderData } from "react-router-dom";
import { API_BASE_URL } from "../../api-config";
import { getAuthToken, getUser } from "../../util/auth";
import KakaoMap from "../../components/KakaoMap";
import UserTable from "../../components/UserTable";

const { kakao } = window;

const StudyCafeFinder = () => {
  const { findCafeInfo } = useLoaderData();

  return (
    <>
      <div>
        <Header />

        <main className="container mt-5">
         
         
          {findCafeInfo && findCafeInfo.users && (
            <UserTable users={findCafeInfo.users} />
          )}

        

          <KakaoMap
            keyword={findCafeInfo.address}
            latitude={findCafeInfo.lat}
            longitude={findCafeInfo.lng}
          />
        </main>
      </div>
     
    </>
  );
};

export default StudyCafeFinder;

// 로더 함수
export async function loader({ request, params }) {
  const token = getAuthToken();
  const user = getUser();


  const response = await fetch(`${API_BASE_URL}/users/studyCafeFinder`, {
    method: "POST",
    body: JSON.stringify(user),
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + token,
    },
  });

  if (!response.ok) {
    return json({ message: "데이터를 가져올 수 없습니다." }, { status: 500 });
  } else {
    //좌표
    let findCafeInfo = "";
    const searchOptionArray = [];
    let users = "";
    let getAddress = "";

    const resData = await response.json();
    users = resData.result;

    const addresses = await users.map((user) => {
      return user.address;
    });

    //샘플 예
    // const addresses = [
    //   "서울 송파구 동남로 99",
    //   "서울 강남구 테헤란로 212",
    //   "대구 수성구 동대구로 441",
    // ];

    const newAddresses = addresses.map((address) => {
      const parts = address.split(" ");
      parts.pop(); // 마지막 요소(번지수)를 제거합니다.
      return parts.join(" "); // 주소를 다시 합칩니다.
    });

    try {
      //1.주소로 좌표 구하기
      await latAndLong(newAddresses, searchOptionArray);

      //2.중간좌표 구하기
      findCafeInfo = await centerPosition(searchOptionArray);

      //3.중간좌표로 주소 구하기
      getAddress = await getAddressFn(findCafeInfo)
        .then((res) => res)
        .catch((err) => console.error(err));
    } catch (error) {
      console.log("error :", error);
    }

    findCafeInfo.users = users;
    findCafeInfo.address = getAddress;

    
    return { findCafeInfo: findCafeInfo };
  }
}

//1.주소로 좌표 구하기
async function latAndLong(addresses, searchOptionArray) {
  const geocoder = new kakao.maps.services.Geocoder();

  const promises = addresses.map(
    (address) =>
      new Promise((resolve, reject) => {
        geocoder.addressSearch(address, function (result, status) {
          if (status === kakao.maps.services.Status.OK) {
            const coords = new kakao.maps.LatLng(result[0].y, result[0].x);

            const searchOption = {
              location: coords,
              radius: 2000,
              sort: kakao.maps.services.SortBy.DISTANCE,
            };

            resolve(coords);
          } else {
            reject(new Error("Failed to get coordinates"));
          }
        });
      })
  );

  await Promise.all(promises)
    .then((coordsArray) => {
      searchOptionArray.push(...coordsArray);
    })
    .catch((error) => console.error(error));
  return searchOptionArray;
}

//2. 중간좌표 구하기
async function centerPosition(locations) {
  //ex) const locations = [
  //   { La: 127.047486752713, Ma: 37.504059366187 },
  //   { La: 126.976425200039, Ma: 37.5626231544518 },
  //   { La: 126.978989954189, Ma: 37.5735042429813 }
  // ];

  let sumLat = 0;
  let sumLng = 0;

  locations.forEach((location) => {
    sumLat += location.Ma;
    sumLng += location.La;
  });

  const avgLat = sumLat / locations.length;
  const avgLng = sumLng / locations.length;

  return { lat: avgLat, lng: avgLng };
}

//3.좌표를 이용해서 주소값 가져오기
async function getAddressFn({ lat, lng }) {
  const geocoder = new window.kakao.maps.services.Geocoder();
  const res = await new Promise((resolve, reject) => {
    geocoder.coord2RegionCode(lng, lat, (result, status) => {
      if (status === window.kakao.maps.services.Status.OK) {
        for (let i = 0; i < result.length; i++) {
          if (result[i].region_type === "H") {
            resolve(result[i].address_name);
          }
        }
      } else {
        reject(new Error("주소를 찾을 수 없습니다."));
      }
    });
  });
  return res;
}

 

 

 

 

 

 

넥스트14  적용 방법

스터디 카페 검색

 

 

useKakaoMaps.ts

// utils/useKakaoMaps.ts
import { useEffect, useState } from 'react';

export function useKakaoMaps(appKey: string) {
  const [kakao, setKakao] = useState<any>(null);

  useEffect(() => {
    const kakaoMapScript = document.createElement('script');
    kakaoMapScript.async = true;
    kakaoMapScript.src = `https://dapi.kakao.com/v2/maps/sdk.js?appkey=${appKey}&libraries=services,clusterer&autoload=false`;
    document.head.appendChild(kakaoMapScript);

    const onLoadKakaoAPI = () => {
      if (window.kakao && window.kakao.maps) {
        setKakao(window.kakao);
      } else {
        console.error('Kakao Maps API is not loaded.');
      }
    };
    kakaoMapScript.addEventListener('load', onLoadKakaoAPI);
    return () => {
      kakaoMapScript.removeEventListener('load', onLoadKakaoAPI);
    };
  }, [appKey]);

  return kakao;
}

 

 

"use client";
import React, { useState, useEffect } from "react";
import { useKakaoMaps } from "@/utils/properties/useKakaoMaps";
import { Map, MapMarker } from "react-kakao-maps-sdk";

interface PropertyKakaoMapProps {
  keyword?: string;
  latitude?: number;
  longitude?: number;
}

const PropertyKakaoMap: React.FC<PropertyKakaoMapProps> = ({ keyword }) => {
  const defaultKeyword = "수원시 장안구 스터디 카페";

  //기본값 설정지역
  const defaultLatitude = 37.5665; // 서울특별시 위도
  const defaultLongitude = 126.978; // 서울특별시 경도

  const currentKeyword = keyword || defaultKeyword;


  const kakao = useKakaoMaps(process.env.NEXT_PUBLIC_KAKAOMAP_API_KEY || "");
  const [info, setInfo] = useState<any>(null);
  const [markers, setMarkers] = useState<{ position: { lat: number; lng: number }; content: string }[]>([]);
  const [map, setMap] = useState<any>(null);
  const [mapLength, setMapLength] = useState<number>(1000);

  useEffect(() => {
    console.log("!map || !kakao   " ,map, kakao );

    if (!map || !kakao) return;

    if (mapLength === 1000) {
      createMap(currentKeyword);
    }

    if (mapLength === 0) {
      createMap(currentKeyword);
      let parts = currentKeyword.split(" ");
      let result = parts[0] + " " + (parts[1] || "");
      createMap(result);
    }
  }, [map, mapLength, currentKeyword, kakao]);

  const createMap = (keyword: string) => {
    if (!kakao) return;

    const ps = new kakao.maps.services.Places();
    ps.keywordSearch(`${keyword}`, (data: any[], status: string) => {
      if (status !== kakao.maps.services.Status.OK) {
        setMapLength(0);
        return;
      }

      setMapLength(data.length);

      if (status === kakao.maps.services.Status.OK) {
        const bounds = new kakao.maps.LatLngBounds();
        const newMarkers = data.map((item) => ({
          position: {
            lat: parseFloat(item.y),
            lng: parseFloat(item.x),
          },
          content: item.place_name,
        }));

        setMarkers(newMarkers);

        newMarkers.forEach((marker) => {
          bounds.extend(new kakao.maps.LatLng(marker.position.lat, marker.position.lng));
        });

        map.setBounds(bounds);
      }
    });
  };

  return (
    <div>
      {kakao && (
        <div id="map" style={{ width: "100%", height: "700px", borderRadius: "20px" }}>
          <Map
            center={{ lat: defaultLatitude, lng: defaultLongitude }}
            style={{ width: "100%", height: "700px", borderRadius: "20px" }}
            level={3}
            onCreate={setMap}
          >
            {markers.map((marker) => (
              <MapMarker
                key={`marker-${marker.content}-${marker.position.lat},${marker.position.lng}`}
                position={marker.position}
                onClick={() => setInfo(marker)}
                image={{
                  src: "https://cdn-icons-png.flaticon.com/512/5737/5737612.png",
                  size: {
                    width: 50,
                    height: 50,
                  },
                }}
              >
                {info && info.content === marker.content && (
                  <div
                    style={{
                      color: "#000",
                      fontSize: "16px",
                      fontWeight: "600",
                      border: "2px solid #e30d7c",
                      borderRadius: "5px",
                      padding: "5px",
                      backgroundColor: "#ffffff",
                      boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)",
                      width: "300px",
                      textAlign: "center",
                    }}
                  >
                    ☕ {marker.content}
                  </div>
                )}
              </MapMarker>
            ))}
          </Map>
        </div>
      )}
    </div>
  );
};

export default PropertyKakaoMap;

 

 

 

 

 

"use client";
import React, { useState, useEffect, useCallback } from "react";
import { useKakaoMaps } from "@/utils/properties/useKakaoMaps";
import { Map, MapMarker, MapTypeId } from "react-kakao-maps-sdk";

interface PropertyKakaoMapProps {
  address: string;
}

const PropertyKakaoMap: React.FC<PropertyKakaoMapProps> = ({ address }) => {
  const defaultAddress = "제주특별자치도 제주시 첨단로 242";
  const currentAddress = address || defaultAddress;

  const defaultLatitude = 33.450701;
  const defaultLongitude = 126.570667;

  const [latitude, setLatitude] = useState(defaultLatitude);
  const [longitude, setLongitude] = useState(defaultLongitude);
  const [level, setLevel] = useState(3);

  const kakao = useKakaoMaps(process.env.NEXT_PUBLIC_KAKAOMAP_API_KEY || "");
  const [info, setInfo] = useState<any>(null);
  const [marker, setMarker] = useState<{ position: { lat: number; lng: number }; content: string } | null>(null);
  const [map, setMap] = useState<any>(null);
  const [mapType, setMapType] = useState<MapTypeId>(kakao.maps.MapTypeId.ROADMAP);

  useEffect(() => {
    if (!kakao) return;

    // SDK 로드가 완료된 후에 지도를 생성합니다.
    window.kakao.maps.load(() => {
      const geocoder = new kakao.maps.services.Geocoder();
      geocoder.addressSearch(currentAddress, (result: any[], status: string) => {
        if (status === kakao.maps.services.Status.OK) {
          const coords = new kakao.maps.LatLng(result[0].y, result[0].x);

          setLatitude(coords.getLat());
          setLongitude(coords.getLng());
          setMarker({
            position: { lat: coords.getLat(), lng: coords.getLng() },
            content: currentAddress,
          });
        } else {
          console.error("주소 검색 실패:", status);
        }
      });
    });
  }, [kakao, currentAddress]);

  useEffect(() => {
    if (!map) return;
    map.setMapTypeId(mapType);
  }, [mapType, map]);

  const toggleMapType = useCallback((type: MapTypeId) => {
    setMapType(type);
  }, []);

  const zoomIn = useCallback(() => {
    setLevel((prev) => (prev > 1 ? prev - 1 : prev));
  }, []);

  const zoomOut = useCallback(() => {
    setLevel((prev) => (prev < 14 ? prev + 1 : prev));
  }, []);

  return (
    <div>
      {kakao && (
        <div style={{ position: "relative" }}>
          <div
            style={{
              position: "absolute",
              top: "10px",
              right: "10px",
              zIndex: 2,
              display: "flex",
              flexDirection: "column",
              gap: "10px",
            }}
          >
            <button
              onClick={() => toggleMapType(kakao.maps.MapTypeId.ROADMAP)}
              style={{
                backgroundColor: "#ffffff",
                border: "none",
                padding: "10px",
                borderRadius: "5px",
                boxShadow: "0 0 5px rgba(0, 0, 0, 0.3)",
                cursor: "pointer",
              }}
            >
              일반지도 보기
            </button>
            <button
              onClick={() => toggleMapType(kakao.maps.MapTypeId.HYBRID)}
              style={{
                backgroundColor: "#ffffff",
                border: "none",
                padding: "10px",
                borderRadius: "5px",
                boxShadow: "0 0 5px rgba(0, 0, 0, 0.3)",
                cursor: "pointer",
              }}
            >
              스카이뷰 보기
            </button>
            <button
              onClick={() => toggleMapType(kakao.maps.MapTypeId.ROADVIEW)}
              style={{
                backgroundColor: "#ffffff",
                border: "none",
                padding: "10px",
                borderRadius: "5px",
                boxShadow: "0 0 5px rgba(0, 0, 0, 0.3)",
                cursor: "pointer",
              }}
            >
              로드뷰 보기
            </button>
            <button
              onClick={zoomIn}
              style={{
                backgroundColor: "#ffffff",
                border: "none",
                padding: "10px",
                borderRadius: "5px",
                boxShadow: "0 0 5px rgba(0, 0, 0, 0.3)",
                cursor: "pointer",
              }}
            >
              +
            </button>
            <button
              onClick={zoomOut}
              style={{
                backgroundColor: "#ffffff",
                border: "none",
                padding: "10px",
                borderRadius: "5px",
                boxShadow: "0 0 5px rgba(0, 0, 0, 0.3)",
                cursor: "pointer",
              }}
            >
              -
            </button>
          </div>
          <div id="map" style={{ width: "100%", height: "700px", borderRadius: "20px" }}>
            <Map
              center={{ lat: latitude, lng: longitude }}
              style={{ width: "100%", height: "700px", borderRadius: "20px" }}
              level={level}
              onCreate={setMap}
            >
              {marker && (
                <MapMarker
                  position={marker.position}
                  onClick={() => setInfo(marker)}
                  image={{
                    src: "https://cdn-icons-png.flaticon.com/512/5737/5737612.png",
                    size: {
                      width: 50,
                      height: 50,
                    },
                  }}
                >
                  {info && info.content === marker.content && (
                    <div
                      style={{
                        color: "#000",
                        fontSize: "16px",
                        fontWeight: "600",
                        border: "2px solid #e30d7c",
                        borderRadius: "5px",
                        padding: "5px",
                        backgroundColor: "#ffffff",
                        boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)",
                        width: "300px",
                        textAlign: "center",
                      }}
                    >
                      ☕ {marker.content}
                    </div>
                  )}
                </MapMarker>
              )}
            </Map>
          </div>
        </div>
      )}
    </div>
  );
};

export default PropertyKakaoMap;

 

 

 

 

 

 

 

tailwind 적용

"use client";
import React, { useState, useEffect } from "react";
import { useKakaoMaps } from "@/utils/properties/useKakaoMaps";
import { Map, MapMarker } from "react-kakao-maps-sdk";

interface PropertyKakaoMapProps {
  address: string;
}

const PropertyKakaoMap: React.FC<PropertyKakaoMapProps> = ({ address }) => {
  const defaultAddress = "제주특별자치도 제주시 첨단로 242";
  const currentAddress = address || defaultAddress;

  const defaultLatitude = 33.450701;
  const defaultLongitude = 126.570667;

  const [latitude, setLatitude] = useState(defaultLatitude);
  const [longitude, setLongitude] = useState(defaultLongitude);
  const [level, setLevel] = useState(3);

  const kakao = useKakaoMaps(process.env.NEXT_PUBLIC_KAKAOMAP_API_KEY || "");
  const [info, setInfo] = useState<any>(null);
  const [marker, setMarker] = useState<{ position: { lat: number; lng: number }; content: string } | null>(null);
  const [map, setMap] = useState<any>(null);
  const [isSkyview, setIsSkyview] = useState(false);

  useEffect(() => {
    if (!kakao) return;

    // SDK 로드가 완료된 후에 지도를 생성합니다.
    window.kakao.maps.load(() => {
      const geocoder = new kakao.maps.services.Geocoder();
      geocoder.addressSearch(currentAddress, (result: any[], status: string) => {
        if (status === kakao.maps.services.Status.OK) {
          const coords = new kakao.maps.LatLng(result[0].y, result[0].x);

          const newMarker = {
            position: { lat: coords.getLat(), lng: coords.getLng() },
            content: currentAddress,
          };

          setLatitude(coords.getLat());
          setLongitude(coords.getLng());
          setMarker(newMarker);
        } else {
          console.error("주소 검색 실패:", status);
        }
      });
    });
  }, [kakao, currentAddress]);

  useEffect(() => {
    if (!map) return;
    map.setMapTypeId(isSkyview ? kakao.maps.MapTypeId.HYBRID : kakao.maps.MapTypeId.ROADMAP);
  }, [isSkyview, map, kakao]);

  const toggleMapType = () => {
    setIsSkyview((prev) => !prev);
  };

  const zoomIn = () => {
    setLevel((prev) => (prev > 1 ? prev - 1 : prev));
  };

  const zoomOut = () => {
    setLevel((prev) => (prev < 14 ? prev + 1 : prev));
  };

  return (
    <div>
      {kakao && (
        <div className="relative">
          <div className="absolute top-2 right-2 z-10 flex flex-col space-y-2">
            <button
              onClick={toggleMapType}
              className="bg-blue-500 border
               border-blue-600 text-white p-2 rounded shadow-lg  hover:bg-blue-600 focus:outline-none"
            >
              {isSkyview ? "일반지도 보기" : "스카이뷰 보기"}
            </button>
            <button
              onClick={zoomIn}
              className="bg-blue-500 border border-blue-600 text-white p-2 rounded shadow-lg hover:bg-blue-600 focus:outline-none"
            >
              +
            </button>
            <button
              onClick={zoomOut}
              className="bg-blue-500 border border-blue-600 text-white p-2 rounded shadow-lg hover:bg-blue-600 focus:outline-none"
            >
              -
            </button>
          </div>
          <div id="map" className="w-full h-[700px] rounded-2xl">
            <Map
              center={{ lat: latitude, lng: longitude }} // 기본 중심 좌표
              style={{ width: "100%", height: "700px", borderRadius: "20px" }}
              level={level}
              onCreate={setMap}
            >
              {marker && (
                <MapMarker
                  position={marker.position}
                  onClick={() => setInfo(marker)}
                  image={{
                    src: "https://cdn-icons-png.flaticon.com/512/5737/5737612.png",
                    size: {
                      width: 50,
                      height: 50,
                    },
                  }}
                >
                  {info && info.content === marker.content && (
                    <div
                      className="text-black text-lg font-semibold border-2 border-blue-500 rounded p-2 bg-white shadow-lg w-[300px] text-center"
                    >
                      ☕ {marker.content}
                    </div>
                  )}
                </MapMarker>
              )}
            </Map>
          </div>
        </div>
      )}
    </div>
  );
};

export default PropertyKakaoMap;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

Better early than late. (쇠뿔도 단김에 빼랬다.)

댓글 ( 0)

댓글 남기기

작성