diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 000000000..0581aac64
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,16 @@
+// eslint-disable-next-line no-undef
+module.exports = {
+ env: {
+ browser: true,
+ es2021: true,
+ },
+ extends: ['eslint:recommended', 'plugin:prettier/recommended'],
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ ecmaVersion: 12,
+ sourceType: 'module',
+ },
+ rules: {},
+};
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..b512c09d4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 000000000..904c1187f
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,11 @@
+{
+ "printWidth": 100,
+ "tabWidth": 2,
+ "singleQuote": true,
+ "trailingComma": "all",
+ "bracketSpacing": true,
+ "semi": true,
+ "useTabs": false,
+ "arrowParens": "avoid",
+ "endOfLine": "lf"
+}
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 000000000..a72e0ed60
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,24 @@
+## ✏️ Todos
+- [X] 기능 목록 정리
+- [X] eslint, prettier 세팅
+- [X] 1, 2, 3, 4번 메뉴 버튼 생성
+- [X] **역** 지하철 역 등록
+- [X] (추가)**역** localStorage에 지하철 역 등록
+- [X] (추가)**역** 역 삭제 (localStorage에서도 삭제)
+- [X] **역** 지하철 역 등록 시 이름 에러 핸들링
+- [X] **역** 지하철 역 목록 조회
+- [X] **노선** 노선 등록 (input 받기)
+- [X] (추가)**노선** localStorage에 노선 등록
+- [X] **노선** 노선 등록 시 이름 에러 핸들링
+- [X] **노선** 노선 삭제
+- [X] **노선** 노선 목록 조회
+- [X] (추가)**역&노선** localStorage가 비어있을 때 출력
+- [X] **구간** 구간 추가
+- [X] (추가)**구간** localStorage에 구간 추가
+- [X] (추가)**노선** 종점역 로직 수정
+- [X] (추가)**구간** 구간 출력
+- [X] **구간** 구간 삭제
+- [X] **구간** 구간 종점 삭제 시 다음 역을 종점으로
+- [X] **구간** 노선 포함 역 2개 이하면 제거 불가
+- [X] 노선에 등록된 역 목록 조회
+- [X] (추가)**역** 노선에 등록된 역 삭제 불가
diff --git a/index.html b/index.html
index fc99deac2..3ad467e6a 100644
--- a/index.html
+++ b/index.html
@@ -7,6 +7,12 @@
🚇 지하철 노선도 관리
+
+ 1. 역 관리
+ 2. 노선 관리
+ 3. 구간 관리
+ 4. 지하철 노선도 출력
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 000000000..e9a1f5b44
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,943 @@
+{
+ "name": "javascript-subway-map-precourse",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
+ "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
+ "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==",
+ "dev": true
+ },
+ "@babel/highlight": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
+ "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.10.4",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ },
+ "dependencies": {
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ }
+ }
+ },
+ "@eslint/eslintrc": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.2.tgz",
+ "integrity": "sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.12.4",
+ "debug": "^4.1.1",
+ "espree": "^7.3.0",
+ "globals": "^12.1.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^3.13.1",
+ "lodash": "^4.17.19",
+ "minimatch": "^3.0.4",
+ "strip-json-comments": "^3.1.1"
+ }
+ },
+ "acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "dev": true
+ },
+ "acorn-jsx": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
+ "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
+ "dev": true
+ },
+ "ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "dev": true
+ },
+ "ansi-regex": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "astral-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
+ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
+ "dev": true
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
+ "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ }
+ },
+ "debug": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+ "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "deep-is": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+ "dev": true
+ },
+ "doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "dev": true
+ },
+ "enquirer": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
+ "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
+ "dev": true,
+ "requires": {
+ "ansi-colors": "^4.1.1"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "eslint": {
+ "version": "7.15.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.15.0.tgz",
+ "integrity": "sha512-Vr64xFDT8w30wFll643e7cGrIkPEU50yIiI36OdSIDoSGguIeaLzBo0vpGvzo9RECUqq7htURfwEtKqwytkqzA==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "@eslint/eslintrc": "^0.2.2",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.0.1",
+ "doctrine": "^3.0.0",
+ "enquirer": "^2.3.5",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^2.1.0",
+ "eslint-visitor-keys": "^2.0.0",
+ "espree": "^7.3.1",
+ "esquery": "^1.2.0",
+ "esutils": "^2.0.2",
+ "file-entry-cache": "^6.0.0",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^5.0.0",
+ "globals": "^12.1.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^3.13.1",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash": "^4.17.19",
+ "minimatch": "^3.0.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "progress": "^2.0.0",
+ "regexpp": "^3.1.0",
+ "semver": "^7.2.1",
+ "strip-ansi": "^6.0.0",
+ "strip-json-comments": "^3.1.0",
+ "table": "^5.2.3",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ }
+ },
+ "eslint-config-prettier": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.0.0.tgz",
+ "integrity": "sha512-8Y8lGLVPPZdaNA7JXqnvETVC7IiVRgAP6afQu9gOQRn90YY3otMNh+x7Vr2vMePQntF+5erdSUBqSzCmU/AxaQ==",
+ "dev": true
+ },
+ "eslint-plugin-prettier": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.2.0.tgz",
+ "integrity": "sha512-kOUSJnFjAUFKwVxuzy6sA5yyMx6+o9ino4gCdShzBNx4eyFRudWRYKCFolKjoM40PEiuU6Cn7wBLfq3WsGg7qg==",
+ "dev": true,
+ "requires": {
+ "prettier-linter-helpers": "^1.0.0"
+ }
+ },
+ "eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ }
+ },
+ "eslint-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
+ "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^1.1.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true
+ }
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz",
+ "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
+ "dev": true
+ },
+ "espree": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
+ "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
+ "dev": true,
+ "requires": {
+ "acorn": "^7.4.0",
+ "acorn-jsx": "^5.3.1",
+ "eslint-visitor-keys": "^1.3.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true
+ }
+ }
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "esquery": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz",
+ "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^5.1.0"
+ },
+ "dependencies": {
+ "estraverse": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
+ "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
+ "dev": true
+ }
+ }
+ },
+ "esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "requires": {
+ "estraverse": "^5.2.0"
+ },
+ "dependencies": {
+ "estraverse": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
+ "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
+ "dev": true
+ }
+ }
+ },
+ "estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true
+ },
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "fast-diff": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
+ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
+ "dev": true
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+ "dev": true
+ },
+ "file-entry-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz",
+ "integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==",
+ "dev": true,
+ "requires": {
+ "flat-cache": "^3.0.4"
+ }
+ },
+ "flat-cache": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+ "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "dev": true,
+ "requires": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ }
+ },
+ "flatted": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.0.tgz",
+ "integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==",
+ "dev": true
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
+ "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "globals": {
+ "version": "12.4.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
+ "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.8.1"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "ignore": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+ "dev": true
+ },
+ "import-fresh": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz",
+ "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==",
+ "dev": true,
+ "requires": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ }
+ },
+ "imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+ "dev": true
+ },
+ "levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ }
+ },
+ "lodash": {
+ "version": "4.17.20",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
+ "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
+ "dev": true
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+ "dev": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "optionator": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
+ "requires": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
+ }
+ },
+ "parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true
+ },
+ "prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true
+ },
+ "prettier": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz",
+ "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==",
+ "dev": true
+ },
+ "prettier-linter-helpers": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+ "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+ "dev": true,
+ "requires": {
+ "fast-diff": "^1.1.2"
+ }
+ },
+ "progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true
+ },
+ "punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "dev": true
+ },
+ "regexpp": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz",
+ "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==",
+ "dev": true
+ },
+ "resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "semver": {
+ "version": "7.3.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
+ "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^3.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true
+ },
+ "slice-ansi": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
+ "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.0",
+ "astral-regex": "^1.0.0",
+ "is-fullwidth-code-point": "^2.0.0"
+ }
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.0"
+ }
+ },
+ "strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "table": {
+ "version": "5.4.6",
+ "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
+ "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.10.2",
+ "lodash": "^4.17.14",
+ "slice-ansi": "^2.1.0",
+ "string-width": "^3.0.0"
+ }
+ },
+ "text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+ "dev": true
+ },
+ "type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1"
+ }
+ },
+ "type-fest": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "dev": true
+ },
+ "uri-js": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz",
+ "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "v8-compile-cache": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz",
+ "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==",
+ "dev": true
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "word-wrap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "dev": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..81ac2d887
--- /dev/null
+++ b/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "javascript-subway-map-precourse",
+ "version": "1.0.0",
+ "description": "## 🚀 기능 요구사항",
+ "main": "index.js",
+ "directories": {
+ "doc": "docs"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/zigsong/javascript-subway-map-precourse.git"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/zigsong/javascript-subway-map-precourse/issues"
+ },
+ "homepage": "https://github.com/zigsong/javascript-subway-map-precourse#readme",
+ "devDependencies": {
+ "eslint": "^7.15.0",
+ "eslint-config-prettier": "^7.0.0",
+ "eslint-plugin-prettier": "^3.2.0",
+ "prettier": "^2.2.1"
+ }
+}
diff --git a/src/index.js b/src/index.js
index e69de29bb..7f55a0144 100644
--- a/src/index.js
+++ b/src/index.js
@@ -0,0 +1,18 @@
+import { initStationManager } from './managers/stationManager.js';
+import { initLineManager } from './managers/lineManager.js';
+import { initSectionManager } from './managers/sectionManager.js';
+import { initMapManager } from './managers/mapManager.js';
+
+export default function SubwayMap() {
+ const stationMngBtn = document.getElementById('station-manager-button');
+ const lineMngBtn = document.getElementById('line-manager-button');
+ const sectionMngBtn = document.getElementById('section-manager-button');
+ const mapPrintMngBtn = document.getElementById('map-print-manager-button');
+
+ stationMngBtn.addEventListener('click', () => initStationManager());
+ lineMngBtn.addEventListener('click', () => initLineManager());
+ sectionMngBtn.addEventListener('click', () => initSectionManager());
+ mapPrintMngBtn.addEventListener('click', () => initMapManager());
+}
+
+new SubwayMap();
diff --git a/src/managers/lineManager.js b/src/managers/lineManager.js
new file mode 100644
index 000000000..afd0dd515
--- /dev/null
+++ b/src/managers/lineManager.js
@@ -0,0 +1,146 @@
+import {
+ clearPage,
+ createTextInput,
+ createSubmitBtn,
+ createSelectbox,
+ createTable,
+} from '../utils/utils.js';
+import { getLocalStorage, setLocalStorage } from '../utils/storage.js';
+import { lineText as T } from '../utils/constants.js';
+
+const app = document.getElementById('app');
+const STORAGE_KEY_STATION = 'stations';
+const STORAGE_KEY_LINE = 'lines';
+const DATA_KEY_LINE = 'line';
+
+export const initLineManager = () => {
+ clearPage();
+ createPage();
+};
+
+const createPage = () => {
+ createTextInput(T.INPUT_LABEL, T.INPUT_ID, T.PLACEHOLDER);
+ createSelectArea();
+ const submitBtn = createSubmitBtn(T.SUBMIT_ID, T.SUBMIT_TEXT);
+ handleSubmit(submitBtn);
+ createResultArea();
+};
+
+const createSelectArea = () => {
+ const selectArea = document.createElement('div');
+ const stations = getLocalStorage(STORAGE_KEY_STATION);
+
+ const upwardSelect = createSelectbox(T.START_SELECTOR_ID, stations);
+ const upwardLabel = document.createElement('b');
+ upwardLabel.innerHTML = T.START_SELECTOR_TEXT;
+
+ const downwardSelect = createSelectbox(T.END_SELECTOR_ID, stations);
+ const downwardLabel = document.createElement('b');
+ downwardLabel.innerHTML = T.END_SELECTOR_TEXT;
+
+ selectArea.append(upwardLabel, upwardSelect, document.createElement('br'));
+ selectArea.append(downwardLabel, downwardSelect);
+
+ app.append(selectArea);
+};
+
+const handleSubmit = submitBtn => {
+ app.append(document.createElement('br'), submitBtn);
+
+ const inputText = document.getElementById(T.INPUT_ID);
+ const upwardSelect = document.getElementById(T.START_SELECTOR_ID);
+ const downwardSelect = document.getElementById(T.END_SELECTOR_ID);
+
+ let startStation = upwardSelect.value;
+ let endStation = downwardSelect.value;
+ upwardSelect.addEventListener('change', () => (startStation = upwardSelect.value));
+ downwardSelect.addEventListener('change', () => (endStation = downwardSelect.value));
+
+ submitBtn.addEventListener('click', () => {
+ addLine(inputText.value, startStation, endStation);
+ inputText.value = '';
+ });
+};
+
+const addLine = (line, start, end) => {
+ if (!validateName(line)) return;
+
+ const currLines = getLocalStorage(STORAGE_KEY_LINE);
+ const updatedLines = currLines ? currLines : {};
+ updatedLines[line] = [start, end];
+ setLocalStorage(STORAGE_KEY_LINE, updatedLines);
+ addToTable(line, start, end);
+};
+
+const validateName = lineName => {
+ const lines = getLocalStorage(STORAGE_KEY_LINE);
+ if (lines && Object.keys(lines).includes(lineName)) {
+ alert(T.ALERT_DUPLICATE_NAME);
+ return false;
+ }
+ return true;
+};
+
+const createResultArea = () => {
+ const tableName = document.createElement('h2');
+ tableName.innerHTML = T.RESULT_TITLE;
+ app.append(tableName);
+
+ const lineTableHeaders = [T.TABLE_HEADER_1, T.TABLE_HEADER_2, T.TABLE_HEADER_3, T.TABLE_HEADER_4];
+ const lineTable = createTable(T.TABLE_ID, lineTableHeaders);
+ const lines = getLocalStorage(STORAGE_KEY_LINE);
+ if (lines) {
+ addTableData(lineTable, lines);
+ }
+
+ app.append(lineTable);
+};
+
+const addTableData = (table, lines) => {
+ Object.entries(lines).map(([line, stations]) => {
+ const tableRow = addTableRow(line, stations[0], stations[stations.length - 1]);
+ table.append(tableRow);
+ });
+};
+
+const addTableRow = (line, upwardEnd, downwardEnd) => {
+ const tableRow = document.createElement('tr');
+ tableRow.dataset[DATA_KEY_LINE] = `_${line}`;
+
+ const nameData = document.createElement('td');
+ nameData.innerHTML = line;
+ const upwardEndData = document.createElement('td');
+ upwardEndData.innerHTML = upwardEnd;
+ const downwardEndData = document.createElement('td');
+ downwardEndData.innerHTML = downwardEnd;
+ const deleteBtn = createDeleteBtn(line);
+
+ tableRow.append(nameData, upwardEndData, downwardEndData, deleteBtn);
+ return tableRow;
+};
+
+const addToTable = (line, start, end) => {
+ const lineTable = document.getElementById(T.TABLE_ID);
+ const newRow = addTableRow(line, start, end);
+ lineTable.append(newRow);
+};
+
+const createDeleteBtn = line => {
+ const deleteBtn = document.createElement('button');
+ deleteBtn.setAttribute('class', T.DELETE_BTN_CLASS);
+ deleteBtn.innerHTML = T.DELETE_BTN_TEXT;
+ deleteBtn.addEventListener('click', () => deleteLine(line));
+
+ return deleteBtn;
+};
+
+const deleteLine = line => {
+ if (confirm(T.ALERT_CONFIRM_DELETE)) {
+ const lineTable = document.getElementById(T.TABLE_ID);
+ const currLines = getLocalStorage(STORAGE_KEY_LINE);
+ delete currLines[line];
+ setLocalStorage(STORAGE_KEY_LINE, currLines);
+ const rowToBeDeleted = lineTable.querySelector(`[data-${DATA_KEY_LINE}=_${line}]`);
+ lineTable.removeChild(rowToBeDeleted);
+ }
+};
diff --git a/src/managers/mapManager.js b/src/managers/mapManager.js
new file mode 100644
index 000000000..314443806
--- /dev/null
+++ b/src/managers/mapManager.js
@@ -0,0 +1,39 @@
+import { clearPage } from '../utils/utils.js';
+import { getLocalStorage } from '../utils/storage.js';
+import { mapText as T } from '../utils/constants.js';
+
+const app = document.getElementById('app');
+const STORAGE_KEY_LINE = 'lines';
+
+export const initMapManager = () => {
+ clearPage();
+ createResultArea();
+};
+
+const createResultArea = () => {
+ const resultArea = document.createElement('div');
+ resultArea.setAttribute('class', T.RESULT_AREA_CLASS);
+
+ const lines = getLocalStorage(STORAGE_KEY_LINE);
+ if (lines) {
+ printResult(resultArea, lines);
+ }
+};
+
+const printResult = (resultArea, data) => {
+ Object.entries(data).map(([line, stations]) => {
+ const lineTitle = document.createElement('h3');
+ lineTitle.innerHTML = line;
+ const stationList = document.createElement('ul');
+
+ stations.map(station => {
+ const stationItem = document.createElement('li');
+ stationItem.innerHTML = station;
+ stationList.append(stationItem);
+ });
+
+ resultArea.append(lineTitle, stationList);
+ });
+
+ app.append(resultArea);
+};
diff --git a/src/managers/sectionManager.js b/src/managers/sectionManager.js
new file mode 100644
index 000000000..6b773c34c
--- /dev/null
+++ b/src/managers/sectionManager.js
@@ -0,0 +1,192 @@
+import { clearPage, createSubmitBtn, createSelectbox, createTable } from '../utils/utils.js';
+import { getLocalStorage, setLocalStorage } from '../utils/storage.js';
+import { sectionText as T } from '../utils/constants.js';
+
+const app = document.getElementById('app');
+const STORAGE_KEY_STATION = 'stations';
+const STORAGE_KEY_LINE = 'lines';
+
+export const initSectionManager = () => {
+ clearPage();
+ printGuideText();
+};
+
+const printGuideText = () => {
+ const sectionHeader = document.createElement('div');
+ const sectionTitle = document.createElement('h3');
+ sectionTitle.innerText = T.GUIDE_TEXT;
+ sectionHeader.append(sectionTitle);
+ createMenuButtons(sectionHeader);
+
+ app.append(sectionHeader);
+};
+
+const createMenuButtons = sectionHeader => {
+ const lines = getLocalStorage(STORAGE_KEY_LINE);
+ if (lines) {
+ Object.keys(lines).map(line => {
+ const menuBtn = document.createElement('button');
+ menuBtn.setAttribute('class', T.MENU_BUTTON_CLASS);
+ menuBtn.innerHTML = line;
+ menuBtn.style.margin = '2px';
+ menuBtn.addEventListener('click', () => {
+ clearInputs();
+ createPage(line);
+ createResultArea(line);
+ });
+ sectionHeader.append(menuBtn);
+ });
+ }
+};
+
+const clearInputs = () => {
+ const sectionInputArea = document.getElementById(T.INPUT_ID);
+ const selectArea = document.getElementById(T.SELECTOR_ID);
+ const sectionTable = document.getElementById(T.TABLE_ID);
+
+ if (sectionInputArea && selectArea && sectionTable) {
+ app.removeChild(sectionInputArea);
+ app.removeChild(selectArea);
+ app.removeChild(sectionTable);
+ }
+};
+
+const createPage = line => {
+ const sectionInputArea = document.createElement('div');
+ sectionInputArea.setAttribute('id', T.INPUT_ID);
+
+ const manageTitle = document.createElement('h3');
+ manageTitle.innerHTML = `${line} ${T.MANAGE_TEXT}`;
+ const registerTitle = document.createElement('b');
+ registerTitle.innerHTML = T.REGISTER_TEXT;
+
+ sectionInputArea.append(manageTitle, registerTitle);
+ app.append(sectionInputArea);
+
+ createSelectArea(line);
+};
+
+const createSelectArea = line => {
+ const selectArea = document.createElement('div');
+ selectArea.setAttribute('id', T.SELECTOR_ID);
+
+ const stations = getLocalStorage(STORAGE_KEY_STATION);
+ const stationSelect = createSelectbox(T.SELECTOR_ID, stations);
+
+ let selectedStation = stationSelect.value;
+ stationSelect.addEventListener('change', () => (selectedStation = stationSelect.value));
+ const orderInput = createNumberInput();
+ const submitBtn = createSubmitBtn(T.SUBMIT_ID, T.SUBMIT_TEXT);
+
+ submitBtn.addEventListener('click', () => addToSection(line, selectedStation, orderInput));
+ selectArea.append(stationSelect, orderInput, submitBtn);
+ app.append(selectArea);
+};
+
+const createNumberInput = () => {
+ const orderInput = document.createElement('input');
+ orderInput.type = 'number';
+ orderInput.setAttribute('id', T.ORDER_INPUT_ID);
+ orderInput.setAttribute('placholder', T.PLACEHOLDER);
+
+ return orderInput;
+};
+
+const addToSection = (line, station, orderInput) => {
+ const order = orderInput.value;
+ if (!validateOrder(line, order)) return;
+ orderInput.value = '';
+
+ const currLines = getLocalStorage(STORAGE_KEY_LINE);
+ const currStations = currLines[line];
+
+ currStations.splice(order, 0, station);
+ const updatedLines = currLines;
+ updatedLines[line] = currStations;
+ setLocalStorage(STORAGE_KEY_LINE, updatedLines);
+
+ removeCurrResult();
+ createResultArea(line);
+};
+
+const validateOrder = (line, order) => {
+ const lines = getLocalStorage(STORAGE_KEY_LINE);
+ if (order < 0) {
+ alert(T.ALERT_NEGATIVE_ORDER);
+ return false;
+ } else if (order > lines[line].length) {
+ alert(T.ALERT_OVERLOAD_ORDER);
+ return false;
+ }
+ return true;
+};
+
+const removeCurrResult = () => {
+ const sectionTable = document.getElementById(T.TABLE_ID);
+ app.removeChild(sectionTable);
+};
+
+const createResultArea = line => {
+ const sectionTableHeaders = [T.TABLE_HEADER_1, T.TABLE_HEADER_2, T.TABLE_HEADER_3];
+ const sectionTable = createTable(T.TABLE_ID, sectionTableHeaders);
+ sectionTable.style.marginTop = '20px';
+ app.append(sectionTable);
+
+ const lines = getLocalStorage(STORAGE_KEY_LINE);
+ if (lines) {
+ const stations = lines[line];
+ addTableData(sectionTable, line, stations);
+ }
+
+ app.append(sectionTable);
+};
+
+const addTableData = (table, line, stations) => {
+ stations.map((station, idx) => {
+ const tableRow = document.createElement('tr');
+ tableRow.dataset['line'] = line;
+ tableRow.dataset['station'] = station;
+
+ const indexData = document.createElement('td');
+ indexData.innerHTML = idx;
+ const nameData = document.createElement('td');
+ nameData.innerHTML = station;
+ const deleteBtn = createDeleteBtn(station, line);
+
+ tableRow.append(indexData, nameData, deleteBtn);
+ table.append(tableRow);
+ });
+};
+
+const createDeleteBtn = (station, line) => {
+ const deleteBtn = document.createElement('button');
+ deleteBtn.setAttribute('class', T.DELETE_BTN_CLASS);
+ deleteBtn.innerHTML = T.DELETE_BTN_TEXT;
+ deleteBtn.addEventListener('click', () => deleteStation(station, line));
+
+ return deleteBtn;
+};
+
+const deleteStation = (station, line) => {
+ const currLines = getLocalStorage(STORAGE_KEY_LINE);
+ const currStations = getLocalStorage(STORAGE_KEY_LINE)[line];
+
+ if (!isAbleToDelete(currStations)) return;
+
+ if (confirm(T.ALERT_CONFIRM_DELETE)) {
+ currStations.splice(currStations.indexOf(station), 1);
+ const updatedLines = currLines;
+ updatedLines[line] = currStations;
+ setLocalStorage(STORAGE_KEY_LINE, updatedLines);
+ removeCurrResult();
+ createResultArea(line);
+ }
+};
+
+const isAbleToDelete = stations => {
+ if (stations.length <= 2) {
+ alert(T.ALERT_STATION_UNDER_TWO);
+ return false;
+ }
+ return true;
+};
diff --git a/src/managers/stationManager.js b/src/managers/stationManager.js
new file mode 100644
index 000000000..9bb177f05
--- /dev/null
+++ b/src/managers/stationManager.js
@@ -0,0 +1,131 @@
+import { clearPage, createSubmitBtn, createTextInput, createTable } from '../utils/utils.js';
+import { getLocalStorage, setLocalStorage } from '../utils/storage.js';
+import { stationText as T } from '../utils/constants.js';
+
+const app = document.getElementById('app');
+const STORAGE_KEY_STATION = 'stations';
+const DATA_KEY_STATION = 'station';
+const STORAGE_KEY_LINE = 'lines';
+
+export const initStationManager = () => {
+ clearPage();
+ createPage();
+};
+
+const createPage = () => {
+ createTextInput(T.INPUT_LABEL, T.INPUT_ID, T.PLACEHOLDER);
+ const submitBtn = createSubmitBtn(T.SUBMIT_ID, T.SUBMIT_TEXT);
+ handleSubmit(submitBtn);
+ createResultArea();
+};
+
+const handleSubmit = submitBtn => {
+ const inputArea = document.getElementById(T.INPUT_CONTAINER);
+ inputArea.append(submitBtn);
+ const inputText = document.getElementById(T.INPUT_ID);
+
+ inputText.addEventListener('keypress', e => {
+ if (e.key === 'Enter') {
+ addStation(inputText.value);
+ inputText.value = '';
+ }
+ });
+
+ submitBtn.addEventListener('click', () => {
+ addStation(inputText.value);
+ inputText.value = '';
+ });
+};
+
+const addStation = name => {
+ if (!validateName(name)) return;
+
+ const currStations = getLocalStorage(STORAGE_KEY_STATION);
+ const updatedStations = currStations ? [...currStations, name] : [name];
+ setLocalStorage(STORAGE_KEY_STATION, updatedStations);
+ addToTable(name);
+};
+
+const validateName = name => {
+ const stations = getLocalStorage(STORAGE_KEY_STATION);
+ if (stations && stations.includes(name)) {
+ alert(T.ALERT_DUPLICATE_NAME);
+ return false;
+ } else if (name.length < 2) {
+ alert(T.ALERT_NAME_UNDER_TWO);
+ return false;
+ }
+ return true;
+};
+
+const createResultArea = () => {
+ const tableName = document.createElement('h2');
+ tableName.innerHTML = T.RESULT_TITLE;
+
+ const stationTableHeaders = [T.TABLE_HEADER_1, T.TABLE_HEADER_2];
+ const stationTable = createTable(T.TABLE_ID, stationTableHeaders);
+
+ const stations = getLocalStorage(STORAGE_KEY_STATION);
+ if (stations) {
+ addTableData(stationTable, stations);
+ }
+
+ app.append(tableName, stationTable);
+};
+
+const addTableData = (table, stations) => {
+ stations.map(station => {
+ const tableRow = addTableRow(station);
+ table.append(tableRow);
+ });
+};
+
+const addTableRow = station => {
+ const tableRow = document.createElement('tr');
+ tableRow.dataset[DATA_KEY_STATION] = station;
+ const nameData = document.createElement('td');
+ nameData.innerHTML = station;
+ const deleteBtn = createDeleteBtn(station);
+
+ tableRow.append(nameData, deleteBtn);
+ return tableRow;
+};
+
+const addToTable = name => {
+ const stationTable = document.getElementById(T.TABLE_ID);
+ const newRow = addTableRow(name);
+ stationTable.append(newRow);
+};
+
+const createDeleteBtn = station => {
+ const deleteBtn = document.createElement('button');
+ deleteBtn.setAttribute('class', T.DELETE_BTN_CLASS);
+ deleteBtn.innerHTML = T.DELETE_BTN_TEXT;
+ deleteBtn.addEventListener('click', () => deleteStation(station));
+
+ return deleteBtn;
+};
+
+const deleteStation = name => {
+ if (!isAbleToDelete(name)) return;
+
+ if (confirm(T.ALERT_CONFIRM_DELETE)) {
+ const stationTable = document.getElementById(T.TABLE_ID);
+ const currStations = getLocalStorage(STORAGE_KEY_STATION);
+ const updatedStations = currStations.filter(station => station !== name);
+ setLocalStorage(STORAGE_KEY_STATION, updatedStations);
+ const rowToBeDeleted = stationTable.querySelector(`[data-${DATA_KEY_STATION}=${name}]`);
+ stationTable.removeChild(rowToBeDeleted);
+ }
+};
+
+const isAbleToDelete = name => {
+ const lines = getLocalStorage(STORAGE_KEY_LINE);
+ for (let stations of Object.values(lines)) {
+ if (stations.includes(name)) {
+ alert(T.ALERT_STATION_IN_LINE);
+ return false;
+ }
+ }
+ return true;
+};
diff --git a/src/utils/constants.js b/src/utils/constants.js
new file mode 100644
index 000000000..8ed8f643d
--- /dev/null
+++ b/src/utils/constants.js
@@ -0,0 +1,67 @@
+export const stationText = {
+ INPUT_LABEL: '역 이름',
+ INPUT_ID: 'station-name-input',
+ PLACEHOLDER: '역 이름을 입력해주세요',
+ SUBMIT_ID: 'station-add-button',
+ SUBMIT_TEXT: '역 추가',
+ INPUT_CONTAINER: 'input-container',
+ ALERT_DUPLICATE_NAME: '중복된 역 이름이 존재합니다.',
+ ALERT_NAME_UNDER_TWO: '역 이름을 2자 이상으로 입력해 주세요.',
+ RESULT_TITLE: '🚉 지하철 역 목록',
+ TABLE_HEADER_1: '역 이름',
+ TABLE_HEADER_2: '설정',
+ TABLE_ID: 'station-table',
+ DELETE_BTN_CLASS: 'station-delete-button',
+ DELETE_BTN_TEXT: '삭제',
+ ALERT_STATION_IN_LINE: '노선에 등록된 역은 삭제할 수 없습니다.',
+ ALERT_CONFIRM_DELETE: '정말 삭제하시겠습니까?',
+};
+
+export const lineText = {
+ INPUT_LABEL: '노선 이름',
+ INPUT_ID: 'line-name-input',
+ PLACEHOLDER: '노선 이름을 입력해주세요',
+ SUBMIT_ID: 'line-add-button',
+ SUBMIT_TEXT: '노선 추가',
+ START_SELECTOR_ID: 'line-start-station-selector',
+ START_SELECTOR_TEXT: '상행 종점',
+ END_SELECTOR_ID: 'line-end-station-selector',
+ END_SELECTOR_TEXT: '하행 종점',
+ ALERT_DUPLICATE_NAME: '중복된 노선 이름이 존재합니다.',
+ RESULT_TITLE: '🚉 지하철 노선 목록',
+ TABLE_HEADER_1: '노선 이름',
+ TABLE_HEADER_2: '상행 종점역',
+ TABLE_HEADER_3: '하행 종점역',
+ TABLE_HEADER_4: '설정',
+ TABLE_ID: 'line-table',
+ DELETE_BTN_CLASS: 'line-delete-button',
+ DELETE_BTN_TEXT: '삭제',
+ ALERT_CONFIRM_DELETE: '정말 삭제하시겠습니까?',
+};
+
+export const sectionText = {
+ GUIDE_TEXT: '구간을 수정할 노선을 선택해주세요.',
+ MENU_BUTTON_CLASS: 'section-line-menu-button',
+ INPUT_ID: 'section-input',
+ TABLE_ID: 'section-table',
+ MANAGE_TEXT: '관리',
+ REGISTER_TEXT: '구간 등록',
+ SELECTOR_ID: 'section-station-selector',
+ ORDER_INPUT_ID: 'section-order-input',
+ PLACEHOLDER: '순서',
+ SUBMIT_ID: 'section-add-button',
+ SUBMIT_TEXT: '등록',
+ TABLE_HEADER_1: '순서',
+ TABLE_HEADER_2: '이름',
+ TABLE_HEADER_3: '설정',
+ DELETE_BTN_CLASS: 'section-delete-button',
+ DELETE_BTN_TEXT: '노선에서 제거',
+ ALERT_NEGATIVE_ORDER: '0보다 큰 순서를 입력해 주세요.',
+ ALERT_OVERLOAD_ORDER: '노선 범위에 포함되는 순서를 입력해 주세요.',
+ ALERT_STATION_UNDER_TWO: '노선에 포함된 역이 2개 이하일 때는 역을 삭제할 수 없습니다.',
+ ALERT_CONFIRM_DELETE: '정말 삭제하시겠습니까?',
+};
+
+export const mapText = {
+ RESULT_AREA_CLASS: 'map',
+};
diff --git a/src/utils/storage.js b/src/utils/storage.js
new file mode 100644
index 000000000..9925c64b3
--- /dev/null
+++ b/src/utils/storage.js
@@ -0,0 +1,7 @@
+export const getLocalStorage = key => {
+ return JSON.parse(localStorage.getItem(key));
+};
+
+export const setLocalStorage = (key, value) => {
+ localStorage.setItem(key, JSON.stringify(value));
+};
diff --git a/src/utils/utils.js b/src/utils/utils.js
new file mode 100644
index 000000000..9f4a300ab
--- /dev/null
+++ b/src/utils/utils.js
@@ -0,0 +1,59 @@
+const app = document.getElementById('app');
+
+export const clearPage = () => {
+ const contents = app.childNodes;
+ let i = contents.length;
+ while (i-- > 4) {
+ app.removeChild(contents[i]);
+ }
+};
+
+export const createTextInput = (labelName, inputId, placeHolder) => {
+ const inputArea = document.createElement('p');
+ inputArea.setAttribute('id', 'input-container');
+ const inputLabel = document.createElement('b');
+ inputLabel.innerHTML = labelName;
+
+ const inputText = document.createElement('input');
+ inputText.setAttribute('id', inputId);
+ inputText.setAttribute('placeholder', placeHolder);
+
+ inputArea.append(inputLabel, document.createElement('br'), inputText);
+ app.append(inputArea);
+};
+
+export const createSubmitBtn = (btnId, btnText) => {
+ const submitBtn = document.createElement('button');
+ submitBtn.setAttribute('id', btnId);
+ submitBtn.innerHTML = btnText;
+
+ return submitBtn;
+};
+
+export const createSelectbox = (selectorId, options) => {
+ const select = document.createElement('select');
+ select.setAttribute('id', selectorId);
+
+ options.map(val => {
+ const option = document.createElement('option');
+ option.value = val;
+ option.text = val;
+ select.options.add(option);
+ });
+
+ return select;
+};
+
+export const createTable = (tableId, tableHeaders) => {
+ const table = document.createElement('table');
+ table.setAttribute('border', 1);
+ table.setAttribute('id', tableId);
+
+ tableHeaders.map(tableHeader => {
+ const header = document.createElement('th');
+ header.innerHTML = tableHeader;
+ table.append(header);
+ });
+
+ return table;
+};