커스텀 오버레이 생성
커스텀 오버레이 객체 생성
먼저 커스텀 오버레이 객체를 생성해준다.
// Main.jsx
/* 생략 */
function MainPage() {
const [customOverlay, setCustomOverlay] = useState(null);
// 카카오맵 초기화
const initMap = () => {
/* 생략 */
// 커스텀 오버레이 생성
const customOverlay = new kakao.maps.CustomOverlay({
// 지도 중심좌표에 마커를 생성
map: map, // 마커를 생성할 지도
position: null, // 마커의 위치(LatLng)
content: "", // 마커의 내용(HTML 형식)
yAnchor: 0 // 컨텐츠의 y축 위치 (0~1 사이 값)
});
setCustomOverlay(customOverlay);
}
/* 생략 */
}
시작하자마자 고정된 좌표에 마커를 띄우고 싶다면, 커스텀 오버레이 생성 시에 position
과 content
값을 넣어주면 된다. 하지만 나는 지도를 클릭할 때, 클릭한 위치에 마커를 띄울 것이기 때문에 position
을 null로 설정해주었다.
지도 클릭 이벤트 리스너 생성 및 맵 위에 커스텀 오버레이 표시
지도를 클릭할 때 발생하는 이벤트 리스너를 생성해준다.
// Main.jsx
/* 생략 */
// map 객체가 바뀔 때 마다 실행
useEffect(() => {
if (!map) { return; }
// 카카오맵에 클릭 시 실행되는 리스너 추가
kakao.maps.event.addListener(map, "click", (mouseEvent) => {
customOverlay.setMap(null); // 맵에 존재하는 커스텀 오버레이 비활성화
customOverlay.setContent("");
// 클릭한 위도, 경도 정보를 가져옵니다
var latlng = mouseEvent.latLng;
console.log(latlng);
// 오버레이에 표시할 컨텍스트 저장
var content = `
<div class="custom-overlay">
<span class="title">
Lat: ${latlng.getLat()}
<br>
Lng: ${latlng.getLng()}
`;
customOverlay.setContent(content);
customOverlay.setPosition(latlng);
customOverlay.setMap(map);
});
}, [map]);
/* 생략 */
컨텐츠 안에 있는 custom-overlay
와 title
은 모두 css 파일에서 설정해주었다.
/* Main.css */
/* overlay */
.custom-overlay {
background-color: #fafafa;
filter: drop-shadow(0px 3px 6px rgba(0, 0, 0, 0.2));
border-radius: 10px;
position: relative;
width: auto;
height: auto;
padding-top: 4px;
padding-bottom: 4px;
padding-left: 8px;
padding-right: 8px;
bottom: 80px;
}
.custom-overlay:after {
border-top: 15px solid #fafafa;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 0px solid transparent;
content: "";
position: absolute;
bottom: -10px;
left: calc(50% - 5px);
}
.custom-overlay .title {
display: block;
text-align: center;
padding: 10px 15px;
font-family: "SC Dream 6";
font-weight: normal;
font-size: 14px;
color: #191919;
}
npm start
로 리액트 앱을 실행시켜 확인해보면 아래 화면과 같이 나온다.
참고로 커스텀 오버레이 마커에 css 스타일을 적용시킬 때, css 내부에서 마커의 위치를 조정하는 것은 지양하는게 좋다.
예를 들어 margin-bottom: 10px;
를 적용했다고 가정했을 때, 계산 방식이 지도상에서의 커스텀 오버레이 위치 + 그 위치를 기준으로 CSS로 이동시킨 픽셀
이기 때문에 확대 축소 정도에 따라 위치가 많이 달라 보일 수 있다.
따라서 마커 자체의 위치를 옮기고 싶다면 xAnchor
와 yAnchor
옵션을 사용해주는 것이 좋다.
Ref: https://devtalk.kakao.com/t/customoverlay-xanchor-yanchor/115607
오버레이 마커에 지번 주소 출력
카카오맵 API에서 제공하는 자바스크립트용 주소 제공 함수가 있지만, 나는 백엔드에서 구현했다. 자세한 설명은 아래 문서에 나와있다.
https://apis.map.kakao.com/web/documentation/#services_Geocoder_coord2Address
백엔드는 파이썬의 Flask를 사용하였다.
def get_pnu(lat: float, lng: float):
"""입력된 좌표의 PNU 코드와 지번주소 조회
Params:
lat `float`:
위도 좌표
lng `float`:
경도 좌표
Returns:
pnu `str`:
19자리 PNU 코드
addressName `str`:
토지 지번 주소
"""
# 로컬 API 인스턴스 생성
local = Local(service_key = "카카오 API에서 제공하는 서비스 키 값")
request_address = local.geo_coord2address(lng, lat, dataframe=False)
request_region = local.geo_coord2regioncode(lng, lat, dataframe=False)
if request_region == None:
return None, None
if request_region["documents"][0]["region_type"] == "B":
# 데이터 형식이 법정동일때
pnu = request_region["documents"][0]["code"]
address_name = request_region["documents"][0]["address_name"]
address_name = address_name + " " + request_address["documents"][0]["address"]["main_address_no"]
if request_address["documents"][0]["address"]["sub_address_no"] != "":
address_name = address_name + "-" + request_address["documents"][0]["address"]["sub_address_no"]
if request_address["documents"][0]["address"]["mountain_yn"] == "N":
mountain = "1" # 산 X
else:
mountain = "2" # 산 O
else:
pnu = request_region["documents"][1]["code"]
addressName = request_region["documents"][1]["address_name"]
addressName = request_region["documents"][1]["address_name"]
addressName = addressName + " " + request_address["documents"][1]["address"]["main_address_no"]
if request_address["documents"][1]["address"]["sub_address_no"] != "":
address_name = address_name + "-" + request_address["documents"][0]["address"]["sub_address_no"]
if request_address["documents"][1]["address"]["mountain_yn"] == "N":
mountain = "1" # 산 X
else:
mountain = "2" # 산 O
# 본번과 부번의 포멧을 '0000'으로 맞춰줌
main_no = request_address["documents"][0]["address"]["main_address_no"].zfill(4)
sub_no = request_address["documents"][0]["address"]["sub_address_no"].zfill(4)
pnu = str(pnu + mountain + main_no + sub_no)
return pnu, address_name
@app.route("/get_pnu", methods=["GET"])
def GetPNU():
"""입력된 좌표의 PNU 코드 조회
Params:
lat `float`:
위도 좌표
lng `float`:
경도 좌표
Returns:
result `str`:
응답 성공 여부 (success, error)
msg `str`:
응답 메시지
pnu `str`:
19자리 PNU 코드
addressName `str`:
토지 지번 주소
"""
lat = request.args.get("lat")
lng = request.args.get("lng")
if not lat or not lng:
return jsonify({"result":"error", "msg":"lat or lng parameter missing"}), 400
pnu, address = get_pnu(float(lat), float(lng))
if pnu == None or address == None:
return jsonify({"result":"error", "msg":"PNU code for the requested coordinates does not exist"}), 422
else:
return jsonify({"result":"success", "msg":"get pnu", "pnu":pnu, "addressName":address}), 200
위 코드는 PNU 코드 값과 지번 주소를 받아오는 코드로, 본인이 지번 주소만 받아오고 싶다 하면 코드는 더 간단해진다.
이제 axios
를 사용해 GET으로 데이터를 받아오면 된다.
// map 객체가 바뀔 때 마다 실행
useEffect(() => {
if (!map) { return; }
// 카카오맵에 클릭 시 실행되는 리스너 추가
kakao.maps.event.addListener(map, "click", (mouseEvent) => {
customOverlay.setMap(null); // 맵에 존재하는 커스텀 오버레이 비활성화
customOverlay.setContent("");
// 클릭한 위도, 경도 정보를 가져옵니다
var latlng = mouseEvent.latLng;
axios.get(`${process.env.REACT_APP_API_URL}/get_pnu?lat=${latlng.getLat()}&lng=${latlng.getLng()}`)
.then(function(response) {
// 오버레이에 표시할 컨텍스트 저장
var content = `
<div class="custom-overlay">
<span class="title">
${response.data.addressName}
<br>
`;
customOverlay.setContent(content);
customOverlay.setPosition(latlng);
customOverlay.setMap(map);
}).catch(function(error) {
alert("토지의 지번 주소를 불러오는 중 문제가 발생했습니다.");
});
});
}, [map]);
카카오맵의 이벤트 리스너는 맵 객체가 바뀔 때 마다 새로 추가해주어야 한다. 맵 객체가 없을 때 이벤트 리스너를 등록하려고 하면 당연히 에러가 나기 때문에, 맵 객체가 null
일 경우 아무 동작도 하지 않고 리턴한다.
맵을 클릭하게 되면 서버에 데이터를 요청하게 되고, 정상적으로 응답이 왔다면 컨텍스트를 HTML 형식으로 지정, 맵에 지정한 위치에다가 커스텀 오버레이를 표시해준다.
정상적으로 지번 주소가 표시가 되는 것을 확인할 수 있다. 폴리곤도 커스텀 오버레이와 크게 다르지 않다.
폴리곤 생성
폴리곤 객체 생성
// Main.jsx
/* 생략 */
function MainPage() {
const [polygon, setPolygon] = useState(null);
// 카카오맵 초기화
const initMap = () => {
/* 생략 */
// 지도 위에 표시할 폴리곤 생성
const polygon = new kakao.maps.Polygon({
strokeWeight: 2, // 선의 두께
strokeColor: '#004c80', // 선의 색깔
strokeOpacity: 0.8, // 선의 불투명도. 1에서 0 사이의 값이며 0에 가까울수록 투명함
strokeStyle: 'solid', // 선의 스타일
fillColor: '#fff', // 채우기 색깔
fillOpacity: 0.7, // 채우기 불투명도
});
setPolygon(polygon);
}
/* 생략 */
}
폴리곤 객체를 생성해준다. 각 파라미터의 역할은 주석으로 설명해두었다.
브이월드 API에서 연속지적도 받아오기
이제 클릭 시 해당 위치의 연속지적도가 표시되도록 할 것이기 때문에, 브이월드에서 연속 지적도를 받아온다.
@app.route("/get_land_geometry", methods=["GET"])
def GetGeo():
"""연속지적도를 받아오는 함수
Params:
lat `float`:
위도 좌표
lng `float`:
경도 좌표
Returns:
result `str`:
응답 성공 여부 (success, error)
msg `str`:
응답 메시지
geometry `list`:
연속지적도 데이터
"""
lat = request.args.get("lat")
lng = request.args.get("lng")
pnu = request.args.get("pnu")
if pnu:
if lat or lng:
return jsonify({"result":"error", "msg":"you cannot request pnu and coordinates at the same time"}), 400
else:
if not lat or not lng:
return jsonify({"result":"error", "msg":"request parameter missing"}), 400
else:
pnu, address = get_pnu(float(lat), float(lng))
# 엔드포인트
endpoint = "http://api.vworld.kr/req/data"
# 요청 파라미터
service = "data"
key = "API 키 값"
req = "GetFeature"
data = "LP_PA_CBND_BUBUN"
page = 1
size = 1000
attrFilter = f"pnu:=:{pnu}"
# 요청 URL
url = f"{endpoint}?service={service}&request={req}&data={data}&key={key}&attrFilter={attrFilter}&page={page}&size={size}"
# 요청 결과
res = json.loads(requests.get(url).text)
if (res["response"]["status"] == "NOT_FOUND"):
return jsonify({"result":"error", "msg":"geometry data for the requested coordinates does not exist"}), 422
# GeoJson 생성
feature_collection = res["response"]["result"]["featureCollection"]
return jsonify({"result":"success", "msg":"get land geometry data", "geometry":feature_collection["features"][0]["geometry"]["coordinates"]}), 200
사실 리액트에서 바로 브이월드 API에 연속 지적도 데이터 요청을 해도 되지만, 나는 백엔드에서 데이터 처리 후 웹으로 보내주는 방식을 선택했다. 어떤 방식을 사용하던 큰 차이는 없는 것 같다고 생각한다.
폴리곤 맵 위에 표시하기
아까 지번 주소와 동일하게 axios
로 데이터를 요청 후 받아온다.
// map 객체가 바뀔 때 마다 실행
useEffect(() => {
if (!map) { return; }
// 카카오맵에 클릭 시 실행되는 리스너 추가
kakao.maps.event.addListener(map, "click", (mouseEvent) => {
polygon.setMap(null); // 맵에 존재하는 폴리곤 비활성화
// 지적도 조회
axios.get(`${process.env.REACT_APP_API_URL}/get_land_geometry?lat=${latlng.getLat()}&lng=${latlng.getLng()}`)
.then(function(geometryResponse) {
// 다각형을 구성하는 좌표 배열. 이 좌표들을 이어서 다각형을 표시한다.
var path = new Array();
for (var i = 0; i < geometryResponse.data.geometry[0][0].length; i++) {
var polygon_latlng = new kakao.maps.LatLng(geometryResponse.data.geometry[0][0][i][1], geometryResponse.data.geometry[0][0][i][0]);
path.push(polygon_latlng);
}
polygon.setPath(path);
polygon.setMap(map);
}).catch(function(error) {
alert("해당 토지의 연속지적도 데이터가 존재하지 않습니다.");
});
});
}, [map]);
연속지적도 값을 배열에 넣어준 후, 폴리곤의 path 값에 넣어준다.
'WEB > React' 카테고리의 다른 글
React: Kakao map API 사용하기 (0) | 2023.11.06 |
---|