├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── frontend ├── .babelrc ├── .eslintrc.js ├── .prettierrc.js ├── package-lock.json ├── package.json ├── src │ ├── App.vue │ ├── api │ │ ├── index.js │ │ └── modules │ │ │ ├── auth.js │ │ │ ├── favorite.js │ │ │ ├── line.js │ │ │ ├── map.js │ │ │ ├── member.js │ │ │ ├── path.js │ │ │ └── station.js │ ├── components │ │ ├── dialogs │ │ │ ├── ConfirmDialog.vue │ │ │ └── Dialog.vue │ │ ├── menus │ │ │ ├── ButtonMenu.vue │ │ │ ├── IconMenu.vue │ │ │ └── MenuListItem.vue │ │ └── snackbars │ │ │ └── Snackbar.vue │ ├── main.js │ ├── mixins │ │ ├── dialog.js │ │ └── responsive.js │ ├── router │ │ ├── index.js │ │ └── modules │ │ │ ├── auth.js │ │ │ ├── favorite.js │ │ │ ├── line.js │ │ │ ├── main.js │ │ │ ├── map.js │ │ │ ├── path.js │ │ │ ├── section.js │ │ │ └── station.js │ ├── store │ │ ├── index.js │ │ ├── modules │ │ │ ├── auth.js │ │ │ ├── favorite.js │ │ │ ├── line.js │ │ │ ├── map.js │ │ │ ├── member.js │ │ │ ├── path.js │ │ │ ├── snackbar.js │ │ │ └── station.js │ │ └── shared │ │ │ ├── actionTypes.js │ │ │ └── mutationTypes.js │ ├── styles │ │ ├── app.scss │ │ ├── color.scss │ │ ├── components │ │ │ └── alert.scss │ │ ├── custom.scss │ │ ├── fonts.scss │ │ ├── index.scss │ │ └── layout.scss │ ├── utils │ │ ├── constants.js │ │ ├── plugin │ │ │ └── vuetify.js │ │ └── validator.js │ └── views │ │ ├── auth │ │ ├── JoinPage.vue │ │ ├── LoginPage.vue │ │ ├── Mypage.vue │ │ └── MypageEdit.vue │ │ ├── base │ │ └── header │ │ │ ├── Header.vue │ │ │ └── components │ │ │ ├── FavoritesButton.vue │ │ │ ├── LogoutButton.vue │ │ │ └── MyPageButton.vue │ │ ├── favorite │ │ ├── Favorites.vue │ │ └── components │ │ │ └── FavoriteDeleteButton.vue │ │ ├── line │ │ ├── LinePage.vue │ │ └── components │ │ │ ├── LineCreateButton.vue │ │ │ ├── LineDeleteButton.vue │ │ │ ├── LineEditButton.vue │ │ │ └── LineForm.vue │ │ ├── main │ │ └── MainPage.vue │ │ ├── map │ │ └── MapPage.vue │ │ ├── path │ │ ├── PathPage.vue │ │ └── components │ │ │ └── AddFavoriteButton.vue │ │ ├── section │ │ ├── SectionPage.vue │ │ └── components │ │ │ ├── SectionCreateButton.vue │ │ │ └── SectionDeleteButton.vue │ │ └── station │ │ └── StationPage.vue ├── webpack.common.js ├── webpack.config.js ├── webpack.dev.js └── webpack.prod.js ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── nextstep │ │ └── subway │ │ ├── BaseEntity.java │ │ ├── DataLoaderConfig.java │ │ ├── PageController.java │ │ ├── SubwayApplication.java │ │ ├── auth │ │ ├── application │ │ │ ├── AuthService.java │ │ │ └── AuthorizationException.java │ │ ├── domain │ │ │ ├── AuthenticationPrincipal.java │ │ │ └── LoginMember.java │ │ ├── dto │ │ │ ├── TokenRequest.java │ │ │ └── TokenResponse.java │ │ ├── infrastructure │ │ │ ├── AuthenticationPrincipalConfig.java │ │ │ ├── AuthorizationExtractor.java │ │ │ └── JwtTokenProvider.java │ │ └── ui │ │ │ ├── AuthController.java │ │ │ └── AuthenticationPrincipalArgumentResolver.java │ │ ├── line │ │ ├── application │ │ │ └── LineService.java │ │ ├── domain │ │ │ ├── Line.java │ │ │ ├── LineRepository.java │ │ │ └── Section.java │ │ ├── dto │ │ │ ├── LineRequest.java │ │ │ ├── LineResponse.java │ │ │ └── SectionRequest.java │ │ └── ui │ │ │ └── LineController.java │ │ ├── member │ │ ├── application │ │ │ └── MemberService.java │ │ ├── domain │ │ │ ├── Member.java │ │ │ └── MemberRepository.java │ │ ├── dto │ │ │ ├── MemberRequest.java │ │ │ └── MemberResponse.java │ │ └── ui │ │ │ └── MemberController.java │ │ └── station │ │ ├── application │ │ └── StationService.java │ │ ├── domain │ │ ├── Station.java │ │ └── StationRepository.java │ │ ├── dto │ │ ├── StationRequest.java │ │ └── StationResponse.java │ │ └── ui │ │ └── StationController.java └── resources │ ├── application.properties │ ├── logback-access.xml │ ├── static │ └── images │ │ ├── logo_small.png │ │ └── main_logo.png │ └── templates │ └── index.html └── test └── java ├── nextstep └── subway │ ├── AcceptanceTest.java │ ├── SubwayApplicationTests.java │ ├── auth │ ├── acceptance │ │ └── AuthAcceptanceTest.java │ └── application │ │ └── AuthServiceTest.java │ ├── favorite │ └── FavoriteAcceptanceTest.java │ ├── line │ └── acceptance │ │ ├── LineAcceptanceTest.java │ │ └── LineSectionAcceptanceTest.java │ ├── member │ └── MemberAcceptanceTest.java │ ├── path │ └── PathAcceptanceTest.java │ ├── station │ └── StationAcceptanceTest.java │ └── utils │ └── DatabaseCleanup.java └── study ├── jgraph └── JgraphTest.java └── unit ├── MockitoExtensionTest.java ├── MockitoTest.java ├── SpringExtensionTest.java └── UnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | 39 | ### Front End ### 40 | frontend/node_modules 41 | fronted/.prttierrc.js 42 | fronted/.eslintrc.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 next-step 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | npm 6 | node 7 | 8 | Website 9 | 10 | GitHub 11 |

