Next.js, Prisma 및 Postgres를 사용하여 Google 지도 클론을 만드는 방법

이 기사는 Next.js 및 Prisma를 통해 Postgres 데이터베이스에 저장된 특정 지점에 마커를 표시하는 지도를 내 웹사이트에 구현하는 과정에 대한 문서입니다.

이 프로젝트를 시작하기 위해 다음 명령을 사용하여 Next.js 프로젝트를 만들었습니다.
npx create-next-app@latest
다음 단계에 따라 Heroku에서 호스팅되는 Postgres 데이터베이스를 만들었습니다.

그런 다음 Prisma를 통해 Next 프로젝트를 Postgres 데이터베이스에 연결해야 했습니다. 첫 번째 단계는 다음 명령으로 Prisma를 설치하는 것입니다.
npm install prisma --save-dev
그런 다음 다음을 실행하여 Prisma 프로젝트를 초기화했습니다.
npx prisma init
이렇게 하면 스키마를 정의하는 prisma.schema 파일이 추가됩니다. 또한 환경 변수를 정의할 수 있는 .env 파일을 생성합니다. 내 .env 파일에서 내 데이터베이스 링크를 정의했습니다. postgres 데이터베이스 설정 링크의 4단계를 따라 이를 찾을 수 있습니다.
DATABASE_URL="postgresql:blahblahblah"
그런 다음 prisma.schema 파일에 스키마를 생성했습니다. 스키마에 주소 필드를 포함해야 프로그램이 마커를 배치할 위치를 알 수 있습니다. 정보 창에 사용자에게 제공하고 싶은 다른 정보도 포함했습니다.

//prisma.schema
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Location {
  id        String     @default(cuid()) @id
  title     String
  address   String?
  website   String?
  phone     String?
}


스키마를 데이터베이스로 푸시
npx prisma db push
프리즈마 클라이언트 설치
npm install @prisma/client
프리즈마 클라이언트 업데이트
npx prisma generate
lib라는 새 디렉터리와 그 안에 prisma.js 파일을 만듭니다.

prisma.js 파일에서 Prisma 클라이언트의 인스턴스를 생성해야 합니다.

그런 다음 Prisma 클라이언트의 인스턴스를 필요한 파일로 가져올 수 있습니다.

//prisma.js
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()

export default prisma


실행npx prisma studio을 실행하여 Prisma 스튜디오를 열고 몇 가지 항목을 추가했습니다.

이제 내 프로젝트가 내 데이터베이스와 연결되었으므로 웹 페이지 구축을 시작할 수 있습니다.

나는 maps.js라는 페이지 디렉토리에 새 파일을 만들었습니다. 먼저 사용해야 하는 모든 패키지를 가져왔습니다. 상태를 관리하려면 React의 useState와 useRef가 필요합니다.
또한 Google 지도 API를 반응 애플리케이션에 연결하도록 설계된 패키지인 @react-google-maps/api 패키지에서 몇 가지를 가져와야 합니다.
또한 react-places-autocomplete 패키지에서 Google Places api searchbar를 애플리케이션에 쉽게 구현할 수 있는 몇 가지가 필요합니다.
또한 prisma.js 파일에서 prisma 인스턴스를 가져오고 next/script에서 스크립트 패키지를 가져왔습니다.

import React, {useState, useRef} from 'react';
import {GoogleMap, useLoadScript, Marker, InfoWindow,} from "@react-google-maps/api";
import PlacesAutocomplete, {geocodeByAddress, getLatLng} from 'react-places-autocomplete'

import Script from "next/script";
import prisma from "../lib/prisma";

const libraries = ['places']


이 모든 것을 가져온 후에는 데이터베이스에서 데이터를 쿼리할 수 있습니다.

export const getServerSideProps = async () => {
  const locations = await prisma.location.findMany();
  return { props: { locations } };
}


그런 다음 쿼리된 데이터를 소품으로 사용하여 새로운 기능적 구성 요소를 만들 수 있습니다.

const App = ({ locations }) => {

}


