diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..3a2021c --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "airbnb-base", + "prettier" + ], + "parserOptions": { + "ecmaVersion": 2015, + "sourceType": "module" + }, + "rules": { + "no-console": "off", + "no-plusplus": "off", + "class-methods-use-this": "off", + "radix": "off", + "no-restricted-properties": "off", + "max-depth" : ["error", 2], + "no-return-assign" : "off", + "no-continue" : "off", + "no-param-reassign" : "off", + "import/extensions" : "off" + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..98d3dc8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.ides +.DS_Store diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..13a6d05 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "arrowParens": "always" +} diff --git a/README.md b/README.md index 51abd25..3fa16a7 100644 --- a/README.md +++ b/README.md @@ -1 +1,129 @@ -# javascript-subway-final \ No newline at end of file +# ๐Ÿš‡ ์ง€ํ•˜์ฒ  ๋…ธ์„ ๋„ ๊ฒฝ๋กœ ์กฐํšŒ ๋ฏธ์…˜ +- ๋“ฑ๋ก๋œ ์ง€ํ•˜์ฒ  ๋…ธ์„ ๋„์—์„œ ๊ฒฝ๋กœ๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ๋‹ค. + +## ๐Ÿš€ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๋ชฉ๋ก +### ์ดˆ๊ธฐ ์„ค์ • +0. ๊ณตํ†ต์กฐ๊ฑด : ์ •์ƒ์ ์œผ๋กœ ํ”„๋กœ๊ทธ๋žจ์ด ์ˆ˜ํ–‰๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ `alert`์œผ๋กœ ์—๋Ÿฌ๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค. +1. ์‚ฌ์šฉ์ž๊ฐ€ ํ”„๋กœ๊ทธ๋žจ์„ ์‹œ์ž‘ํ•˜๋ฉด ์—ญ, ๋…ธ์„ , ๊ตฌ๊ฐ„ ๋ฐ์ดํ„ฐ๊ฐ€ ์ดˆ๊ธฐํ™”๋œ๋‹ค. + - [์กฐ๊ฑด] ์†Œ์š” ์‹œ๊ฐ„๊ณผ ๊ฑฐ๋ฆฌ๋Š” ์–‘์˜ ์ •์ˆ˜์ด๋‹ค. +``` +1. ์ง€ํ•˜์ฒ ์—ญ์œผ๋กœ ๊ต๋Œ€, ๊ฐ•๋‚จ, ์—ญ์‚ผ, ๋‚จ๋ถ€ํ„ฐ๋ฏธ๋„, ์–‘์žฌ, ์–‘์žฌ์‹œ๋ฏผ์˜์ˆฒ, ๋งค๋ด‰ ์—ญ ์ •๋ณด๊ฐ€ ๋“ฑ๋ก๋˜์–ด ์žˆ๋‹ค. +2. ์ง€ํ•˜์ฒ  ๋…ธ์„ ์œผ๋กœ 2ํ˜ธ์„ , 3ํ˜ธ์„ , ์‹ ๋ถ„๋‹น์„ ์ด ๋“ฑ๋ก๋˜์–ด ์žˆ๋‹ค. +3. ๋…ธ์„ ์— ์—ญ์ด ์•„๋ž˜์™€ ๊ฐ™์ด ๋“ฑ๋ก๋˜์–ด ์žˆ๋‹ค.(์™ผ์ชฝ ๋์ด ์ƒํ–‰ ์ข…์ ) + - 2ํ˜ธ์„ : ๊ต๋Œ€ - ( 2km / 3๋ถ„ ) - ๊ฐ•๋‚จ - ( 2km / 3๋ถ„ ) - ์—ญ์‚ผ + - 3ํ˜ธ์„ : ๊ต๋Œ€ - ( 3km / 2๋ถ„ ) - ๋‚จ๋ถ€ํ„ฐ๋ฏธ๋„ - ( 6km / 5๋ถ„ ) - ์–‘์žฌ - ( 1km / 1๋ถ„ ) - ๋งค๋ด‰ + - ์‹ ๋ถ„๋‹น์„ : ๊ฐ•๋‚จ - ( 2km / 8๋ถ„ ) - ์–‘์žฌ - ( 10km / 3๋ถ„ ) - ์–‘์žฌ์‹œ๋ฏผ์˜์ˆฒ +``` +2. ์‚ฌ์šฉ์ž๋Š” ์ถœ๋ฐœ์—ญ๊ณผ ๋„์ฐฉ์—ญ์„ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋‹ค. + - [์˜ˆ์™ธ์ฒ˜๋ฆฌ] ๋“ฑ๋ก๋˜์–ด ์žˆ์ง€ ์•Š์€ ์—ญ์„ ์ž…๋ ฅํ•œ๊ฑด ์•„๋‹Œ์ง€ ๊ฒ€์ฆํ•œ๋‹ค. + - [์˜ˆ์™ธ์ฒ˜๋ฆฌ] ์—ญ์˜ ์ด๋ฆ„์€ ๋‘ ๊ธ€์ž ์ด์ƒ์œผ๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ๋‹ค. + - [์˜ˆ์™ธ์ฒ˜๋ฆฌ] ์ถœ๋ฐœ์—ญ๊ณผ ๋„์ฐฉ์—ญ์ด ๋‹ค๋ฅธ ์—ญ์ธ์ง€ ๊ฒ€์ฆํ•ด์•ผ ํ•œ๋‹ค. +3. ์‚ฌ์šฉ์ž๊ฐ€ ๊ธธ์ฐพ๊ธฐ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด `์ตœ๋‹จ ๊ฑฐ๋ฆฌ` ๋˜๋Š” `์ตœ์†Œ ์‹œ๊ฐ„` ์˜ต์…˜์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋‹ค. +4. ์ตœ๋‹จ๊ฑฐ๋ฆฌ, ์ตœ์†Œ์‹œ๊ฐ„์˜ ์„ ํƒ์— ๋”ฐ๋ผ ์ด ๊ฑฐ๋ฆฌ ๋ฐ ์ด ์†Œ์š”์‹œ๊ฐ„์„ ํ•จ๊ป˜ ์ถœ๋ ฅํ•œ๋‹ค. + - [์˜ˆ์™ธ์ฒ˜๋ฆฌ] ์ถœ๋ฐœ์—ญ๊ณผ ๋„์ฐฉ์—ญ์ด ์—ฐ๊ฒฐ๋˜์—ˆ๋Š”์ง€ ๊ฒ€์ฆํ•œ๋‹ค. + +## โœ… ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์š”๊ตฌ์‚ฌํ•ญ +### ๊ธธ์ฐพ๊ธฐ ๊ด€๋ จ ๊ธฐ๋Šฅ +- ์ถœ๋ฐœ์—ญ์„ ์ž…๋ ฅํ•˜๋Š” input ํƒœ๊ทธ๋Š” `departure-station-name-input` id ์†์„ฑ๊ฐ’์„ ๊ฐ€์ง„๋‹ค. +- ๋„์ฐฉ์—ญ์„ ์ž…๋ ฅํ•˜๋Š” input ํƒœ๊ทธ๋Š” `arrival-station-name-input` id ์†์„ฑ๊ฐ’์„ ๊ฐ€์ง„๋‹ค. +- ์ตœ๋‹จ๊ฑฐ๋ฆฌ, ์ตœ์†Œ์‹œ๊ฐ„์„ ์„ ํƒํ•˜๋Š” radio๋Š” `search-type` name ์†์„ฑ๊ฐ’์„ ๊ฐ€์ง„๋‹ค. + - **radio option์˜ default ๊ฐ’์€ ์ตœ๋‹จ๊ฑฐ๋ฆฌ์ด๋‹ค.** +- ๊ธธ์ฐพ๊ธฐ ๋ฒ„ํŠผ์€ `search-button` id ์†์„ฑ๊ฐ’์„ ๊ฐ€์ง„๋‹ค. +- ๐Ÿ“ ๊ฒฐ๊ณผ๋Š” `table`์„ ์ด์šฉํ•˜์—ฌ ๋ณด์—ฌ์ค€๋‹ค. + +## โ—๏ธํžŒํŠธ +## ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” +์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ• ์ค‘์— ํ•˜๋‚˜๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด data๋ฅผ `export`ํ•˜๊ณ , `import`ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. + +```javascript +export const users = [ + { + name: 'Alt' + }, + { + name: 'Jamie' + }, + { + name: 'Sony' + } +] + +export const courses = [ + { + name: 'frontend', + }, + { + name: 'backend', + }, + { + name: 'iOS', + }, + { + name: 'Android', + } +] + +``` +์œ„์™€ ๊ฐ™์ด ๋ฐ์ดํ„ฐ๋ฅผ `export`ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ๋ฐ์ดํ„ฐ๋ฅผ `import` ํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. +```javascript +import { users, courses } from './data.js' + +function App() { + this.users = users + this.courses = courses +} +``` + +## ์ตœ๋‹จ ๊ฒฝ๋กœ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ +- `utils/Dijkstra.js` ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๊ฐ„ํŽธํ•˜๊ฒŒ ์ตœ๋‹จ๊ฑฐ๋ฆฌ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค. +- ์ •์ (Vertex)๊ณผ ๊ฐ„์„ (Edge), ๊ทธ๋ฆฌ๊ณ  ๊ฐ€์ค‘์น˜ ๊ฐœ๋…์„ ์ด์šฉ + - ์ •์ : ์ง€ํ•˜์ฒ ์—ญ + - ๊ฐ„์„ : ์ง€ํ•˜์ฒ ์—ญ ์—ฐ๊ฒฐ์ •๋ณด + - ๊ฐ€์ค‘์น˜: ๊ฑฐ๋ฆฌ or ์†Œ์š” ์‹œ๊ฐ„ +- ์ตœ๋‹จ ๊ฑฐ๋ฆฌ ๊ธฐ์ค€ ์กฐํšŒ ์‹œ ๊ฐ€์ค‘์น˜๋ฅผ ๊ฑฐ๋ฆฌ๋กœ ์„ค์ • +- ์ตœ์†Œ ์‹œ๊ฐ„ ๊ธฐ์ค€ ์กฐํšŒ ์‹œ ๊ฐ€์ค‘์น˜๋ฅผ ์‹œ๊ฐ„์œผ๋กœ ์„ค์ • + +```javascript +import Dijkstra from "./utils/Dijkstra.js"; +const dijkstra = new Dijkstra() + +//dijkstra.addEdge("์ถœ๋ฐœ์—ญ", "๋„์ฐฉ์—ญ", ๊ฑฐ๋ฆฌ ๋˜๋Š” ์‹œ๊ฐ„); +dijkstra.addEdge("V1", "V2", 2); +dijkstra.addEdge("V2", "V3", 2); +dijkstra.addEdge("V1", "V3", 100); + +const result = dijkstra.findShortestPath("V1", "V3"); +// result = ["V1", "V2", "V3"] +``` + +#### ํ…Œ์ŠคํŠธ์„ค๋ช… + + +- ์—ญ ์‚ฌ์ด์˜ ๊ฑฐ๋ฆฌ๋ฅผ ๊ณ ๋ คํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ V1->V3 ๊ฒฝ๋กœ๊ฐ€ ์ตœ๋‹จ ๊ฒฝ๋กœ +- ์—ญ ์‚ฌ์ด์˜ ๊ฑฐ๋ฆฌ๋ฅผ ๊ณ ๋ คํ•  ๊ฒฝ์šฐ V1->V3 ๊ฒฝ๋กœ์˜ ๊ฑฐ๋ฆฌ๋Š” 100km, V1->V2->V3 ๊ฒฝ๋กœ์˜ ๊ฑฐ๋ฆฌ๋Š” 4km์ด๋ฏ€๋กœ ์ตœ๋‹จ ๊ฒฝ๋กœ๋Š” V1->V2->V3 + +
+ +### ์š”๊ตฌ์‚ฌํ•ญ +- ์‚ฌ์šฉ์ž๊ฐ€ ์ž˜๋ชป๋œ ์ž…๋ ฅ ๊ฐ’์„ ์ž‘์„ฑํ•œ ๊ฒฝ์šฐ `alert`์„ ์ด์šฉํ•ด ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ , ์žฌ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค. +- ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(jQuery, Lodash ๋“ฑ)๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , ์ˆœ์ˆ˜ Vanilla JS๋กœ๋งŒ ๊ตฌํ˜„ํ•œ๋‹ค. +- **์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ฝ”๋“œ ์ปจ๋ฒค์…˜์„ ์ง€ํ‚ค๋ฉด์„œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ** ํ•œ๋‹ค + - [https://google.github.io/styleguide/jsguide.html](https://google.github.io/styleguide/jsguide.html) + - [https://ui.toast.com/fe-guide/ko_CODING-CONVENSION/](https://ui.toast.com/fe-guide/ko_CODING-CONVENTION) +- **indent(์ธ๋ดํŠธ, ๋“ค์—ฌ์“ฐ๊ธฐ) depth๋ฅผ 3์ด ๋„˜์ง€ ์•Š๋„๋ก ๊ตฌํ˜„ํ•œ๋‹ค. 2๊นŒ์ง€๋งŒ ํ—ˆ์šฉ**ํ•œ๋‹ค. + - ์˜ˆ๋ฅผ ๋“ค์–ด while๋ฌธ ์•ˆ์— if๋ฌธ์ด ์žˆ์œผ๋ฉด ๋“ค์—ฌ์“ฐ๊ธฐ๋Š” 2์ด๋‹ค. + - ํžŒํŠธ: indent(์ธ๋ดํŠธ, ๋“ค์—ฌ์“ฐ๊ธฐ) depth๋ฅผ ์ค„์ด๋Š” ์ข‹์€ ๋ฐฉ๋ฒ•์€ ํ•จ์ˆ˜(๋˜๋Š” ๋ฉ”์†Œ๋“œ)๋ฅผ ๋ถ„๋ฆฌํ•˜๋ฉด ๋œ๋‹ค. +- **ํ•จ์ˆ˜(๋˜๋Š” ๋ฉ”์†Œ๋“œ)์˜ ๊ธธ์ด๊ฐ€ 15๋ผ์ธ์„ ๋„˜์–ด๊ฐ€์ง€ ์•Š๋„๋ก ๊ตฌํ˜„ํ•œ๋‹ค.** + - ํ•จ์ˆ˜(๋˜๋Š” ๋ฉ”์†Œ๋“œ)๊ฐ€ ํ•œ ๊ฐ€์ง€ ์ผ๋งŒ ์ž˜ ํ•˜๋„๋ก ๊ตฌํ˜„ํ•œ๋‹ค. +- ๋ณ€์ˆ˜ ์„ ์–ธ์‹œ [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) ์„ ์‚ฌ์šฉํ•œ๋‹ค. +- [import](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/import) ๋ฌธ์„ ์ด์šฉํ•ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋ชจ๋“ˆํ™”ํ•˜๊ณ  ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ ๋‹ค. +- [template literal](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Template_literals)์„ ์ด์šฉํ•ด ๋ฐ์ดํ„ฐ์™€ html string์„ ๊ฐ€๋…์„ฑ ์ข‹๊ฒŒ ํ‘œํ˜„ํ•œ๋‹ค. + +
+ +## ๐Ÿ“ ๋ฏธ์…˜ ์ €์žฅ์†Œ ๋ฐ ์ง„ํ–‰ ์š”๊ตฌ์‚ฌํ•ญ + +- ๋ฏธ์…˜์€ [https://github.com/woowacourse/javascript-subway-path-precourse](https://github.com/woowacourse/javascript-subway-path-precourse) ์ €์žฅ์†Œ๋ฅผ fork/cloneํ•ด ์‹œ์ž‘ํ•œ๋‹ค. +- **๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์ „์— javascript-subway-path-precourse/docs/README.md ํŒŒ์ผ์— ๊ตฌํ˜„ํ•  ๊ธฐ๋Šฅ ๋ชฉ๋ก**์„ ์ •๋ฆฌํ•ด ์ถ”๊ฐ€ํ•œ๋‹ค. +- **git์˜ commit ๋‹จ์œ„๋Š” ์•ž ๋‹จ๊ณ„์—์„œ README.md ํŒŒ์ผ์— ์ •๋ฆฌํ•œ ๊ธฐ๋Šฅ ๋ชฉ๋ก ๋‹จ์œ„๋กœ ์ถ”๊ฐ€**ํ•œ๋‹ค. +- [ํ”„๋ฆฌ์ฝ”์Šค ๊ณผ์ œ ์ œ์ถœ](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) ๋ฌธ์„œ ์ ˆ์ฐจ๋ฅผ ๋”ฐ๋ผ ๋ฏธ์…˜์„ ์ œ์ถœํ•œ๋‹ค. diff --git a/images/dijkstra_example.png b/images/dijkstra_example.png new file mode 100644 index 0000000..7c75197 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 0000000..ee394bd 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 0000000..40a4bed Binary files /dev/null and b/images/path_result.jpg differ diff --git a/index.css b/index.css new file mode 100644 index 0000000..77314c6 --- /dev/null +++ b/index.css @@ -0,0 +1,18 @@ +#input-search-station-container{ + margin-bottom: 1rem; +} + +.station-input-form-container >input { + height: 1.2rem; + margin-bottom: 1rem; +} + +.path-selectors { + margin-bottom: 1rem; +} + +#path-result-title { + font-size: 1.1rem; + font-weight: bold; + margin-bottom: 1rem; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..cf5b93e --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + + ์ง€ํ•˜์ฒ  ๊ธธ์ฐพ๊ธฐ + + + +
+
+