12 | 13 |
14 | 15 | # 지하철 노선도 미션 16 | [ATDD 강의](https://edu.nextstep.camp/c/R89PYi5H) 실습을 위한 지하철 노선도 애플리케이션 17 | 18 |
19 | 20 | ## 🚀 Getting Started 21 | 22 | ### Install 23 | #### npm 설치 24 | ``` 25 | cd frontend 26 | npm install 27 | ``` 28 | > `frontend` 디렉토리에서 수행해야 합니다. 29 | 30 | ### Usage 31 | #### webpack server 구동 32 | ``` 33 | npm run dev 34 | ``` 35 | #### application 구동 36 | ``` 37 | ./gradlew bootRun 38 | ``` 39 |
40 | 41 | ## ✏️ Code Review Process 42 | [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) 43 | 44 |
45 | 46 | ## 🐞 Bug Report 47 | 48 | 버그를 발견한다면, [Issues](https://github.com/next-step/atdd-subway-service/issues) 에 등록해주세요 :) 49 | 50 |
51 | 52 | ## 📝 License 53 | 54 | This project is [MIT](https://github.com/next-step/atdd-subway-service/blob/master/LICENSE.md) licensed. 55 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.5.1' 3 | id 'io.spring.dependency-management' version '1.0.11.RELEASE' 4 | id 'java' 5 | } 6 | 7 | group = 'nextstep' 8 | version = '0.0.1-SNAPSHOT' 9 | sourceCompatibility = '1.8' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | // spring 17 | implementation 'org.springframework.boot:spring-boot-starter-web' 18 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 19 | 20 | // handlebars 21 | implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.3.0' 22 | 23 | // log 24 | implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' 25 | 26 | // jgraph 27 | implementation 'org.jgrapht:jgrapht-core:1.0.1' 28 | 29 | // jwt 30 | implementation 'io.jsonwebtoken:jjwt:0.9.1' 31 | 32 | testImplementation 'io.rest-assured:rest-assured:3.3.0' 33 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 34 | 35 | runtimeOnly 'com.h2database:h2' 36 | } 37 | 38 | test { 39 | useJUnitPlatform() 40 | } 41 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/env", { "modules": false }]], 3 | "env": { 4 | "test": { 5 | "presets": [ 6 | [ 7 | "@babel/env", 8 | { 9 | "targets": { 10 | "node": "current" 11 | } 12 | } 13 | ] 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | jest: true 6 | }, 7 | extends: ['plugin:vue/essential', 'airbnb-base', 'prettier'], 8 | globals: { 9 | Atomics: 'readonly', 10 | SharedArrayBuffer: 'readonly' 11 | }, 12 | parserOptions: { 13 | ecmaVersion: 2018, 14 | sourceType: 'module' 15 | }, 16 | plugins: ['vue'], 17 | rules: { 18 | semi: 0, 19 | 'import/no-unresolved': 'off', 20 | 'comma-dangle': 'off', 21 | 'no-new': 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | semi: false, 4 | singleQuote: true, 5 | endOfLine: 'lf', 6 | trailingComma: 'none', 7 | bracketSpacing: true, 8 | printWidth: 150, 9 | jsxBracketSameLine: false 10 | } 11 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atdd-subway-path", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "prod": "webpack --env=prod", 8 | "dev": "webpack-dev-server --env=dev", 9 | "start": "npm run dev", 10 | "test": "jest" 11 | }, 12 | "jest": { 13 | "moduleFileExtensions": [ 14 | "js", 15 | "json", 16 | "vue" 17 | ], 18 | "moduleNameMapper": { 19 | "^@/(.*)$": "/src/$1" 20 | }, 21 | "transform": { 22 | ".*\\.(vue)$": "vue-jest", 23 | "^.+\\.js$": "/node_modules/babel-jest" 24 | }, 25 | "collectCoverage": true, 26 | "collectCoverageFrom": [ 27 | "**/*.{js,vue}", 28 | "!**/node_modules/**" 29 | ], 30 | "snapshotSerializers": [ 31 | "jest-serializer-vue" 32 | ] 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/next-step/atdd-subway-path.git" 37 | }, 38 | "keywords": [], 39 | "author": "", 40 | "license": "ISC", 41 | "bugs": { 42 | "url": "https://github.com/next-step/atdd-subway-path/issues" 43 | }, 44 | "engines": { 45 | "npm": ">=5.5.0", 46 | "node": ">=9.3.0" 47 | }, 48 | "homepage": "https://github.com/next-step/atdd-subway-path#readme", 49 | "devDependencies": { 50 | "@babel/preset-env": "^7.9.0", 51 | "@vue/test-utils": "^1.0.0-beta.32", 52 | "babel-core": "^7.0.0-bridge.0", 53 | "babel-jest": "^25.1.0", 54 | "babel-loader": "^8.0.6", 55 | "babel-polyfill": "^6.26.0", 56 | "case-sensitive-paths-webpack-plugin": "^2.3.0", 57 | "css-loader": "^3.4.2", 58 | "deepmerge": "^4.2.2", 59 | "eslint": "^6.8.0", 60 | "eslint-config-airbnb": "^18.0.1", 61 | "eslint-config-prettier": "^6.10.0", 62 | "eslint-plugin-import": "^2.20.1", 63 | "eslint-plugin-jsx-a11y": "^6.2.3", 64 | "eslint-plugin-prettier": "^3.1.2", 65 | "eslint-plugin-vue": "^6.1.2", 66 | "fibers": "^4.0.2", 67 | "file-loader": "^5.0.2", 68 | "html-webpack-plugin": "^3.2.0", 69 | "jest": "^25.1.0", 70 | "jest-serializer-vue": "^2.0.2", 71 | "mini-css-extract-plugin": "^0.9.0", 72 | "node-sass": "^4.13.1", 73 | "optimize-css-assets-webpack-plugin": "^5.0.3", 74 | "prettier": "^1.19.1", 75 | "sass": "^1.25.0", 76 | "sass-loader": "^8.0.2", 77 | "style-loader": "^1.1.3", 78 | "terser-webpack-plugin": "^2.3.4", 79 | "url-loader": "^3.0.0", 80 | "vue-jest": "^3.0.5", 81 | "vue-loader": "^15.8.3", 82 | "vue-style-loader": "^4.1.2", 83 | "vue-template-compiler": "^2.6.11", 84 | "vuetify-loader": "^1.4.3", 85 | "webpack": "^4.41.5", 86 | "webpack-bundle-analyzer": "^3.6.0", 87 | "webpack-cli": "^3.3.10", 88 | "webpack-dev-server": "^3.10.3", 89 | "webpack-merge": "^4.2.2", 90 | "yargs": "^15.3.1" 91 | }, 92 | "dependencies": { 93 | "axios": "^0.19.2", 94 | "eslint-plugin-react": "^7.21.5", 95 | "eslint-plugin-react-hooks": "^4.2.0", 96 | "vue": "^2.6.11", 97 | "vue-axios": "^2.1.5", 98 | "vue-router": "^3.1.5", 99 | "vuetify": "^2.2.11", 100 | "vuex": "^3.1.2" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 35 | -------------------------------------------------------------------------------- /frontend/src/api/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import axios from 'axios' 3 | import VueAxios from 'vue-axios' 4 | 5 | const ApiService = { 6 | init() { 7 | Vue.use(VueAxios, axios) 8 | }, 9 | get(uri) { 10 | return Vue.axios.get(`${uri}`, { 11 | headers: { 12 | Authorization: `Bearer ${localStorage.getItem('token')}` || '', 13 | Accept: 'application/json' 14 | } 15 | }) 16 | }, 17 | login(uri, config) { 18 | return Vue.axios.post(`${uri}`, {}, config) 19 | }, 20 | post(uri, params) { 21 | return Vue.axios.post(`${uri}`, params, { 22 | headers: { 23 | Authorization: `Bearer ${localStorage.getItem('token')}` || '' 24 | } 25 | }) 26 | }, 27 | update(uri, params) { 28 | return Vue.axios.put(uri, params, { 29 | headers: { 30 | Authorization: `Bearer ${localStorage.getItem('token')}` || '' 31 | } 32 | }) 33 | }, 34 | delete(uri) { 35 | return Vue.axios.delete(uri, { 36 | headers: { 37 | Authorization: `Bearer ${localStorage.getItem('token')}` || '' 38 | } 39 | }) 40 | } 41 | } 42 | 43 | ApiService.init() 44 | 45 | export default ApiService 46 | -------------------------------------------------------------------------------- /frontend/src/api/modules/auth.js: -------------------------------------------------------------------------------- 1 | import ApiService from '@/api' 2 | 3 | const AuthService = { 4 | login(userInfo) { 5 | return ApiService.post(`/login/token`, userInfo) 6 | } 7 | } 8 | 9 | export default AuthService 10 | -------------------------------------------------------------------------------- /frontend/src/api/modules/favorite.js: -------------------------------------------------------------------------------- 1 | import ApiService from '@/api' 2 | 3 | const BASE_URL = '/favorites' 4 | 5 | const FavoriteService = { 6 | get() { 7 | return ApiService.get(`${BASE_URL}`) 8 | }, 9 | create(newFavorite) { 10 | return ApiService.post(`${BASE_URL}`, newFavorite) 11 | }, 12 | delete(favoriteId) { 13 | return ApiService.delete(`${BASE_URL}/${favoriteId}`) 14 | } 15 | } 16 | 17 | export default FavoriteService 18 | -------------------------------------------------------------------------------- /frontend/src/api/modules/line.js: -------------------------------------------------------------------------------- 1 | import ApiService from '@/api' 2 | import station from "../../store/modules/station"; 3 | 4 | const BASE_URL = '/lines' 5 | 6 | const LineService = { 7 | get(lineId) { 8 | return ApiService.get(`${BASE_URL}/${lineId}`) 9 | }, 10 | getAll() { 11 | return ApiService.get(`${BASE_URL}`) 12 | }, 13 | create(newLine) { 14 | return ApiService.post(`${BASE_URL}`, newLine) 15 | }, 16 | update(editingLine) { 17 | return ApiService.update(`${BASE_URL}/${editingLine.lineId}`, editingLine.line) 18 | }, 19 | delete(lineId) { 20 | return ApiService.delete(`${BASE_URL}/${lineId}`) 21 | }, 22 | createSection({ lineId, section }) { 23 | return ApiService.post(`${BASE_URL}/${lineId}/sections`, section) 24 | }, 25 | deleteSection({ lineId, stationId }) { 26 | return ApiService.delete(`${BASE_URL}/${lineId}/sections?stationId=${stationId}`) 27 | } 28 | } 29 | 30 | export default LineService 31 | -------------------------------------------------------------------------------- /frontend/src/api/modules/map.js: -------------------------------------------------------------------------------- 1 | import ApiService from '@/api' 2 | 3 | const BASE_URL = '/maps' 4 | 5 | const MapService = { 6 | get() { 7 | return ApiService.get(`${BASE_URL}`) 8 | } 9 | } 10 | 11 | export default MapService 12 | -------------------------------------------------------------------------------- /frontend/src/api/modules/member.js: -------------------------------------------------------------------------------- 1 | import ApiService from '@/api' 2 | 3 | const BASE_URL = '/members' 4 | 5 | const MemberService = { 6 | get() { 7 | return ApiService.get(`${BASE_URL}/me`) 8 | }, 9 | create(newMember) { 10 | return ApiService.post(`${BASE_URL}`, newMember) 11 | }, 12 | update(updateMemberView) { 13 | return ApiService.update(`${BASE_URL}/me`, updateMemberView) 14 | }, 15 | delete() { 16 | return ApiService.delete(`${BASE_URL}/me`) 17 | } 18 | } 19 | 20 | export default MemberService -------------------------------------------------------------------------------- /frontend/src/api/modules/path.js: -------------------------------------------------------------------------------- 1 | import ApiService from '@/api' 2 | 3 | const BASE_URL = '/paths' 4 | 5 | const PathService = { 6 | get({ source, target }) { 7 | return ApiService.get(`${BASE_URL}/?source=${source}&target=${target}`) 8 | } 9 | } 10 | 11 | export default PathService 12 | -------------------------------------------------------------------------------- /frontend/src/api/modules/station.js: -------------------------------------------------------------------------------- 1 | import ApiService from '@/api' 2 | 3 | const BASE_URL = '/stations' 4 | 5 | const StationService = { 6 | get(stationId) { 7 | return ApiService.get(`${BASE_URL}/${stationId}`) 8 | }, 9 | getAll() { 10 | return ApiService.get(`${BASE_URL}`) 11 | }, 12 | create(newStationName) { 13 | return ApiService.post(`${BASE_URL}`, newStationName) 14 | }, 15 | update(station) { 16 | return ApiService.put(`${BASE_URL}/${station.id}`, station) 17 | }, 18 | delete(stationId) { 19 | return ApiService.delete(`${BASE_URL}/${stationId}`) 20 | } 21 | } 22 | 23 | export default StationService 24 | -------------------------------------------------------------------------------- /frontend/src/components/dialogs/ConfirmDialog.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 71 | -------------------------------------------------------------------------------- /frontend/src/components/dialogs/Dialog.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 82 | -------------------------------------------------------------------------------- /frontend/src/components/menus/ButtonMenu.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /frontend/src/components/menus/IconMenu.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 44 | -------------------------------------------------------------------------------- /frontend/src/components/menus/MenuListItem.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /frontend/src/components/snackbars/Snackbar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 34 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import vuetify from '@/utils/plugin/vuetify' 3 | import router from '@/router' 4 | import store from '@/store' 5 | import App from './App.vue' 6 | import '@/api' 7 | import '@/styles/index.scss' 8 | 9 | new Vue({ 10 | el: '#app', 11 | vuetify, 12 | router, 13 | store, 14 | render: h => h(App) 15 | }) 16 | -------------------------------------------------------------------------------- /frontend/src/mixins/dialog.js: -------------------------------------------------------------------------------- 1 | import validator from '@/utils/validator' 2 | 3 | const dialog = { 4 | methods: { 5 | closeDialog() { 6 | this.close = !this.close 7 | }, 8 | isValid($form) { 9 | return $form.validate() 10 | } 11 | }, 12 | data() { 13 | return { 14 | close: false, 15 | rules: { 16 | ...validator 17 | }, 18 | valid: false, 19 | isRequested: false 20 | } 21 | } 22 | } 23 | 24 | export default dialog 25 | -------------------------------------------------------------------------------- /frontend/src/mixins/responsive.js: -------------------------------------------------------------------------------- 1 | const responsive = { 2 | methods: { 3 | xsOnly() { 4 | return this.$vuetify.breakpoint.xsOnly 5 | }, 6 | smAndUp() { 7 | return this.$vuetify.breakpoint.smAndUp 8 | } 9 | } 10 | } 11 | 12 | export default responsive 13 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import stationRoutes from '@/router/modules/station' 4 | import lineRoutes from '@/router/modules/line' 5 | import mainRoutes from '@/router/modules/main' 6 | import sectionRoutes from '@/router/modules/section' 7 | import mapRoutes from '@/router/modules/map' 8 | import pathRoutes from '@/router/modules/path' 9 | import authRoutes from '@/router/modules/auth' 10 | import favoriteRoutes from '@/router/modules/favorite' 11 | 12 | Vue.use(VueRouter) 13 | 14 | export default new VueRouter({ 15 | mode: 'history', 16 | routes: [...mapRoutes, ...pathRoutes, ...stationRoutes, ...lineRoutes, ...sectionRoutes, ...mainRoutes, ...authRoutes, ...favoriteRoutes] 17 | }) 18 | -------------------------------------------------------------------------------- /frontend/src/router/modules/auth.js: -------------------------------------------------------------------------------- 1 | import LoginPage from '@/views/auth/LoginPage' 2 | import JoinPage from '@/views/auth/JoinPage' 3 | import Mypage from '@/views/auth/Mypage' 4 | import MypageEdit from '@/views/auth/MypageEdit' 5 | 6 | const authRoutes = [ 7 | { 8 | path: '/login', 9 | component: LoginPage 10 | }, 11 | { 12 | path: '/join', 13 | component: JoinPage 14 | }, 15 | { 16 | path: '/mypage', 17 | component: Mypage 18 | }, 19 | { 20 | path: '/mypage/edit', 21 | component: MypageEdit 22 | } 23 | ] 24 | export default authRoutes 25 | -------------------------------------------------------------------------------- /frontend/src/router/modules/favorite.js: -------------------------------------------------------------------------------- 1 | import Favorites from '@/views/favorite/Favorites' 2 | 3 | const favoriteRoutes = [ 4 | { 5 | path: '/favorites', 6 | component: Favorites 7 | } 8 | ] 9 | export default favoriteRoutes 10 | -------------------------------------------------------------------------------- /frontend/src/router/modules/line.js: -------------------------------------------------------------------------------- 1 | import LinePage from '@/views/line/LinePage' 2 | 3 | const lineRoutes = [ 4 | { 5 | path: '/lines', 6 | component: LinePage 7 | } 8 | ] 9 | export default lineRoutes 10 | -------------------------------------------------------------------------------- /frontend/src/router/modules/main.js: -------------------------------------------------------------------------------- 1 | import MainPage from '@/views/main/MainPage' 2 | 3 | const mainRoutes = [ 4 | { 5 | path: '/', 6 | component: MainPage 7 | } 8 | ] 9 | export default mainRoutes 10 | -------------------------------------------------------------------------------- /frontend/src/router/modules/map.js: -------------------------------------------------------------------------------- 1 | import MapPage from '../../views/map/MapPage' 2 | 3 | const mapRoutes = [ 4 | { 5 | path: '/maps', 6 | component: MapPage 7 | } 8 | ] 9 | export default mapRoutes 10 | -------------------------------------------------------------------------------- /frontend/src/router/modules/path.js: -------------------------------------------------------------------------------- 1 | import PathPage from '../../views/path/PathPage' 2 | 3 | const pathRoutes = [ 4 | { 5 | path: '/path', 6 | component: PathPage 7 | } 8 | ] 9 | export default pathRoutes 10 | -------------------------------------------------------------------------------- /frontend/src/router/modules/section.js: -------------------------------------------------------------------------------- 1 | import SectionPage from '@/views/section/SectionPage' 2 | 3 | const sectionRoutes = [ 4 | { 5 | path: '/sections', 6 | component: SectionPage 7 | } 8 | ] 9 | export default sectionRoutes 10 | -------------------------------------------------------------------------------- /frontend/src/router/modules/station.js: -------------------------------------------------------------------------------- 1 | import StationPage from '@/views/station/StationPage' 2 | 3 | const stationRoutes = [ 4 | { 5 | path: '/stations', 6 | component: StationPage 7 | } 8 | ] 9 | export default stationRoutes 10 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import station from '@/store/modules/station' 4 | import line from '@/store/modules/line' 5 | import snackbar from '@/store/modules/snackbar' 6 | import map from '@/store/modules/map' 7 | import path from '@/store/modules/path' 8 | import member from '@/store/modules/member' 9 | import auth from '@/store/modules/auth' 10 | import favorite from '@/store/modules/favorite' 11 | 12 | Vue.use(Vuex) 13 | 14 | export default new Vuex.Store({ 15 | modules: { 16 | station, 17 | line, 18 | snackbar, 19 | map, 20 | path, 21 | member, 22 | favorite, 23 | auth 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /frontend/src/store/modules/auth.js: -------------------------------------------------------------------------------- 1 | import { SET_ACCESS_TOKEN } from '@/store/shared/mutationTypes' 2 | import { FETCH_MEMBER, LOGIN } from '@/store/shared/actionTypes' 3 | import AuthService from '@/api/modules/auth' 4 | 5 | const state = { 6 | accessToken: null 7 | } 8 | 9 | const getters = { 10 | accessToken(state) { 11 | return state.accessToken 12 | } 13 | } 14 | 15 | const mutations = { 16 | [SET_ACCESS_TOKEN](state, accessToken) { 17 | state.accessToken = accessToken 18 | } 19 | } 20 | 21 | const actions = { 22 | async [LOGIN]({ commit, dispatch }, loginInfo) { 23 | return AuthService.login(loginInfo).then(({ data }) => { 24 | commit(SET_ACCESS_TOKEN, data.accessToken) 25 | localStorage.setItem('token', data.accessToken) 26 | dispatch(FETCH_MEMBER) 27 | }) 28 | } 29 | } 30 | 31 | export default { 32 | state, 33 | getters, 34 | actions, 35 | mutations 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/store/modules/favorite.js: -------------------------------------------------------------------------------- 1 | import { SET_FAVORITES } from '@/store/shared/mutationTypes' 2 | import { CREATE_FAVORITE, DELETE_FAVORITE, FETCH_FAVORITES } from '@/store/shared/actionTypes' 3 | import FavoriteService from '@/api/modules/favorite' 4 | 5 | const state = { 6 | favorites: [] 7 | } 8 | 9 | const getters = { 10 | favorites(state) { 11 | return state.favorites 12 | } 13 | } 14 | 15 | const mutations = { 16 | [SET_FAVORITES](state, favorites) { 17 | state.favorites = favorites 18 | } 19 | } 20 | 21 | const actions = { 22 | async [FETCH_FAVORITES]({ commit }) { 23 | return FavoriteService.get().then(({ data }) => { 24 | commit(SET_FAVORITES, data) 25 | }) 26 | }, 27 | async [CREATE_FAVORITE]({ commit }, newFavorite) { 28 | return FavoriteService.create(newFavorite) 29 | }, 30 | async [DELETE_FAVORITE]({ commit }, favoriteId) { 31 | return FavoriteService.delete(favoriteId) 32 | } 33 | } 34 | 35 | export default { 36 | state, 37 | getters, 38 | actions, 39 | mutations 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/store/modules/line.js: -------------------------------------------------------------------------------- 1 | import { SET_LINE, SET_LINES } from '@/store/shared/mutationTypes' 2 | import { CREATE_LINE, DELETE_LINE, FETCH_LINES, EDIT_LINE, DELETE_SECTION, CREATE_SECTION, FETCH_LINE } from '@/store/shared/actionTypes' 3 | import LineService from '@/api/modules/line' 4 | 5 | const state = { 6 | line: {}, 7 | lines: [] 8 | } 9 | 10 | const getters = { 11 | line(state) { 12 | return state.line 13 | }, 14 | lines(state) { 15 | return state.lines 16 | } 17 | } 18 | 19 | const mutations = { 20 | [SET_LINE](state, line) { 21 | state.line = line 22 | }, 23 | [SET_LINES](state, lines) { 24 | state.lines = lines 25 | } 26 | } 27 | 28 | const actions = { 29 | async [CREATE_LINE]({ commit }, newLine) { 30 | return LineService.create(newLine) 31 | }, 32 | async [FETCH_LINE]({ commit }, lineId) { 33 | return LineService.get(lineId).then(({ data }) => { 34 | commit(SET_LINE, data) 35 | return data 36 | }) 37 | }, 38 | async [FETCH_LINES]({ commit }) { 39 | return LineService.getAll().then(({ data }) => { 40 | commit(SET_LINES, data) 41 | }) 42 | }, 43 | async [EDIT_LINE]({ commit }, editingLine) { 44 | return LineService.update(editingLine) 45 | }, 46 | async [DELETE_LINE]({ commit }, lineId) { 47 | return LineService.delete(lineId) 48 | }, 49 | async [DELETE_SECTION]({ commit }, { lineId, stationId }) { 50 | return LineService.deleteSection({ lineId, stationId }) 51 | }, 52 | async [CREATE_SECTION]({ commit }, { lineId, section }) { 53 | return LineService.createSection({ lineId, section }) 54 | } 55 | } 56 | 57 | export default { 58 | state, 59 | getters, 60 | actions, 61 | mutations 62 | } 63 | -------------------------------------------------------------------------------- /frontend/src/store/modules/map.js: -------------------------------------------------------------------------------- 1 | import { SET_MAP } from '@/store/shared/mutationTypes' 2 | import { FETCH_MAP } from '@/store/shared/actionTypes' 3 | import MapService from '@/api/modules/map' 4 | 5 | const state = { 6 | map: {} 7 | } 8 | 9 | const getters = { 10 | map(state) { 11 | return state.map 12 | } 13 | } 14 | 15 | const mutations = { 16 | [SET_MAP](state, map) { 17 | state.map = map 18 | } 19 | } 20 | 21 | const actions = { 22 | async [FETCH_MAP]({ commit }, lineId) { 23 | return MapService.get(lineId).then(({ data: { lineResponses } }) => { 24 | commit(SET_MAP, lineResponses) 25 | return lineResponses 26 | }) 27 | } 28 | } 29 | 30 | export default { 31 | state, 32 | getters, 33 | actions, 34 | mutations 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/store/modules/member.js: -------------------------------------------------------------------------------- 1 | import { CREATE_MEMBER, DELETE_MEMBER, FETCH_MEMBER, UPDATE_MEMBER } from '@/store/shared/actionTypes' 2 | import MemberService from '@/api/modules/member' 3 | import { SET_MEMBER } from '@/store/shared/mutationTypes' 4 | 5 | const state = { 6 | member: null 7 | } 8 | 9 | const getters = { 10 | member(state) { 11 | return state.member 12 | } 13 | } 14 | 15 | const mutations = { 16 | [SET_MEMBER](state, member) { 17 | state.member = member 18 | } 19 | } 20 | 21 | const actions = { 22 | async [CREATE_MEMBER]({ commit }, newMemberView) { 23 | return MemberService.create(newMemberView) 24 | }, 25 | async [FETCH_MEMBER]({ commit }) { 26 | return MemberService.get().then(({ data }) => { 27 | commit(SET_MEMBER, data) 28 | }) 29 | }, 30 | async [DELETE_MEMBER]({ commit }, memberId) { 31 | return MemberService.delete().then(() => { 32 | commit(SET_MEMBER, null) 33 | localStorage.setItem('token', null) 34 | }) 35 | }, 36 | async [UPDATE_MEMBER]({ commit, dispatch }, updateMemberView) { 37 | return MemberService.update(updateMemberView).then(() => { 38 | dispatch(FETCH_MEMBER) 39 | }) 40 | } 41 | } 42 | 43 | export default { 44 | state, 45 | getters, 46 | mutations, 47 | actions 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/store/modules/path.js: -------------------------------------------------------------------------------- 1 | import { SET_PATH } from '@/store/shared/mutationTypes' 2 | import { SEARCH_PATH } from '@/store/shared/actionTypes' 3 | import PathService from '@/api/modules/path' 4 | 5 | const state = { 6 | pathResult: null 7 | } 8 | 9 | const getters = { 10 | pathResult(state) { 11 | return state.pathResult 12 | } 13 | } 14 | 15 | const mutations = { 16 | [SET_PATH](state, pathResult) { 17 | state.pathResult = pathResult 18 | } 19 | } 20 | 21 | const actions = { 22 | async [SEARCH_PATH]({ commit }, { source, target, type }) { 23 | return PathService.get({ source, target, type }).then(({ data }) => { 24 | commit(SET_PATH, data) 25 | }) 26 | } 27 | } 28 | 29 | export default { 30 | state, 31 | getters, 32 | actions, 33 | mutations 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/store/modules/snackbar.js: -------------------------------------------------------------------------------- 1 | import { SHOW_SNACKBAR, HIDE_SNACKBAR } from '@/store/shared/mutationTypes' 2 | 3 | const state = { 4 | isShow: false, 5 | message: '' 6 | } 7 | 8 | const getters = { 9 | isShow(state) { 10 | return state.isShow 11 | }, 12 | message(state) { 13 | return state.message 14 | } 15 | } 16 | 17 | const mutations = { 18 | [SHOW_SNACKBAR](state, message) { 19 | state.isShow = !state.isShow 20 | state.message = message 21 | }, 22 | [HIDE_SNACKBAR](state) { 23 | state.isShow = !state.isShow 24 | } 25 | } 26 | 27 | export default { 28 | state, 29 | getters, 30 | mutations 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/store/modules/station.js: -------------------------------------------------------------------------------- 1 | import { SET_STATIONS } from '@/store/shared/mutationTypes' 2 | import { CREATE_STATION, DELETE_STATION, FETCH_STATIONS } from '@/store/shared/actionTypes' 3 | import StationService from '@/api/modules/station' 4 | 5 | const state = { 6 | stations: [] 7 | } 8 | 9 | const getters = { 10 | stations(state) { 11 | return state.stations 12 | } 13 | } 14 | 15 | const mutations = { 16 | [SET_STATIONS](state, stations) { 17 | state.stations = stations 18 | } 19 | } 20 | 21 | const actions = { 22 | async [CREATE_STATION]({ commit }, newStationName) { 23 | return StationService.create(newStationName) 24 | }, 25 | async [FETCH_STATIONS]({ commit }) { 26 | return StationService.getAll().then(({ data }) => { 27 | commit(SET_STATIONS, data) 28 | }) 29 | }, 30 | async [DELETE_STATION]({ commit }, stationId) { 31 | return StationService.delete(stationId) 32 | } 33 | } 34 | 35 | export default { 36 | state, 37 | getters, 38 | actions, 39 | mutations 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/store/shared/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const CREATE_STATION = 'createStation' 2 | export const FETCH_STATIONS = 'fetchStations' 3 | export const DELETE_STATION = 'deleteStation' 4 | export const CREATE_LINE = 'createLine' 5 | export const FETCH_LINE = 'fetchLine' 6 | export const FETCH_LINES = 'fetchLines' 7 | export const DELETE_LINE = 'deleteLine' 8 | export const EDIT_LINE = 'editLine' 9 | export const DELETE_SECTION = 'deleteSection' 10 | export const CREATE_SECTION = 'createSection' 11 | export const FETCH_MAP = 'fetchMap' 12 | export const SEARCH_PATH = 'searchPath' 13 | export const CREATE_MEMBER = 'createMember' 14 | export const LOGIN = 'login' 15 | export const CREATE_FAVORITE = 'createFavorite' 16 | export const DELETE_FAVORITE = 'deleteFavorite' 17 | export const FETCH_FAVORITES = 'fetchFavorites' 18 | export const FETCH_MEMBER = 'fetchMember' 19 | export const UPDATE_MEMBER = 'updateMember' 20 | export const DELETE_MEMBER = 'deleteMember' 21 | -------------------------------------------------------------------------------- /frontend/src/store/shared/mutationTypes.js: -------------------------------------------------------------------------------- 1 | export const SET_STATIONS = 'setStations' 2 | export const SHOW_SNACKBAR = 'showSnackbar' 3 | export const HIDE_SNACKBAR = 'hideSnackbar' 4 | export const SET_LINES = 'setLines' 5 | export const SET_LINE = 'setLine' 6 | export const SET_MAP = 'setMap' 7 | export const SET_PATH = 'setPath' 8 | export const SET_MEMBER = 'setMember' 9 | export const SET_FAVORITES = 'setFavorites' 10 | export const SET_ACCESS_TOKEN = 'setAccessToken' 11 | -------------------------------------------------------------------------------- /frontend/src/styles/color.scss: -------------------------------------------------------------------------------- 1 | .bg-grey-lighten-5 { 2 | background-color: #fafafa; 3 | } 4 | 5 | .bg-grey-lighten-4 { 6 | background-color: #f5f5f5; 7 | } 8 | 9 | .bg-grey-lighten-3 { 10 | background-color: #eeeeee; 11 | } 12 | 13 | .bg-grey-lighten-2 { 14 | background-color: #e0e0e0; 15 | } 16 | 17 | .bg-grey-lighten-1 { 18 | background-color: #bdbdbd; 19 | } 20 | 21 | .bg-light-gray { 22 | background-color: #f5f5f5 !important; 23 | } 24 | 25 | .bg-apricot { 26 | background-color: #ffe0b3; 27 | } 28 | 29 | .bg-aliceblue { 30 | background-color: aliceblue !important; 31 | } 32 | 33 | .bg-lightblue { 34 | background-color: lightblue !important; 35 | } 36 | 37 | .bg-darkseagreen { 38 | background-color: darkseagreen !important; 39 | } 40 | 41 | .bg-bisque { 42 | background-color: bisque !important; 43 | } 44 | 45 | .bg-gray { 46 | background-color: gray !important; 47 | } 48 | 49 | .bg-whitesmoke { 50 | background-color: whitesmoke !important; 51 | } 52 | 53 | .bg-f8 { 54 | background-color: #f8f8f8 !important; 55 | } 56 | 57 | .bg-gainsboro { 58 | background-color: gainsboro; 59 | } 60 | 61 | .bg-black { 62 | background-color: #333 !important; 63 | } 64 | 65 | .bg-blue { 66 | background-color: cornflowerblue !important; 67 | } 68 | 69 | .bg-green { 70 | background-color: #64c5b1 !important; 71 | } -------------------------------------------------------------------------------- /frontend/src/styles/components/alert.scss: -------------------------------------------------------------------------------- 1 | .alert-success-border { 2 | border-left: 8px solid #4caf50 !important; 3 | } 4 | 5 | .alert-warning-border { 6 | border-left: 8px solid #fb8c00 !important; 7 | } 8 | 9 | .alert-info-border { 10 | border-left: 8px solid #2196f3 !important; 11 | } 12 | 13 | .alert-error-border { 14 | border-left: 8px solid #ff5252 !important; 15 | } 16 | 17 | .alert-cyan-border { 18 | border-left: 8px solid #80deea !important; 19 | background-color: #eefdff !important; 20 | color: #006064 !important; 21 | } 22 | 23 | .alert-dark-border { 24 | border-left: 8px solid #333 !important; 25 | background-color: #eee !important; 26 | color: #333 !important; 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/styles/fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'hanna'; 3 | src: url(/fonts/BMHANNAPro.otf) format('truetype'); 4 | } 5 | 6 | @font-face { 7 | font-family: 'hannaair'; 8 | src: url(/fonts/BMHANNAAir.otf) format('truetype'); 9 | } 10 | 11 | @font-face { 12 | font-family: 'euljiro'; 13 | src: url(/fonts/BMEULJIRO.otf) format('truetype'); 14 | } 15 | 16 | @font-face { 17 | font-family: 'jua'; 18 | src: url(/fonts/BMJUA.otf) format('truetype'); 19 | } 20 | 21 | .font-bamin { 22 | font-family: hanna, 'Noto Sans KR', 'Roboto', sans-serif, 'Apple SD Gothic Neo' !important; 23 | } 24 | 25 | .font-hanna { 26 | font-family: hanna, 'Noto Sans KR', 'Roboto', sans-serif, 'Apple SD Gothic Neo' !important; 27 | } 28 | 29 | .font-hannaair { 30 | font-family: hannaair, 'Noto Sans KR', 'Roboto', sans-serif, 'Apple SD Gothic Neo' !important; 31 | } 32 | 33 | .font-euljiro { 34 | font-family: euljiro, 'Noto Sans KR', 'Roboto', sans-serif, 'Apple SD Gothic Neo' !important; 35 | } 36 | 37 | .font-jua { 38 | font-family: jua, 'Noto Sans KR', 'Roboto', sans-serif, 'Apple SD Gothic Neo' !important; 39 | } 40 | 41 | .font-bamin-light { 42 | font-family: hannaair, 'Noto Sans KR', 'Roboto', sans-serif, 'Apple SD Gothic Neo' !important; 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './app.scss'; 2 | @import './layout.scss'; 3 | -------------------------------------------------------------------------------- /frontend/src/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const SNACKBAR_MESSAGES = { 2 | COMMON: { 3 | SUCCESS: '😀 성공적으로 변경되었습니다.', 4 | FAIL: '😰 오류가 발생했습니다.' 5 | }, 6 | LOGIN: { 7 | SUCCESS: '😀 방문을 환영합니다.', 8 | FAIL: '😰 로그인 중에 오류가 발생했습니다.' 9 | }, 10 | LOGOUT: { 11 | SUCCESS: '😀 로그아웃 되었습니다. 다음에 또 만나요.', 12 | FAIL: '😰 로그아웃 중에 오류가 발생했습니다.' 13 | }, 14 | MEMBER: { 15 | EDIT: { 16 | SUCCESS: '😀 정보가 수정되었습니다.', 17 | FAIL: '😰 정보를 수정하는 과정에 오류가 발생했습니다.' 18 | }, 19 | DELETE: { 20 | SUCCESS: '😀 정상적으로 탈퇴되었습니다. 다음에 또 만나요.', 21 | FAIL: '😰 탈퇴 하는 과정에 오류가 발생했습니다.' 22 | } 23 | }, 24 | FAVORITE: { 25 | FETCH: { 26 | FAIL: '😰 즐겨찾기 목록을 불러오는 과정에 오류가 발생했습니다.' 27 | }, 28 | ADD: { 29 | SUCCESS: '😀 즐겨찾기에 추가되었습니다.', 30 | FAIL: '😰 즐겨찾기에 추가하는 과정에 오류가 발생했습니다.' 31 | }, 32 | DELETE: { 33 | SUCCESS: '😀 성공적으로 삭제 하습니다.', 34 | FAIL: '😰 즐겨찾기 항목을 삭제하는 과정에 오류가 발생했습니다.' 35 | } 36 | }, 37 | PATH: { 38 | ARRIVAL_TIME: { 39 | SUCCESS: '😀 빠른 도착으로 다시 검색 하습니다.', 40 | FAIL: '😰 빠른 도착으로 다시 검색하는 과정에 오류가 발생했습니다.' 41 | } 42 | } 43 | } 44 | 45 | export const PATH_TYPE = { 46 | DISTANCE: 'DISTANCE', 47 | DURATION: 'DURATION', 48 | ARRIVAL_TIME: 'ARRIVAL_TIME' 49 | } 50 | 51 | export const LINE_COLORS = [ 52 | 'grey lighten-5', 53 | 'grey lighten-4', 54 | 'grey lighten-3', 55 | 'grey lighten-2', 56 | 'grey lighten-1', 57 | 'grey darken-1', 58 | 'grey darken-2', 59 | 'grey darken-3', 60 | 'grey darken-4', 61 | 62 | 'red lighten-5', 63 | 'red lighten-4', 64 | 'red lighten-3', 65 | 'red lighten-2', 66 | 'red lighten-1', 67 | 'red darken-1', 68 | 'red darken-2', 69 | 'red darken-3', 70 | 'red darken-4', 71 | 72 | 'orange lighten-5', 73 | 'orange lighten-4', 74 | 'orange lighten-3', 75 | 'orange lighten-2', 76 | 'orange lighten-1', 77 | 'orange darken-1', 78 | 'orange darken-2', 79 | 'orange darken-3', 80 | 'orange darken-4', 81 | 82 | 'yellow lighten-5', 83 | 'yellow lighten-4', 84 | 'yellow lighten-3', 85 | 'yellow lighten-2', 86 | 'yellow lighten-1', 87 | 'yellow darken-1', 88 | 'yellow darken-2', 89 | 'yellow darken-3', 90 | 'yellow darken-4', 91 | 92 | 'green lighten-5', 93 | 'green lighten-4', 94 | 'green lighten-3', 95 | 'green lighten-2', 96 | 'green lighten-1', 97 | 'green darken-1', 98 | 'green darken-2', 99 | 'green darken-3', 100 | 'green darken-4', 101 | 102 | 'teal lighten-5', 103 | 'teal lighten-4', 104 | 'teal lighten-3', 105 | 'teal lighten-2', 106 | 'teal lighten-1', 107 | 'teal darken-1', 108 | 'teal darken-2', 109 | 'teal darken-3', 110 | 'teal darken-4', 111 | 112 | 'blue lighten-5', 113 | 'blue lighten-4', 114 | 'blue lighten-3', 115 | 'blue lighten-2', 116 | 'blue lighten-1', 117 | 'blue darken-1', 118 | 'blue darken-2', 119 | 'blue darken-3', 120 | 'blue darken-4', 121 | 122 | 'indigo lighten-5', 123 | 'indigo lighten-4', 124 | 'indigo lighten-3', 125 | 'indigo lighten-2', 126 | 'indigo lighten-1', 127 | 'indigo darken-1', 128 | 'indigo darken-2', 129 | 'indigo darken-3', 130 | 'indigo darken-4', 131 | 132 | 'purple lighten-5', 133 | 'purple lighten-4', 134 | 'purple lighten-3', 135 | 'purple lighten-2', 136 | 'purple lighten-1', 137 | 'purple darken-1', 138 | 'purple darken-2', 139 | 'purple darken-3', 140 | 'purple darken-4', 141 | 142 | 'pink lighten-5', 143 | 'pink lighten-4', 144 | 'pink lighten-3', 145 | 'pink lighten-2', 146 | 'pink lighten-1', 147 | 'pink darken-1', 148 | 'pink darken-2', 149 | 'pink darken-3', 150 | 'pink darken-4' 151 | ] 152 | -------------------------------------------------------------------------------- /frontend/src/utils/plugin/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify' 3 | import 'vuetify/dist/vuetify.min.css' 4 | 5 | Vue.use(Vuetify) 6 | 7 | const opts = {} 8 | 9 | export default new Vuetify(opts) 10 | -------------------------------------------------------------------------------- /frontend/src/utils/validator.js: -------------------------------------------------------------------------------- 1 | const validator = { 2 | path: { 3 | source: [], 4 | target: [] 5 | }, 6 | departureTime: { 7 | dayTime: [], 8 | hour: [], 9 | minute: [] 10 | }, 11 | stationName: [(v) => !!v || '이름 입력이 필요합니다.', (v) => v.length > 0 || '이름은 1글자 이상 입력해야 합니다.'], 12 | line: { 13 | name: [(v) => !!v || '이름 입력이 필요합니다.'], 14 | color: [(v) => !!v || '색상 입력이 필요합니다.'], 15 | }, 16 | section: { 17 | upStationId: [(v) => !!v || '상행역을 선택하세요.'], 18 | downStationId: [(v) => !!v || '하행역을 선택하세요.'], 19 | distance: [(v) => !!v || '거리 입력이 필요합니다.'] 20 | }, 21 | member: { 22 | email: [(v) => !!v || '이메일 입력이 필요합니다.', (v) => /.+@.+/.test(v) || '유효한 이메일을 입력해주세요'], 23 | age: [(v) => !!v || '나이 입력이 필요합니다.', (v) => v > 0 || '나이는 1살 이상 이어야 합니다.'], 24 | password: [(v) => !!v || '비밀번호 입력이 필요합니다.'], 25 | confirmPassword: [(v) => !!v || '비밀번호 확인이 필요합니다.', (v, c) => v === c || '비밀번호가 일치하지 않습니다.'] 26 | } 27 | } 28 | 29 | export default validator 30 | -------------------------------------------------------------------------------- /frontend/src/views/auth/JoinPage.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 112 | -------------------------------------------------------------------------------- /frontend/src/views/auth/LoginPage.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 93 | -------------------------------------------------------------------------------- /frontend/src/views/auth/Mypage.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 72 | -------------------------------------------------------------------------------- /frontend/src/views/auth/MypageEdit.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 131 | -------------------------------------------------------------------------------- /frontend/src/views/base/header/Header.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 79 | 84 | -------------------------------------------------------------------------------- /frontend/src/views/base/header/components/FavoritesButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /frontend/src/views/base/header/components/LogoutButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 36 | -------------------------------------------------------------------------------- /frontend/src/views/base/header/components/MyPageButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /frontend/src/views/favorite/Favorites.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 74 | 79 | -------------------------------------------------------------------------------- /frontend/src/views/favorite/components/FavoriteDeleteButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 36 | -------------------------------------------------------------------------------- /frontend/src/views/line/LinePage.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 65 | 66 | 71 | -------------------------------------------------------------------------------- /frontend/src/views/line/components/LineCreateButton.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 160 | 161 | 170 | -------------------------------------------------------------------------------- /frontend/src/views/line/components/LineDeleteButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /frontend/src/views/line/components/LineEditButton.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 110 | 115 | -------------------------------------------------------------------------------- /frontend/src/views/line/components/LineForm.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 108 | 109 | 114 | -------------------------------------------------------------------------------- /frontend/src/views/main/MainPage.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 46 | 47 | 56 | -------------------------------------------------------------------------------- /frontend/src/views/map/MapPage.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 39 | 58 | -------------------------------------------------------------------------------- /frontend/src/views/path/PathPage.vue: -------------------------------------------------------------------------------- 1 | 103 | 104 | 160 | -------------------------------------------------------------------------------- /frontend/src/views/path/components/AddFavoriteButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 41 | -------------------------------------------------------------------------------- /frontend/src/views/section/SectionPage.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 117 | -------------------------------------------------------------------------------- /frontend/src/views/section/components/SectionCreateButton.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 178 | 179 | 184 | -------------------------------------------------------------------------------- /frontend/src/views/section/components/SectionDeleteButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /frontend/src/views/station/StationPage.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 105 | 106 | 111 | -------------------------------------------------------------------------------- /frontend/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const VuetifyLoaderPlugin = require('vuetify-loader/lib/plugin') 3 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 4 | const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin') 5 | 6 | const clientPath = path.resolve(__dirname, 'src') 7 | 8 | module.exports = { 9 | entry: { 10 | 'js/vendors': ['vue', 'vue-router', 'vuex', 'vuetify', 'axios', 'vue-axios'], 11 | 'js/main': ['babel-polyfill', `${clientPath}/main.js`] 12 | }, 13 | resolve: { 14 | alias: { 15 | vue$: 'vue/dist/vue.esm.js', 16 | '@': path.join(__dirname, 'src') 17 | }, 18 | extensions: ['*', '.js', '.vue', '.json'] 19 | }, 20 | optimization: { 21 | splitChunks: { 22 | chunks: 'all', 23 | cacheGroups: { 24 | vendors: { 25 | test: /[\\/]node_modules[\\/]/, 26 | name: 'js/vendors' 27 | } 28 | } 29 | } 30 | }, 31 | module: { 32 | rules: [ 33 | { test: /\.vue$/, loader: 'vue-loader' }, 34 | { 35 | test: /\.s(c|a)ss$/, 36 | use: [ 37 | 'vue-style-loader', 38 | 'css-loader', 39 | { 40 | loader: 'sass-loader', 41 | options: { 42 | implementation: require('sass'), 43 | sassOptions: { 44 | fiber: require('fibers') 45 | } 46 | } 47 | } 48 | ] 49 | }, 50 | { 51 | test: /\.(jpe?g|png|gif)$/i, 52 | use: [ 53 | { 54 | loader: 'url-loader' 55 | } 56 | ] 57 | }, 58 | { 59 | test: /\.css$/, 60 | loader: 'style-loader!css-loader' 61 | }, 62 | { 63 | test: /\.js$/, 64 | exclude: /node_modules/, 65 | use: { 66 | loader: 'babel-loader' 67 | } 68 | } 69 | ] 70 | }, 71 | plugins: [new VuetifyLoaderPlugin(), new VueLoaderPlugin(), new CaseSensitivePathsPlugin()] 72 | } 73 | -------------------------------------------------------------------------------- /frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const commonConfig = require('./webpack.common.js') 2 | const webpackMerge = require('webpack-merge') 3 | const { argv } = require('yargs') 4 | 5 | module.exports = () => { 6 | const envConfig = require(`./webpack.${argv.env}.js`) 7 | return webpackMerge(commonConfig, envConfig) 8 | } 9 | -------------------------------------------------------------------------------- /frontend/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const outputPath = path.resolve(__dirname, 'out') 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devtool: 'cheap-eval-source-map', 8 | output: { 9 | path: outputPath, 10 | filename: '[name].js' 11 | }, 12 | devServer: { 13 | contentBase: outputPath, 14 | publicPath: '/', 15 | host: '0.0.0.0', 16 | port: 8081, 17 | proxy: { 18 | '/resources/\\d*/js/(main|vendors).js': { 19 | target: 'http://127.0.0.1:8081', 20 | pathRewrite: {'/resources/\\d*' : ''} 21 | }, 22 | '**': 'http://127.0.0.1:8080' 23 | }, 24 | inline: true, 25 | hot: false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin') 3 | const TerserPlugin = require('terser-webpack-plugin') 4 | 5 | const outputPath = path.resolve(__dirname, '../src/main/resources/static') 6 | 7 | module.exports = { 8 | mode: 'production', 9 | output: { 10 | path: outputPath, 11 | filename: '[name].js' 12 | }, 13 | optimization: { 14 | splitChunks: { 15 | chunks: 'all', 16 | cacheGroups: { 17 | vendors: { 18 | test: /[\\/]node_modules[\\/]/, 19 | name: 'js/vendors' 20 | } 21 | } 22 | }, 23 | minimizer: [ 24 | new TerserPlugin({ 25 | cache: true, 26 | parallel: true, 27 | terserOptions: { 28 | warnings: false, 29 | compress: { 30 | warnings: false, 31 | unused: true 32 | }, 33 | ecma: 6, 34 | mangle: true, 35 | unused: true 36 | }, 37 | sourceMap: true 38 | }), 39 | new OptimizeCssAssetsPlugin() 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/next-step/atdd-subway-service/6b0b91c3cf22015ff7d519e3c96f7de0cc7b4692/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { url 'https://repo.spring.io/milestone' } 4 | maven { url 'https://repo.spring.io/snapshot' } 5 | gradlePluginPortal() 6 | } 7 | resolutionStrategy { 8 | eachPlugin { 9 | if (requested.id.id == 'org.springframework.boot') { 10 | useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}") 11 | } 12 | } 13 | } 14 | } 15 | rootProject.name = 'subway' 16 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway; 2 | 3 | import org.springframework.data.annotation.CreatedDate; 4 | import org.springframework.data.annotation.LastModifiedDate; 5 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 6 | 7 | import javax.persistence.EntityListeners; 8 | import javax.persistence.MappedSuperclass; 9 | import java.time.LocalDateTime; 10 | 11 | @MappedSuperclass 12 | @EntityListeners(AuditingEntityListener.class) 13 | public class BaseEntity { 14 | @CreatedDate 15 | private LocalDateTime createdDate; 16 | 17 | @LastModifiedDate 18 | private LocalDateTime modifiedDate; 19 | 20 | public LocalDateTime getCreatedDate() { 21 | return createdDate; 22 | } 23 | 24 | public LocalDateTime getModifiedDate() { 25 | return modifiedDate; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/DataLoaderConfig.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway; 2 | 3 | import com.google.common.collect.Lists; 4 | import nextstep.subway.line.domain.Line; 5 | import nextstep.subway.line.domain.LineRepository; 6 | import nextstep.subway.member.domain.Member; 7 | import nextstep.subway.member.domain.MemberRepository; 8 | import nextstep.subway.station.domain.Station; 9 | import org.springframework.boot.CommandLineRunner; 10 | import org.springframework.context.annotation.Profile; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | @Profile("!test") 15 | public class DataLoaderConfig implements CommandLineRunner { 16 | private LineRepository lineRepository; 17 | private MemberRepository memberRepository; 18 | 19 | public DataLoaderConfig(LineRepository lineRepository, MemberRepository memberRepository) { 20 | this.lineRepository = lineRepository; 21 | this.memberRepository = memberRepository; 22 | } 23 | 24 | @Override 25 | public void run(String... args) throws Exception { 26 | Station 강남역 = new Station("강남역"); 27 | Station 교대역 = new Station("교대역"); 28 | Station 양재역 = new Station("양재역"); 29 | Station 남부터미널역 = new Station("남부터미널역"); 30 | 31 | Line 신분당선 = new Line("신분당선", "red lighten-1", 강남역, 양재역, 10); 32 | Line 이호선 = new Line("2호선", "green lighten-1", 교대역, 강남역, 10); 33 | Line 삼호선 = new Line("3호선", "orange darken-1", 교대역, 양재역, 10); 34 | 35 | lineRepository.saveAll(Lists.newArrayList(신분당선, 이호선, 삼호선)); 36 | 37 | memberRepository.save(new Member("probitanima11@gmail.com", "11", 10)); 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/PageController.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway; 2 | 3 | import org.springframework.http.MediaType; 4 | import org.springframework.stereotype.Controller; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | 7 | @Controller 8 | public class PageController { 9 | @GetMapping(value = { 10 | "/", 11 | "/stations", 12 | "/lines", 13 | "/sections", 14 | "/path", 15 | "/login", 16 | "/join", 17 | "/mypage", 18 | "/mypage/edit", 19 | "/favorites"}, produces = MediaType.TEXT_HTML_VALUE) 20 | public String index() { 21 | return "index"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/SubwayApplication.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 6 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 7 | 8 | @EnableJpaRepositories 9 | @EnableJpaAuditing 10 | @SpringBootApplication 11 | public class SubwayApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(SubwayApplication.class, args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/auth/application/AuthService.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.auth.application; 2 | 3 | import nextstep.subway.auth.domain.LoginMember; 4 | import nextstep.subway.auth.dto.TokenRequest; 5 | import nextstep.subway.auth.dto.TokenResponse; 6 | import nextstep.subway.auth.infrastructure.JwtTokenProvider; 7 | import nextstep.subway.member.domain.Member; 8 | import nextstep.subway.member.domain.MemberRepository; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | @Service 13 | public class AuthService { 14 | private MemberRepository memberRepository; 15 | private JwtTokenProvider jwtTokenProvider; 16 | 17 | public AuthService(MemberRepository memberRepository, JwtTokenProvider jwtTokenProvider) { 18 | this.memberRepository = memberRepository; 19 | this.jwtTokenProvider = jwtTokenProvider; 20 | } 21 | 22 | public TokenResponse login(TokenRequest request) { 23 | Member member = memberRepository.findByEmail(request.getEmail()).orElseThrow(AuthorizationException::new); 24 | member.checkPassword(request.getPassword()); 25 | 26 | String token = jwtTokenProvider.createToken(request.getEmail()); 27 | return new TokenResponse(token); 28 | } 29 | 30 | public LoginMember findMemberByToken(String credentials) { 31 | if (!jwtTokenProvider.validateToken(credentials)) { 32 | return new LoginMember(); 33 | } 34 | 35 | String email = jwtTokenProvider.getPayload(credentials); 36 | Member member = memberRepository.findByEmail(email).orElseThrow(RuntimeException::new); 37 | return new LoginMember(member.getId(), member.getEmail(), member.getAge()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/auth/application/AuthorizationException.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.auth.application; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.UNAUTHORIZED) 7 | public class AuthorizationException extends RuntimeException { 8 | public AuthorizationException() { 9 | } 10 | 11 | public AuthorizationException(String message) { 12 | super(message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/auth/domain/AuthenticationPrincipal.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.auth.domain; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.PARAMETER) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface AuthenticationPrincipal { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/auth/domain/LoginMember.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.auth.domain; 2 | 3 | public class LoginMember { 4 | private Long id; 5 | private String email; 6 | private Integer age; 7 | 8 | public LoginMember() { 9 | } 10 | 11 | public LoginMember(Long id, String email, Integer age) { 12 | this.id = id; 13 | this.email = email; 14 | this.age = age; 15 | } 16 | 17 | public Long getId() { 18 | return id; 19 | } 20 | 21 | public String getEmail() { 22 | return email; 23 | } 24 | 25 | public Integer getAge() { 26 | return age; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/auth/dto/TokenRequest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.auth.dto; 2 | 3 | public class TokenRequest { 4 | private String email; 5 | private String password; 6 | 7 | public TokenRequest() { 8 | } 9 | 10 | public TokenRequest(String email, String password) { 11 | this.email = email; 12 | this.password = password; 13 | } 14 | 15 | public String getEmail() { 16 | return email; 17 | } 18 | 19 | public String getPassword() { 20 | return password; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/auth/dto/TokenResponse.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.auth.dto; 2 | 3 | public class TokenResponse { 4 | private String accessToken; 5 | 6 | public TokenResponse() { 7 | } 8 | 9 | public TokenResponse(String accessToken) { 10 | this.accessToken = accessToken; 11 | } 12 | 13 | public String getAccessToken() { 14 | return accessToken; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/auth/infrastructure/AuthenticationPrincipalConfig.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.auth.infrastructure; 2 | 3 | import nextstep.subway.auth.application.AuthService; 4 | import nextstep.subway.auth.ui.AuthenticationPrincipalArgumentResolver; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 8 | 9 | import java.util.List; 10 | 11 | @Configuration 12 | public class AuthenticationPrincipalConfig implements WebMvcConfigurer { 13 | private final AuthService authService; 14 | 15 | public AuthenticationPrincipalConfig(AuthService authService) { 16 | this.authService = authService; 17 | } 18 | 19 | @Override 20 | public void addArgumentResolvers(List argumentResolvers) { 21 | argumentResolvers.add(createAuthenticationPrincipalArgumentResolver()); 22 | } 23 | 24 | @Bean 25 | public AuthenticationPrincipalArgumentResolver createAuthenticationPrincipalArgumentResolver() { 26 | return new AuthenticationPrincipalArgumentResolver(authService); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/auth/infrastructure/AuthorizationExtractor.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.auth.infrastructure; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import java.util.Enumeration; 5 | 6 | public class AuthorizationExtractor { 7 | public static final String AUTHORIZATION = "Authorization"; 8 | public static String BEARER_TYPE = "Bearer"; 9 | public static final String ACCESS_TOKEN_TYPE = AuthorizationExtractor.class.getSimpleName() + ".ACCESS_TOKEN_TYPE"; 10 | 11 | public static String extract(HttpServletRequest request) { 12 | Enumeration headers = request.getHeaders(AUTHORIZATION); 13 | while (headers.hasMoreElements()) { 14 | String value = headers.nextElement(); 15 | if ((value.toLowerCase().startsWith(BEARER_TYPE.toLowerCase()))) { 16 | String authHeaderValue = value.substring(BEARER_TYPE.length()).trim(); 17 | request.setAttribute(ACCESS_TOKEN_TYPE, value.substring(0, BEARER_TYPE.length()).trim()); 18 | int commaIndex = authHeaderValue.indexOf(','); 19 | if (commaIndex > 0) { 20 | authHeaderValue = authHeaderValue.substring(0, commaIndex); 21 | } 22 | return authHeaderValue; 23 | } 24 | } 25 | 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/auth/infrastructure/JwtTokenProvider.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.auth.infrastructure; 2 | 3 | import io.jsonwebtoken.*; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.Date; 8 | 9 | @Component 10 | public class JwtTokenProvider { 11 | @Value("${security.jwt.token.secret-key}") 12 | private String secretKey; 13 | @Value("${security.jwt.token.expire-length}") 14 | private long validityInMilliseconds; 15 | 16 | public String createToken(String payload) { 17 | Claims claims = Jwts.claims().setSubject(payload); 18 | Date now = new Date(); 19 | Date validity = new Date(now.getTime() + validityInMilliseconds); 20 | 21 | return Jwts.builder() 22 | .setClaims(claims) 23 | .setIssuedAt(now) 24 | .setExpiration(validity) 25 | .signWith(SignatureAlgorithm.HS256, secretKey) 26 | .compact(); 27 | } 28 | 29 | public String getPayload(String token) { 30 | return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); 31 | } 32 | 33 | public boolean validateToken(String token) { 34 | try { 35 | Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); 36 | 37 | return !claims.getBody().getExpiration().before(new Date()); 38 | } catch (JwtException | IllegalArgumentException e) { 39 | return false; 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/auth/ui/AuthController.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.auth.ui; 2 | 3 | import nextstep.subway.auth.application.AuthService; 4 | import nextstep.subway.auth.dto.TokenRequest; 5 | import nextstep.subway.auth.dto.TokenResponse; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @RestController 12 | public class AuthController { 13 | private AuthService authService; 14 | 15 | public AuthController(AuthService authService) { 16 | this.authService = authService; 17 | } 18 | 19 | @PostMapping("/login/token") 20 | public ResponseEntity login(@RequestBody TokenRequest request) { 21 | TokenResponse token = authService.login(request); 22 | return ResponseEntity.ok().body(token); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/auth/ui/AuthenticationPrincipalArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.auth.ui; 2 | 3 | import nextstep.subway.auth.application.AuthService; 4 | import nextstep.subway.auth.domain.AuthenticationPrincipal; 5 | import nextstep.subway.auth.infrastructure.AuthorizationExtractor; 6 | import org.springframework.core.MethodParameter; 7 | import org.springframework.web.bind.support.WebDataBinderFactory; 8 | import org.springframework.web.context.request.NativeWebRequest; 9 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 10 | import org.springframework.web.method.support.ModelAndViewContainer; 11 | 12 | import javax.servlet.http.HttpServletRequest; 13 | 14 | public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver { 15 | private AuthService authService; 16 | 17 | public AuthenticationPrincipalArgumentResolver(AuthService authService) { 18 | this.authService = authService; 19 | } 20 | 21 | @Override 22 | public boolean supportsParameter(MethodParameter parameter) { 23 | return parameter.hasParameterAnnotation(AuthenticationPrincipal.class); 24 | } 25 | 26 | @Override 27 | public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { 28 | String credentials = AuthorizationExtractor.extract(webRequest.getNativeRequest(HttpServletRequest.class)); 29 | return authService.findMemberByToken(credentials); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/line/application/LineService.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.line.application; 2 | 3 | import nextstep.subway.line.domain.Line; 4 | import nextstep.subway.line.domain.LineRepository; 5 | import nextstep.subway.line.domain.Section; 6 | import nextstep.subway.line.dto.LineRequest; 7 | import nextstep.subway.line.dto.LineResponse; 8 | import nextstep.subway.line.dto.SectionRequest; 9 | import nextstep.subway.station.application.StationService; 10 | import nextstep.subway.station.domain.Station; 11 | import nextstep.subway.station.dto.StationResponse; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Arrays; 17 | import java.util.List; 18 | import java.util.Optional; 19 | import java.util.stream.Collectors; 20 | 21 | @Service 22 | @Transactional 23 | public class LineService { 24 | private LineRepository lineRepository; 25 | private StationService stationService; 26 | 27 | public LineService(LineRepository lineRepository, StationService stationService) { 28 | this.lineRepository = lineRepository; 29 | this.stationService = stationService; 30 | } 31 | 32 | public LineResponse saveLine(LineRequest request) { 33 | Station upStation = stationService.findById(request.getUpStationId()); 34 | Station downStation = stationService.findById(request.getDownStationId()); 35 | Line persistLine = lineRepository.save(new Line(request.getName(), request.getColor(), upStation, downStation, request.getDistance())); 36 | List stations = getStations(persistLine).stream() 37 | .map(it -> StationResponse.of(it)) 38 | .collect(Collectors.toList()); 39 | return LineResponse.of(persistLine, stations); 40 | } 41 | 42 | public List findLines() { 43 | List persistLines = lineRepository.findAll(); 44 | return persistLines.stream() 45 | .map(line -> { 46 | List stations = getStations(line).stream() 47 | .map(it -> StationResponse.of(it)) 48 | .collect(Collectors.toList()); 49 | return LineResponse.of(line, stations); 50 | }) 51 | .collect(Collectors.toList()); 52 | } 53 | 54 | public Line findLineById(Long id) { 55 | return lineRepository.findById(id).orElseThrow(RuntimeException::new); 56 | } 57 | 58 | 59 | public LineResponse findLineResponseById(Long id) { 60 | Line persistLine = findLineById(id); 61 | List stations = getStations(persistLine).stream() 62 | .map(it -> StationResponse.of(it)) 63 | .collect(Collectors.toList()); 64 | return LineResponse.of(persistLine, stations); 65 | } 66 | 67 | public void updateLine(Long id, LineRequest lineUpdateRequest) { 68 | Line persistLine = lineRepository.findById(id).orElseThrow(RuntimeException::new); 69 | persistLine.update(new Line(lineUpdateRequest.getName(), lineUpdateRequest.getColor())); 70 | } 71 | 72 | public void deleteLineById(Long id) { 73 | lineRepository.deleteById(id); 74 | } 75 | 76 | public void addLineStation(Long lineId, SectionRequest request) { 77 | Line line = findLineById(lineId); 78 | Station upStation = stationService.findStationById(request.getUpStationId()); 79 | Station downStation = stationService.findStationById(request.getDownStationId()); 80 | List stations = getStations(line); 81 | boolean isUpStationExisted = stations.stream().anyMatch(it -> it == upStation); 82 | boolean isDownStationExisted = stations.stream().anyMatch(it -> it == downStation); 83 | 84 | if (isUpStationExisted && isDownStationExisted) { 85 | throw new RuntimeException("이미 등록된 구간 입니다."); 86 | } 87 | 88 | if (!stations.isEmpty() && stations.stream().noneMatch(it -> it == upStation) && 89 | stations.stream().noneMatch(it -> it == downStation)) { 90 | throw new RuntimeException("등록할 수 없는 구간 입니다."); 91 | } 92 | 93 | if (stations.isEmpty()) { 94 | line.getSections().add(new Section(line, upStation, downStation, request.getDistance())); 95 | return; 96 | } 97 | 98 | if (isUpStationExisted) { 99 | line.getSections().stream() 100 | .filter(it -> it.getUpStation() == upStation) 101 | .findFirst() 102 | .ifPresent(it -> it.updateUpStation(downStation, request.getDistance())); 103 | 104 | line.getSections().add(new Section(line, upStation, downStation, request.getDistance())); 105 | } else if (isDownStationExisted) { 106 | line.getSections().stream() 107 | .filter(it -> it.getDownStation() == downStation) 108 | .findFirst() 109 | .ifPresent(it -> it.updateDownStation(upStation, request.getDistance())); 110 | 111 | line.getSections().add(new Section(line, upStation, downStation, request.getDistance())); 112 | } else { 113 | throw new RuntimeException(); 114 | } 115 | } 116 | 117 | public void removeLineStation(Long lineId, Long stationId) { 118 | Line line = findLineById(lineId); 119 | Station station = stationService.findStationById(stationId); 120 | if (line.getSections().size() <= 1) { 121 | throw new RuntimeException(); 122 | } 123 | 124 | Optional
upLineStation = line.getSections().stream() 125 | .filter(it -> it.getUpStation() == station) 126 | .findFirst(); 127 | Optional
downLineStation = line.getSections().stream() 128 | .filter(it -> it.getDownStation() == station) 129 | .findFirst(); 130 | 131 | if (upLineStation.isPresent() && downLineStation.isPresent()) { 132 | Station newUpStation = downLineStation.get().getUpStation(); 133 | Station newDownStation = upLineStation.get().getDownStation(); 134 | int newDistance = upLineStation.get().getDistance() + downLineStation.get().getDistance(); 135 | line.getSections().add(new Section(line, newUpStation, newDownStation, newDistance)); 136 | } 137 | 138 | upLineStation.ifPresent(it -> line.getSections().remove(it)); 139 | downLineStation.ifPresent(it -> line.getSections().remove(it)); 140 | } 141 | 142 | 143 | public List getStations(Line line) { 144 | if (line.getSections().isEmpty()) { 145 | return Arrays.asList(); 146 | } 147 | 148 | List stations = new ArrayList<>(); 149 | Station downStation = findUpStation(line); 150 | stations.add(downStation); 151 | 152 | while (downStation != null) { 153 | Station finalDownStation = downStation; 154 | Optional
nextLineStation = line.getSections().stream() 155 | .filter(it -> it.getUpStation() == finalDownStation) 156 | .findFirst(); 157 | if (!nextLineStation.isPresent()) { 158 | break; 159 | } 160 | downStation = nextLineStation.get().getDownStation(); 161 | stations.add(downStation); 162 | } 163 | 164 | return stations; 165 | } 166 | 167 | private Station findUpStation(Line line) { 168 | Station downStation = line.getSections().get(0).getUpStation(); 169 | while (downStation != null) { 170 | Station finalDownStation = downStation; 171 | Optional
nextLineStation = line.getSections().stream() 172 | .filter(it -> it.getDownStation() == finalDownStation) 173 | .findFirst(); 174 | if (!nextLineStation.isPresent()) { 175 | break; 176 | } 177 | downStation = nextLineStation.get().getUpStation(); 178 | } 179 | 180 | return downStation; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/line/domain/Line.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.line.domain; 2 | 3 | import nextstep.subway.BaseEntity; 4 | import nextstep.subway.station.domain.Station; 5 | 6 | import javax.persistence.*; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | @Entity 11 | public class Line extends BaseEntity { 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.IDENTITY) 14 | private Long id; 15 | @Column(unique = true) 16 | private String name; 17 | private String color; 18 | 19 | @OneToMany(mappedBy = "line", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true) 20 | private List
sections = new ArrayList<>(); 21 | 22 | public Line() { 23 | } 24 | 25 | public Line(String name, String color) { 26 | this.name = name; 27 | this.color = color; 28 | } 29 | 30 | public Line(String name, String color, Station upStation, Station downStation, int distance) { 31 | this.name = name; 32 | this.color = color; 33 | sections.add(new Section(this, upStation, downStation, distance)); 34 | } 35 | 36 | public void update(Line line) { 37 | this.name = line.getName(); 38 | this.color = line.getColor(); 39 | } 40 | 41 | public Long getId() { 42 | return id; 43 | } 44 | 45 | public String getName() { 46 | return name; 47 | } 48 | 49 | public String getColor() { 50 | return color; 51 | } 52 | 53 | public List
getSections() { 54 | return sections; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/line/domain/LineRepository.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.line.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface LineRepository extends JpaRepository { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/line/domain/Section.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.line.domain; 2 | 3 | import nextstep.subway.station.domain.Station; 4 | 5 | import javax.persistence.*; 6 | 7 | @Entity 8 | public class Section { 9 | @Id 10 | @GeneratedValue(strategy = GenerationType.IDENTITY) 11 | private Long id; 12 | 13 | @ManyToOne(cascade = CascadeType.PERSIST) 14 | @JoinColumn(name = "line_id") 15 | private Line line; 16 | 17 | @ManyToOne(cascade = CascadeType.PERSIST) 18 | @JoinColumn(name = "up_station_id") 19 | private Station upStation; 20 | 21 | @ManyToOne(cascade = CascadeType.PERSIST) 22 | @JoinColumn(name = "down_station_id") 23 | private Station downStation; 24 | 25 | private int distance; 26 | 27 | public Section() { 28 | } 29 | 30 | public Section(Line line, Station upStation, Station downStation, int distance) { 31 | this.line = line; 32 | this.upStation = upStation; 33 | this.downStation = downStation; 34 | this.distance = distance; 35 | } 36 | 37 | public Long getId() { 38 | return id; 39 | } 40 | 41 | public Line getLine() { 42 | return line; 43 | } 44 | 45 | public Station getUpStation() { 46 | return upStation; 47 | } 48 | 49 | public Station getDownStation() { 50 | return downStation; 51 | } 52 | 53 | public int getDistance() { 54 | return distance; 55 | } 56 | 57 | public void updateUpStation(Station station, int newDistance) { 58 | if (this.distance <= newDistance) { 59 | throw new RuntimeException("역과 역 사이의 거리보다 좁은 거리를 입력해주세요"); 60 | } 61 | this.upStation = station; 62 | this.distance -= newDistance; 63 | } 64 | 65 | public void updateDownStation(Station station, int newDistance) { 66 | if (this.distance <= newDistance) { 67 | throw new RuntimeException("역과 역 사이의 거리보다 좁은 거리를 입력해주세요"); 68 | } 69 | this.downStation = station; 70 | this.distance -= newDistance; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/line/dto/LineRequest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.line.dto; 2 | 3 | import nextstep.subway.line.domain.Line; 4 | 5 | public class LineRequest { 6 | private String name; 7 | private String color; 8 | private Long upStationId; 9 | private Long downStationId; 10 | private int distance; 11 | 12 | public LineRequest() { 13 | } 14 | 15 | public LineRequest(String name, String color, Long upStationId, Long downStationId, int distance) { 16 | this.name = name; 17 | this.color = color; 18 | this.upStationId = upStationId; 19 | this.downStationId = downStationId; 20 | this.distance = distance; 21 | } 22 | 23 | public String getName() { 24 | return name; 25 | } 26 | 27 | public String getColor() { 28 | return color; 29 | } 30 | 31 | public Long getUpStationId() { 32 | return upStationId; 33 | } 34 | 35 | public Long getDownStationId() { 36 | return downStationId; 37 | } 38 | 39 | public int getDistance() { 40 | return distance; 41 | } 42 | 43 | public Line toLine() { 44 | return new Line(name, color); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/line/dto/LineResponse.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.line.dto; 2 | 3 | import nextstep.subway.line.domain.Line; 4 | import nextstep.subway.station.dto.StationResponse; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.List; 8 | 9 | public class LineResponse { 10 | private Long id; 11 | private String name; 12 | private String color; 13 | private List stations; 14 | private LocalDateTime createdDate; 15 | private LocalDateTime modifiedDate; 16 | 17 | public LineResponse() { 18 | } 19 | 20 | public LineResponse(Long id, String name, String color, List stations, LocalDateTime createdDate, LocalDateTime modifiedDate) { 21 | this.id = id; 22 | this.name = name; 23 | this.color = color; 24 | this.stations = stations; 25 | this.createdDate = createdDate; 26 | this.modifiedDate = modifiedDate; 27 | } 28 | 29 | public static LineResponse of(Line line, List stations) { 30 | return new LineResponse(line.getId(), line.getName(), line.getColor(), stations, line.getCreatedDate(), line.getModifiedDate()); 31 | } 32 | 33 | public Long getId() { 34 | return id; 35 | } 36 | 37 | public String getName() { 38 | return name; 39 | } 40 | 41 | public String getColor() { 42 | return color; 43 | } 44 | 45 | public List getStations() { 46 | return stations; 47 | } 48 | 49 | public LocalDateTime getCreatedDate() { 50 | return createdDate; 51 | } 52 | 53 | public LocalDateTime getModifiedDate() { 54 | return modifiedDate; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/line/dto/SectionRequest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.line.dto; 2 | 3 | public class SectionRequest { 4 | private Long upStationId; 5 | private Long downStationId; 6 | private int distance; 7 | 8 | public SectionRequest() { 9 | } 10 | 11 | public SectionRequest(Long upStationId, Long downStationId, int distance) { 12 | this.upStationId = upStationId; 13 | this.downStationId = downStationId; 14 | this.distance = distance; 15 | } 16 | 17 | public Long getUpStationId() { 18 | return upStationId; 19 | } 20 | 21 | public Long getDownStationId() { 22 | return downStationId; 23 | } 24 | 25 | public int getDistance() { 26 | return distance; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/line/ui/LineController.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.line.ui; 2 | 3 | import nextstep.subway.line.application.LineService; 4 | import nextstep.subway.line.dto.LineRequest; 5 | import nextstep.subway.line.dto.LineResponse; 6 | import nextstep.subway.line.dto.SectionRequest; 7 | import org.springframework.dao.DataIntegrityViolationException; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import java.net.URI; 12 | import java.util.List; 13 | 14 | @RestController 15 | @RequestMapping("/lines") 16 | public class LineController { 17 | private final LineService lineService; 18 | 19 | public LineController(final LineService lineService) { 20 | this.lineService = lineService; 21 | } 22 | 23 | @PostMapping 24 | public ResponseEntity createLine(@RequestBody LineRequest lineRequest) { 25 | LineResponse line = lineService.saveLine(lineRequest); 26 | return ResponseEntity.created(URI.create("/lines/" + line.getId())).body(line); 27 | } 28 | 29 | @GetMapping 30 | public ResponseEntity> findAllLines() { 31 | return ResponseEntity.ok(lineService.findLines()); 32 | } 33 | 34 | @GetMapping("/{id}") 35 | public ResponseEntity findLineById(@PathVariable Long id) { 36 | return ResponseEntity.ok(lineService.findLineResponseById(id)); 37 | } 38 | 39 | @PutMapping("/{id}") 40 | public ResponseEntity updateLine(@PathVariable Long id, @RequestBody LineRequest lineUpdateRequest) { 41 | lineService.updateLine(id, lineUpdateRequest); 42 | return ResponseEntity.ok().build(); 43 | } 44 | 45 | @DeleteMapping("/{id}") 46 | public ResponseEntity deleteLine(@PathVariable Long id) { 47 | lineService.deleteLineById(id); 48 | return ResponseEntity.noContent().build(); 49 | } 50 | 51 | @PostMapping("/{lineId}/sections") 52 | public ResponseEntity addLineStation(@PathVariable Long lineId, @RequestBody SectionRequest sectionRequest) { 53 | lineService.addLineStation(lineId, sectionRequest); 54 | return ResponseEntity.ok().build(); 55 | } 56 | 57 | @DeleteMapping("/{lineId}/sections") 58 | public ResponseEntity removeLineStation(@PathVariable Long lineId, @RequestParam Long stationId) { 59 | lineService.removeLineStation(lineId, stationId); 60 | return ResponseEntity.ok().build(); 61 | } 62 | 63 | @ExceptionHandler(DataIntegrityViolationException.class) 64 | public ResponseEntity handleIllegalArgsException(DataIntegrityViolationException e) { 65 | return ResponseEntity.badRequest().build(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/member/application/MemberService.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.member.application; 2 | 3 | import nextstep.subway.member.domain.Member; 4 | import nextstep.subway.member.domain.MemberRepository; 5 | import nextstep.subway.member.dto.MemberRequest; 6 | import nextstep.subway.member.dto.MemberResponse; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.transaction.annotation.Transactional; 9 | 10 | @Service 11 | public class MemberService { 12 | private MemberRepository memberRepository; 13 | 14 | public MemberService(MemberRepository memberRepository) { 15 | this.memberRepository = memberRepository; 16 | } 17 | 18 | public MemberResponse createMember(MemberRequest request) { 19 | Member member = memberRepository.save(request.toMember()); 20 | return MemberResponse.of(member); 21 | } 22 | 23 | public MemberResponse findMember(Long id) { 24 | Member member = memberRepository.findById(id).orElseThrow(RuntimeException::new); 25 | return MemberResponse.of(member); 26 | } 27 | 28 | public void updateMember(Long id, MemberRequest param) { 29 | Member member = memberRepository.findById(id).orElseThrow(RuntimeException::new); 30 | member.update(param.toMember()); 31 | } 32 | 33 | public void deleteMember(Long id) { 34 | memberRepository.deleteById(id); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/member/domain/Member.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.member.domain; 2 | 3 | import nextstep.subway.BaseEntity; 4 | import nextstep.subway.auth.application.AuthorizationException; 5 | import org.apache.commons.lang3.StringUtils; 6 | 7 | import javax.persistence.Entity; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.GenerationType; 10 | import javax.persistence.Id; 11 | 12 | @Entity 13 | public class Member extends BaseEntity { 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.IDENTITY) 16 | private Long id; 17 | private String email; 18 | private String password; 19 | private Integer age; 20 | 21 | public Member() { 22 | } 23 | 24 | public Member(String email, String password, Integer age) { 25 | this.email = email; 26 | this.password = password; 27 | this.age = age; 28 | } 29 | 30 | public Long getId() { 31 | return id; 32 | } 33 | 34 | public String getEmail() { 35 | return email; 36 | } 37 | 38 | public String getPassword() { 39 | return password; 40 | } 41 | 42 | public Integer getAge() { 43 | return age; 44 | } 45 | 46 | public void update(Member member) { 47 | this.email = member.email; 48 | this.password = member.password; 49 | this.age = member.age; 50 | } 51 | 52 | public void checkPassword(String password) { 53 | if (!StringUtils.equals(this.password, password)) { 54 | throw new AuthorizationException(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/member/domain/MemberRepository.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.member.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.Optional; 6 | 7 | public interface MemberRepository extends JpaRepository { 8 | Optional findByEmail(String email); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/member/dto/MemberRequest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.member.dto; 2 | 3 | import nextstep.subway.member.domain.Member; 4 | 5 | public class MemberRequest { 6 | private String email; 7 | private String password; 8 | private Integer age; 9 | 10 | public MemberRequest() { 11 | } 12 | 13 | public MemberRequest(String email, String password, Integer age) { 14 | this.email = email; 15 | this.password = password; 16 | this.age = age; 17 | } 18 | 19 | public String getEmail() { 20 | return email; 21 | } 22 | 23 | public String getPassword() { 24 | return password; 25 | } 26 | 27 | public Integer getAge() { 28 | return age; 29 | } 30 | 31 | public Member toMember() { 32 | return new Member(email, password, age); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/member/dto/MemberResponse.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.member.dto; 2 | 3 | import nextstep.subway.member.domain.Member; 4 | 5 | public class MemberResponse { 6 | private Long id; 7 | private String email; 8 | private Integer age; 9 | 10 | public MemberResponse() { 11 | } 12 | 13 | public MemberResponse(Long id, String email, Integer age) { 14 | this.id = id; 15 | this.email = email; 16 | this.age = age; 17 | } 18 | 19 | public static MemberResponse of(Member member) { 20 | return new MemberResponse(member.getId(), member.getEmail(), member.getAge()); 21 | } 22 | 23 | public Long getId() { 24 | return id; 25 | } 26 | 27 | public String getEmail() { 28 | return email; 29 | } 30 | 31 | public Integer getAge() { 32 | return age; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/member/ui/MemberController.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.member.ui; 2 | 3 | import nextstep.subway.auth.domain.LoginMember; 4 | import nextstep.subway.auth.domain.AuthenticationPrincipal; 5 | import nextstep.subway.auth.domain.LoginMember; 6 | import nextstep.subway.member.application.MemberService; 7 | import nextstep.subway.member.dto.MemberRequest; 8 | import nextstep.subway.member.dto.MemberResponse; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | import java.net.URI; 13 | 14 | @RestController 15 | public class MemberController { 16 | private MemberService memberService; 17 | 18 | public MemberController(MemberService memberService) { 19 | this.memberService = memberService; 20 | } 21 | 22 | @PostMapping("/members") 23 | public ResponseEntity createMember(@RequestBody MemberRequest request) { 24 | MemberResponse member = memberService.createMember(request); 25 | return ResponseEntity.created(URI.create("/members/" + member.getId())).build(); 26 | } 27 | 28 | @GetMapping("/members/{id}") 29 | public ResponseEntity findMember(@PathVariable Long id) { 30 | MemberResponse member = memberService.findMember(id); 31 | return ResponseEntity.ok().body(member); 32 | } 33 | 34 | @PutMapping("/members/{id}") 35 | public ResponseEntity updateMember(@PathVariable Long id, @RequestBody MemberRequest param) { 36 | memberService.updateMember(id, param); 37 | return ResponseEntity.ok().build(); 38 | } 39 | 40 | @DeleteMapping("/members/{id}") 41 | public ResponseEntity deleteMember(@PathVariable Long id) { 42 | memberService.deleteMember(id); 43 | return ResponseEntity.noContent().build(); 44 | } 45 | 46 | @GetMapping("/members/me") 47 | public ResponseEntity findMemberOfMine(LoginMember loginMember) { 48 | MemberResponse member = memberService.findMember(loginMember.getId()); 49 | return ResponseEntity.ok().body(member); 50 | } 51 | 52 | @PutMapping("/members/me") 53 | public ResponseEntity updateMemberOfMine(LoginMember loginMember, @RequestBody MemberRequest param) { 54 | memberService.updateMember(loginMember.getId(), param); 55 | return ResponseEntity.ok().build(); 56 | } 57 | 58 | @DeleteMapping("/members/me") 59 | public ResponseEntity deleteMemberOfMine(LoginMember loginMember) { 60 | memberService.deleteMember(loginMember.getId()); 61 | return ResponseEntity.noContent().build(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/station/application/StationService.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.station.application; 2 | 3 | import nextstep.subway.station.domain.Station; 4 | import nextstep.subway.station.domain.StationRepository; 5 | import nextstep.subway.station.dto.StationRequest; 6 | import nextstep.subway.station.dto.StationResponse; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.transaction.annotation.Transactional; 9 | 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | @Service 14 | public class StationService { 15 | private StationRepository stationRepository; 16 | 17 | public StationService(StationRepository stationRepository) { 18 | this.stationRepository = stationRepository; 19 | } 20 | 21 | public StationResponse saveStation(StationRequest stationRequest) { 22 | Station persistStation = stationRepository.save(stationRequest.toStation()); 23 | return StationResponse.of(persistStation); 24 | } 25 | 26 | public List findAllStations() { 27 | List stations = stationRepository.findAll(); 28 | 29 | return stations.stream() 30 | .map(station -> StationResponse.of(station)) 31 | .collect(Collectors.toList()); 32 | } 33 | 34 | public void deleteStationById(Long id) { 35 | stationRepository.deleteById(id); 36 | } 37 | 38 | public Station findStationById(Long id) { 39 | return stationRepository.findById(id).orElseThrow(RuntimeException::new); 40 | } 41 | 42 | public Station findById(Long id) { 43 | return stationRepository.findById(id).orElseThrow(RuntimeException::new); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/station/domain/Station.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.station.domain; 2 | 3 | import nextstep.subway.BaseEntity; 4 | 5 | import javax.persistence.*; 6 | import java.util.Objects; 7 | 8 | @Entity 9 | public class Station extends BaseEntity { 10 | @Id 11 | @GeneratedValue(strategy = GenerationType.IDENTITY) 12 | private Long id; 13 | @Column(unique = true) 14 | private String name; 15 | 16 | public Station() { 17 | } 18 | 19 | public Station(String name) { 20 | this.name = name; 21 | } 22 | 23 | public Long getId() { 24 | return id; 25 | } 26 | 27 | public String getName() { 28 | return name; 29 | } 30 | 31 | @Override 32 | public boolean equals(Object o) { 33 | if (this == o) return true; 34 | if (o == null || getClass() != o.getClass()) return false; 35 | Station station = (Station) o; 36 | return Objects.equals(id, station.id) && 37 | Objects.equals(name, station.name); 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return Objects.hash(id, name); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/station/domain/StationRepository.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.station.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.List; 6 | 7 | public interface StationRepository extends JpaRepository { 8 | @Override 9 | List findAll(); 10 | } -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/station/dto/StationRequest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.station.dto; 2 | 3 | import nextstep.subway.station.domain.Station; 4 | 5 | public class StationRequest { 6 | private String name; 7 | 8 | public StationRequest() { 9 | } 10 | 11 | public StationRequest(String name) { 12 | this.name = name; 13 | } 14 | 15 | public String getName() { 16 | return name; 17 | } 18 | 19 | public Station toStation() { 20 | return new Station(name); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/station/dto/StationResponse.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.station.dto; 2 | 3 | import nextstep.subway.station.domain.Station; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | public class StationResponse { 8 | private Long id; 9 | private String name; 10 | private LocalDateTime createdDate; 11 | private LocalDateTime modifiedDate; 12 | 13 | public static StationResponse of(Station station) { 14 | return new StationResponse(station.getId(), station.getName(), station.getCreatedDate(), station.getModifiedDate()); 15 | } 16 | 17 | public StationResponse() { 18 | } 19 | 20 | public StationResponse(Long id, String name, LocalDateTime createdDate, LocalDateTime modifiedDate) { 21 | this.id = id; 22 | this.name = name; 23 | this.createdDate = createdDate; 24 | this.modifiedDate = modifiedDate; 25 | } 26 | 27 | public Long getId() { 28 | return id; 29 | } 30 | 31 | public String getName() { 32 | return name; 33 | } 34 | 35 | public LocalDateTime getCreatedDate() { 36 | return createdDate; 37 | } 38 | 39 | public LocalDateTime getModifiedDate() { 40 | return modifiedDate; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/station/ui/StationController.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.station.ui; 2 | 3 | import nextstep.subway.station.application.StationService; 4 | import nextstep.subway.station.dto.StationRequest; 5 | import nextstep.subway.station.dto.StationResponse; 6 | import org.springframework.dao.DataIntegrityViolationException; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import java.net.URI; 12 | import java.util.List; 13 | 14 | @RestController 15 | public class StationController { 16 | private StationService stationService; 17 | 18 | public StationController(StationService stationService) { 19 | this.stationService = stationService; 20 | } 21 | 22 | @PostMapping("/stations") 23 | public ResponseEntity createStation(@RequestBody StationRequest stationRequest) { 24 | StationResponse station = stationService.saveStation(stationRequest); 25 | return ResponseEntity.created(URI.create("/stations/" + station.getId())).body(station); 26 | } 27 | 28 | @GetMapping(value = "/stations", produces = MediaType.APPLICATION_JSON_VALUE) 29 | public ResponseEntity> showStations() { 30 | return ResponseEntity.ok().body(stationService.findAllStations()); 31 | } 32 | 33 | @DeleteMapping("/stations/{id}") 34 | public ResponseEntity deleteStation(@PathVariable Long id) { 35 | stationService.deleteStationById(id); 36 | return ResponseEntity.noContent().build(); 37 | } 38 | 39 | @ExceptionHandler(DataIntegrityViolationException.class) 40 | public ResponseEntity handleIllegalArgsException(DataIntegrityViolationException e) { 41 | return ResponseEntity.badRequest().build(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | handlebars.suffix=.html 2 | handlebars.enabled=true 3 | 4 | spring.jpa.properties.hibernate.show_sql=true 5 | spring.jpa.properties.hibernate.format_sql=true 6 | 7 | logging.level.org.hibernate.type.descriptor.sql=trace 8 | 9 | security.jwt.token.secret-key= eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.ih1aovtQShabQ7l0cINw4k1fagApg3qLWiB8Kt59Lno 10 | security.jwt.token.expire-length= 3600000 -------------------------------------------------------------------------------- /src/main/resources/logback-access.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %fullRequest%n%n%fullResponse 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/static/images/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/next-step/atdd-subway-service/6b0b91c3cf22015ff7d519e3c96f7de0cc7b4692/src/main/resources/static/images/logo_small.png -------------------------------------------------------------------------------- /src/main/resources/static/images/main_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/next-step/atdd-subway-service/6b0b91c3cf22015ff7d519e3c96f7de0cc7b4692/src/main/resources/static/images/main_logo.png -------------------------------------------------------------------------------- /src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Running Map 5 | 9 | 13 | 18 | 19 | 23 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/test/java/nextstep/subway/AcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway; 2 | 3 | import io.restassured.RestAssured; 4 | import nextstep.subway.utils.DatabaseCleanup; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.boot.web.server.LocalServerPort; 9 | 10 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 11 | public class AcceptanceTest { 12 | @LocalServerPort 13 | int port; 14 | 15 | @Autowired 16 | private DatabaseCleanup databaseCleanup; 17 | 18 | @BeforeEach 19 | public void setUp() { 20 | RestAssured.port = port; 21 | databaseCleanup.execute(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/nextstep/subway/SubwayApplicationTests.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class SubwayApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/nextstep/subway/auth/acceptance/AuthAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.auth.acceptance; 2 | 3 | import nextstep.subway.AcceptanceTest; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class AuthAcceptanceTest extends AcceptanceTest { 8 | 9 | @DisplayName("Bearer Auth") 10 | @Test 11 | void myInfoWithBearerAuth() { 12 | } 13 | 14 | @DisplayName("Bearer Auth 로그인 실패") 15 | @Test 16 | void myInfoWithBadBearerAuth() { 17 | } 18 | 19 | @DisplayName("Bearer Auth 유효하지 않은 토큰") 20 | @Test 21 | void myInfoWithWrongBearerAuth() { 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/nextstep/subway/auth/application/AuthServiceTest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.auth.application; 2 | 3 | import nextstep.subway.member.domain.Member; 4 | import nextstep.subway.member.domain.MemberRepository; 5 | import nextstep.subway.auth.dto.TokenRequest; 6 | import nextstep.subway.auth.dto.TokenResponse; 7 | import nextstep.subway.auth.infrastructure.JwtTokenProvider; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | import org.mockito.Mock; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | 14 | import java.util.Optional; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | import static org.mockito.ArgumentMatchers.anyString; 18 | import static org.mockito.Mockito.when; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | public class AuthServiceTest { 22 | public static final String EMAIL = "email@email.com"; 23 | public static final String PASSWORD = "password"; 24 | public static final int AGE = 10; 25 | 26 | private AuthService authService; 27 | 28 | @Mock 29 | private MemberRepository memberRepository; 30 | @Mock 31 | private JwtTokenProvider jwtTokenProvider; 32 | 33 | @BeforeEach 34 | void setUp() { 35 | authService = new AuthService(memberRepository, jwtTokenProvider); 36 | } 37 | 38 | @Test 39 | void login() { 40 | when(memberRepository.findByEmail(anyString())).thenReturn(Optional.of(new Member(EMAIL, PASSWORD, AGE))); 41 | when(jwtTokenProvider.createToken(anyString())).thenReturn("TOKEN"); 42 | 43 | TokenResponse token = authService.login(new TokenRequest(EMAIL, PASSWORD)); 44 | 45 | assertThat(token.getAccessToken()).isNotBlank(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/nextstep/subway/favorite/FavoriteAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.favorite; 2 | 3 | import nextstep.subway.AcceptanceTest; 4 | import org.junit.jupiter.api.DisplayName; 5 | 6 | @DisplayName("즐겨찾기 관련 기능") 7 | public class FavoriteAcceptanceTest extends AcceptanceTest { 8 | } -------------------------------------------------------------------------------- /src/test/java/nextstep/subway/line/acceptance/LineAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.line.acceptance; 2 | 3 | import io.restassured.RestAssured; 4 | import io.restassured.response.ExtractableResponse; 5 | import io.restassured.response.Response; 6 | import nextstep.subway.AcceptanceTest; 7 | import nextstep.subway.line.dto.LineRequest; 8 | import nextstep.subway.line.dto.LineResponse; 9 | import nextstep.subway.station.StationAcceptanceTest; 10 | import nextstep.subway.station.dto.StationResponse; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.DisplayName; 13 | import org.junit.jupiter.api.Test; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.http.MediaType; 16 | 17 | import java.util.Arrays; 18 | import java.util.List; 19 | import java.util.stream.Collectors; 20 | 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | 23 | @DisplayName("지하철 노선 관련 기능") 24 | public class LineAcceptanceTest extends AcceptanceTest { 25 | private StationResponse 강남역; 26 | private StationResponse 광교역; 27 | private LineRequest lineRequest1; 28 | private LineRequest lineRequest2; 29 | 30 | @BeforeEach 31 | public void setUp() { 32 | super.setUp(); 33 | 34 | // given 35 | 강남역 = StationAcceptanceTest.지하철역_등록되어_있음("강남역").as(StationResponse.class); 36 | 광교역 = StationAcceptanceTest.지하철역_등록되어_있음("광교역").as(StationResponse.class); 37 | 38 | lineRequest1 = new LineRequest("신분당선", "bg-red-600", 강남역.getId(), 광교역.getId(), 10); 39 | lineRequest2 = new LineRequest("구신분당선", "bg-red-600", 강남역.getId(), 광교역.getId(), 15); 40 | } 41 | 42 | @DisplayName("지하철 노선을 생성한다.") 43 | @Test 44 | void createLine() { 45 | // when 46 | ExtractableResponse response = 지하철_노선_생성_요청(lineRequest1); 47 | 48 | // then 49 | 지하철_노선_생성됨(response); 50 | } 51 | 52 | @DisplayName("기존에 존재하는 지하철 노선 이름으로 지하철 노선을 생성한다.") 53 | @Test 54 | void createLineWithDuplicateName() { 55 | // given 56 | 지하철_노선_등록되어_있음(lineRequest1); 57 | 58 | // when 59 | ExtractableResponse response = 지하철_노선_생성_요청(lineRequest1); 60 | 61 | // then 62 | 지하철_노선_생성_실패됨(response); 63 | } 64 | 65 | @DisplayName("지하철 노선 목록을 조회한다.") 66 | @Test 67 | void getLines() { 68 | // given 69 | ExtractableResponse createResponse1 = 지하철_노선_등록되어_있음(lineRequest1); 70 | ExtractableResponse createResponse2 = 지하철_노선_등록되어_있음(lineRequest2); 71 | 72 | // when 73 | ExtractableResponse response = 지하철_노선_목록_조회_요청(); 74 | 75 | // then 76 | 지하철_노선_목록_응답됨(response); 77 | 지하철_노선_목록_포함됨(response, Arrays.asList(createResponse1, createResponse2)); 78 | } 79 | 80 | @DisplayName("지하철 노선을 조회한다.") 81 | @Test 82 | void getLine() { 83 | // given 84 | ExtractableResponse createResponse = 지하철_노선_등록되어_있음(lineRequest1); 85 | 86 | // when 87 | ExtractableResponse response = 지하철_노선_목록_조회_요청(createResponse); 88 | 89 | // then 90 | 지하철_노선_응답됨(response, createResponse); 91 | } 92 | 93 | @DisplayName("지하철 노선을 수정한다.") 94 | @Test 95 | void updateLine() { 96 | // given 97 | String name = "신분당선"; 98 | ExtractableResponse createResponse = 지하철_노선_등록되어_있음(lineRequest1); 99 | 100 | // when 101 | ExtractableResponse response = 지하철_노선_수정_요청(createResponse, lineRequest2); 102 | 103 | // then 104 | 지하철_노선_수정됨(response); 105 | } 106 | 107 | @DisplayName("지하철 노선을 제거한다.") 108 | @Test 109 | void deleteLine() { 110 | // given 111 | ExtractableResponse createResponse = 지하철_노선_등록되어_있음(lineRequest1); 112 | 113 | // when 114 | ExtractableResponse response = 지하철_노선_제거_요청(createResponse); 115 | 116 | // then 117 | 지하철_노선_삭제됨(response); 118 | } 119 | 120 | public static ExtractableResponse 지하철_노선_등록되어_있음(LineRequest params) { 121 | return 지하철_노선_생성_요청(params); 122 | } 123 | 124 | public static ExtractableResponse 지하철_노선_생성_요청(LineRequest params) { 125 | return RestAssured 126 | .given().log().all() 127 | .contentType(MediaType.APPLICATION_JSON_VALUE) 128 | .body(params) 129 | .when().post("/lines") 130 | .then().log().all(). 131 | extract(); 132 | } 133 | 134 | public static ExtractableResponse 지하철_노선_목록_조회_요청() { 135 | return 지하철_노선_목록_조회_요청("/lines"); 136 | } 137 | 138 | public static ExtractableResponse 지하철_노선_목록_조회_요청(ExtractableResponse response) { 139 | String uri = response.header("Location"); 140 | 141 | return 지하철_노선_목록_조회_요청(uri); 142 | } 143 | 144 | private static ExtractableResponse 지하철_노선_목록_조회_요청(String uri) { 145 | return RestAssured 146 | .given().log().all() 147 | .accept(MediaType.APPLICATION_JSON_VALUE) 148 | .when().get(uri) 149 | .then().log().all() 150 | .extract(); 151 | } 152 | 153 | public static ExtractableResponse 지하철_노선_조회_요청(LineResponse response) { 154 | return RestAssured 155 | .given().log().all() 156 | .accept(MediaType.APPLICATION_JSON_VALUE) 157 | .when().get("/lines/{lineId}", response.getId()) 158 | .then().log().all() 159 | .extract(); 160 | } 161 | 162 | public static ExtractableResponse 지하철_노선_수정_요청(ExtractableResponse response, LineRequest params) { 163 | String uri = response.header("Location"); 164 | 165 | return RestAssured 166 | .given().log().all() 167 | .contentType(MediaType.APPLICATION_JSON_VALUE) 168 | .body(params) 169 | .when().put(uri) 170 | .then().log().all() 171 | .extract(); 172 | } 173 | 174 | public static ExtractableResponse 지하철_노선_제거_요청(ExtractableResponse response) { 175 | String uri = response.header("Location"); 176 | 177 | return RestAssured 178 | .given().log().all() 179 | .when().delete(uri) 180 | .then().log().all() 181 | .extract(); 182 | } 183 | 184 | public static void 지하철_노선_생성됨(ExtractableResponse response) { 185 | assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); 186 | assertThat(response.header("Location")).isNotBlank(); 187 | } 188 | 189 | public static void 지하철_노선_생성_실패됨(ExtractableResponse response) { 190 | assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); 191 | } 192 | 193 | public static void 지하철_노선_목록_응답됨(ExtractableResponse response) { 194 | assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); 195 | } 196 | 197 | public static void 지하철_노선_응답됨(ExtractableResponse response, ExtractableResponse createdResponse) { 198 | assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); 199 | assertThat(response.as(LineResponse.class)).isNotNull(); 200 | } 201 | 202 | public static void 지하철_노선_목록_포함됨(ExtractableResponse response, List> createdResponses) { 203 | List expectedLineIds = createdResponses.stream() 204 | .map(it -> Long.parseLong(it.header("Location").split("/")[2])) 205 | .collect(Collectors.toList()); 206 | 207 | List resultLineIds = response.jsonPath().getList(".", LineResponse.class).stream() 208 | .map(LineResponse::getId) 209 | .collect(Collectors.toList()); 210 | 211 | assertThat(resultLineIds).containsAll(expectedLineIds); 212 | } 213 | 214 | public static void 지하철_노선_수정됨(ExtractableResponse response) { 215 | assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); 216 | } 217 | 218 | public static void 지하철_노선_삭제됨(ExtractableResponse response) { 219 | assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/test/java/nextstep/subway/line/acceptance/LineSectionAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.line.acceptance; 2 | 3 | import io.restassured.RestAssured; 4 | import io.restassured.response.ExtractableResponse; 5 | import io.restassured.response.Response; 6 | import nextstep.subway.AcceptanceTest; 7 | import nextstep.subway.line.dto.LineRequest; 8 | import nextstep.subway.line.dto.LineResponse; 9 | import nextstep.subway.line.dto.SectionRequest; 10 | import nextstep.subway.station.StationAcceptanceTest; 11 | import nextstep.subway.station.dto.StationResponse; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.DisplayName; 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.http.MediaType; 17 | 18 | import java.util.Arrays; 19 | import java.util.List; 20 | import java.util.stream.Collectors; 21 | 22 | import static org.assertj.core.api.Assertions.assertThat; 23 | 24 | @DisplayName("지하철 구간 관련 기능") 25 | public class LineSectionAcceptanceTest extends AcceptanceTest { 26 | private LineResponse 신분당선; 27 | private StationResponse 강남역; 28 | private StationResponse 양재역; 29 | private StationResponse 정자역; 30 | private StationResponse 광교역; 31 | 32 | @BeforeEach 33 | public void setUp() { 34 | super.setUp(); 35 | 36 | 강남역 = StationAcceptanceTest.지하철역_등록되어_있음("강남역").as(StationResponse.class); 37 | 양재역 = StationAcceptanceTest.지하철역_등록되어_있음("양재역").as(StationResponse.class); 38 | 정자역 = StationAcceptanceTest.지하철역_등록되어_있음("정자역").as(StationResponse.class); 39 | 광교역 = StationAcceptanceTest.지하철역_등록되어_있음("광교역").as(StationResponse.class); 40 | 41 | LineRequest lineRequest = new LineRequest("신분당선", "bg-red-600", 강남역.getId(), 광교역.getId(), 10); 42 | 신분당선 = LineAcceptanceTest.지하철_노선_등록되어_있음(lineRequest).as(LineResponse.class); 43 | } 44 | 45 | @DisplayName("지하철 구간을 등록한다.") 46 | @Test 47 | void addLineSection() { 48 | // when 49 | 지하철_노선에_지하철역_등록_요청(신분당선, 강남역, 양재역, 3); 50 | 51 | // then 52 | ExtractableResponse response = LineAcceptanceTest.지하철_노선_조회_요청(신분당선); 53 | 지하철_노선에_지하철역_등록됨(response); 54 | 지하철_노선에_지하철역_순서_정렬됨(response, Arrays.asList(강남역, 양재역, 광교역)); 55 | } 56 | 57 | @DisplayName("지하철 노선에 여러개의 역을 순서 상관 없이 등록한다.") 58 | @Test 59 | void addLineSection2() { 60 | // when 61 | 지하철_노선에_지하철역_등록_요청(신분당선, 강남역, 양재역, 2); 62 | 지하철_노선에_지하철역_등록_요청(신분당선, 정자역, 강남역, 5); 63 | 64 | // then 65 | ExtractableResponse response = LineAcceptanceTest.지하철_노선_조회_요청(신분당선); 66 | 지하철_노선에_지하철역_등록됨(response); 67 | 지하철_노선에_지하철역_순서_정렬됨(response, Arrays.asList(정자역, 강남역, 양재역, 광교역)); 68 | } 69 | 70 | @DisplayName("지하철 노선에 이미 등록되어있는 역을 등록한다.") 71 | @Test 72 | void addLineSectionWithSameStation() { 73 | // when 74 | ExtractableResponse response = 지하철_노선에_지하철역_등록_요청(신분당선, 강남역, 광교역, 3); 75 | 76 | // then 77 | 지하철_노선에_지하철역_등록_실패됨(response); 78 | } 79 | 80 | @DisplayName("지하철 노선에 등록되지 않은 역을 기준으로 등록한다.") 81 | @Test 82 | void addLineSectionWithNoStation() { 83 | // when 84 | ExtractableResponse response = 지하철_노선에_지하철역_등록_요청(신분당선, 정자역, 양재역, 3); 85 | 86 | // then 87 | 지하철_노선에_지하철역_등록_실패됨(response); 88 | } 89 | 90 | @DisplayName("지하철 노선에 등록된 지하철역을 제외한다.") 91 | @Test 92 | void removeLineSection1() { 93 | // given 94 | 지하철_노선에_지하철역_등록_요청(신분당선, 강남역, 양재역, 2); 95 | 지하철_노선에_지하철역_등록_요청(신분당선, 양재역, 정자역, 2); 96 | 97 | // when 98 | ExtractableResponse removeResponse = 지하철_노선에_지하철역_제외_요청(신분당선, 양재역); 99 | 100 | // then 101 | 지하철_노선에_지하철역_제외됨(removeResponse); 102 | ExtractableResponse response = LineAcceptanceTest.지하철_노선_조회_요청(신분당선); 103 | 지하철_노선에_지하철역_순서_정렬됨(response, Arrays.asList(강남역, 정자역, 광교역)); 104 | } 105 | 106 | @DisplayName("지하철 노선에 등록된 지하철역이 두개일 때 한 역을 제외한다.") 107 | @Test 108 | void removeLineSection2() { 109 | // when 110 | ExtractableResponse removeResponse = 지하철_노선에_지하철역_제외_요청(신분당선, 강남역); 111 | 112 | // then 113 | 지하철_노선에_지하철역_제외_실패됨(removeResponse); 114 | } 115 | 116 | public static ExtractableResponse 지하철_노선에_지하철역_등록_요청(LineResponse line, StationResponse upStation, StationResponse downStation, int distance) { 117 | SectionRequest sectionRequest = new SectionRequest(upStation.getId(), downStation.getId(), distance); 118 | 119 | return RestAssured 120 | .given().log().all() 121 | .contentType(MediaType.APPLICATION_JSON_VALUE) 122 | .body(sectionRequest) 123 | .when().post("/lines/{lineId}/sections", line.getId()) 124 | .then().log().all() 125 | .extract(); 126 | } 127 | 128 | public static void 지하철_노선에_지하철역_등록됨(ExtractableResponse response) { 129 | assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); 130 | } 131 | 132 | public static void 지하철_노선에_지하철역_등록_실패됨(ExtractableResponse response) { 133 | assertThat(response.statusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value()); 134 | } 135 | 136 | public static void 지하철_노선에_지하철역_순서_정렬됨(ExtractableResponse response, List expectedStations) { 137 | LineResponse line = response.as(LineResponse.class); 138 | List stationIds = line.getStations().stream() 139 | .map(it -> it.getId()) 140 | .collect(Collectors.toList()); 141 | 142 | List expectedStationIds = expectedStations.stream() 143 | .map(it -> it.getId()) 144 | .collect(Collectors.toList()); 145 | 146 | assertThat(stationIds).containsExactlyElementsOf(expectedStationIds); 147 | } 148 | 149 | public static ExtractableResponse 지하철_노선에_지하철역_제외_요청(LineResponse line, StationResponse station) { 150 | return RestAssured 151 | .given().log().all() 152 | .when().delete("/lines/{lineId}/sections?stationId={stationId}", line.getId(), station.getId()) 153 | .then().log().all() 154 | .extract(); 155 | } 156 | 157 | public static void 지하철_노선에_지하철역_제외됨(ExtractableResponse response) { 158 | assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); 159 | } 160 | 161 | public static void 지하철_노선에_지하철역_제외_실패됨(ExtractableResponse response) { 162 | assertThat(response.statusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value()); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/test/java/nextstep/subway/member/MemberAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.member; 2 | 3 | import io.restassured.RestAssured; 4 | import io.restassured.response.ExtractableResponse; 5 | import io.restassured.response.Response; 6 | import nextstep.subway.AcceptanceTest; 7 | import nextstep.subway.member.dto.MemberRequest; 8 | import nextstep.subway.member.dto.MemberResponse; 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.MediaType; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | public class MemberAcceptanceTest extends AcceptanceTest { 17 | public static final String EMAIL = "email@email.com"; 18 | public static final String PASSWORD = "password"; 19 | public static final String NEW_EMAIL = "newemail@email.com"; 20 | public static final String NEW_PASSWORD = "newpassword"; 21 | public static final int AGE = 20; 22 | public static final int NEW_AGE = 21; 23 | 24 | @DisplayName("회원 정보를 관리한다.") 25 | @Test 26 | void manageMember() { 27 | // when 28 | ExtractableResponse createResponse = 회원_생성을_요청(EMAIL, PASSWORD, AGE); 29 | // then 30 | 회원_생성됨(createResponse); 31 | 32 | // when 33 | ExtractableResponse findResponse = 회원_정보_조회_요청(createResponse); 34 | // then 35 | 회원_정보_조회됨(findResponse, EMAIL, AGE); 36 | 37 | // when 38 | ExtractableResponse updateResponse = 회원_정보_수정_요청(createResponse, NEW_EMAIL, NEW_PASSWORD, NEW_AGE); 39 | // then 40 | 회원_정보_수정됨(updateResponse); 41 | 42 | // when 43 | ExtractableResponse deleteResponse = 회원_삭제_요청(createResponse); 44 | // then 45 | 회원_삭제됨(deleteResponse); 46 | } 47 | 48 | @DisplayName("나의 정보를 관리한다.") 49 | @Test 50 | void manageMyInfo() { 51 | 52 | } 53 | 54 | public static ExtractableResponse 회원_생성을_요청(String email, String password, Integer age) { 55 | MemberRequest memberRequest = new MemberRequest(email, password, age); 56 | 57 | return RestAssured 58 | .given().log().all() 59 | .contentType(MediaType.APPLICATION_JSON_VALUE) 60 | .body(memberRequest) 61 | .when().post("/members") 62 | .then().log().all() 63 | .extract(); 64 | } 65 | 66 | public static ExtractableResponse 회원_정보_조회_요청(ExtractableResponse response) { 67 | String uri = response.header("Location"); 68 | 69 | return RestAssured 70 | .given().log().all() 71 | .accept(MediaType.APPLICATION_JSON_VALUE) 72 | .when().get(uri) 73 | .then().log().all() 74 | .extract(); 75 | } 76 | 77 | public static ExtractableResponse 회원_정보_수정_요청(ExtractableResponse response, String email, String password, Integer age) { 78 | String uri = response.header("Location"); 79 | MemberRequest memberRequest = new MemberRequest(email, password, age); 80 | 81 | return RestAssured 82 | .given().log().all() 83 | .contentType(MediaType.APPLICATION_JSON_VALUE) 84 | .body(memberRequest) 85 | .when().put(uri) 86 | .then().log().all() 87 | .extract(); 88 | } 89 | 90 | public static ExtractableResponse 회원_삭제_요청(ExtractableResponse response) { 91 | String uri = response.header("Location"); 92 | return RestAssured 93 | .given().log().all() 94 | .when().delete(uri) 95 | .then().log().all() 96 | .extract(); 97 | } 98 | 99 | public static void 회원_생성됨(ExtractableResponse response) { 100 | assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); 101 | } 102 | 103 | public static void 회원_정보_조회됨(ExtractableResponse response, String email, int age) { 104 | MemberResponse memberResponse = response.as(MemberResponse.class); 105 | assertThat(memberResponse.getId()).isNotNull(); 106 | assertThat(memberResponse.getEmail()).isEqualTo(email); 107 | assertThat(memberResponse.getAge()).isEqualTo(age); 108 | } 109 | 110 | public static void 회원_정보_수정됨(ExtractableResponse response) { 111 | assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); 112 | } 113 | 114 | public static void 회원_삭제됨(ExtractableResponse response) { 115 | assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/nextstep/subway/path/PathAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.path; 2 | 3 | import nextstep.subway.AcceptanceTest; 4 | import org.junit.jupiter.api.DisplayName; 5 | 6 | 7 | @DisplayName("지하철 경로 조회") 8 | public class PathAcceptanceTest extends AcceptanceTest { 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/nextstep/subway/station/StationAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.station; 2 | 3 | import io.restassured.RestAssured; 4 | import io.restassured.response.ExtractableResponse; 5 | import io.restassured.response.Response; 6 | import nextstep.subway.AcceptanceTest; 7 | import nextstep.subway.station.dto.StationRequest; 8 | import nextstep.subway.station.dto.StationResponse; 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.MediaType; 13 | 14 | import java.util.Arrays; 15 | import java.util.List; 16 | import java.util.stream.Collectors; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | @DisplayName("지하철역 관련 기능") 21 | public class StationAcceptanceTest extends AcceptanceTest { 22 | private static final String 강남역 = "강남역"; 23 | private static final String 역삼역 = "역삼역"; 24 | 25 | @DisplayName("지하철역을 생성한다.") 26 | @Test 27 | void createStation() { 28 | // when 29 | ExtractableResponse response = 지하철역_생성_요청(강남역); 30 | 31 | // then 32 | 지하철역_생성됨(response); 33 | } 34 | 35 | @DisplayName("기존에 존재하는 지하철역 이름으로 지하철역을 생성한다.") 36 | @Test 37 | void createStationWithDuplicateName() { 38 | //given 39 | 지하철역_등록되어_있음(강남역); 40 | 41 | // when 42 | ExtractableResponse response = 지하철역_생성_요청(강남역); 43 | 44 | // then 45 | 지하철역_생성_실패됨(response); 46 | } 47 | 48 | @DisplayName("지하철역을 조회한다.") 49 | @Test 50 | void getStations() { 51 | // given 52 | ExtractableResponse createResponse1 = 지하철역_등록되어_있음(강남역); 53 | ExtractableResponse createResponse2 = 지하철역_등록되어_있음(역삼역); 54 | 55 | // when 56 | ExtractableResponse response = 지하철역_목록_조회_요청(); 57 | 58 | // then 59 | 지하철역_목록_응답됨(response); 60 | 지하철역_목록_포함됨(response, Arrays.asList(createResponse1, createResponse2)); 61 | } 62 | 63 | @DisplayName("지하철역을 제거한다.") 64 | @Test 65 | void deleteStation() { 66 | // given 67 | ExtractableResponse createResponse = 지하철역_등록되어_있음(강남역); 68 | 69 | // when 70 | ExtractableResponse response = 지하철역_제거_요청(createResponse); 71 | 72 | // then 73 | 지하철역_삭제됨(response); 74 | } 75 | 76 | public static ExtractableResponse 지하철역_등록되어_있음(String name) { 77 | return 지하철역_생성_요청(name); 78 | } 79 | 80 | public static ExtractableResponse 지하철역_생성_요청(String name) { 81 | StationRequest stationRequest = new StationRequest(name); 82 | 83 | return RestAssured 84 | .given().log().all() 85 | .body(stationRequest) 86 | .contentType(MediaType.APPLICATION_JSON_VALUE) 87 | .when().post("/stations") 88 | .then().log().all() 89 | .extract(); 90 | } 91 | 92 | public static ExtractableResponse 지하철역_목록_조회_요청() { 93 | return RestAssured 94 | .given().log().all() 95 | .when().get("/stations") 96 | .then().log().all() 97 | .extract(); 98 | } 99 | 100 | public static ExtractableResponse 지하철역_제거_요청(ExtractableResponse response) { 101 | String uri = response.header("Location"); 102 | 103 | return RestAssured 104 | .given().log().all() 105 | .when().delete(uri) 106 | .then().log().all() 107 | .extract(); 108 | } 109 | 110 | public static void 지하철역_생성됨(ExtractableResponse response) { 111 | assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); 112 | assertThat(response.header("Location")).isNotBlank(); 113 | } 114 | 115 | public static void 지하철역_생성_실패됨(ExtractableResponse response) { 116 | assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); 117 | } 118 | 119 | public static void 지하철역_목록_응답됨(ExtractableResponse response) { 120 | assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); 121 | } 122 | 123 | public static void 지하철역_삭제됨(ExtractableResponse response) { 124 | assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); 125 | } 126 | 127 | public static void 지하철역_목록_포함됨(ExtractableResponse response, List> createdResponses) { 128 | List expectedLineIds = createdResponses.stream() 129 | .map(it -> Long.parseLong(it.header("Location").split("/")[2])) 130 | .collect(Collectors.toList()); 131 | 132 | List resultLineIds = response.jsonPath().getList(".", StationResponse.class).stream() 133 | .map(StationResponse::getId) 134 | .collect(Collectors.toList()); 135 | 136 | assertThat(resultLineIds).containsAll(expectedLineIds); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/test/java/nextstep/subway/utils/DatabaseCleanup.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.utils; 2 | 3 | import com.google.common.base.CaseFormat; 4 | import org.springframework.beans.factory.InitializingBean; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.test.context.ActiveProfiles; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | import javax.persistence.Entity; 10 | import javax.persistence.EntityManager; 11 | import javax.persistence.PersistenceContext; 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | @Service 16 | @ActiveProfiles("test") 17 | public class DatabaseCleanup implements InitializingBean { 18 | @PersistenceContext 19 | private EntityManager entityManager; 20 | 21 | private List tableNames; 22 | 23 | @Override 24 | public void afterPropertiesSet() { 25 | tableNames = entityManager.getMetamodel().getEntities().stream() 26 | .filter(e -> e.getJavaType().getAnnotation(Entity.class) != null) 27 | .map(e -> CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, e.getName())) 28 | .collect(Collectors.toList()); 29 | } 30 | 31 | @Transactional 32 | public void execute() { 33 | entityManager.flush(); 34 | entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); 35 | 36 | for (String tableName : tableNames) { 37 | entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate(); 38 | entityManager.createNativeQuery("ALTER TABLE " + tableName + " ALTER COLUMN ID RESTART WITH 1").executeUpdate(); 39 | } 40 | 41 | entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); 42 | } 43 | } -------------------------------------------------------------------------------- /src/test/java/study/jgraph/JgraphTest.java: -------------------------------------------------------------------------------- 1 | package study.jgraph; 2 | 3 | import org.jgrapht.GraphPath; 4 | import org.jgrapht.alg.shortestpath.DijkstraShortestPath; 5 | import org.jgrapht.alg.shortestpath.KShortestPaths; 6 | import org.jgrapht.graph.DefaultWeightedEdge; 7 | import org.jgrapht.graph.WeightedMultigraph; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.util.List; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | public class JgraphTest { 15 | @Test 16 | public void getDijkstraShortestPath() { 17 | String source = "v3"; 18 | String target = "v1"; 19 | WeightedMultigraph graph = new WeightedMultigraph(DefaultWeightedEdge.class); 20 | graph.addVertex("v1"); 21 | graph.addVertex("v2"); 22 | graph.addVertex("v3"); 23 | graph.setEdgeWeight(graph.addEdge("v1", "v2"), 2); 24 | graph.setEdgeWeight(graph.addEdge("v2", "v3"), 2); 25 | graph.setEdgeWeight(graph.addEdge("v1", "v3"), 100); 26 | 27 | DijkstraShortestPath dijkstraShortestPath = new DijkstraShortestPath(graph); 28 | List shortestPath = dijkstraShortestPath.getPath(source, target).getVertexList(); 29 | 30 | assertThat(shortestPath.size()).isEqualTo(3); 31 | } 32 | 33 | @Test 34 | public void getKShortestPaths() { 35 | String source = "v3"; 36 | String target = "v1"; 37 | 38 | WeightedMultigraph graph = new WeightedMultigraph(DefaultWeightedEdge.class); 39 | graph.addVertex("v1"); 40 | graph.addVertex("v2"); 41 | graph.addVertex("v3"); 42 | graph.setEdgeWeight(graph.addEdge("v1", "v2"), 2); 43 | graph.setEdgeWeight(graph.addEdge("v2", "v3"), 2); 44 | graph.setEdgeWeight(graph.addEdge("v1", "v3"), 100); 45 | 46 | List paths = new KShortestPaths(graph, 100).getPaths(source, target); 47 | 48 | assertThat(paths).hasSize(2); 49 | paths.stream() 50 | .forEach(it -> { 51 | assertThat(it.getVertexList()).startsWith(source); 52 | assertThat(it.getVertexList()).endsWith(target); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/study/unit/MockitoExtensionTest.java: -------------------------------------------------------------------------------- 1 | package study.unit; 2 | 3 | import com.google.common.collect.Lists; 4 | import nextstep.subway.line.application.LineService; 5 | import nextstep.subway.line.domain.Line; 6 | import nextstep.subway.line.domain.LineRepository; 7 | import nextstep.subway.line.dto.LineResponse; 8 | import nextstep.subway.station.application.StationService; 9 | import nextstep.subway.station.domain.StationRepository; 10 | import org.junit.jupiter.api.DisplayName; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.jupiter.MockitoExtension; 15 | 16 | import java.util.List; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.mockito.Mockito.when; 20 | 21 | @DisplayName("단위 테스트 - mockito의 MockitoExtension을 활용한 가짜 협력 객체 사용") 22 | @ExtendWith(MockitoExtension.class) 23 | public class MockitoExtensionTest { 24 | @Mock 25 | private LineRepository lineRepository; 26 | @Mock 27 | private StationService stationService; 28 | 29 | @Test 30 | void findAllLines() { 31 | // given 32 | when(lineRepository.findAll()).thenReturn(Lists.newArrayList(new Line())); 33 | LineService lineService = new LineService(lineRepository, stationService); 34 | 35 | // when 36 | List responses = lineService.findLines(); 37 | 38 | // then 39 | assertThat(responses).hasSize(1); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/study/unit/MockitoTest.java: -------------------------------------------------------------------------------- 1 | package study.unit; 2 | 3 | import com.google.common.collect.Lists; 4 | import nextstep.subway.line.application.LineService; 5 | import nextstep.subway.line.domain.Line; 6 | import nextstep.subway.line.domain.LineRepository; 7 | import nextstep.subway.line.dto.LineResponse; 8 | import nextstep.subway.station.application.StationService; 9 | import nextstep.subway.station.domain.StationRepository; 10 | import org.junit.jupiter.api.DisplayName; 11 | import org.junit.jupiter.api.Test; 12 | 13 | import java.util.List; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.mockito.Mockito.mock; 17 | import static org.mockito.Mockito.when; 18 | 19 | @DisplayName("단위 테스트 - mockito를 활용한 가짜 협력 객체 사용") 20 | public class MockitoTest { 21 | @Test 22 | void findAllLines() { 23 | // given 24 | LineRepository lineRepository = mock(LineRepository.class); 25 | StationService stationService = mock(StationService.class); 26 | 27 | when(lineRepository.findAll()).thenReturn(Lists.newArrayList(new Line())); 28 | LineService lineService = new LineService(lineRepository, stationService); 29 | 30 | // when 31 | List responses = lineService.findLines(); 32 | 33 | // then 34 | assertThat(responses).hasSize(1); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/study/unit/SpringExtensionTest.java: -------------------------------------------------------------------------------- 1 | package study.unit; 2 | 3 | import com.google.common.collect.Lists; 4 | import nextstep.subway.line.application.LineService; 5 | import nextstep.subway.line.domain.Line; 6 | import nextstep.subway.line.domain.LineRepository; 7 | import nextstep.subway.line.dto.LineResponse; 8 | import nextstep.subway.station.application.StationService; 9 | import nextstep.subway.station.domain.StationRepository; 10 | import org.junit.jupiter.api.DisplayName; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.springframework.boot.test.mock.mockito.MockBean; 14 | 15 | import java.util.List; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | import static org.mockito.Mockito.when; 19 | 20 | @DisplayName("단위 테스트 - SpringExtension을 활용한 가짜 협력 객체 사용") 21 | @ExtendWith(org.springframework.test.context.junit.jupiter.SpringExtension.class) 22 | public class SpringExtensionTest { 23 | @MockBean 24 | private LineRepository lineRepository; 25 | @MockBean 26 | private StationService stationService; 27 | 28 | @Test 29 | void findAllLines() { 30 | // given 31 | when(lineRepository.findAll()).thenReturn(Lists.newArrayList(new Line())); 32 | LineService lineService = new LineService(lineRepository, stationService); 33 | 34 | // when 35 | List responses = lineService.findLines(); 36 | 37 | // then 38 | assertThat(responses).hasSize(1); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/study/unit/UnitTest.java: -------------------------------------------------------------------------------- 1 | package study.unit; 2 | 3 | import nextstep.subway.line.domain.Line; 4 | import nextstep.subway.station.domain.Station; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | @DisplayName("단위 테스트") 11 | public class UnitTest { 12 | @Test 13 | void update() { 14 | // given 15 | String newName = "구분당선"; 16 | 17 | Station upStation = new Station("강남역"); 18 | Station downStation = new Station("광교역"); 19 | Line line = new Line("신분당선", "RED", upStation, downStation, 10); 20 | Line newLine = new Line(newName, "GREEN"); 21 | 22 | // when 23 | line.update(newLine); 24 | 25 | // then 26 | assertThat(line.getName()).isEqualTo(newName); 27 | } 28 | } 29 | --------------------------------------------------------------------------------