그런 다음 상태를 만들 것입니다. 나는 많은 상태를 만들었고 이것은 아마도 더 효율적인 방법으로 수행될 수 있지만 작동하므로 함께 가겠습니다.

const App = ({ locations }) => {

  const [center, setCenter] = useState({
    lat: 0,
    lng: 0,
  });

  const [address, setAddress] = useState("");

  const [coords, setCoords] = useState([]);

  const [mapRef, setMapRef] = useState(null);

  const [selected, setSelected] = useState(null);

  const mapRef2 = useRef();

  const options = {
    disableDefaultUI: true,
    zoomControl: true,
  }

}


mapRef2는 꽤 멍청하지만 누가 신경 쓰겠습니까.

다음으로 Google 지도 API에 연결해야 합니다. 앞에서 가져온 useLoadScript 함수를 통해 이 작업을 수행합니다. 첫 번째 단계는 Google 지도 API 키를 얻는 것입니다. 이에 대한 지침은 여기에서 찾을 수 있습니다.

The second step is to create a .env.local file in the root directory. You might be able to use the .env file that Prisma created but this is the way that I did it. In the .env.local file add the following line and insert your API Key.

NEXT_PUBLIC_MAPS_API_KEY=your-api-key

You can then use this api key in your component with the following function:

  const { isLoaded } = useLoadScript({
    googleMapsApiKey: process.env.NEXT_PUBLIC_MAPS_API_KEY,
    libraries,
  })

The libraries line at the end importants the places library.

Now we need to define a few functions that will be called later on in our code.

The first function takes the address that the user selects from the places autocomplete dropdown and it converts the address to latitude and longitude. It also sets the center to the new latitude and longitude.

  const handleSelect = async (value) => {
    const results = await geocodeByAddress(value);
    const latLng = await getLatLng(results[0]);
    setAddress(value);
    setCenter(latLng);
  };

The next function is the convertAddress function which is called onMapLoad and converts all of the addresses stored in the database to latitude and longitude points so that we can use those coordinates to display markers later on.

  const convertAddress = async (value) => {
    const results = await geocodeByAddress(value.address);
    const latLng = await getLatLng(results[0]);
    const locationData = {
      title: value.title,
      address: value.address,
      website: value.website,
      phone: value.phone,
      lat: latLng.lat,
      lng: latLng.lng
    }
    setCoords(coords => [...coords, locationData])
  };

The next function is called when someone clicks on a marker. What this function does is set the center of the map to whatever the current center is. It gets the current center through calling getCenter() on the mapRef.

  const onCenterChanged = () => {
    if (mapRef) {
      const newCenter = mapRef.getCenter();
      console.log(newCenter);
      setCenter({
        lat: mapRef.getCenter().lat(),
        lng: mapRef.getCenter().lng()
      })
    }
  }

The next function is called when the map loads, and it initializes the map as well as converts all of our addresses into latitude and longitude as mentioned earlier.

  const onCenterChanged = () => {
    if (mapRef) {
      const newCenter = mapRef.getCenter();
      console.log(newCenter);
      setCenter({
        lat: mapRef.getCenter().lat(),
        lng: mapRef.getCenter().lng()
      })
    }
  }

The final function just pans the map to a certain lat and long.

  const panTo = React.useCallback(({lat, lng}) => {
    mapRef2.current.panTo({lat, lng});
  }, [])

Overall our component looks like this right now:

