[Vue.js] Vue Kakao 지도 구현 05 - Polyline 03 경로 수정
[Vue.js] Vue Kakao 지도 구현 05 - Polyline 03 경로 수정
Vue.js 기반 프로젝트에서 Kakao 지도 api에서 제공하는 Polyline을 사용해서 경로를 편집합니다.
- 생성된 경로 클릭해서 선택
- 경로를 구성하는 위치마다 마커 생성
- 마커를 드래그해서 경로 수정

경로 정보 생성 및 편집 기능을 담당하는 상태 클래스들을 구현합니다.
- NewState - 새로운 경로 생성을 담당함
- NormalState - 경로 생성을 완료한 이후의 기본 상태
- EditState - 경로 편집 기능을 담당함
상태전이
NEW
|
[완료버튼]
|
V
NORMAL ---> [경로선택] ----> EDIT
^ |
| |
+--------- [편집완료]----------+
Section 1에서는 소스코드를 첨부합니다.
Section 2에서는 코드에 대해 설명합니다.
1. 소스코드
이전 구현(경로 생성)에서 seg-state.js가 추가됨
프로젝트 구조[PROJECT_ROOT]
+- public
| +- index.html
|
+- src
| +- App.vue (E)
| +- main.js
| +- segment.js (E)
| +- seg-state.js (+)
|
+- package.json
- index.html - 수정없음
- main.js - 수정없음
- App.vue - 애플리케이션 Root Component(수정)
- segment.js - Polyline을 사용해서 경로를 관리(수정)
- seg-state.js - 상태 관리용 클래스들(+)
1.1. seg-state.js(추가)
세가지 상태(NEW, NORMAL, EDIT)에 따라 지도에 다양한 리스너를 연결하고 해제하는 역할을 합니다. 이외에도 각 상태를 반영하는 지도나 경로의 생김새(선의 색깔 굵기 등)를 제어합니다.
seg-state.jsconst { addListener, removeListener } = window.kakao.maps.event;
/**
* 경로 생성 상태
* - 완료 시 NORMAL 상태로 전환
*/
class NewState {
constructor() {
this.name = "NEW";
this.segments = [];
this.option = {};
this.listeners = {
click: (seg, latLng) => {
seg.addPoint(latLng);
seg.render();
},
};
}
ready(seg) {
const { map } = seg;
map.setCursor("crosshair");
const listeners = {
click: (e) => this.listeners.click(seg, e.latLng),
};
addListener(map, "click", listeners.click);
this.segments.push({ seg, listeners });
}
release(seg) {
// 상태 해제 : 리스너 제거함
const index = this.segments.findIndex((elem) => elem.seg === seg);
if (index >= 0) {
const elem = this.segments[index];
const { seg, listeners } = elem;
removeListener(seg.map, "click", listeners.click);
this.segments.splice(index, 1); // 제거함
}
}
}
class NormalState {
constructor() {
this.name = "NORMAL";
this.segments = [];
this.options = {
HOVER: {
strokeWidth: 5,
strokeColor: "#FF00FF",
},
NORMAL: {
strokeWidth: 3,
strokeColor: "blue",
},
};
}
ready(seg) {
// normal state
const { poly } = seg;
const listeners = {
mouseover: () => {
poly.setOptions(this.options.HOVER);
},
mouseout: () => {
poly.setOptions(this.options.NORMAL);
},
click: () => {
// console.log("[편집 상태로 전환");
seg.setState("EDIT");
},
};
seg.poly.setOptions(this.options.NORMAL);
addListener(poly, "mouseover", listeners.mouseover);
addListener(poly, "mouseout", listeners.mouseout);
addListener(poly, "click", listeners.click);
this.segments.push({ seg, listeners });
}
release(seg) {
// 리스너 해제함
const index = this.segments.findIndex((elem) => elem.seg === seg);
if (index >= 0) {
const elem = this.segments[index];
const { seg, listeners } = elem;
const { poly } = seg;
removeListener(poly, "mouseover", listeners.mouseover);
removeListener(poly, "mouseout", listeners.mouseout);
removeListener(poly, "click", listeners.click);
this.segments.splice(index, 1); // 제거함
}
}
}
/**
* 편집 상태 - 경로 위치를 수정함
* 완료 후 normal 로 복귀
*/
class EditState {
constructor() {
this.name = "EDIT";
this.segments = [];
}
ready(seg) {
if (this.segments.length === 1) {
// 편집은 하나의 경로만 허용하고 싶음
// 이미 존재하는 편집 상태가 있으면 해제함
this.segments[0].seg.setState("NORMAL");
}
// 마커 생성
const markers = seg.points.map((latLng, index) => {
const marker = new window.kakao.maps.Marker({
map: seg.map,
position: latLng,
draggable: true,
});
// addListener(marker, 'dragstart', () => {{}})
addListener(marker, "dragend", () => {
const latLng = marker.getPosition();
seg.points[marker.index] = latLng;
seg.render();
});
marker.index = index;
return marker;
});
this.segments.push({ seg, markers });
}
release(seg) {
// 리스너 해제함
const index = this.segments.findIndex((elem) => elem.seg === seg);
if (index >= 0) {
const elem = this.segments[index];
const { markers } = elem;
markers.forEach((marker) => {
marker.setMap(null); // 마커 지움
});
this.segments.splice(index, 1);
}
}
}
export { NewState, NormalState, EditState };
1.2. segment.js(수정)
경로의 상태 관리를 3개의 상태클래스들에게 맡깁니다.
segment.jsimport Vue from "vue";
import { NewState, NormalState, EditState } from "./seg-state";
const DEFAULT_KAKAOMAP_CURSOR =
'url("http://t1.daumcdn.net/mapjsapi/images/cursor/openhand.cur.ico") 7 5, url("http://t1.daumcdn.net/mapjsapi/images/cursor/openhand.cur.ico"), default';
const segState = Vue.observable({ target: null });
/**
* NEW(경로 생성 상태) -- 완료시 --> NORMAL ----> EDIT
* ^ |
* +-----완료---+
*/
const stateMap = new Map();
stateMap.set("NEW", new NewState());
stateMap.set("NORMAL", new NormalState());
stateMap.set("EDIT", new EditState());
// let currentSegment;
const startSegment = (mapInstance) => {
segState.target = new Segment(mapInstance);
segState.target.setState("NEW");
};
/**
* 경로를 나타내는 클래스
*/
class Segment {
/**
*
* @param {kakao.maps.Map} mapInstance 지도 인스턴스
* @param {object} props 경로 메타 정보들(이름 거리 등...)
*/
constructor(mapInstance, props) {
this.map = mapInstance;
this.done = false;
this.points = []; // list for LatLng
this.poly = new window.kakao.maps.Polyline({
map: this.map,
path: [],
});
// this.listeners = { click: null };
this.props = props || {};
}
get name() {
return this.props.name || "NO NAME";
}
get state() {
return (this.stateHandler && this.stateHandler.name) || null;
}
render() {
// 경로를 그림
this.poly.setPath(this.points);
}
addPoint(latLng) {
this.points.push(latLng);
}
setState(stateName) {
const prevStateHanlder = this.stateHandler;
if (prevStateHanlder) {
// 상태 해제
prevStateHanlder.release(this);
}
const stateHandler = stateMap.get(stateName);
this.stateHandler = stateHandler;
if (this.stateHandler) {
this.stateHandler.ready(this);
}
if (this.state === "NORMAL") {
segState.target = null;
} else {
segState.target = this;
}
}
commit() {
// 경로 작성 완료
this.done = true;
// 커서를 가져오는 API가 없습니다.
// 걍 이렇게 받아놓고 복구시킴
this.map.setCursor(DEFAULT_KAKAOMAP_CURSOR);
console.log("[해제 완료]");
this.setState("NORMAL");
}
dispose() {
console.log("경로 제거");
this.commit(); // 커서 복귀
this.poly.setMap(null);
}
}
export { segState };
export default startSegment;
- installListener() 기능이 NormalState로 옮겨감
- Vue.observable(..) 을 사용해서 경로의 상태를 반응형 객체로(reactive) 변경합니다. 화면은 반응형 객체의 상태를 곧바로 반영해서 갱신됩니다.
- 반응형 객체인 segState를 export 합니다.
1.3. App.vue(수정)
segment.js에서 노출시킨 segState를 통해서 경로의 상태 변화를 화면에 반영하도록 코드를 수정함
App.vue<template>
<div id="app">
<div class="path-list">
<h3>경로</h3>
<button @click="processNewPath()">새로운 경로</button>
<div class="list-of-seg">
<div class="segment" v-for="(seg, index) in segments" :key="index">
<h4>{{ seg.name }}</h4>
<button @click="deletePath(seg, index)">삭제</button>
</div>
</div>
</div>
<div class="map-wrapper" ref="kakaomap">
<div class="map-controll" v-if="activeSegment">
<template v-if="activeSegment.state === 'NEW'">
<span>경로 생성 중</span>
<button class="btn-commit-seg" @click="commitPath()">생성 완료</button
><!--경로 종료할때 버튼-->
<button @click="cancelPath()">취소</button>
</template>
<template v-else-if="activeSegment.state === 'EDIT'">
<span>경로 수정 중</span>
<button class="btn-commit-seg" @click="commitPath()">
편집 완료
</button>
</template>
</div>
</div>
</div>
</template>
<script>
import startSegment, { segState } from "./segment";
export default {
name: "App",
components: {},
data() {
return {
mapInstance: null,
// activeSegment: null,
segments: [], // list of pathes
};
},
computed: {
activeSegment: () => segState.target,
},
mounted() {
// init map here
var container = this.$refs.kakaomap;
var options = {
center: new window.kakao.maps.LatLng(33.450701, 126.570667),
level: 3,
};
this.mapInstance = new window.kakao.maps.Map(container, options); //지도 생성 및 객체 리턴
},
methods: {
processNewPath() {
console.log("[new path] start");
// 지도 객체 전달
startSegment(this.mapInstance);
},
commitPath() {
if (this.activeSegment.state === "NEW") {
// 신규 생성인 경우만 추가함
this.segments.push(this.activeSegment); // 먼저 등록 후
}
this.activeSegment.commit(); // 참조 삭제됨
},
cancelPath() {
this.activeSegment.dispose(); // 경로 제거용
},
/**
* @param seg 지울 세그먼트(경로)
* @param index 인덱스값
*/
deletePath(seg, index) {
this.segments.splice(index, 1); // 1개만
seg.dispose();
},
},
};
</script>
<style lang="scss">
html,
body {
height: 100%;
margin: 0;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
height: 100%;
display: flex;
.path-list {
width: 240px;
}
.list-of-seg {
.segment {
display: flex;
padding-right: 8px;
margin: 4px 0px;
h4 {
margin: 0;
flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 4px 8px;
}
button {
white-space: nowrap;
padding: 4px 8px;
border: 1px solid #ccc;
background-color: white;
border-radius: 4px;
color: #777;
&:hover {
background-color: #efefef;
color: #333;
cursor: pointer;
}
&:active {
background-color: #ddd;
color: #000;
}
}
}
}
.map-wrapper {
flex: 1 1 auto;
.map-controll {
position: absolute;
top: 5px;
left: 5px;
z-index: 1000;
}
}
}
</style>
- data() 에 있던 activeSegment를 computed 로 옮김. 이 가상의 변수는 segState.target의 값을 반환합니다.
- 경로 생성과 경로 수정 상태에 따라 다른 컨트롤 버튼을 렌더링함
2. 설명
나중에...