diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..955ca5e0 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,13 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module" + }, + "rules": {}, + "parser": "babel-eslint" +} diff --git a/README.md b/README.md index 51abd254..3190784e 100644 --- a/README.md +++ b/README.md @@ -1 +1,56 @@ -# javascript-subway-final \ No newline at end of file +# ๐Ÿš‡ ์ง€ํ•˜์ฒ  ๋…ธ์„ ๋„ ๊ฒฝ๋กœ ์กฐํšŒ ๋ฏธ์…˜ + +- ๋“ฑ๋ก๋œ ์ง€ํ•˜์ฒ  ๋…ธ์„ ๋„์—์„œ ๊ฒฝ๋กœ๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ๋‹ค. + +## ๊ตฌํ˜„ ๊ธฐ๋Šฅ ๋ชฉ๋ก + +### ์ดˆ๊ธฐ ์„ค์ • + +- ํ”„๋กœ๊ทธ๋žจ ์‹œ์ž‘ ์‹œ ์—ญ, ๋…ธ์„ , ๊ตฌ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐ ์„ค์ • ํ•ด์•ผ ํ•œ๋‹ค. +- ๊ฑฐ๋ฆฌ : ์–‘์˜์ •์ˆ˜, ๋‹จ์œ„ km +- ์†Œ์š”์‹œ๊ฐ„ ์กฐ๊ฑด: ์–‘์˜์ •์ˆ˜, ๋‹จ์œ„ ๋ถ„ +- ์•„๋ž˜์˜ ์‚ฌ์ „ ๋“ฑ๋ก ์ •๋ณด๋กœ ๋ฐ˜๋“œ์‹œ ์ดˆ๊ธฐ ์„ค์ •์„ ํ•œ๋‹ค. + +``` +1. ์ง€ํ•˜์ฒ ์—ญ์œผ๋กœ ๊ต๋Œ€, ๊ฐ•๋‚จ, ์—ญ์‚ผ, ๋‚จ๋ถ€ํ„ฐ๋ฏธ๋„, ์–‘์žฌ, ์–‘์žฌ์‹œ๋ฏผ์˜์ˆฒ, ๋งค๋ด‰ ์—ญ ์ •๋ณด๊ฐ€ ๋“ฑ๋ก๋˜์–ด ์žˆ๋‹ค. +2. ์ง€ํ•˜์ฒ  ๋…ธ์„ ์œผ๋กœ 2ํ˜ธ์„ , 3ํ˜ธ์„ , ์‹ ๋ถ„๋‹น์„ ์ด ๋“ฑ๋ก๋˜์–ด ์žˆ๋‹ค. +3. ๋…ธ์„ ์— ์—ญ์ด ์•„๋ž˜์™€ ๊ฐ™์ด ๋“ฑ๋ก๋˜์–ด ์žˆ๋‹ค.(์™ผ์ชฝ ๋์ด ์ƒํ–‰ ์ข…์ ) + - 2ํ˜ธ์„ : ๊ต๋Œ€ - ( 2km / 3๋ถ„ ) - ๊ฐ•๋‚จ - ( 2km / 3๋ถ„ ) - ์—ญ์‚ผ + - 3ํ˜ธ์„ : ๊ต๋Œ€ - ( 3km / 2๋ถ„ ) - ๋‚จ๋ถ€ํ„ฐ๋ฏธ๋„ - ( 6km / 5๋ถ„ ) - ์–‘์žฌ - ( 1km / 1๋ถ„ ) - ๋งค๋ด‰ + - ์‹ ๋ถ„๋‹น์„ : ๊ฐ•๋‚จ - ( 2km / 8๋ถ„ ) - ์–‘์žฌ - ( 10km / 3๋ถ„ ) - ์–‘์žฌ์‹œ๋ฏผ์˜์ˆฒ +``` + + + +### ๊ฒฝ๋กœ ์กฐํšŒ ๊ธฐ๋Šฅ + +- [x] ์ถœ๋ฐœ์—ญ๊ณผ ๋„์ฐฉ์—ญ์„ ์ž…๋ ฅ๋ฐ›๋Š”๋‹ค. +- [x] ์ตœ๋‹จ๊ฑฐ๋ฆฌ ๋˜๋Š” ์ตœ์†Œ์‹œ๊ฐ„ ์˜ต์…˜์„ ์„ ํƒํ•œ๋‹ค. +- [x] ๊ธธ์ฐพ๊ธฐ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์‹คํ–‰ํ•œ๋‹ค. + +- ๊ฒฝ๋กœ ์กฐํšŒ ๊ธฐ๋Šฅ + + - Dijkstra.js ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•œ๋‹ค. + - ๋ชจ๋“  ๊ฐ€๋Šฅํ•œ ๊ฒฝ๋กœ๋ฅผ ์ฐพ๋Š”๋‹ค. + - ๊ฒฝ๋กœ ๋‚ด ์‹œ๊ฐ„ / ๊ฑฐ๋ฆฌ๋ฅผ ๋ชจ๋‘ ๋”ํ•œ๋‹ค.`.addEdge(์ถœ๋ฐœ์—ญ, ๋„์ฐฉ์—ญ, ๊ฑฐ๋ฆฌ)` + - ์ตœ๋‹จ๊ฑฐ๋ฆฌ/์ตœ์†Œ์‹œ๊ฐ„์„ ์ฐพ๋Š”๋‹ค. `findShortestPath` + +- ์ถœ๋ ฅํ•œ๋‹ค: ์„ ํƒํ•œ ์˜ต์…˜, ์ด ๊ฑฐ๋ฆฌ, ์ด ์†Œ์š”์‹œ๊ฐ„, ๊ฒฝ๋กœ + +### ์ž…๋ ฅ๊ฐ’ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ + +- ์ถœ๋ฐœ์—ญ๊ณผ ๋„์ฐฉ์—ญ์€ 2๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•œ๋‹ค. +- ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์—ญ์„ ์ž…๋ ฅํ•  ์ˆ˜ ์—†๋‹ค. +- ๊ฒฝ๋กœ ์กฐํšŒ ์‹œ ์ถœ๋ฐœ์—ญ๊ณผ ๋„์ฐฉ์—ญ์„ ๋‹ค๋ฅด๊ฒŒ ํ•ด์•ผํ•œ๋‹ค. +- ๊ฒฝ๋กœ ์กฐํšŒ ์‹œ ์ถœ๋ฐœ์—ญ๊ณผ ๋„์ฐฉ์—ญ์ด ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์œผ๋ฉด ๊ฒฝ๋กœ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์—†๋‹ค. +- ๊ทธ ์™ธ ์ •์ƒ์ ์œผ๋กœ ํ”„๋กœ๊ทธ๋žจ์ด ์ˆ˜ํ–‰๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ `alert`์œผ๋กœ ์—๋Ÿฌ๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค. + +### HTML Element + +- ์ถœ๋ฐœ์—ญ์„ ์ž…๋ ฅํ•˜๋Š” input ํƒœ๊ทธ id: `departure-station-name-input` +- ๋„์ฐฉ์—ญ์„ ์ž…๋ ฅํ•˜๋Š” input ํƒœ๊ทธ id: `arrival-station-name-input` +- ์ตœ๋‹จ๊ฑฐ๋ฆฌ, ์ตœ์†Œ์‹œ๊ฐ„์„ ์„ ํƒํ•˜๋Š” radio์˜ name ์†์„ฑ: `search-type` +- radio option์˜ default ๊ฐ’์€ ์ตœ๋‹จ๊ฑฐ๋ฆฌ์ด๋‹ค. +- ๊ธธ์ฐพ๊ธฐ ๋ฒ„ํŠผ id: `search-button` +- ๊ฒฐ๊ณผ๋Š” `table`์„ ์ด์šฉํ•˜์—ฌ ๋ณด์—ฌ์ค€๋‹ค. diff --git a/images/dijkstra_example.png b/images/dijkstra_example.png new file mode 100644 index 00000000..7c751970 Binary files /dev/null and b/images/dijkstra_example.png differ diff --git a/images/path_result.gif b/images/path_result.gif new file mode 100644 index 00000000..ee394bd1 Binary files /dev/null and b/images/path_result.gif differ diff --git a/images/path_result.jpg b/images/path_result.jpg new file mode 100644 index 00000000..40a4bedd Binary files /dev/null and b/images/path_result.jpg differ diff --git a/index.html b/index.html new file mode 100644 index 00000000..0409860a --- /dev/null +++ b/index.html @@ -0,0 +1,44 @@ + + + + + ์ง€ํ•˜์ฒ  ๊ธธ์ฐพ๊ธฐ + + + +
+

