├── 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 |
--------------------------------------------------------------------------------