๐Ÿš‡ ์ง€ํ•˜์ฒ  ๊ธธ์ฐพ๊ธฐ

+
+ +
+ +
+ + + diff --git a/src/controllers/MainController.js b/src/controllers/MainController.js new file mode 100644 index 0000000..cecdc86 --- /dev/null +++ b/src/controllers/MainController.js @@ -0,0 +1,109 @@ +import SearchPathInputFormView from '../views/SearchPathInputFormView.js'; +import ResultSearchPathView from '../views/SearchPathResultView.js'; + +import StationModel from '../models/StationModel.js'; +import StationDistanceModel from '../models/StationDistanceModel.js'; +import StationTimeModel from '../models/StationTimeModel.js'; + +import { Lines2, Lines3, LineSinbundang } from '../utils/data.js'; +import { SEARCH_PATH_TYPE, ERROR_MESSAGE } from '../utils/constants.js'; +import stationInputValidator from '../utils/stationInputValidator.js'; + +export default class MainController { + init() { + this.initStation(); + + this.searchPathInputForm = new SearchPathInputFormView() + .setup(document.querySelector('#input-search-station-container')) + .on('submitSearchInputValue', (e) => this.onSubmitSearchInputHandler(e.detail)) + .on('onChangeSelector', (e) => this.onChangeSelectorHandler(e.detail)); + + this.resultSearchPath = new ResultSearchPathView() + .setup(document.querySelector('#path-search-result-container')) + .hide(); + + this.selectPathType = SEARCH_PATH_TYPE.MIN_DISTANCE; + this.stationTimeModel = new StationTimeModel(); + this.stationDistanceModel = new StationDistanceModel(); + } + + initStation() { + this.stationmodel = new StationModel(); + + this.createStation(Lines2); + this.createStation(Lines3); + this.createStation(LineSinbundang); + } + + createStation(Line) { + Line.forEach((station) => { + if (!this.stationmodel.getStationByName(station.stationName)) { + this.stationmodel.createStation(station.id, station.stationName, station.line); + } else { + this.stationmodel.addLIne(station.stationName, station.line); + } + }); + } + + findPath(departureStationName, arrivalStationName) { + this.selectPathType === SEARCH_PATH_TYPE.MIN_DISTANCE + ? this.getShortestDistancePath(departureStationName, arrivalStationName) + : this.getShortestTimePath(departureStationName, arrivalStationName); + } + + getShortestDistancePath(departureStationName, arrivalStationName) { + const shortestDistancePath = this.stationDistanceModel.getShortestDistancePath( + departureStationName, + arrivalStationName + ); + const totalShortestDistance = this.stationDistanceModel.getTotalDistance(shortestDistancePath); + const totalTime = this.stationTimeModel.getTotalTime(shortestDistancePath); + this.renderResultView(shortestDistancePath, totalShortestDistance, totalTime); + } + + getShortestTimePath(departureStationName, arrivalStationName) { + const shortestTimePath = this.stationTimeModel.getShortestTimePath( + departureStationName, + arrivalStationName + ); + console.log(this.stationTimeModel.getTotalTime(shortestTimePath)); + const totalShortestTime = this.stationTimeModel.getTotalTime(shortestTimePath); + const totalDistance = this.stationDistanceModel.getTotalDistance(shortestTimePath); + this.renderResultView(shortestTimePath, totalDistance, totalShortestTime); + } + + renderResultView(path, totalDistance, totalTime) { + this.resultSearchPath.renderSearchPathResult( + path, + totalDistance, + totalTime, + this.selectPathType + ); + this.resultSearchPath.show(); + } + + isValidStation(departureStationName, arrivalStationName) { + this.departureStationName = departureStationName.trim(); + this.arrivalStationName = arrivalStationName.trim(); + + stationInputValidator( + this.departureStationName, + this.arrivalStationName, + this.stationmodel.getStationNames() + ) + ? this.findPath(departureStationName, arrivalStationName) + : this.searchPathInputForm.resetInputForm(); + } + + onSubmitSearchInputHandler({ departureStationName, arrivalStationName }) { + this.isValidStation(departureStationName, arrivalStationName); + } + + onChangeSelectorHandler(targetId) { + if (!this.departureStationName || !this.arrivalStationName) { + return alert(ERROR_MESSAGE.IS_NOT_INPUT); + } + this.selectPathType = targetId; + this.findPath(this.departureStationName, this.arrivalStationName); + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..38001d7 --- /dev/null +++ b/src/index.js @@ -0,0 +1,5 @@ +import MainController from './controllers/MainController.js'; + +document.addEventListener('DOMContentLoaded', () => { + new MainController().init(); +}); diff --git a/src/models/Station.js b/src/models/Station.js new file mode 100644 index 0000000..777695c --- /dev/null +++ b/src/models/Station.js @@ -0,0 +1,11 @@ +export default class Station { + constructor(id, name) { + this.id = id; + this.name = name; + this.lineNumber = []; + } + + setLine(lineNumber) { + this.lineNumber.push(lineNumber); + } +} diff --git a/src/models/StationDistanceModel.js b/src/models/StationDistanceModel.js new file mode 100644 index 0000000..4a23b80 --- /dev/null +++ b/src/models/StationDistanceModel.js @@ -0,0 +1,40 @@ +import { stationDistance } from '../utils/data.js'; +import Dijkstra from '../utils/Dijkstra.js'; + +export default class StationDistanceModel { + constructor() { + this.distances = stationDistance; + this.dijkstra = new Dijkstra(); + } + + setDistance() { + this.distances.forEach((station) => { + this.dijkstra.addEdge(station.departureStation, station.endStation, station.distance); + }); + } + + getShortestDistancePath(departureStation, endStation) { + this.setDistance(); + // todo ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์€ ๊ณณ์ผ ๋•Œ ์˜ˆ์™ธ์ฒ˜๋ฆฌ + return this.dijkstra.findShortestPath(departureStation, endStation); + } + + getTotalDistance(path) { + return path.reduce((totalDistance, station, index) => { + if (index !== path.length - 1) { + totalDistance += this.getOneDistance(station, path[index + 1]); + } + return totalDistance; + }, 0); + } + + getOneDistance(departureStation, endStation) { + return parseInt( + this.distances.find( + (time) => + (time.departureStation === departureStation && time.endStation === endStation) || + (time.departureStation === endStation && time.endStation === departureStation) + ).distance + ); + } +} diff --git a/src/models/StationModel.js b/src/models/StationModel.js new file mode 100644 index 0000000..da6ec4f --- /dev/null +++ b/src/models/StationModel.js @@ -0,0 +1,25 @@ +import Station from './Station.js'; + +export default class StationModel { + constructor() { + this.stations = []; + } + + createStation(id, stationName, lineNumber) { + const station = new Station(id, stationName); + station.setLine(lineNumber); + this.stations.push(station); + } + + getStationByName(stationName) { + return this.stations.find((station) => station.name === stationName); + } + + getStationNames() { + return this.stations.map((station) => station.name); + } + + addLIne(stationName, stationLine) { + this.getStationByName(stationName).setLine(stationLine); + } +} diff --git a/src/models/StationTimeModel.js b/src/models/StationTimeModel.js new file mode 100644 index 0000000..a066e73 --- /dev/null +++ b/src/models/StationTimeModel.js @@ -0,0 +1,39 @@ +import { stationTime } from '../utils/data.js'; +import Dijkstra from '../utils/Dijkstra.js'; + +export default class StationTimeModel { + constructor() { + this.times = stationTime; + this.dijkstra = new Dijkstra(); + } + + setTime() { + this.times.forEach((station) => { + this.dijkstra.addEdge(station.departureStation, station.endStation, station.time); + }); + } + + getShortestTimePath(departureStation, endStation) { + this.setTime(); + return this.dijkstra.findShortestPath(departureStation, endStation); + } + + getTotalTime(path) { + return path.reduce((totalTime, station, index) => { + if (index !== path.length - 1) { + totalTime += this.getOneTime(station, path[index + 1]); + } + return totalTime; + }, 0); + } + + getOneTime(departureStation, endStation) { + return parseInt( + this.times.find( + (time) => + (time.departureStation === departureStation && time.endStation === endStation) || + (time.departureStation === endStation && time.endStation === departureStation) + ).time + ); + } +} diff --git a/src/utils/Dijkstra.js b/src/utils/Dijkstra.js new file mode 100644 index 0000000..7c6cfd5 --- /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(); +} diff --git a/src/utils/constants.js b/src/utils/constants.js new file mode 100644 index 0000000..61a2ba4 --- /dev/null +++ b/src/utils/constants.js @@ -0,0 +1,27 @@ +const MIN_LENGTH_OF_NAME = 2; + +const LINE_NUMBER = { + LINE2: 2, + LINE3: 3, + LINE_SINBUNDANG: 4, +}; + +const ERROR_MESSAGE = { + IS_EMPTY_STRING: '์—ญ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”', + IS_SAME_STATION: '์„œ๋กœ ๋‹ค๋ฅธ ์—ญ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”', + IS_TOO_SHORT_STRING: '์—ญ์ด๋ฆ„์€ ๋‘ ๊ธ€์ž ์ด์ƒ์ž…๋‹ˆ๋‹ค', + IS_NOT_EXIST: '์กด์žฌํ•˜์ง€ ์•Š๋Š” ์—ญ์ž…๋‹ˆ๋‹ค', + IS_NOT_INPUT: '์—ญ์„ ๋จผ์ € ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”', +}; + +const SEARCH_PATH_TYPE = { + MIN_TIME: '์ตœ์†Œ์‹œ๊ฐ„', + MIN_DISTANCE: '์ตœ๋‹จ๊ฑฐ๋ฆฌ', +}; + +const UNIT = { + DISTANCE: 'km', + TIME: '๋ถ„', +}; + +export { MIN_LENGTH_OF_NAME, ERROR_MESSAGE, LINE_NUMBER, SEARCH_PATH_TYPE, UNIT }; diff --git a/src/utils/data.js b/src/utils/data.js new file mode 100644 index 0000000..2fa7783 --- /dev/null +++ b/src/utils/data.js @@ -0,0 +1,136 @@ +import { LINE_NUMBER } from './constants.js'; + +export const Lines2 = [ + { + id: 1, + stationName: '๊ต๋Œ€', + line: LINE_NUMBER.LINE2, + }, + { + id: 2, + stationName: '๊ฐ•๋‚จ', + line: LINE_NUMBER.LINE2, + }, + { + id: 3, + stationName: '์—ญ์‚ผ', + line: LINE_NUMBER.LINE2, + }, +]; + +export const Lines3 = [ + { + id: 4, + stationName: '๊ต๋Œ€', + line: LINE_NUMBER.LINE3, + }, + { + id: 5, + stationName: '๋‚จ๋ถ€ํ„ฐ๋ฏธ๋„', + line: LINE_NUMBER.LINE3, + }, + { + id: 6, + stationName: '์–‘์žฌ', + line: LINE_NUMBER.LINE3, + }, + { + id: 7, + stationName: '๋งค๋ด‰', + line: LINE_NUMBER.LINE3, + }, +]; + +export const LineSinbundang = [ + { + id: 8, + stationName: '๊ฐ•๋‚จ', + line: LINE_NUMBER.LINE_SINBUNDANG, + }, + { + id: 9, + stationName: '์–‘์žฌ', + line: LINE_NUMBER.LINE_SINBUNDANG, + }, + { + id: 10, + stationName: '์–‘์žฌ์‹œ๋ฏผ์˜์ˆฒ', + line: LINE_NUMBER.LINE_SINBUNDANG, + }, +]; + +export const stationTime = [ + { + departureStation: '๊ต๋Œ€', + endStation: '๊ฐ•๋‚จ', + time: 3, + }, + { + departureStation: '๊ฐ•๋‚จ', + endStation: '์—ญ์‚ผ', + time: 3, + }, + { + departureStation: '๊ต๋Œ€', + endStation: '๋‚จ๋ถ€ํ„ฐ๋ฏธ๋„', + time: 2, + }, + { + departureStation: '๋‚จ๋ถ€ํ„ฐ๋ฏธ๋„', + endStation: '์–‘์žฌ', + time: 5, + }, + { + departureStation: '์–‘์žฌ', + endStation: '๋งค๋ด‰', + time: 1, + }, + { + departureStation: '๊ฐ•๋‚จ', + endStation: '์–‘์žฌ', + time: 8, + }, + { + departureStation: '์–‘์žฌ', + endStation: '์–‘์žฌ์‹œ๋ฏผ์˜์ˆฒ', + time: 3, + }, +]; + +export const stationDistance = [ + { + departureStation: '๊ต๋Œ€', + endStation: '๊ฐ•๋‚จ', + distance: 2, + }, + { + departureStation: '๊ฐ•๋‚จ', + endStation: '์—ญ์‚ผ', + distance: 2, + }, + { + departureStation: '๊ต๋Œ€', + endStation: '๋‚จ๋ถ€ํ„ฐ๋ฏธ๋„', + distance: 3, + }, + { + departureStation: '๋‚จ๋ถ€ํ„ฐ๋ฏธ๋„', + endStation: '์–‘์žฌ', + distance: 6, + }, + { + departureStation: '์–‘์žฌ', + endStation: '๋งค๋ด‰', + distance: 1, + }, + { + departureStation: '๊ฐ•๋‚จ', + endStation: '์–‘์žฌ', + distance: 2, + }, + { + departureStation: '์–‘์žฌ', + endStation: '์–‘์žฌ์‹œ๋ฏผ์˜์ˆฒ', + distance: 10, + }, +]; diff --git a/src/utils/stationInputValidator.js b/src/utils/stationInputValidator.js new file mode 100644 index 0000000..3ea04fe --- /dev/null +++ b/src/utils/stationInputValidator.js @@ -0,0 +1,36 @@ +import { ERROR_MESSAGE } from './constants.js'; + +const isNotNull = (departureStationName, arrivalStationName) => { + return departureStationName === '' || arrivalStationName === '' + ? alert(ERROR_MESSAGE.IS_EMPTY_STRING) + : true; +}; + +const isMoreThanOneString = (departureStationName, arrivalStationName) => { + return departureStationName.length > 1 && arrivalStationName.length > 1 + ? true + : alert(ERROR_MESSAGE.IS_TOO_SHORT_STRING); +}; + +const isNotSameStation = (departureStationName, arrivalStationName) => { + return departureStationName !== arrivalStationName ? true : alert(ERROR_MESSAGE.IS_SAME_STATION); +}; + +const isExistStation = (departureStationName, arrivalStationName, stationNames) => { + return stationNames.includes(departureStationName) && stationNames.includes(arrivalStationName) + ? true + : alert(ERROR_MESSAGE.IS_NOT_EXIST); +}; + +export default function stationInputValidator( + departureStationName, + arrivalStationName, + stationNames +) { + return ( + isNotNull(departureStationName, arrivalStationName) && + isMoreThanOneString(departureStationName, arrivalStationName) && + isNotSameStation(departureStationName, arrivalStationName) && + isExistStation(departureStationName, arrivalStationName, stationNames) + ); +} diff --git a/src/views/SearchPathInputFormView.js b/src/views/SearchPathInputFormView.js new file mode 100644 index 0000000..80e5461 --- /dev/null +++ b/src/views/SearchPathInputFormView.js @@ -0,0 +1,76 @@ +import View from './View.js'; +import { SEARCH_PATH_TYPE } from '../utils/constants.js'; + +export default class SearchPathInputFormView extends View { + setup($element) { + this.init($element); + this.renderSearchInputForm(); + return this; + } + + renderSearchInputForm() { + this.$element.innerHTML = + this.getInputStationFormHTML() + this.getSelectSearchTypeBtnHTML() + this.getSubmitBtnHTML(); + + this.$departureStationInput = this.$element.querySelector('#departure-station-name-input'); + this.$ArrivalStationInput = this.$element.querySelector('#arrival-station-name-input'); + this.$pathSelectorContainer = this.$element.querySelector('.path-selectors'); + + this.bindInputEvent(); + this.bindClickEvent(); + return this; + } + + getInputStationFormHTML() { + return `
+ + +
+ + +
`; + } + + getSelectSearchTypeBtnHTML() { + return `
+ + +
`; + } + + getSubmitBtnHTML() { + return ``; + } + + resetInputForm() { + this.$departureStationInput.value = ''; + this.$ArrivalStationInput.value = ''; + } + + bindInputEvent() { + this.$element + .querySelector('#search-button') + .addEventListener('click', () => this.onSubmitSearchPathHandler()); + } + + onSubmitSearchPathHandler() { + const searchInputValue = { + departureStationName: this.$departureStationInput.value, + arrivalStationName: this.$ArrivalStationInput.value, + }; + + this.emit('submitSearchInputValue', searchInputValue); + } + + bindClickEvent() { + this.$pathSelectorContainer.addEventListener('click', (e) => + this.onChangeSearchPathHandler(e.target.id) + ); + } + + onChangeSearchPathHandler(targetId) { + if (targetId === SEARCH_PATH_TYPE.MIN_DISTANCE || targetId === SEARCH_PATH_TYPE.MIN_TIME) { + this.emit('onChangeSelector', targetId); + } + } +} diff --git a/src/views/SearchPathResultView.js b/src/views/SearchPathResultView.js new file mode 100644 index 0000000..220bd6f --- /dev/null +++ b/src/views/SearchPathResultView.js @@ -0,0 +1,46 @@ +import View from './View.js'; +import { UNIT } from '../utils/constants.js'; + +export default class SearchPathInputFormView extends View { + setup($element) { + this.init($element); + return this; + } + + renderSearchPathResult(path, totalDistance, totalTime, searchType) { + this.$element.innerHTML = this.getSearchPathResultHTML( + path, + totalDistance, + totalTime, + searchType + ); + return this; + } + + getSearchPathResultHTML(path, totalDistance, totalTime, searchType) { + return `

๐Ÿ“ ๊ฒฐ๊ณผ

+ + + + + + + + + + + + + + + + + +
${searchType}
์ด ๊ฑฐ๋ฆฌ์ด ์†Œ์š”์‹œ๊ฐ„
${totalDistance}${UNIT.DISTANCE}${totalTime}${UNIT.TIME}
${this.getPathHTML(path)}
+ `; + } + + getPathHTML(path) { + return `${path.join('๐Ÿ‘‰๐Ÿป')}`; + } +} diff --git a/src/views/View.js b/src/views/View.js new file mode 100644 index 0000000..99f7c03 --- /dev/null +++ b/src/views/View.js @@ -0,0 +1,28 @@ +export default class View { + init($element) { + if (!$element) throw $element; + this.$element = $element; + return this; + } + + on(event, eventHandler) { + this.$element.addEventListener(event, eventHandler); + return this; + } + + emit(event, data) { + const newEvent = new CustomEvent(event, { detail: data }); + this.$element.dispatchEvent(newEvent); + return this; + } + + hide() { + this.$element.style.display = 'none'; + return this; + } + + show() { + this.$element.style.display = ''; + return this; + } +}