├── src ├── index.js └── utils │ └── Dijkstra.js ├── images ├── path_result.gif ├── path_result.jpg └── dijkstra_example.png ├── index.html └── README.md /src/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/path_result.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse/javascript-subway-path-precourse/HEAD/images/path_result.gif -------------------------------------------------------------------------------- /images/path_result.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse/javascript-subway-path-precourse/HEAD/images/path_result.jpg -------------------------------------------------------------------------------- /images/dijkstra_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse/javascript-subway-path-precourse/HEAD/images/dijkstra_example.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 지하철 길찾기 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚇 지하철 노선도 경로 조회 미션 2 | - 등록된 지하철 노선도에서 경로를 조회하는 기능을 구현한다. 3 | 4 | ## 🚀 기능 요구사항 5 | > 프리코스 3주차 미션에서 사용한 코드를 참고해도 무관하다. 6 | 7 | ### 초기 설정 8 | - 프로그램 시작 시 역, 노선, 구간 데이터를 초기 설정 해야 한다. 9 | - 거리와 소요 시간은 양의 정수이며 단위는 km와 분을 의미한다. 10 | - 아래의 사전 등록 정보로 반드시 초기 설정을 한다. 11 | 12 | ``` 13 | 1. 지하철역으로 교대, 강남, 역삼, 남부터미널, 양재, 양재시민의숲, 매봉 역 정보가 등록되어 있다. 14 | 2. 지하철 노선으로 2호선, 3호선, 신분당선이 등록되어 있다. 15 | 3. 노선에 역이 아래와 같이 등록되어 있다.(왼쪽 끝이 상행 종점) 16 | - 2호선: 교대 - ( 2km / 3분 ) - 강남 - ( 2km / 3분 ) - 역삼 17 | - 3호선: 교대 - ( 3km / 2분 ) - 남부터미널 - ( 6km / 5분 ) - 양재 - ( 1km / 1분 ) - 매봉 18 | - 신분당선: 강남 - ( 2km / 8분 ) - 양재 - ( 10km / 3분 ) - 양재시민의숲 19 | ``` 20 | 21 | ### 경로 조회 기능 22 | 23 | 24 | - 출발역과 도착역을 입력받아 경로를 조회한다. 25 | - 경로 조회 시 총 거리, 총 소요 시간을 함께 출력한다. 26 | - 경로 조회 시 `최단 거리` 또는 `최소 시간` 옵션을 선택할 수 있다. 27 | 28 | ### 예외 처리 29 | - 출발역과 도착역은 2글자 이상이어야 한다. 30 | - 존재하지 않는 역을 출발역 또는 도착역으로 입력할 수 없다. 31 | - 경로 조회 시 출발역과 도착역이 같을 수 없다. 32 | - 경로 조회 시 출발역과 도착역이 연결되지 않으면 경로를 조회할 수 없다. 33 | - 그 외 정상적으로 프로그램이 수행되지 않은 경우 `alert`으로 에러를 출력한다. 34 | 35 |
36 | 37 | ## 💻 프로그래밍 실행 결과 38 | ### 경로 조회 39 | 40 | 41 | 42 | ## ✅ 프로그래밍 요구사항 43 | ### 길찾기 관련 기능 44 | - 출발역을 입력하는 input 태그는 `departure-station-name-input` id 속성값을 가진다. 45 | - 도착역을 입력하는 input 태그는 `arrival-station-name-input` id 속성값을 가진다. 46 | - 최단거리, 최소시간을 선택하는 radio는 `search-type` name 속성값을 가진다. 47 | - **radio option의 default 값은 최단거리이다.** 48 | - 길찾기 버튼은 `search-button` id 속성값을 가진다. 49 | - 📝 결과는 `table`을 이용하여 보여준다. 50 | 51 | ## ❗️힌트 52 | ## 데이터 초기화 53 | 자바스크립트에서 데이터를 초기화하는 방법 중에 하나는 아래와 같이 data를 `export`하고, `import`하는 것이다. 54 | 55 | ```javascript 56 | export const users = [ 57 | { 58 | name: 'Alt' 59 | }, 60 | { 61 | name: 'Jamie' 62 | }, 63 | { 64 | name: 'Sony' 65 | } 66 | ] 67 | 68 | export const courses = [ 69 | { 70 | name: 'frontend', 71 | }, 72 | { 73 | name: 'backend', 74 | }, 75 | { 76 | name: 'iOS', 77 | }, 78 | { 79 | name: 'Android', 80 | } 81 | ] 82 | 83 | ``` 84 | 위와 같이 데이터를 `export`하면 아래와 같이 데이터를 `import` 하여 사용할 수 있다. 85 | ```javascript 86 | import { users, courses } from './data.js' 87 | 88 | function App() { 89 | this.users = users 90 | this.courses = courses 91 | } 92 | ``` 93 | 94 | ## 최단 경로 라이브러리 95 | - `utils/Dijkstra.js` 라이브러리를 활용하면 간편하게 최단거리를 조회할 수 있다. 96 | - 정점(Vertex)과 간선(Edge), 그리고 가중치 개념을 이용 97 | - 정점: 지하철역 98 | - 간선: 지하철역 연결정보 99 | - 가중치: 거리 or 소요 시간 100 | - 최단 거리 기준 조회 시 가중치를 거리로 설정 101 | - 최소 시간 기준 조회 시 가중치를 시간으로 설정 102 | 103 | ```javascript 104 | import Dijkstra from "./utils/Dijkstra.js"; 105 | const dijkstra = new Dijkstra() 106 | 107 | //dijkstra.addEdge("출발역", "도착역", 거리 또는 시간); 108 | dijkstra.addEdge("V1", "V2", 2); 109 | dijkstra.addEdge("V2", "V3", 2); 110 | dijkstra.addEdge("V1", "V3", 100); 111 | 112 | const result = dijkstra.findShortestPath("V1", "V3"); 113 | // result = ["V1", "V2", "V3"] 114 | ``` 115 | 116 | #### 테스트설명 117 | 118 | 119 | - 역 사이의 거리를 고려하지 않는 경우 V1->V3 경로가 최단 경로 120 | - 역 사이의 거리를 고려할 경우 V1->V3 경로의 거리는 100km, V1->V2->V3 경로의 거리는 4km이므로 최단 경로는 V1->V2->V3 121 | 122 |
123 | 124 | ### 요구사항 125 | - 사용자가 잘못된 입력 값을 작성한 경우 `alert`을 이용해 메시지를 보여주고, 재입력할 수 있게 한다. 126 | - 외부 라이브러리(jQuery, Lodash 등)를 사용하지 않고, 순수 Vanilla JS로만 구현한다. 127 | - **자바스크립트 코드 컨벤션을 지키면서 프로그래밍** 한다 128 | - [https://google.github.io/styleguide/jsguide.html](https://google.github.io/styleguide/jsguide.html) 129 | - [https://ui.toast.com/fe-guide/ko_CODING-CONVENSION/](https://ui.toast.com/fe-guide/ko_CODING-CONVENTION) 130 | - **indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용**한다. 131 | - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. 132 | - 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다. 133 | - **함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다.** 134 | - 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다. 135 | - 변수 선언시 [var](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/var) 를 사용하지 않는다. [const](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/const) 와 [let](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/let) 을 사용한다. 136 | - [import](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/import) 문을 이용해 스크립트를 모듈화하고 불러올 수 있게 만든다. 137 | - [template literal](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Template_literals)을 이용해 데이터와 html string을 가독성 좋게 표현한다. 138 | 139 |
140 | 141 | ## 📝 미션 저장소 및 진행 요구사항 142 | 143 | - 미션은 [https://github.com/woowacourse/javascript-subway-path-precourse](https://github.com/woowacourse/javascript-subway-path-precourse) 저장소를 fork/clone해 시작한다. 144 | - **기능을 구현하기 전에 javascript-subway-path-precourse/docs/README.md 파일에 구현할 기능 목록**을 정리해 추가한다. 145 | - **git의 commit 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위로 추가**한다. 146 | - [프리코스 과제 제출](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 문서 절차를 따라 미션을 제출한다. 147 | -------------------------------------------------------------------------------- /src/utils/Dijkstra.js: -------------------------------------------------------------------------------- 1 | export default function Dijkstra() { 2 | const Node = { 3 | init: function (val, priority) { 4 | this.val = val; 5 | this.priority = priority; 6 | }, 7 | }; 8 | 9 | const PriorityQueue = { 10 | init: function () { 11 | this.values = []; 12 | }, 13 | enqueue: function (val, priority) { 14 | const newNode = Object.create(Node); 15 | newNode.init(val, priority); 16 | 17 | this.values.push(newNode); 18 | 19 | let idxOfNewNode = this.values.length - 1; 20 | 21 | while (idxOfNewNode > 0) { 22 | const idxOfParentNode = Math.floor((idxOfNewNode - 1) / 2); 23 | 24 | const parentNode = this.values[idxOfParentNode]; 25 | 26 | if (priority < parentNode.priority) { 27 | this.values[idxOfParentNode] = newNode; 28 | this.values[idxOfNewNode] = parentNode; 29 | idxOfNewNode = idxOfParentNode; 30 | continue; 31 | } 32 | break; 33 | } 34 | return this.values; 35 | }, 36 | dequeue: function () { 37 | if (this.values.length == 0) { 38 | return; 39 | } 40 | const dequeued = this.values.shift(); 41 | const lastItem = this.values.pop(); 42 | if (!lastItem) { 43 | return dequeued; 44 | } 45 | this.values.unshift(lastItem); 46 | 47 | let idxOfTarget = 0; 48 | 49 | while (true) { 50 | let idxOfLeftChild = idxOfTarget * 2 + 1; 51 | let idxOfRightChild = idxOfTarget * 2 + 2; 52 | let leftChild = this.values[idxOfLeftChild]; 53 | let rightChild = this.values[idxOfRightChild]; 54 | 55 | function swap(direction) { 56 | const idxOfChild = 57 | direction == "left" ? idxOfLeftChild : idxOfRightChild; 58 | const child = direction == "left" ? leftChild : rightChild; 59 | this.values[idxOfChild] = this.values[idxOfTarget]; 60 | this.values[idxOfTarget] = child; 61 | idxOfTarget = idxOfChild; 62 | } 63 | 64 | if (!leftChild) { 65 | return dequeued; 66 | } 67 | 68 | if (!rightChild) { 69 | if (leftChild.priority < lastItem.priority) { 70 | swap.call(this, "left"); 71 | continue; 72 | } 73 | return dequeued; 74 | } 75 | 76 | if (leftChild.priority == rightChild.priority) { 77 | swap.call(this, "left"); 78 | continue; 79 | } 80 | 81 | if ( 82 | leftChild.priority < rightChild.priority && 83 | leftChild.priority < lastItem.priority 84 | ) { 85 | swap.call(this, "left"); 86 | continue; 87 | } 88 | 89 | if ( 90 | rightChild.priority < leftChild.priority && 91 | rightChild.priority < lastItem.priority 92 | ) { 93 | swap.call(this, "right"); 94 | continue; 95 | } 96 | } 97 | }, 98 | }; 99 | 100 | const WeightedGraph = { 101 | init: function () { 102 | this.adjacencyList = {}; 103 | this.length = 0; 104 | }, 105 | addVertex: function (vertex) { 106 | if (!this.adjacencyList.hasOwnProperty(vertex)) { 107 | this.adjacencyList[vertex] = {}; 108 | this.length++; 109 | } 110 | }, 111 | addEdge: function (vertex1, vertex2, weight) { 112 | this.addVertex(vertex1); 113 | this.addVertex(vertex2); 114 | this.adjacencyList[vertex1][vertex2] = weight; 115 | this.adjacencyList[vertex2][vertex1] = weight; 116 | return this.adjacencyList; 117 | }, 118 | removeEdge: function (vertex1, vertex2) { 119 | if (!this.adjacencyList.hasOwnProperty(vertex1)) { 120 | return `There's no ${vertex1}`; 121 | } 122 | if (!this.adjacencyList.hasOwnProperty(vertex2)) { 123 | return `There's no ${vertex2}`; 124 | } 125 | 126 | function removeHelper(v1, v2) { 127 | if (!this.adjacencyList.hasOwnProperty(v1)) { 128 | return `There's no edge between ${v1} and ${v2}`; 129 | } 130 | delete this.adjacencyList[v1][v2]; 131 | if (Object.keys(this.adjacencyList[v1]).length == 0) { 132 | delete this.adjacencyList[v1]; 133 | } 134 | } 135 | 136 | removeHelper.call(this, vertex1, vertex2); 137 | removeHelper.call(this, vertex2, vertex1); 138 | 139 | return this.adjacencyList; 140 | }, 141 | removeVertex: function (vertex) { 142 | if (!this.adjacencyList.hasOwnProperty(vertex)) { 143 | return `There's no ${vertex}`; 144 | } 145 | const edges = this.adjacencyList[vertex]; 146 | for (const key in edges) { 147 | this.removeEdge(key, vertex); 148 | } 149 | return this.adjacencyList; 150 | }, 151 | findShortestRoute: function (start, end) { 152 | if (!start || !end) { 153 | throw Error("출발지와 도착지를 모두 입력해야 합니다."); 154 | } 155 | const distance = {}; 156 | const previous = {}; 157 | const pq = Object.create(PriorityQueue); 158 | pq.init(); 159 | pq.enqueue(start, 0); 160 | const visited = {}; 161 | 162 | const hashOfVertex = this.adjacencyList; 163 | for (const vertexName in hashOfVertex) { 164 | const priority = vertexName == start ? 0 : Infinity; 165 | distance[vertexName] = priority; 166 | previous[vertexName] = null; 167 | } 168 | 169 | while (true) { 170 | let current = pq.dequeue(); 171 | if (!current?.val) { 172 | return; 173 | } 174 | current = current.val; 175 | if (current == end) { 176 | break; 177 | } 178 | const neighbors = hashOfVertex[current]; 179 | 180 | for (const vertexName in neighbors) { 181 | if (visited.hasOwnProperty(vertexName)) { 182 | continue; 183 | } 184 | const distFromStart = distance[current] + neighbors[vertexName]; 185 | 186 | if (distFromStart < distance[vertexName]) { 187 | pq.enqueue(vertexName, distFromStart); 188 | distance[vertexName] = distFromStart; 189 | previous[vertexName] = current; 190 | } 191 | } 192 | visited[current] = true; 193 | } 194 | 195 | let node = end; 196 | 197 | const route = []; 198 | while (node) { 199 | route.unshift(node); 200 | node = previous[node]; 201 | } 202 | 203 | return route; 204 | }, 205 | }; 206 | 207 | this.addEdge = (source, target, weight) => { 208 | WeightedGraph.addEdge(source, target, weight); 209 | }; 210 | 211 | this.findShortestPath = (source, target) => { 212 | return WeightedGraph.findShortestRoute(source, target); 213 | }; 214 | 215 | this.addVertex = (vertex) => { 216 | WeightedGraph.addVertex(vertex); 217 | }; 218 | 219 | WeightedGraph.init(); 220 | } 221 | --------------------------------------------------------------------------------