์ง€ํ•˜์ฒ  ๊ธธ์ฐพ๊ธฐ

+ +
+ + +
+ + ์ตœ๋‹จ๊ฑฐ๋ฆฌ + ์ตœ์†Œ์‹œ๊ฐ„ +
+ + + +
+

๊ฒฐ๊ณผ

+ + + + + + + + +
์ด ๊ฑฐ๋ฆฌ์ด ์†Œ์š” ์‹œ๊ฐ„
+
+
+ + + diff --git a/package.json b/package.json new file mode 100644 index 00000000..3d0fb443 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "javascript-subway-path-precourse", + "version": "1.0.0", + "description": "- ๋“ฑ๋ก๋œ ์ง€ํ•˜์ฒ  ๋…ธ์„ ๋„์—์„œ ๊ฒฝ๋กœ๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ๋‹ค.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/woowacourse/javascript-subway-path-precourse.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/woowacourse/javascript-subway-path-precourse/issues" + }, + "homepage": "https://github.com/woowacourse/javascript-subway-path-precourse#readme" +} diff --git a/src/data/line.js b/src/data/line.js new file mode 100644 index 00000000..2165bb20 --- /dev/null +++ b/src/data/line.js @@ -0,0 +1,66 @@ +export const lines = [ + { + name: "2ํ˜ธ์„ ", + sections: [ + { + id: 0, + distance: 2, + time: 3, + departureStation: "๊ต๋Œ€", //id๋กœ? + arrivalStation: "๊ฐ•๋‚จ", + }, + { + id: 1, + distance: 2, + time: 3, + departureStation: "๊ฐ•๋‚จ", + arrivalStation: "์—ญ์‚ผ", + }, + ], + }, + { + name: "3ํ˜ธ์„ ", + sections: [ + { + id: 0, + distance: 3, + time: 2, + departureStation: "๊ต๋Œ€", + arrivalStation: "๋‚จ๋ถ€ํ„ฐ๋ฏธ๋„", + }, + { + id: 1, + distance: 6, + time: 5, + departureStation: "๋‚จ๋ถ€ํ„ฐ๋ฏธ๋„", + arrivalStation: "์–‘์žฌ", + }, + { + id: 2, + distance: 1, + time: 1, + departureStation: "์–‘์žฌ", + arrivalStation: "๋งค๋ด‰", + }, + ], + }, + { + name: "์‹ ๋ถ„๋‹น์„ ", + sections: [ + { + id: 0, + distance: 2, + time: 8, + departureStation: "๊ฐ•๋‚จ", + arrivalStation: "์–‘์žฌ", + }, + { + id: 1, + distance: 10, + time: 3, + departureStation: "์–‘์žฌ", + arrivalStation: "์–‘์žฌ์‹œ๋ฏผ์˜์ˆฒ", + }, + ], + }, +]; diff --git a/src/data/station.js b/src/data/station.js new file mode 100644 index 00000000..6fd036f3 --- /dev/null +++ b/src/data/station.js @@ -0,0 +1,30 @@ +export const stations = [ + { + id: 0, + name: "๊ต๋Œ€", + }, + { + id: 1, + name: "๊ฐ•๋‚จ", + }, + { + id: 2, + name: "์—ญ์‚ผ", + }, + { + id: 3, + name: "๋‚จ๋ถ€ํ„ฐ๋ฏธ๋„", + }, + { + id: 4, + name: "์–‘์žฌ", + }, + { + id: 5, + name: "์–‘์žฌ์‹œ๋ฏผ์˜์ˆฒ", + }, + { + id: 6, + name: "๋งค๋ด‰", + }, +]; diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..b923f8ce --- /dev/null +++ b/src/index.js @@ -0,0 +1,85 @@ +import { lines } from "./data/line.js"; +import StationService from "./service/station.service.js"; +import SectionService from "./service/section.service.js"; +import Dijkstra from "./utils/Dijkstra.js"; +const dijkstra = new Dijkstra(); + +export default class App { + constructor() { + this.sectionService = new SectionService(); + this.stationService = new StationService(); + + this.addClickEvent(); + } + + getDepartureStationInput() { + const depatureStationInputField = document.getElementById("departure-station-name-input"); + return depatureStationInputField.value; + } + + getArrivalStationInput() { + const arrivalStationInputField = document.getElementById("arrival-station-name-input"); + return arrivalStationInputField.value; + } + + getSearchTypeInput() { + const searchTypeInputField = document.querySelector('input[name="search-type"]:checked'); + return searchTypeInputField.value; + } + + validateStationNameLength(departureStationName, arrivalStationName) { + if ( + departureStationName.length < this.stationService.MIN_STATION_NAME_LENGTH || + arrivalStationName.length < this.stationService.MIN_STATION_NAME_LENGTH + ) { + throw new Error("์—ญ ์ด๋ฆ„์€ ๋‘๊ธ€์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + } + + validateStationExist(departureStationName, arrivalStationName) { + const isDepartureStationExist = this.stationService.findByName(departureStationName).length; + const isArrivalStationExist = this.stationService.findByName(arrivalStationName).length; + + if (!isDepartureStationExist || !isArrivalStationExist) { + throw new Error("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์—ญ์ž…๋‹ˆ๋‹ค."); + } + } + + searchPath() { + try { + const departureStation = this.getDepartureStationInput(); + const arrivalStation = this.getArrivalStationInput(); + const searchType = this.getSearchTypeInput(); + this.validateStationNameLength(departureStation, arrivalStation); + this.validateStationExist(departureStation, arrivalStation); + + const paths = this.sectionService.findShortestPath(departureStation, arrivalStation); + this.renderPathTable(paths); + } catch (error) { + alert(error); + } + } + + renderPathTable(paths) { + const pathRowHTML = ` + + ${paths.length} + + + + ${paths.join("->")} + + `; + const table = document.getElementById("result-table").querySelector("tbody"); + table.innerHTML = pathRowHTML; + } + + addClickEvent() { + const button = document.getElementById("search-button"); + button.addEventListener("click", () => { + this.searchPath(); + }); + } +} + +const app = new App(); diff --git a/src/service/section.service.js b/src/service/section.service.js new file mode 100644 index 00000000..15625037 --- /dev/null +++ b/src/service/section.service.js @@ -0,0 +1,64 @@ +import { lines } from "../data/line.js"; +import Dijkstra from "../utils/Dijkstra.js"; + +export default class SectionService { + constructor() { + this.lines = lines; + this.paths = []; + } + + findAllSections() { + const sections = []; + for (let line of lines) { + sections.push(...line.sections); + } + return sections; + } + + findPath(departureStation, arrivalStation, paths) { + const sections = this.findSectionsByDepartureStation(departureStation); + + for (let section of sections) { + //base contidition + if (section.arrivalStation === arrivalStation) { + paths.push(section); + this.paths.push(paths); + return paths; + } + const newPaths = [...paths]; + newPaths.push(section); + this.findPath(section.arrivalStation, arrivalStation, newPaths); + } + return this.paths; + } + + findShortestPath(departureStation, arrivalStation) { + this.paths = []; + const dijkstra = new Dijkstra(); + const paths = this.findPath(departureStation, arrivalStation, []); + console.log(paths); + + paths.forEach((path) => { + if (paths.length === 1) { + dijkstra.addEdge(path.departureStation, path.arrivalStation, path.distance); + } else { + path.forEach((section) => { + dijkstra.addEdge(section.departureStation, section.arrivalStation, section.distance); + }); + } + }); + + const result = dijkstra.findShortestPath(departureStation, arrivalStation); + return result; + } + + findSectionsByDepartureStation(departureStationName) { + const sections = this.findAllSections(); + + const targets = sections.filter((section) => { + return section.departureStation === departureStationName; + }); + + return targets; + } +} diff --git a/src/service/station.service.js b/src/service/station.service.js new file mode 100644 index 00000000..0340e8f7 --- /dev/null +++ b/src/service/station.service.js @@ -0,0 +1,16 @@ +import { stations } from "../data/station.js"; + +export default class StationService { + constructor() { + this.stations = stations; + this.MIN_STATION_NAME_LENGTH = 2; + } + + findByName(stationName) { + const station = this.stations.filter((station) => { + return station.name === stationName; + }); + + return station; + } +} diff --git a/src/utils/Dijkstra.js b/src/utils/Dijkstra.js new file mode 100644 index 00000000..7c6cfd56 --- /dev/null +++ b/src/utils/Dijkstra.js @@ -0,0 +1,220 @@ +export default function Dijkstra() { + const Node = { + init: function (val, priority) { + this.val = val; + this.priority = priority; + }, + }; + + const PriorityQueue = { + init: function () { + this.values = []; + }, + enqueue: function (val, priority) { + const newNode = Object.create(Node); + newNode.init(val, priority); + + this.values.push(newNode); + + let idxOfNewNode = this.values.length - 1; + + while (idxOfNewNode > 0) { + const idxOfParentNode = Math.floor((idxOfNewNode - 1) / 2); + + const parentNode = this.values[idxOfParentNode]; + + if (priority < parentNode.priority) { + this.values[idxOfParentNode] = newNode; + this.values[idxOfNewNode] = parentNode; + idxOfNewNode = idxOfParentNode; + continue; + } + break; + } + return this.values; + }, + dequeue: function () { + if (this.values.length == 0) { + return; + } + const dequeued = this.values.shift(); + const lastItem = this.values.pop(); + if (!lastItem) { + return dequeued; + } + this.values.unshift(lastItem); + + let idxOfTarget = 0; + + while (true) { + let idxOfLeftChild = idxOfTarget * 2 + 1; + let idxOfRightChild = idxOfTarget * 2 + 2; + let leftChild = this.values[idxOfLeftChild]; + let rightChild = this.values[idxOfRightChild]; + + function swap(direction) { + const idxOfChild = + direction == "left" ? idxOfLeftChild : idxOfRightChild; + const child = direction == "left" ? leftChild : rightChild; + this.values[idxOfChild] = this.values[idxOfTarget]; + this.values[idxOfTarget] = child; + idxOfTarget = idxOfChild; + } + + if (!leftChild) { + return dequeued; + } + + if (!rightChild) { + if (leftChild.priority < lastItem.priority) { + swap.call(this, "left"); + continue; + } + return dequeued; + } + + if (leftChild.priority == rightChild.priority) { + swap.call(this, "left"); + continue; + } + + if ( + leftChild.priority < rightChild.priority && + leftChild.priority < lastItem.priority + ) { + swap.call(this, "left"); + continue; + } + + if ( + rightChild.priority < leftChild.priority && + rightChild.priority < lastItem.priority + ) { + swap.call(this, "right"); + continue; + } + } + }, + }; + + const WeightedGraph = { + init: function () { + this.adjacencyList = {}; + this.length = 0; + }, + addVertex: function (vertex) { + if (!this.adjacencyList.hasOwnProperty(vertex)) { + this.adjacencyList[vertex] = {}; + this.length++; + } + }, + addEdge: function (vertex1, vertex2, weight) { + this.addVertex(vertex1); + this.addVertex(vertex2); + this.adjacencyList[vertex1][vertex2] = weight; + this.adjacencyList[vertex2][vertex1] = weight; + return this.adjacencyList; + }, + removeEdge: function (vertex1, vertex2) { + if (!this.adjacencyList.hasOwnProperty(vertex1)) { + return `There's no ${vertex1}`; + } + if (!this.adjacencyList.hasOwnProperty(vertex2)) { + return `There's no ${vertex2}`; + } + + function removeHelper(v1, v2) { + if (!this.adjacencyList.hasOwnProperty(v1)) { + return `There's no edge between ${v1} and ${v2}`; + } + delete this.adjacencyList[v1][v2]; + if (Object.keys(this.adjacencyList[v1]).length == 0) { + delete this.adjacencyList[v1]; + } + } + + removeHelper.call(this, vertex1, vertex2); + removeHelper.call(this, vertex2, vertex1); + + return this.adjacencyList; + }, + removeVertex: function (vertex) { + if (!this.adjacencyList.hasOwnProperty(vertex)) { + return `There's no ${vertex}`; + } + const edges = this.adjacencyList[vertex]; + for (const key in edges) { + this.removeEdge(key, vertex); + } + return this.adjacencyList; + }, + findShortestRoute: function (start, end) { + if (!start || !end) { + throw Error("์ถœ๋ฐœ์ง€์™€ ๋„์ฐฉ์ง€๋ฅผ ๋ชจ๋‘ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + const distance = {}; + const previous = {}; + const pq = Object.create(PriorityQueue); + pq.init(); + pq.enqueue(start, 0); + const visited = {}; + + const hashOfVertex = this.adjacencyList; + for (const vertexName in hashOfVertex) { + const priority = vertexName == start ? 0 : Infinity; + distance[vertexName] = priority; + previous[vertexName] = null; + } + + while (true) { + let current = pq.dequeue(); + if (!current?.val) { + return; + } + current = current.val; + if (current == end) { + break; + } + const neighbors = hashOfVertex[current]; + + for (const vertexName in neighbors) { + if (visited.hasOwnProperty(vertexName)) { + continue; + } + const distFromStart = distance[current] + neighbors[vertexName]; + + if (distFromStart < distance[vertexName]) { + pq.enqueue(vertexName, distFromStart); + distance[vertexName] = distFromStart; + previous[vertexName] = current; + } + } + visited[current] = true; + } + + let node = end; + + const route = []; + while (node) { + route.unshift(node); + node = previous[node]; + } + + return route; + }, + }; + + this.addEdge = (source, target, weight) => { + WeightedGraph.addEdge(source, target, weight); + }; + + this.findShortestPath = (source, target) => { + return WeightedGraph.findShortestRoute(source, target); + }; + + this.addVertex = (vertex) => { + WeightedGraph.addVertex(vertex); + }; + + WeightedGraph.init(); +}