[Vue.js] Vue Kakao 지도 구현 05 - Polyline 03 경로 수정

[Vue.js] Vue Kakao 지도 구현 05 - Polyline 03 경로 수정

Vue.js 기반 프로젝트에서 Kakao 지도 api에서 제공하는 Polyline을 사용해서 경로를 편집합니다.

  1. 생성된 경로 클릭해서 선택
  2. 경로를 구성하는 위치마다 마커 생성
  3. 마커를 드래그해서 경로 수정

경로 정보 생성 및 편집 기능을 담당하는 상태 클래스들을 구현합니다.

  • 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.js
const { 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.js
import 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. 설명

나중에...