const App = ({ locations }) => {

  const [center, setCenter] = useState({
    lat: 0,
    lng: 0,
  });

  const [address, setAddress] = useState("");

  const [coords, setCoords] = useState([]);

  const [mapRef, setMapRef] = useState(null);

  const [selected, setSelected] = useState(null);

  const mapRef2 = useRef();

  const options = {
    disableDefaultUI: true,
    zoomControl: true,
  }

  const { isLoaded } = useLoadScript({
    googleMapsApiKey: process.env.NEXT_PUBLIC_MAPS_API_KEY,
    libraries,
  })

  const handleSelect = async (value) => {
    const results = await geocodeByAddress(value);
    const latLng = await getLatLng(results[0]);
    setAddress(value);
    setCenter(latLng);
  };

  const convertAddress = async (value) => {
    const results = await geocodeByAddress(value.address);
    const latLng = await getLatLng(results[0]);
    const locationData = {
      title: value.title,
      address: value.address,
      website: value.website,
      phone: value.phone,
      lat: latLng.lat,
      lng: latLng.lng
    }
    setCoords(coords => [...coords, locationData])
  };

  const onCenterChanged = () => {
    if (mapRef) {
      const newCenter = mapRef.getCenter();
      console.log(newCenter);
      setCenter({
        lat: mapRef.getCenter().lat(),
        lng: mapRef.getCenter().lng()
      })
    }
  }



  const onMapLoad = (map) => {
    mapRef2.current = map
    setMapRef(map);
    {locations.map(location => {
      convertAddress(location)
    })}
  }

  const panTo = React.useCallback(({lat, lng}) => {
    mapRef2.current.panTo({lat, lng});
  }, [])

The first thing I did was create a button that got the coordinates of the user and panned the map to those coordinates.

<button className='locate' onClick={() => {
          setAddress('')
          navigator.geolocation.getCurrentPosition((position) => {
            panTo({
              lat: position.coords.latitude,
              lng: position.coords.longitude,
            })
            setCenter({
              lat: position.coords.latitude,
              lng: position.coords.longitude,
            })
          }, () => null);
        }}>Locate</button>

Then I created the map itself. Inside the map I mapped through the different coordinates that had been converted from our database, and I displayed a marker at each place. I also included an info window that displays the information of each place.

<GoogleMap
          zoom={10}
          center={{lat: center.lat, lng: center.lng}}
          mapContainerClassName='map-container'
          options={options}
          onLoad={onMapLoad}
          // onBoundsChanged={onCenterChanged}
        >
          {coords.map(coord => {
            return(
              <Marker
                key={coord.lat}
                position={{ lat: parseFloat(coord.lat), lng: parseFloat(coord.lng) }}
                onClick={() => {
                  onCenterChanged()
                  setSelected(coord);
                }}
              />
            )
          })}
          {selected ? (
            <InfoWindow
              position={{ lat: selected.lat, lng: selected.lng }}
              onCloseClick={() => {
                setSelected(null);
              }}
            >
              <div>
                <h2>
                  {selected.title}
                </h2>
                <p>{selected.address}</p>
              </div>
            </InfoWindow>
          ) : null
          }



        </GoogleMap>

Finally I added the places autocomplete searchbox. I also loaded the google maps places api through the script tag.

        <PlacesAutocomplete
          value={address}
          onChange={setAddress}
          onSelect={handleSelect}
        >
          {({ getInputProps, suggestions, getSuggestionItemProps }) => (
            <div>
              <input {...getInputProps({ placeholder: "Type address" })} />

              <div>
                {suggestions.map(suggestion => {
                  const style = {
                    backgroundColor: suggestion.active ? "#41b6e6" : "#fff"
                  };

                  return (
                    <div {...getSuggestionItemProps(suggestion, { style })}>
                      {suggestion.description}
                    </div>
                  );
                })}
              </div>
            </div>
          )}
        </PlacesAutocomplete>
        <Script
          src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBMePTwqFO2xPCaxUYqq0Vq4JQc631jo0o&libraries=places"
          strategy="beforeInteractive"
        ></Script>

That is pretty much it. Keep in mind that this code is far from perfect. Also this code has literally zero styling so it is very ugly. It works though which is pretty cool. All in all this is the final code.

//maps.js

import React, {useState, useRef} from 'react';
import {GoogleMap, useLoadScript, Marker, InfoWindow,} from "@react-google-maps/api";
import PlacesAutocomplete, {geocodeByAddress, getLatLng} from 'react-places-autocomplete'

import Script from "next/script";
import prisma from "../lib/prisma";

const libraries = ['places']

export const getServerSideProps = async () => {
  const locations = await prisma.location.findMany();
  return { props: { locations } };
}

const App = ({ locations }) => {

  const [center, setCenter] = useState({
    lat: 0,
    lng: 0,
  });

  const [address, setAddress] = useState("");

  const [coords, setCoords] = useState([]);

  const [mapRef, setMapRef] = useState(null);

  const [selected, setSelected] = useState(null);

  const mapRef2 = useRef();

  const options = {
    disableDefaultUI: true,
    zoomControl: true,
  }

  const { isLoaded } = useLoadScript({
    googleMapsApiKey: process.env.NEXT_PUBLIC_MAPS_API_KEY,
    libraries,
  })

  const handleSelect = async (value) => {
    const results = await geocodeByAddress(value);
    const latLng = await getLatLng(results[0]);
    setAddress(value);
    setCenter(latLng);
  };

  const convertAddress = async (value) => {
    const results = await geocodeByAddress(value.address);
    const latLng = await getLatLng(results[0]);
    const locationData = {
      title: value.title,
      address: value.address,
      website: value.website,
      phone: value.phone,
      lat: latLng.lat,
      lng: latLng.lng
    }
    setCoords(coords => [...coords, locationData])
  };

  const onCenterChanged = () => {
    if (mapRef) {
      const newCenter = mapRef.getCenter();
      console.log(newCenter);
      setCenter({
        lat: mapRef.getCenter().lat(),
        lng: mapRef.getCenter().lng()
      })
    }
  }



  const onMapLoad = (map) => {
    mapRef2.current = map
    setMapRef(map);
    {locations.map(location => {
      convertAddress(location)
    })}
  }

  const panTo = React.useCallback(({lat, lng}) => {
    mapRef2.current.panTo({lat, lng});
  }, [])

  if (!isLoaded) {
    return (
      <div>
        <p>Loading...</p>
      </div>
    )
  }

  if (isLoaded) {
    return(
      <div>
        <button className='locate' onClick={() => {
          setAddress('')
          navigator.geolocation.getCurrentPosition((position) => {
            panTo({
              lat: position.coords.latitude,
              lng: position.coords.longitude,
            })
            setCenter({
              lat: position.coords.latitude,
              lng: position.coords.longitude,
            })
          }, () => null);
        }}>Locate</button>

        <GoogleMap
          zoom={10}
          center={{lat: center.lat, lng: center.lng}}
          mapContainerClassName='map-container'
          options={options}
          onLoad={onMapLoad}
          // onBoundsChanged={onCenterChanged}
        >
          {coords.map(coord => {
            return(
              <Marker
                key={coord.lat}
                position={{ lat: parseFloat(coord.lat), lng: parseFloat(coord.lng) }}
                onClick={() => {
                  onCenterChanged()
                  setSelected(coord);
                }}
              />
            )
          })}
          {selected ? (
            <InfoWindow
              position={{ lat: selected.lat, lng: selected.lng }}
              onCloseClick={() => {
                setSelected(null);
              }}
            >
              <div>
                <h2>
                  {selected.title}
                </h2>
                <p>{selected.address}</p>
              </div>
            </InfoWindow>
          ) : null
          }

        </GoogleMap>

        <PlacesAutocomplete
          value={address}
          onChange={setAddress}
          onSelect={handleSelect}
        >
          {({ getInputProps, suggestions, getSuggestionItemProps }) => (
            <div>
              <input {...getInputProps({ placeholder: "Type address" })} />

              <div>
                {suggestions.map(suggestion => {
                  const style = {
                    backgroundColor: suggestion.active ? "#41b6e6" : "#fff"
                  };

                  return (
                    <div {...getSuggestionItemProps(suggestion, { style })}>
                      {suggestion.description}
                    </div>
                  );
                })}
              </div>
            </div>
          )}
        </PlacesAutocomplete>



        <Script
          src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBMePTwqFO2xPCaxUYqq0Vq4JQc631jo0o&libraries=places"
          strategy="beforeInteractive"
        ></Script>
      </div>
    )
  }
}


export default App;

Also there is an error on line 168 because I didn't include a key. It is not breaking but you can just add a key to solve it.

Booh yah.

좋은 웹페이지 즐겨찾기