在 Nextjs 中体验 Kakao 地图 API

힘센캥거루
2025년 1월 20일(수정됨)
56
nextjs
在 Nextjs 中体验 Kakao 地图 API-1

有时候会有这样的日子。

'哦... 刚才好像在超速摄像头前快速经过了?'

但是想查看超速摄像头的位置也不容易找到。

所以决定自己做一个。

1. 京畿公共数据

在 Nextjs 中体验 Kakao 地图 API-2

由于我主要去的地方是京畿道,所以决定使用京畿公共数据

进去一看,发现已经有用 Kakao 地图 API 实现的例子了。

做出来应该是这样的感觉。

在 Nextjs 中体验 Kakao 地图 API-3

从数据来看,摄像头不仅只是超速,还有其他许多种类。

种类包括 01:速度,02:信号,03:通行违规,04:非法停车,99:其他,总共 5 类。

现在因为是 TypeScript,所以需要指定返回类型...

在 Nextjs 中体验 Kakao 地图 API-4

输出值总共有 24 个?未免太多了。

先下载一个 json 数据,然后在 Ruitn 中丢进去,输出类型。

还是 AI 方便。

在 Nextjs 中体验 Kakao 地图 API-5
interface cameraDataType {
  MNLSS_TEFCM_INFO: string;
  SIDO_NM: string;
  SIGUN_NM: string;
  ROAD_KIND_NM: string;
  ROUTE_MANAGE_NO_INFO: string | null; // 可能为空,因此允许为 null
  ROUTE_NM: string;
  ROUTE_INFO: string;
  REFINE_ROADNM_ADDR: string;
  REFINE_LOTNO_ADDR: string;
  REFINE_WGS84_LAT: string;
  REFINE_WGS84_LOGT: string;
  INSTL_PLC: string;
  REGLT_DIV_NM: string;
  LIMITN_SPEED_INFO: string;
  PRTCAREA_DIV: string;
  INSTL_YY: string;
  MNGINST_NM: string;
  MNGINST_TELNO: string;
  DATA_STD_DE: string;
}

指定类型后,读入 JSON 文件并在地图上显示。

因为摄像头不会突然大量增加,所以这样做没问题。

2. Kakao 地图 API

现在该为使用 Kakao 地图 API 做基本设置了。

进入Kakao 开发者

在 Nextjs 中体验 Kakao 地图 API-6

然后进入我的应用程序。

如果没有自己的应用程序,就创建一个。

在 Nextjs 中体验 Kakao 地图 API-7

注册平台。

基本上除注册在这里的站点域名外,API 使用受限。

在 Nextjs 中体验 Kakao 地图 API-8

然后进入下方的 Kakao 地图,启用设置。

在 Nextjs 中体验 Kakao 地图 API-9

最后确认我的 API key。

在 Nextjs 中体验 Kakao 地图 API-10

将这个 key 复制到我的 .env 中。

因为是要暴露的 nextpublic key,直接绑定在 JavaScript 中也无所谓。

3. 使用 Kakao Map API SDK 实现地图

迫不及待想要制作了,于是决定使用在 GitHub 上找到的 Kakao 地图库。

链接JaeSeoKim 的 GitHub。

首先安装库。

yarn add react-kakao-maps-sdk

该库支持多种类型。

在 tsconfig.json 中按如下设置。

{
  ...,
  "compilerOptions": {
    ...,
    "types": [
      ...,
      "kakao.maps.d.ts"
    ]
  }
}

如果有 typeRoots,这个就不起作用。

这时按如下设置即可。

{
  "compilerOptions": {
     ...
    "typeRoots": [
      "./node_modules/kakao.maps.d.ts/@types"
    ]
     ...
  }
}

基本准备完成后,我们创建调用 Kakao 地图的组件。

脚本加载完成前调用 Map 会出错。

因此,通过 onReady 进行检查,加载完成后显示地图。

"use client";

import { Map, MapTypeControl, ZoomControl } from "react-kakao-maps-sdk";
import { useEffect, useState } from "react";
import { cameraCustomOverlay } from "@/types/allTypes";
import Script from "next/script";

export default function KakaoMapPage({
  data,
}: {
  data: cameraCustomOverlay[];
}) {
  const [scriptLoad, setScriptLoad] = useState<boolean>(false);

  return (
    <div className="items-center">
      <Script
        src={`//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.NEXT_PUBLIC_KAKAO_API_KEY}&autoload=false&libraries=clusterer`}
        onReady = {() => setScriptLoad(true)}
      />
      {scriptLoad ? (
        <Map // 显示地图的容器
          style={{ height: "calc(100svh - 100px)" }}
          className="w-full"
          id="map"
          center={{ lat: 37.44749167, lng: 127.1477194 }}
          level={3} // 地图缩放级别
        >
          <MapTypeControl position={"TOPRIGHT"} />
          <ZoomControl position={"RIGHT"} />
        </Map>
      ) : (
        <div></div>
      )}
    </div>
  );
}

现在可以在 Page 中调用这个,或把它插入 Page,运行结果如下。

已经能显示地图,一半成功了。

在 Nextjs 中体验 Kakao 地图 API-11

4. 制作标记

现在从摄像头数据中提取经度、纬度和速度限制数据,进行地图显示。

SDK 中标记的显示方法相当简单。

只需填入想显示位置的经度和纬度就完事。

<MapMarker position={{ lat: 37, lng: 128 }} />

粗略填入纬度和经度,结果标在忠州一处山野上。

在 Nextjs 中体验 Kakao 地图 API-12

在标签中加入 ReactNode,还能显示文字。

<MapMarker position={{ lat: 37, lng: 128 }}>
  <div>哦.. 这里是</div>
  <div>纬度 37 度吗?</div>
</MapMarker>
在 Nextjs 中体验 Kakao 地图 API-13

其实不算漂亮。

所以也支持自定义覆盖物。

使用 customOverlayMap 后即可使用。

<MapMarker position={{ lat: 37, lng: 128 }}></MapMarker>
<CustomOverlayMap position={{ lat: 37, lng: 128 }} yAnchor={2}>
<div>
    <div className="rounded-lg p-2 border border-red-500 bg-violet-600 text-white">
    <div>哦.. 这里是</div>
    <div>纬度 37 度吗?</div>
    </div>
</div>
</CustomOverlayMap>
在 Nextjs 中体验 Kakao 地图 API-14

关键是要在 MapMarker 标签外写。

在 MapMarker 内部声明会始终留着白框。

现在将其变为函数。

根据函数中数据的需要制作 cameraCustomOverlay 类型。

interface cameraCustomOverlay {
  lat: number;
  lng: number;
  title: string;
  limit: number;
}

const EventMarkerContainer = ({ data }: { data: cameraCustomOverlay }) => {
  const [isVisible, setIsVisible] = useState(false);
  const position = { lat: data.lat, lng: data.lng };

  return (
    <>
      <MapMarker
        position={position} // 显示标记的位置
        onClick={() => setIsVisible(!isVisible)}
      />
      {isVisible ? (
        <CustomOverlayMap
          position={position}
          yAnchor={1.5}
          key={`overlay-${data.lat}-${data.lng}`}
        >
          <div>
            <div className="bg-gray-50 text-black border-gray-500 rounded-lg border-2 relative">
              <div className="bg-slate-500 text-white p-2">摄像头信息</div>
              <div className="absolute top-0 right-0 p-2">
                <XCircleIcon
                  className="size-6 text-white"
                  onClick={() => setIsVisible(!isVisible)}
                />
              </div>
              <div className="p-2">
                <div className="break-words">{data.title}</div>
                <div>限速 : {data.limit}</div>
              </div>
            </div>
          </div>
        </CustomOverlayMap>
      ) : null}
    </>
  );
};

将鼠标悬停事件改变为点击事件,因为在手机上不能运行。

用 useState 使覆盖物可开关。

加工 JSON 后用 map 将标签插入即可。

5. 数据加载

读取 JSON,并解析后,用 filter 移除停车违规。

然后将经纬度、安装位置、限速等必要信息传递给组件。

import KakaoMapPage from "@/components/KakaoMapPage";
import fs from "fs";
import { cameraJsonPath } from "@/data/filePaths";
import { cameraDataType } from "@/types/allTypes";

export default function Page() {
  const dataJson = fs.readFileSync(cameraJsonPath, "utf-8");
  const allData: cameraDataType[] = JSON.parse(dataJson);
  const filterdMarkerData = allData.filter(
    (data) => !data.REGLT_DIV_NM.includes("4")
  );
  const markerData = filterdMarkerData.map((data) => {
    return {
      lat: Number(data.REFINE_WGS84_LAT),
      lng: Number(data.REFINE_WGS84_LOGT),
      title: data.INSTL_PLC,
      limit: Number(data.LIMITN_SPEED_INFO),
    };
  });

  return (
    <div className="items-center">
      <KakaoMapPage data={markerData} />
    </div>
  );
}

现在组件中利用这些数据显示标记。

export default function KakaoMapPage({
  data,
}: {
  data: cameraCustomOverlay[];
}) {
  ...
      {scriptLoad ? (
        <Map // 显示地图的容器
          style={{ height: "calc(100svh - 100px)" }}
          className="w-full"
          id="map"
          center={{lat: 37.44749167,lng: 127.1477194}}
          level={3} // 地图缩放级别
        >
          {data.map((camera, idx) => (
            <EventMarkerContainer // 创建标记
              key={idx}
              data={camera}
            />
          ))}
          <MapTypeControl position={"TOPRIGHT"} />
          <ZoomControl position={"RIGHT"} />
        </Map>
      ) : (
        <div></div>
   ...
  );
}

这样就可以显示标记,并在点击时显示自定义覆盖物。

一试,能很好地运行。

在 Nextjs 中体验 Kakao 地图 API-15

6. 获取用户位置信息

利用 navigator 可以获取用户的 GPS 信息。

用 useState 指定基本位置,在 useEffect 中用 navigator 获取用户位置信息,并重新分配地图位置。

  ...
  const [mapCenter, setMapCenter] = useState({
    lat: 37.44749167,
    lng: 127.1477194, // 基本位置 京畿道 城南
  });
  useEffect(() => {
    const { geolocation } = navigator;
    geolocation.getCurrentPosition(
      (position) => {
        const { latitude, longitude } = position.coords;
        setMapCenter({
          lat: latitude,
          lng: longitude, // 重新分配为用户位置
        });
      },
      (err) => console.log(err),
      { enableHighAccuracy: true, timeout: 5000, maximumAge: 0 }
    );
  }, []);
  ...

7. 问题

由于摄像头数据太多,地图非常卡顿。

现在用聚类来解决这个问题。

<MarkerClusterer averageCenter={true} minLevel={6}>
  {data.map((camera, idx) => (
    <EventMarkerContainer // 创建标记
      key={idx}
      data={camera}
    />
  ))}
</MarkerClusterer>

在 SDK 中,只需用 MarkerClusterer 标签包裹即可。

但问题在于 CustomOverlayMap 标签也被识别为标记,响应略有不同。

如果同时使用聚类和自定义覆盖物,需要像下面这样分离标签。

<MarkerClusterer averageCenter={true} minLevel={6}>
  {data.map((camera, idx) => (
    <MapMarker // 创建标记
      key={idx}
      position={{ lat: camera.lat, lng: camera.lng }}
    />
  ))}
</MarkerClusterer>;
{
  data.map((camera, idx) => (
    <CustomOverlayMap key={idx} position={{ lat: camera.lat, lng: camera.lng }}>
      我想要的内容
    </CustomOverlayMap>
  ));
}

现在要让各自的 MapMarker 和 CustomOverlayMap 共享 useState。

可以利用 key 实现。

 // 先用 useState 指定激活标记的 key。
  const [activeMarkerId, setActiveMarkerId] = useState<null|number>(null);
  ...
    <MarkerClusterer averageCenter={true} minLevel={6}>
    {data.map((camera, idx) => (
        <MapMarker // 创建标记
        key={idx}
        position={{ lat: camera.lat, lng: camera.lng }}
        onClick={()=> {
            setActiveMarkerId(activeMarkerId === idx ? null : idx);
            // 点击标记时,将标记的 idx 设置为激活 key。
            // 如果现有 key 与标记的 key 相同,则解除绑定。
        }}
        />
    ))}
    </MarkerClusterer>
    {data.map((camera, idx) => (
        // 仅当与激活的 key 一致时才显示覆盖物。
    activeMarkerId === idx && (
    <CustomOverlayMap
        key={idx}
        yAnchor={1.5}
        position={{ lat: camera.lat, lng: camera.lng }}
    >
        <div>
        <div className="bg-gray-50 text-black border-gray-500 rounded-lg border-2 relative">
            <div className="bg-slate-500 text-white p-2">摄像头信息</div>
            <div className="absolute top-0 right-0 p-2">
            <XCircleIcon className="size-6 text-white" 
            onClick={() => setActiveMarkerId(null)}
            />
            </div>
            <div className="p-2">
            <div className="break-words">{camera.title}</div>
            <div>限速 : {camera.limit}</div>
            </div>
        </div>
        </div>
    </CustomOverlayMap>
    )
    ))}

对于地理科学老师来说,这样已经走得太深了。

最后再试一次。

在 Nextjs 中体验 Kakao 地图 API-16

8. 后记

用了两天时间研究 Kakao 地图 API SDK。

找到不工作原因时很伤脑筋,但最后解决时感觉很棒。

如果有时间要试试 API 本身,但可能没有时间。

我大概就只能到这儿了...

不过,能学到新东西,度过了愉快的时光。

댓글을 불러오는 중...