├── .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 │ │ ├── PageController.java │ │ ├── SubwayApplication.java │ │ ├── auth │ │ ├── application │ │ │ ├── AuthService.java │ │ │ └── AuthorizationException.java │ │ ├── domain │ │ │ ├── AuthenticationPrincipal.java │ │ │ └── LoginMember.java │ │ ├── dto │ │ │ ├── TokenRequest.java │ │ │ └── TokenResponse.java │ │ ├── infrastructure │ │ │ ├── AuthorizationExtractor.java │ │ │ └── JwtTokenProvider.java │ │ └── ui │ │ │ ├── AuthController.java │ │ │ └── AuthenticationPrincipalArgumentResolver.java │ │ ├── common │ │ ├── AuthenticationPrincipalConfig.java │ │ └── BaseEntity.java │ │ ├── favorite │ │ ├── application │ │ │ └── FavoriteService.java │ │ ├── domain │ │ │ ├── Favorite.java │ │ │ ├── FavoriteRepository.java │ │ │ └── HasNotPermissionException.java │ │ ├── dto │ │ │ ├── FavoriteRequest.java │ │ │ └── FavoriteResponse.java │ │ └── ui │ │ │ └── FavoriteController.java │ │ ├── line │ │ ├── application │ │ │ └── LineService.java │ │ ├── domain │ │ │ ├── Line.java │ │ │ ├── LineRepository.java │ │ │ └── Section.java │ │ ├── dto │ │ │ ├── LineRequest.java │ │ │ ├── LineResponse.java │ │ │ └── SectionRequest.java │ │ └── ui │ │ │ └── LineController.java │ │ ├── map │ │ ├── application │ │ │ ├── MapService.java │ │ │ └── PathService.java │ │ ├── domain │ │ │ ├── SectionEdge.java │ │ │ ├── SubwayGraph.java │ │ │ └── SubwayPath.java │ │ ├── dto │ │ │ ├── PathResponse.java │ │ │ └── PathResponseAssembler.java │ │ └── ui │ │ │ └── MapController.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 │ └── js │ │ ├── main.js │ │ ├── vendors.js │ │ └── vendors.js.LICENSE.txt │ └── templates │ └── index.html └── test └── java ├── nextstep └── subway │ ├── AcceptanceTest.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 | 17 |
18 | 19 | ## 🚀 Getting Started 20 | 21 | ### Install 22 | #### npm 설치 23 | ``` 24 | cd frontend 25 | npm install 26 | ``` 27 | > `frontend` 디렉토리에서 수행해야 합니다. 28 | 29 | ### Usage 30 | #### webpack server 구동 31 | ``` 32 | npm run dev 33 | ``` 34 | #### application 구동 35 | ``` 36 | ./gradlew clean build 37 | ``` 38 |
39 | 40 | ## 미션 41 | 42 | * 미션 진행 후에 아래 질문의 답을 README.md 파일에 작성하여 PR을 보내주세요. 43 | 44 | ### 0단계 - pem 키 생성하기 45 | 46 | 1. 서버에 접속을 위한 pem키를 [구글드라이브](https://drive.google.com/drive/folders/1dZiCUwNeH1LMglp8dyTqqsL1b2yBnzd1?usp=sharing)에 업로드해주세요 47 | 48 | 2. 업로드한 pem키는 무엇인가요. 49 | 50 | ### 1단계 - 망 구성하기 51 | 1. 구성한 망의 서브넷 대역을 알려주세요 52 | - 대역 : 53 | 54 | 2. 배포한 서비스의 공인 IP(혹은 URL)를 알려주세요 55 | 56 | - URL : 57 | 58 | 59 | 60 | --- 61 | 62 | ### 2단계 - 배포하기 63 | 1. TLS가 적용된 URL을 알려주세요 64 | 65 | - URL : 66 | 67 | --- 68 | 69 | ### 3단계 - 배포 스크립트 작성하기 70 | 71 | 1. 작성한 배포 스크립트를 공유해주세요. 72 | 73 | 74 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.4.0-SNAPSHOT' 3 | id 'io.spring.dependency-management' version '1.0.10.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 | maven { url 'https://repo.spring.io/milestone' } 14 | maven { url 'https://repo.spring.io/snapshot' } 15 | } 16 | 17 | dependencies { 18 | // spring 19 | implementation 'org.springframework.boot:spring-boot-starter-web' 20 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 21 | 22 | // handlebars 23 | implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.3.0' 24 | 25 | // log 26 | implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' 27 | 28 | // jgraph 29 | implementation 'org.jgrapht:jgrapht-core:1.0.1' 30 | 31 | // jwt 32 | implementation 'io.jsonwebtoken:jjwt:0.9.1' 33 | 34 | testImplementation 'io.rest-assured:rest-assured:3.3.0' 35 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 36 | 37 | runtimeOnly 'com.h2database:h2' 38 | } 39 | 40 | test { 41 | useJUnitPlatform() 42 | } 43 | -------------------------------------------------------------------------------- /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 | } 14 | }) 15 | }, 16 | login(uri, config) { 17 | return Vue.axios.post(`${uri}`, {}, config) 18 | }, 19 | post(uri, params) { 20 | return Vue.axios.post(`${uri}`, params, { 21 | headers: { 22 | Authorization: `Bearer ${localStorage.getItem('token')}` || '' 23 | } 24 | }) 25 | }, 26 | update(uri, params) { 27 | return Vue.axios.put(uri, params, { 28 | headers: { 29 | Authorization: `Bearer ${localStorage.getItem('token')}` || '' 30 | } 31 | }) 32 | }, 33 | delete(uri) { 34 | return Vue.axios.delete(uri, { 35 | headers: { 36 | Authorization: `Bearer ${localStorage.getItem('token')}` || '' 37 | } 38 | }) 39 | } 40 | } 41 | 42 | ApiService.init() 43 | 44 | export default ApiService 45 | -------------------------------------------------------------------------------- /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 | 3 | const BASE_URL = '/lines' 4 | 5 | const LineService = { 6 | get(lineId) { 7 | return ApiService.get(`${BASE_URL}/${lineId}`) 8 | }, 9 | getAll() { 10 | return ApiService.get(`${BASE_URL}`) 11 | }, 12 | create(newLine) { 13 | return ApiService.post(`${BASE_URL}`, newLine) 14 | }, 15 | update(editingLine) { 16 | return ApiService.update(`${BASE_URL}/${editingLine.lineId}`, editingLine.line) 17 | }, 18 | delete(lineId) { 19 | return ApiService.delete(`${BASE_URL}/${lineId}`) 20 | }, 21 | createSection({ lineId, section }) { 22 | return ApiService.post(`${BASE_URL}/${lineId}/sections`, section) 23 | }, 24 | deleteSection({ lineId, stationId }) { 25 | return ApiService.delete(`${BASE_URL}/${lineId}/sections?stationId=${stationId}`) 26 | } 27 | } 28 | 29 | export default LineService 30 | -------------------------------------------------------------------------------- /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('woff2'); 4 | } 5 | 6 | @font-face { 7 | font-family: 'hannaair'; 8 | src: url(/fonts/BMHANNAAir.otf) format('woff2'); 9 | } 10 | 11 | @font-face { 12 | font-family: 'euljiro'; 13 | src: url(/fonts/BMEULJIRO.otf) format('woff2'); 14 | } 15 | 16 | @font-face { 17 | font-family: 'jua'; 18 | src: url(/fonts/BMJUA.otf) format('woff2'); 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 | 101 | 102 | 158 | -------------------------------------------------------------------------------- /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 | 55 | 56 | 169 | 170 | 175 | -------------------------------------------------------------------------------- /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 | 3 | const outputPath = path.resolve(__dirname, '../src/main/resources/static') 4 | 5 | module.exports = { 6 | mode: 'production', 7 | output: { 8 | path: outputPath, 9 | filename: '[name].js' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/next-step/infra-subway-deploy/22d6124f5936782075f2b09f5d3d9052fe39140d/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.6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /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/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 | @Transactional 14 | public class AuthService { 15 | private MemberRepository memberRepository; 16 | private JwtTokenProvider jwtTokenProvider; 17 | 18 | public AuthService(MemberRepository memberRepository, JwtTokenProvider jwtTokenProvider) { 19 | this.memberRepository = memberRepository; 20 | this.jwtTokenProvider = jwtTokenProvider; 21 | } 22 | 23 | public TokenResponse login(TokenRequest request) { 24 | Member member = memberRepository.findByEmail(request.getEmail()).orElseThrow(AuthorizationException::new); 25 | member.checkPassword(request.getPassword()); 26 | 27 | String token = jwtTokenProvider.createToken(request.getEmail()); 28 | return new TokenResponse(token); 29 | } 30 | 31 | public LoginMember findMemberByToken(String credentials) { 32 | if (!jwtTokenProvider.validateToken(credentials)) { 33 | throw new AuthorizationException(); 34 | } 35 | 36 | String email = jwtTokenProvider.getPayload(credentials); 37 | Member member = memberRepository.findByEmail(email).orElseThrow(RuntimeException::new); 38 | return new LoginMember(member.getId(), member.getEmail(), member.getAge()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /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(Long id, String email, Integer age) { 9 | this.id = id; 10 | this.email = email; 11 | this.age = age; 12 | } 13 | 14 | public Long getId() { 15 | return id; 16 | } 17 | 18 | public String getEmail() { 19 | return email; 20 | } 21 | 22 | public Integer getAge() { 23 | return age; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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/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 final String BEARER_TYPE = "Bearer"; 9 | public static final String ACCESS_TOKEN_TYPE = AuthorizationExtractor.class.getSimpleName() + ".ACCESS_TOKEN_TYPE"; 10 | 11 | private AuthorizationExtractor() { 12 | } 13 | 14 | public static String extract(HttpServletRequest request) { 15 | Enumeration headers = request.getHeaders(AUTHORIZATION); 16 | while (headers.hasMoreElements()) { 17 | String value = headers.nextElement(); 18 | if (isBearerType(value)) { 19 | request.setAttribute(ACCESS_TOKEN_TYPE, BEARER_TYPE); 20 | return extractAuthHeader(value); 21 | } 22 | } 23 | 24 | return null; 25 | } 26 | 27 | private static String extractAuthHeader(String value) { 28 | String authHeaderValue = value.substring(BEARER_TYPE.length()).trim(); 29 | return (authHeaderValue.contains(",")) 30 | ? authHeaderValue.split(",")[0] 31 | : authHeaderValue; 32 | } 33 | 34 | private static boolean isBearerType(String value) { 35 | return value.toLowerCase().startsWith(BEARER_TYPE.toLowerCase()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /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/common/AuthenticationPrincipalConfig.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.common; 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/common/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.common; 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/favorite/application/FavoriteService.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.favorite.application; 2 | 3 | import nextstep.subway.auth.domain.LoginMember; 4 | import nextstep.subway.favorite.domain.Favorite; 5 | import nextstep.subway.favorite.domain.FavoriteRepository; 6 | import nextstep.subway.favorite.domain.HasNotPermissionException; 7 | import nextstep.subway.favorite.dto.FavoriteRequest; 8 | import nextstep.subway.favorite.dto.FavoriteResponse; 9 | import nextstep.subway.station.domain.Station; 10 | import nextstep.subway.station.domain.StationRepository; 11 | import nextstep.subway.station.dto.StationResponse; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.util.HashSet; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.Set; 18 | import java.util.function.Function; 19 | import java.util.stream.Collectors; 20 | 21 | @Service 22 | public class FavoriteService { 23 | private FavoriteRepository favoriteRepository; 24 | private StationRepository stationRepository; 25 | 26 | public FavoriteService(FavoriteRepository favoriteRepository, StationRepository stationRepository) { 27 | this.favoriteRepository = favoriteRepository; 28 | this.stationRepository = stationRepository; 29 | } 30 | 31 | public void createFavorite(LoginMember loginMember, FavoriteRequest request) { 32 | Favorite favorite = new Favorite(loginMember.getId(), request.getSource(), request.getTarget()); 33 | favoriteRepository.save(favorite); 34 | } 35 | 36 | public List findFavorites(LoginMember loginMember) { 37 | List favorites = favoriteRepository.findByMemberId(loginMember.getId()); 38 | Map stations = extractStations(favorites); 39 | 40 | return favorites.stream() 41 | .map(it -> FavoriteResponse.of( 42 | it, 43 | StationResponse.of(stations.get(it.getSourceStationId())), 44 | StationResponse.of(stations.get(it.getTargetStationId())))) 45 | .collect(Collectors.toList()); 46 | } 47 | 48 | public void deleteFavorite(LoginMember loginMember, Long id) { 49 | Favorite favorite = favoriteRepository.findById(id).orElseThrow(RuntimeException::new); 50 | if (!favorite.isCreatedBy(loginMember.getId())) { 51 | throw new HasNotPermissionException(loginMember.getId() + "는 삭제할 권한이 없습니다."); 52 | } 53 | favoriteRepository.deleteById(id); 54 | } 55 | 56 | private Map extractStations(List favorites) { 57 | Set stationIds = extractStationIds(favorites); 58 | return stationRepository.findAllById(stationIds).stream() 59 | .collect(Collectors.toMap(Station::getId, Function.identity())); 60 | } 61 | 62 | private Set extractStationIds(List favorites) { 63 | Set stationIds = new HashSet<>(); 64 | for (Favorite favorite : favorites) { 65 | stationIds.add(favorite.getSourceStationId()); 66 | stationIds.add(favorite.getTargetStationId()); 67 | } 68 | return stationIds; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/favorite/domain/Favorite.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.favorite.domain; 2 | 3 | import nextstep.subway.common.BaseEntity; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | 10 | @Entity 11 | public class Favorite extends BaseEntity { 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.IDENTITY) 14 | private Long id; 15 | private Long memberId; 16 | private Long sourceStationId; 17 | private Long targetStationId; 18 | 19 | public Favorite() { 20 | } 21 | 22 | public Favorite(Long memberId, Long sourceStationId, Long targetStationId) { 23 | this.memberId = memberId; 24 | this.sourceStationId = sourceStationId; 25 | this.targetStationId = targetStationId; 26 | } 27 | 28 | public Long getId() { 29 | return id; 30 | } 31 | 32 | public Long getMemberId() { 33 | return memberId; 34 | } 35 | 36 | public Long getSourceStationId() { 37 | return sourceStationId; 38 | } 39 | 40 | public Long getTargetStationId() { 41 | return targetStationId; 42 | } 43 | 44 | public boolean isCreatedBy(Long memberId) { 45 | return this.memberId.equals(memberId); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/favorite/domain/FavoriteRepository.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.favorite.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.List; 6 | 7 | public interface FavoriteRepository extends JpaRepository { 8 | List findByMemberId(Long memberId); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/favorite/domain/HasNotPermissionException.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.favorite.domain; 2 | 3 | public class HasNotPermissionException extends RuntimeException { 4 | public HasNotPermissionException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/favorite/dto/FavoriteRequest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.favorite.dto; 2 | 3 | public class FavoriteRequest { 4 | private Long source; 5 | private Long target; 6 | 7 | public FavoriteRequest() { 8 | } 9 | 10 | public FavoriteRequest(Long source, Long target) { 11 | this.source = source; 12 | this.target = target; 13 | } 14 | 15 | public Long getSource() { 16 | return source; 17 | } 18 | 19 | public Long getTarget() { 20 | return target; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/favorite/dto/FavoriteResponse.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.favorite.dto; 2 | 3 | import nextstep.subway.favorite.domain.Favorite; 4 | import nextstep.subway.station.dto.StationResponse; 5 | 6 | public class FavoriteResponse { 7 | private Long id; 8 | private StationResponse source; 9 | private StationResponse target; 10 | 11 | public FavoriteResponse() { 12 | } 13 | 14 | public FavoriteResponse(Long id, StationResponse source, StationResponse target) { 15 | this.id = id; 16 | this.source = source; 17 | this.target = target; 18 | } 19 | 20 | public static FavoriteResponse of(Favorite favorite, StationResponse source, StationResponse target) { 21 | return new FavoriteResponse(favorite.getId(), source, target); 22 | } 23 | 24 | public Long getId() { 25 | return id; 26 | } 27 | 28 | public StationResponse getSource() { 29 | return source; 30 | } 31 | 32 | public StationResponse getTarget() { 33 | return target; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/favorite/ui/FavoriteController.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.favorite.ui; 2 | 3 | import nextstep.subway.auth.domain.AuthenticationPrincipal; 4 | import nextstep.subway.auth.domain.LoginMember; 5 | import nextstep.subway.favorite.application.FavoriteService; 6 | import nextstep.subway.favorite.dto.FavoriteRequest; 7 | import nextstep.subway.favorite.dto.FavoriteResponse; 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 FavoriteController { 16 | private FavoriteService favoriteService; 17 | 18 | public FavoriteController(FavoriteService favoriteService) { 19 | this.favoriteService = favoriteService; 20 | } 21 | 22 | @PostMapping("/favorites") 23 | public ResponseEntity createFavorite(@AuthenticationPrincipal LoginMember loginMember, @RequestBody FavoriteRequest request) { 24 | favoriteService.createFavorite(loginMember, request); 25 | return ResponseEntity 26 | .created(URI.create("/favorites/" + 1L)) 27 | .build(); 28 | } 29 | 30 | @GetMapping("/favorites") 31 | public ResponseEntity> getFavorites(@AuthenticationPrincipal LoginMember loginMember) { 32 | List favorites = favoriteService.findFavorites(loginMember); 33 | return ResponseEntity.ok().body(favorites); 34 | } 35 | 36 | @DeleteMapping("/favorites/{id}") 37 | public ResponseEntity deleteFavorite(@AuthenticationPrincipal LoginMember loginMember, @PathVariable Long id) { 38 | favoriteService.deleteFavorite(loginMember, id); 39 | return ResponseEntity.noContent().build(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /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.dto.LineRequest; 6 | import nextstep.subway.line.dto.LineResponse; 7 | import nextstep.subway.line.dto.SectionRequest; 8 | import nextstep.subway.station.application.StationService; 9 | import nextstep.subway.station.domain.Station; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | @Service 17 | @Transactional 18 | public class LineService { 19 | private LineRepository lineRepository; 20 | private StationService stationService; 21 | 22 | public LineService(LineRepository lineRepository, StationService stationService) { 23 | this.lineRepository = lineRepository; 24 | this.stationService = stationService; 25 | } 26 | 27 | public LineResponse saveLine(LineRequest request) { 28 | Station upStation = stationService.findById(request.getUpStationId()); 29 | Station downStation = stationService.findById(request.getDownStationId()); 30 | Line persistLine = lineRepository.save(new Line(request.getName(), request.getColor(), upStation, downStation, request.getDistance())); 31 | return LineResponse.of(persistLine); 32 | } 33 | 34 | public List findLineResponses() { 35 | List persistLines = lineRepository.findAll(); 36 | return persistLines.stream() 37 | .map(LineResponse::of) 38 | .collect(Collectors.toList()); 39 | } 40 | 41 | public List findLines() { 42 | return lineRepository.findAll(); 43 | } 44 | 45 | public Line findLineById(Long id) { 46 | return lineRepository.findById(id).orElseThrow(RuntimeException::new); 47 | } 48 | 49 | 50 | public LineResponse findLineResponseById(Long id) { 51 | Line persistLine = findLineById(id); 52 | return LineResponse.of(persistLine); 53 | } 54 | 55 | public void updateLine(Long id, LineRequest lineUpdateRequest) { 56 | Line persistLine = lineRepository.findById(id).orElseThrow(RuntimeException::new); 57 | persistLine.update(new Line(lineUpdateRequest.getName(), lineUpdateRequest.getColor())); 58 | } 59 | 60 | public void deleteLineById(Long id) { 61 | lineRepository.deleteById(id); 62 | } 63 | 64 | public void addLineStation(Long lineId, SectionRequest request) { 65 | Line line = findLineById(lineId); 66 | Station upStation = stationService.findStationById(request.getUpStationId()); 67 | Station downStation = stationService.findStationById(request.getDownStationId()); 68 | line.addLineSection(upStation, downStation, request.getDistance()); 69 | } 70 | 71 | public void removeLineStation(Long lineId, Long stationId) { 72 | Line line = findLineById(lineId); 73 | line.removeStation(stationId); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /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 | import java.io.Serializable; 7 | 8 | @Entity 9 | public class Section implements Serializable { 10 | @Id 11 | @GeneratedValue(strategy = GenerationType.IDENTITY) 12 | private Long id; 13 | 14 | @ManyToOne(cascade = CascadeType.PERSIST) 15 | @JoinColumn(name = "line_id") 16 | private Line line; 17 | 18 | @ManyToOne(cascade = CascadeType.PERSIST) 19 | @JoinColumn(name = "up_station_id") 20 | private Station upStation; 21 | 22 | @ManyToOne(cascade = CascadeType.PERSIST) 23 | @JoinColumn(name = "down_station_id") 24 | private Station downStation; 25 | 26 | private int distance; 27 | 28 | public Section() { 29 | } 30 | 31 | public Section(Line line, Station upStation, Station downStation, int distance) { 32 | this.line = line; 33 | this.upStation = upStation; 34 | this.downStation = downStation; 35 | this.distance = distance; 36 | } 37 | 38 | public Long getId() { 39 | return id; 40 | } 41 | 42 | public Line getLine() { 43 | return line; 44 | } 45 | 46 | public Station getUpStation() { 47 | return upStation; 48 | } 49 | 50 | public Boolean equalUpStation(Long stationId) { 51 | return this.upStation.getId().equals(stationId); 52 | } 53 | 54 | public Boolean equalUpStation(Station station) { 55 | return this.upStation.equals(station); 56 | } 57 | 58 | public Boolean equalDownStation(Long stationId) { 59 | return this.downStation.getId().equals(stationId); 60 | } 61 | 62 | public Boolean equalDownStation(Station station) { 63 | return this.downStation.equals(station); 64 | } 65 | 66 | public Station getDownStation() { 67 | return downStation; 68 | } 69 | 70 | public int getDistance() { 71 | return distance; 72 | } 73 | 74 | public void updateUpStation(Station station, int newDistance) { 75 | if (this.distance < newDistance) { 76 | throw new IllegalArgumentException("역과 역 사이의 거리보다 좁은 거리를 입력해주세요"); 77 | } 78 | this.upStation = station; 79 | this.distance -= newDistance; 80 | } 81 | 82 | public void updateDownStation(Station station, int newDistance) { 83 | if (this.distance < newDistance) { 84 | throw new IllegalArgumentException("역과 역 사이의 거리보다 좁은 거리를 입력해주세요"); 85 | } 86 | this.downStation = station; 87 | this.distance -= newDistance; 88 | } 89 | 90 | public boolean existDownStation() { 91 | return this.downStation != null; 92 | } 93 | 94 | public boolean existUpStation() { 95 | return this.upStation != null; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /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 | private LineRequest() { 13 | } 14 | 15 | public String getName() { 16 | return name; 17 | } 18 | 19 | public String getColor() { 20 | return color; 21 | } 22 | 23 | public Long getUpStationId() { 24 | return upStationId; 25 | } 26 | 27 | public Long getDownStationId() { 28 | return downStationId; 29 | } 30 | 31 | public int getDistance() { 32 | return distance; 33 | } 34 | 35 | public Line toLine() { 36 | return new Line(name, color); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /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.ArrayList; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | 11 | public class LineResponse { 12 | private Long id; 13 | private String name; 14 | private String color; 15 | private List stations; 16 | private LocalDateTime createdDate; 17 | private LocalDateTime modifiedDate; 18 | 19 | public LineResponse() { 20 | } 21 | 22 | public LineResponse(Long id, String name, String color, List stations, LocalDateTime createdDate, LocalDateTime modifiedDate) { 23 | this.id = id; 24 | this.name = name; 25 | this.color = color; 26 | this.stations = stations; 27 | this.createdDate = createdDate; 28 | this.modifiedDate = modifiedDate; 29 | } 30 | 31 | public static LineResponse of(Line line) { 32 | if(isEmpty(line)) { 33 | return new LineResponse(line.getId(), line.getName(), line.getColor(), new ArrayList(), line.getCreatedDate(), line.getModifiedDate()); 34 | } 35 | return new LineResponse(line.getId(), line.getName(), line.getColor(), assembleStations(line), line.getCreatedDate(), line.getModifiedDate()); 36 | } 37 | 38 | private static boolean isEmpty(Line line) { 39 | return line.getStations().isEmpty(); 40 | } 41 | 42 | private static List assembleStations(Line line) { 43 | return line.getStations().stream() 44 | .map(StationResponse::of) 45 | .collect(Collectors.toList()); 46 | } 47 | 48 | public Long getId() { 49 | return id; 50 | } 51 | 52 | public String getName() { 53 | return name; 54 | } 55 | 56 | public String getColor() { 57 | return color; 58 | } 59 | 60 | public List getStations() { 61 | return stations; 62 | } 63 | 64 | public LocalDateTime getCreatedDate() { 65 | return createdDate; 66 | } 67 | 68 | public LocalDateTime getModifiedDate() { 69 | return modifiedDate; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /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 | private SectionRequest() { 9 | } 10 | 11 | public Long getUpStationId() { 12 | return upStationId; 13 | } 14 | 15 | public Long getDownStationId() { 16 | return downStationId; 17 | } 18 | 19 | public int getDistance() { 20 | return distance; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /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.findLineResponses()); 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/map/application/MapService.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.map.application; 2 | 3 | import nextstep.subway.line.application.LineService; 4 | import nextstep.subway.line.domain.Line; 5 | import nextstep.subway.map.domain.SubwayPath; 6 | import nextstep.subway.map.dto.PathResponse; 7 | import nextstep.subway.map.dto.PathResponseAssembler; 8 | import nextstep.subway.station.application.StationService; 9 | import nextstep.subway.station.domain.Station; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | import java.util.List; 14 | 15 | @Service 16 | @Transactional 17 | public class MapService { 18 | private LineService lineService; 19 | private StationService stationService; 20 | private PathService pathService; 21 | 22 | public MapService(LineService lineService, StationService stationService, PathService pathService) { 23 | this.lineService = lineService; 24 | this.stationService = stationService; 25 | this.pathService = pathService; 26 | } 27 | 28 | public PathResponse findPath(Long source, Long target) { 29 | List lines = lineService.findLines(); 30 | Station sourceStation = stationService.findById(source); 31 | Station targetStation = stationService.findById(target); 32 | SubwayPath subwayPath = pathService.findPath(lines, sourceStation, targetStation); 33 | 34 | return PathResponseAssembler.assemble(subwayPath); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/map/application/PathService.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.map.application; 2 | 3 | import nextstep.subway.line.domain.Line; 4 | import nextstep.subway.map.domain.SectionEdge; 5 | import nextstep.subway.map.domain.SubwayGraph; 6 | import nextstep.subway.map.domain.SubwayPath; 7 | import nextstep.subway.station.domain.Station; 8 | import org.jgrapht.GraphPath; 9 | import org.jgrapht.alg.shortestpath.DijkstraShortestPath; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | @Service 16 | public class PathService { 17 | public SubwayPath findPath(List lines, Station source, Station target) { 18 | SubwayGraph graph = new SubwayGraph(SectionEdge.class); 19 | graph.addVertexWith(lines); 20 | graph.addEdge(lines); 21 | 22 | // 다익스트라 최단 경로 찾기 23 | DijkstraShortestPath dijkstraShortestPath = new DijkstraShortestPath(graph); 24 | GraphPath path = dijkstraShortestPath.getPath(source, target); 25 | 26 | return convertSubwayPath(path); 27 | } 28 | 29 | private SubwayPath convertSubwayPath(GraphPath graphPath) { 30 | List edges = (List) graphPath.getEdgeList().stream().collect(Collectors.toList()); 31 | List stations = graphPath.getVertexList(); 32 | return new SubwayPath(edges, stations); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/map/domain/SectionEdge.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.map.domain; 2 | 3 | import nextstep.subway.line.domain.Section; 4 | import org.jgrapht.graph.DefaultWeightedEdge; 5 | 6 | public class SectionEdge extends DefaultWeightedEdge { 7 | private Section section; 8 | private Long lineId; 9 | 10 | public SectionEdge(Section section, Long lineId) { 11 | this.section = section; 12 | this.lineId = lineId; 13 | } 14 | 15 | public Section getSection() { 16 | return section; 17 | } 18 | 19 | public Long getLineId() { 20 | return lineId; 21 | } 22 | 23 | @Override 24 | protected Object getSource() { 25 | return this.section.getUpStation(); 26 | } 27 | 28 | @Override 29 | protected Object getTarget() { 30 | return this.section.getDownStation(); 31 | } 32 | 33 | @Override 34 | protected double getWeight() { 35 | return this.section.getDistance(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/map/domain/SubwayGraph.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.map.domain; 2 | 3 | import nextstep.subway.line.domain.Line; 4 | import nextstep.subway.line.domain.Section; 5 | import nextstep.subway.station.domain.Station; 6 | import org.jgrapht.graph.WeightedMultigraph; 7 | 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | 11 | public class SubwayGraph extends WeightedMultigraph { 12 | public SubwayGraph(Class edgeClass) { 13 | super(edgeClass); 14 | } 15 | 16 | public void addVertexWith(List lines) { 17 | // 지하철 역(정점)을 등록 18 | lines.stream() 19 | .flatMap(it -> it.getStations().stream()) 20 | .distinct() 21 | .collect(Collectors.toList()) 22 | .forEach(this::addVertex); 23 | } 24 | 25 | public void addEdge(List lines) { 26 | // 지하철 역의 연결 정보(간선)을 등록 27 | for (Line line : lines) { 28 | line.getSections().stream() 29 | .forEach(it -> addEdge(it, line)); 30 | } 31 | } 32 | 33 | private void addEdge(Section section, Line line) { 34 | SectionEdge sectionEdge = new SectionEdge(section, line.getId()); 35 | addEdge(section.getUpStation(), section.getDownStation(), sectionEdge); 36 | setEdgeWeight(sectionEdge, section.getDistance()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/map/domain/SubwayPath.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.map.domain; 2 | 3 | import nextstep.subway.station.domain.Station; 4 | 5 | import java.util.List; 6 | 7 | public class SubwayPath { 8 | private List sectionEdges; 9 | private List stations; 10 | 11 | public SubwayPath(List sectionEdges, List stations) { 12 | this.sectionEdges = sectionEdges; 13 | this.stations = stations; 14 | } 15 | 16 | public List getSectionEdges() { 17 | return sectionEdges; 18 | } 19 | 20 | public List getStations() { 21 | return stations; 22 | } 23 | 24 | public int calculateDistance() { 25 | return sectionEdges.stream().mapToInt(it -> it.getSection().getDistance()).sum(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/map/dto/PathResponse.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.map.dto; 2 | 3 | import nextstep.subway.station.dto.StationResponse; 4 | 5 | import java.util.List; 6 | 7 | public class PathResponse { 8 | private List stations; 9 | private int distance; 10 | 11 | public PathResponse() { 12 | } 13 | 14 | public PathResponse(List stations, int distance) { 15 | this.stations = stations; 16 | this.distance = distance; 17 | } 18 | 19 | public List getStations() { 20 | return stations; 21 | } 22 | 23 | public int getDistance() { 24 | return distance; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/map/dto/PathResponseAssembler.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.map.dto; 2 | 3 | import nextstep.subway.map.domain.SubwayPath; 4 | import nextstep.subway.station.dto.StationResponse; 5 | 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | public class PathResponseAssembler { 10 | public static PathResponse assemble(SubwayPath subwayPath) { 11 | List stationResponses = subwayPath.getStations().stream() 12 | .map(StationResponse::of) 13 | .collect(Collectors.toList()); 14 | 15 | int distance = subwayPath.calculateDistance(); 16 | 17 | return new PathResponse(stationResponses, distance); 18 | } 19 | 20 | private PathResponseAssembler() { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/map/ui/MapController.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.map.ui; 2 | 3 | import nextstep.subway.map.application.MapService; 4 | import nextstep.subway.map.dto.PathResponse; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestParam; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | @RestController 11 | public class MapController { 12 | private MapService mapService; 13 | 14 | public MapController(MapService mapService) { 15 | this.mapService = mapService; 16 | } 17 | 18 | @GetMapping("/paths") 19 | public ResponseEntity findPath(@RequestParam Long source, @RequestParam Long target) { 20 | return ResponseEntity.ok(mapService.findPath(source, target)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /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 | @Transactional 12 | public class MemberService { 13 | private MemberRepository memberRepository; 14 | 15 | public MemberService(MemberRepository memberRepository) { 16 | this.memberRepository = memberRepository; 17 | } 18 | 19 | public MemberResponse createMember(MemberRequest request) { 20 | Member member = memberRepository.save(request.toMember()); 21 | return MemberResponse.of(member); 22 | } 23 | 24 | public MemberResponse findMember(Long id) { 25 | Member member = memberRepository.findById(id).orElseThrow(RuntimeException::new); 26 | return MemberResponse.of(member); 27 | } 28 | 29 | public void updateMember(Long id, MemberRequest param) { 30 | Member member = memberRepository.findById(id).orElseThrow(RuntimeException::new); 31 | member.update(param.toMember()); 32 | } 33 | 34 | public void deleteMember(Long id) { 35 | memberRepository.deleteById(id); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/member/domain/Member.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.member.domain; 2 | 3 | import nextstep.subway.common.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.AuthenticationPrincipal; 4 | import nextstep.subway.auth.domain.LoginMember; 5 | import nextstep.subway.member.application.MemberService; 6 | import nextstep.subway.member.dto.MemberRequest; 7 | import nextstep.subway.member.dto.MemberResponse; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import java.net.URI; 12 | 13 | @RestController 14 | public class MemberController { 15 | private MemberService memberService; 16 | 17 | public MemberController(MemberService memberService) { 18 | this.memberService = memberService; 19 | } 20 | 21 | @PostMapping("/members") 22 | public ResponseEntity createMember(@RequestBody MemberRequest request) { 23 | MemberResponse member = memberService.createMember(request); 24 | return ResponseEntity.created(URI.create("/members/" + member.getId())).build(); 25 | } 26 | 27 | @GetMapping("/members/{id}") 28 | public ResponseEntity findMember(@PathVariable Long id) { 29 | MemberResponse member = memberService.findMember(id); 30 | return ResponseEntity.ok().body(member); 31 | } 32 | 33 | @PutMapping("/members/{id}") 34 | public ResponseEntity updateMember(@PathVariable Long id, @RequestBody MemberRequest param) { 35 | memberService.updateMember(id, param); 36 | return ResponseEntity.ok().build(); 37 | } 38 | 39 | @DeleteMapping("/members/{id}") 40 | public ResponseEntity deleteMember(@PathVariable Long id) { 41 | memberService.deleteMember(id); 42 | return ResponseEntity.noContent().build(); 43 | } 44 | 45 | @GetMapping("/members/me") 46 | public ResponseEntity findMemberOfMine(@AuthenticationPrincipal LoginMember loginMember) { 47 | MemberResponse member = memberService.findMember(loginMember.getId()); 48 | return ResponseEntity.ok().body(member); 49 | } 50 | 51 | @PutMapping("/members/me") 52 | public ResponseEntity updateMemberOfMine(@AuthenticationPrincipal LoginMember loginMember, @RequestBody MemberRequest param) { 53 | memberService.updateMember(loginMember.getId(), param); 54 | return ResponseEntity.ok().build(); 55 | } 56 | 57 | @DeleteMapping("/members/me") 58 | public ResponseEntity deleteMemberOfMine(@AuthenticationPrincipal LoginMember loginMember) { 59 | memberService.deleteMember(loginMember.getId()); 60 | return ResponseEntity.noContent().build(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /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 | @Transactional 15 | public class StationService { 16 | private StationRepository stationRepository; 17 | 18 | public StationService(StationRepository stationRepository) { 19 | this.stationRepository = stationRepository; 20 | } 21 | 22 | public StationResponse saveStation(StationRequest stationRequest) { 23 | Station persistStation = stationRepository.save(stationRequest.toStation()); 24 | return StationResponse.of(persistStation); 25 | } 26 | 27 | @Transactional(readOnly = true) 28 | public List findAllStations() { 29 | List stations = stationRepository.findAll(); 30 | 31 | return stations.stream() 32 | .map(StationResponse::of) 33 | .collect(Collectors.toList()); 34 | } 35 | 36 | public void deleteStationById(Long id) { 37 | stationRepository.deleteById(id); 38 | } 39 | 40 | public Station findStationById(Long id) { 41 | return stationRepository.findById(id).orElseThrow(RuntimeException::new); 42 | } 43 | 44 | public Station findById(Long id) { 45 | return stationRepository.findById(id).orElseThrow(RuntimeException::new); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/nextstep/subway/station/domain/Station.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.station.domain; 2 | 3 | import nextstep.subway.common.BaseEntity; 4 | 5 | import javax.persistence.*; 6 | import java.io.Serializable; 7 | import java.util.Objects; 8 | 9 | @Entity 10 | public class Station extends BaseEntity implements Serializable { 11 | @Id 12 | @GeneratedValue(strategy = GenerationType.IDENTITY) 13 | private Long id; 14 | @Column(unique = true) 15 | private String name; 16 | 17 | public Station() { 18 | } 19 | 20 | public Station(String name) { 21 | this.name = name; 22 | } 23 | 24 | public Long getId() { 25 | return id; 26 | } 27 | 28 | public String getName() { 29 | return name; 30 | } 31 | 32 | @Override 33 | public boolean equals(Object o) { 34 | if (this == o) return true; 35 | if (o == null || getClass() != o.getClass()) return false; 36 | Station station = (Station) o; 37 | return Objects.equals(id, station.id) && 38 | Objects.equals(name, station.name); 39 | } 40 | 41 | @Override 42 | public int hashCode() { 43 | return Objects.hash(id, name); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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 String getName() { 9 | return name; 10 | } 11 | 12 | public Station toStation() { 13 | return new Station(name); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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 | spring.profiles.active=local 2 | 3 | handlebars.suffix=.html 4 | handlebars.enabled=true 5 | 6 | logging.level.org.hibernate.type.descriptor.sql=trace 7 | 8 | spring.jpa.properties.hibernate.show_sql=true 9 | spring.jpa.properties.hibernate.format_sql=true 10 | 11 | security.jwt.token.secret-key= eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.ih1aovtQShabQ7l0cINw4k1fagApg3qLWiB8Kt59Lno 12 | security.jwt.token.expire-length= 3600000 13 | -------------------------------------------------------------------------------- /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/infra-subway-deploy/22d6124f5936782075f2b09f5d3d9052fe39140d/src/main/resources/static/images/logo_small.png -------------------------------------------------------------------------------- /src/main/resources/static/images/main_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/next-step/infra-subway-deploy/22d6124f5936782075f2b09f5d3d9052fe39140d/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 | import org.springframework.test.context.ActiveProfiles; 10 | 11 | @ActiveProfiles("test") 12 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 13 | public class AcceptanceTest { 14 | @LocalServerPort 15 | int port; 16 | 17 | @Autowired 18 | private DatabaseCleanup databaseCleanup; 19 | 20 | @BeforeEach 21 | public void setUp() { 22 | if (RestAssured.port == RestAssured.UNDEFINED_PORT) { 23 | RestAssured.port = port; 24 | databaseCleanup.afterPropertiesSet(); 25 | } 26 | 27 | databaseCleanup.execute(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/nextstep/subway/auth/acceptance/AuthAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.auth.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.auth.dto.TokenResponse; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.MediaType; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | import static nextstep.subway.member.MemberAcceptanceTest.회원_생성을_요청; 17 | import static nextstep.subway.member.MemberAcceptanceTest.회원_정보_조회됨; 18 | 19 | public class AuthAcceptanceTest extends AcceptanceTest { 20 | private static final String EMAIL = "email@email.com"; 21 | private static final String PASSWORD = "password"; 22 | private static final Integer AGE = 20; 23 | 24 | @DisplayName("Bearer Auth") 25 | @Test 26 | void myInfoWithBearerAuth() { 27 | 회원_등록되어_있음(EMAIL, PASSWORD, AGE); 28 | TokenResponse tokenResponse = 로그인_되어_있음(EMAIL, PASSWORD); 29 | 30 | ExtractableResponse response = 내_회원_정보_조회_요청(tokenResponse); 31 | 32 | 회원_정보_조회됨(response, EMAIL, AGE); 33 | } 34 | 35 | @DisplayName("Bearer Auth 로그인 실패") 36 | @Test 37 | void myInfoWithBadBearerAuth() { 38 | 회원_등록되어_있음(EMAIL, PASSWORD, AGE); 39 | 40 | Map params = new HashMap<>(); 41 | params.put("email", EMAIL + "OTHER"); 42 | params.put("password", PASSWORD); 43 | 44 | RestAssured.given().log().all(). 45 | contentType(MediaType.APPLICATION_JSON_VALUE). 46 | body(params). 47 | when(). 48 | post("/login/token"). 49 | then(). 50 | log().all(). 51 | statusCode(HttpStatus.UNAUTHORIZED.value()); 52 | } 53 | 54 | @DisplayName("Bearer Auth 유효하지 않은 토큰") 55 | @Test 56 | void myInfoWithWrongBearerAuth() { 57 | TokenResponse tokenResponse = new TokenResponse("accesstoken"); 58 | 59 | RestAssured.given().log().all(). 60 | auth().oauth2(tokenResponse.getAccessToken()). 61 | accept(MediaType.APPLICATION_JSON_VALUE). 62 | when(). 63 | get("/members/me"). 64 | then(). 65 | log().all(). 66 | statusCode(HttpStatus.UNAUTHORIZED.value()); 67 | } 68 | 69 | public static ExtractableResponse 회원_등록되어_있음(String email, String password, Integer age) { 70 | return 회원_생성을_요청(email, password, age); 71 | } 72 | 73 | public static TokenResponse 로그인_되어_있음(String email, String password) { 74 | ExtractableResponse response = 로그인_요청(email, password); 75 | return response.as(TokenResponse.class); 76 | } 77 | 78 | public static ExtractableResponse 로그인_요청(String email, String password) { 79 | Map params = new HashMap<>(); 80 | params.put("email", email); 81 | params.put("password", password); 82 | 83 | return RestAssured.given().log().all(). 84 | contentType(MediaType.APPLICATION_JSON_VALUE). 85 | body(params). 86 | when(). 87 | post("/login/token"). 88 | then(). 89 | log().all(). 90 | statusCode(HttpStatus.OK.value()). 91 | extract(); 92 | } 93 | 94 | public static ExtractableResponse 내_회원_정보_조회_요청(TokenResponse tokenResponse) { 95 | return RestAssured.given().log().all(). 96 | auth().oauth2(tokenResponse.getAccessToken()). 97 | accept(MediaType.APPLICATION_JSON_VALUE). 98 | when(). 99 | get("/members/me"). 100 | then(). 101 | log().all(). 102 | statusCode(HttpStatus.OK.value()). 103 | extract(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/test/java/nextstep/subway/auth/application/AuthServiceTest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.auth.application; 2 | 3 | import nextstep.subway.auth.dto.TokenRequest; 4 | import nextstep.subway.auth.dto.TokenResponse; 5 | import nextstep.subway.auth.infrastructure.JwtTokenProvider; 6 | import nextstep.subway.member.domain.Member; 7 | import nextstep.subway.member.domain.MemberRepository; 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 io.restassured.RestAssured; 4 | import io.restassured.response.ExtractableResponse; 5 | import io.restassured.response.Response; 6 | import nextstep.subway.AcceptanceTest; 7 | import nextstep.subway.auth.dto.TokenResponse; 8 | import nextstep.subway.line.acceptance.LineAcceptanceTest; 9 | import nextstep.subway.line.dto.LineResponse; 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.HashMap; 19 | import java.util.Map; 20 | 21 | import static nextstep.subway.auth.acceptance.AuthAcceptanceTest.로그인_되어_있음; 22 | import static nextstep.subway.auth.acceptance.AuthAcceptanceTest.회원_등록되어_있음; 23 | import static nextstep.subway.line.acceptance.LineSectionAcceptanceTest.지하철_노선에_지하철역_등록_요청; 24 | import static org.assertj.core.api.Assertions.assertThat; 25 | 26 | @DisplayName("즐겨찾기 관련 기능") 27 | public class FavoriteAcceptanceTest extends AcceptanceTest { 28 | public static final String EMAIL = "email@email.com"; 29 | public static final String PASSWORD = "password"; 30 | 31 | private LineResponse 신분당선; 32 | private StationResponse 강남역; 33 | private StationResponse 양재역; 34 | private StationResponse 정자역; 35 | private StationResponse 광교역; 36 | 37 | private TokenResponse 사용자; 38 | 39 | @BeforeEach 40 | public void setUp() { 41 | super.setUp(); 42 | 43 | 강남역 = StationAcceptanceTest.지하철역_등록되어_있음("강남역").as(StationResponse.class); 44 | 양재역 = StationAcceptanceTest.지하철역_등록되어_있음("양재역").as(StationResponse.class); 45 | 정자역 = StationAcceptanceTest.지하철역_등록되어_있음("정자역").as(StationResponse.class); 46 | 광교역 = StationAcceptanceTest.지하철역_등록되어_있음("광교역").as(StationResponse.class); 47 | 48 | Map lineCreateParams; 49 | lineCreateParams = new HashMap<>(); 50 | lineCreateParams.put("name", "신분당선"); 51 | lineCreateParams.put("color", "bg-red-600"); 52 | lineCreateParams.put("upStationId", 강남역.getId() + ""); 53 | lineCreateParams.put("downStationId", 광교역.getId() + ""); 54 | lineCreateParams.put("distance", 10 + ""); 55 | 신분당선 = LineAcceptanceTest.지하철_노선_등록되어_있음(lineCreateParams).as(LineResponse.class); 56 | 57 | 지하철_노선에_지하철역_등록_요청(신분당선, 강남역, 양재역, 3); 58 | 지하철_노선에_지하철역_등록_요청(신분당선, 양재역, 정자역, 3); 59 | 60 | 회원_등록되어_있음(EMAIL, PASSWORD, 20); 61 | 사용자 = 로그인_되어_있음(EMAIL, PASSWORD); 62 | } 63 | 64 | @DisplayName("즐겨찾기를 관리한다.") 65 | @Test 66 | void manageMember() { 67 | // when 68 | ExtractableResponse createResponse = 즐겨찾기_생성을_요청(사용자, 강남역, 정자역); 69 | // then 70 | 즐겨찾기_생성됨(createResponse); 71 | 72 | // when 73 | ExtractableResponse findResponse = 즐겨찾기_목록_조회_요청(사용자); 74 | // then 75 | 즐겨찾기_목록_조회됨(findResponse); 76 | 77 | // when 78 | ExtractableResponse deleteResponse = 즐겨찾기_삭제_요청(사용자, createResponse); 79 | // then 80 | 즐겨찾기_삭제됨(deleteResponse); 81 | } 82 | 83 | public static ExtractableResponse 즐겨찾기_생성을_요청(TokenResponse tokenResponse, StationResponse source, StationResponse target) { 84 | Map params = new HashMap<>(); 85 | params.put("source", source.getId() + ""); 86 | params.put("target", target.getId() + ""); 87 | 88 | return RestAssured.given().log().all(). 89 | auth().oauth2(tokenResponse.getAccessToken()). 90 | contentType(MediaType.APPLICATION_JSON_VALUE). 91 | body(params). 92 | when(). 93 | post("/favorites"). 94 | then(). 95 | log().all(). 96 | extract(); 97 | } 98 | 99 | public static ExtractableResponse 즐겨찾기_목록_조회_요청(TokenResponse tokenResponse) { 100 | return RestAssured.given().log().all(). 101 | auth().oauth2(tokenResponse.getAccessToken()). 102 | accept(MediaType.APPLICATION_JSON_VALUE). 103 | when(). 104 | get("/favorites"). 105 | then(). 106 | log().all(). 107 | extract(); 108 | } 109 | 110 | public static ExtractableResponse 즐겨찾기_삭제_요청(TokenResponse tokenResponse, ExtractableResponse response) { 111 | String uri = response.header("Location"); 112 | 113 | return RestAssured.given().log().all(). 114 | auth().oauth2(tokenResponse.getAccessToken()). 115 | when(). 116 | delete(uri). 117 | then(). 118 | log().all(). 119 | extract(); 120 | } 121 | 122 | public static void 즐겨찾기_생성됨(ExtractableResponse response) { 123 | assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); 124 | } 125 | 126 | public static void 즐겨찾기_목록_조회됨(ExtractableResponse response) { 127 | assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); 128 | } 129 | 130 | public static void 즐겨찾기_삭제됨(ExtractableResponse response) { 131 | assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); 132 | } 133 | } -------------------------------------------------------------------------------- /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.MemberResponse; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.MediaType; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | 18 | public class MemberAcceptanceTest extends AcceptanceTest { 19 | public static final String EMAIL = "email@email.com"; 20 | public static final String PASSWORD = "password"; 21 | public static final int AGE = 20; 22 | 23 | @DisplayName("회원 정보를 관리한다.") 24 | @Test 25 | void manageMember() { 26 | // when 27 | ExtractableResponse createResponse = 회원_생성을_요청(EMAIL, PASSWORD, AGE); 28 | // then 29 | 회원_생성됨(createResponse); 30 | 31 | // when 32 | ExtractableResponse findResponse = 회원_정보_조회_요청(createResponse); 33 | // then 34 | 회원_정보_조회됨(findResponse, EMAIL, AGE); 35 | 36 | // when 37 | ExtractableResponse updateResponse = 회원_정보_수정_요청(createResponse, "new" + EMAIL, "new" + PASSWORD, AGE + 2); 38 | // then 39 | 회원_정보_수정됨(updateResponse); 40 | 41 | // when 42 | ExtractableResponse deleteResponse = 회원_삭제_요청(createResponse); 43 | // then 44 | 회원_삭제됨(deleteResponse); 45 | } 46 | 47 | public static ExtractableResponse 회원_생성을_요청(String email, String password, Integer age) { 48 | Map params = new HashMap<>(); 49 | params.put("email", email); 50 | params.put("password", password); 51 | params.put("age", age + ""); 52 | 53 | return RestAssured.given().log().all(). 54 | contentType(MediaType.APPLICATION_JSON_VALUE). 55 | body(params). 56 | when(). 57 | post("/members"). 58 | then(). 59 | log().all(). 60 | extract(); 61 | } 62 | 63 | public static ExtractableResponse 회원_정보_조회_요청(ExtractableResponse response) { 64 | String uri = response.header("Location"); 65 | 66 | return RestAssured.given().log().all(). 67 | accept(MediaType.APPLICATION_JSON_VALUE). 68 | when(). 69 | get(uri). 70 | then(). 71 | log().all(). 72 | extract(); 73 | } 74 | 75 | public static ExtractableResponse 회원_정보_수정_요청(ExtractableResponse response, String email, String password, Integer age) { 76 | String uri = response.header("Location"); 77 | 78 | Map params = new HashMap<>(); 79 | params.put("email", email); 80 | params.put("password", password); 81 | params.put("age", age + ""); 82 | 83 | return RestAssured.given().log().all(). 84 | contentType(MediaType.APPLICATION_JSON_VALUE). 85 | body(params). 86 | when(). 87 | put(uri). 88 | then(). 89 | log().all(). 90 | extract(); 91 | } 92 | 93 | public static ExtractableResponse 회원_삭제_요청(ExtractableResponse response) { 94 | String uri = response.header("Location"); 95 | return RestAssured.given().log().all(). 96 | when(). 97 | delete(uri). 98 | then(). 99 | log().all(). 100 | extract(); 101 | } 102 | 103 | public static void 회원_생성됨(ExtractableResponse response) { 104 | assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); 105 | } 106 | 107 | public static void 회원_정보_조회됨(ExtractableResponse response, String email, int age) { 108 | MemberResponse memberResponse = response.as(MemberResponse.class); 109 | assertThat(memberResponse.getId()).isNotNull(); 110 | assertThat(memberResponse.getEmail()).isEqualTo(email); 111 | assertThat(memberResponse.getAge()).isEqualTo(age); 112 | } 113 | 114 | public static void 회원_정보_수정됨(ExtractableResponse response) { 115 | assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); 116 | } 117 | 118 | public static void 회원_삭제됨(ExtractableResponse response) { 119 | assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/test/java/nextstep/subway/path/PathAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package nextstep.subway.path; 2 | 3 | import com.google.common.collect.Lists; 4 | import io.restassured.RestAssured; 5 | import io.restassured.response.ExtractableResponse; 6 | import io.restassured.response.Response; 7 | import nextstep.subway.AcceptanceTest; 8 | import nextstep.subway.line.acceptance.LineAcceptanceTest; 9 | import nextstep.subway.line.acceptance.LineSectionAcceptanceTest; 10 | import nextstep.subway.line.dto.LineResponse; 11 | import nextstep.subway.map.dto.PathResponse; 12 | import nextstep.subway.station.StationAcceptanceTest; 13 | import nextstep.subway.station.dto.StationResponse; 14 | import org.junit.jupiter.api.BeforeEach; 15 | import org.junit.jupiter.api.DisplayName; 16 | import org.junit.jupiter.api.Test; 17 | import org.springframework.http.MediaType; 18 | 19 | import java.util.ArrayList; 20 | import java.util.HashMap; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.stream.Collectors; 24 | 25 | import static org.assertj.core.api.Assertions.assertThat; 26 | 27 | 28 | @DisplayName("지하철 경로 조회") 29 | public class PathAcceptanceTest extends AcceptanceTest { 30 | private LineResponse 신분당선; 31 | private LineResponse 이호선; 32 | private LineResponse 삼호선; 33 | private StationResponse 강남역; 34 | private StationResponse 양재역; 35 | private StationResponse 교대역; 36 | private StationResponse 남부터미널역; 37 | 38 | /** 39 | * 교대역 --- *2호선* --- 강남역 40 | * | | 41 | * *3호선* *신분당선* 42 | * | | 43 | * 남부터미널역 --- *3호선* --- 양재 44 | */ 45 | @BeforeEach 46 | public void setUp() { 47 | super.setUp(); 48 | 49 | 강남역 = StationAcceptanceTest.지하철역_등록되어_있음("강남역").as(StationResponse.class); 50 | 양재역 = StationAcceptanceTest.지하철역_등록되어_있음("양재역").as(StationResponse.class); 51 | 교대역 = StationAcceptanceTest.지하철역_등록되어_있음("교대역").as(StationResponse.class); 52 | 남부터미널역 = StationAcceptanceTest.지하철역_등록되어_있음("남부터미널역").as(StationResponse.class); 53 | 54 | 신분당선 = 지하철_노선_등록되어_있음("신분당선", "bg-red-600", 강남역, 양재역, 10); 55 | 이호선 = 지하철_노선_등록되어_있음("이호선", "bg-red-600", 교대역, 강남역, 10); 56 | 삼호선 = 지하철_노선_등록되어_있음("삼호선", "bg-red-600", 교대역, 양재역, 5); 57 | 58 | 지하철_노선에_지하철역_등록되어_있음(삼호선, 교대역, 남부터미널역, 3); 59 | } 60 | 61 | @DisplayName("두 역의 최단 거리 경로를 조회한다.") 62 | @Test 63 | void findPathByDistance() { 64 | //when 65 | ExtractableResponse response = 거리_경로_조회_요청(3L, 2L); 66 | 67 | //then 68 | 적절한_경로를_응답(response, Lists.newArrayList(교대역, 남부터미널역, 양재역)); 69 | 총_거리와_소요_시간을_함께_응답함(response, 5); 70 | } 71 | 72 | private LineResponse 지하철_노선_등록되어_있음(String name, String color, StationResponse upStation, StationResponse downStation, int distance) { 73 | Map lineCreateParams; 74 | lineCreateParams = new HashMap<>(); 75 | lineCreateParams.put("name", name); 76 | lineCreateParams.put("color", color); 77 | lineCreateParams.put("upStationId", upStation.getId() + ""); 78 | lineCreateParams.put("downStationId", downStation.getId() + ""); 79 | lineCreateParams.put("distance", distance + ""); 80 | return LineAcceptanceTest.지하철_노선_등록되어_있음(lineCreateParams).as(LineResponse.class); 81 | } 82 | 83 | private void 지하철_노선에_지하철역_등록되어_있음(LineResponse line, StationResponse upStation, StationResponse downStation, int distance) { 84 | LineSectionAcceptanceTest.지하철_노선에_지하철역_등록_요청(line, upStation, downStation, distance); 85 | } 86 | 87 | public static ExtractableResponse 거리_경로_조회_요청(long source, long target) { 88 | return RestAssured.given().log().all(). 89 | accept(MediaType.APPLICATION_JSON_VALUE). 90 | when(). 91 | get("/paths?source={sourceId}&target={targetId}", source, target). 92 | then(). 93 | log().all(). 94 | extract(); 95 | } 96 | 97 | public static void 적절한_경로를_응답(ExtractableResponse response, ArrayList expectedPath) { 98 | PathResponse pathResponse = response.as(PathResponse.class); 99 | 100 | List stationIds = pathResponse.getStations().stream() 101 | .map(StationResponse::getId) 102 | .collect(Collectors.toList()); 103 | 104 | List expectedPathIds = expectedPath.stream() 105 | .map(StationResponse::getId) 106 | .collect(Collectors.toList()); 107 | 108 | assertThat(stationIds).containsExactlyElementsOf(expectedPathIds); 109 | } 110 | 111 | public static void 총_거리와_소요_시간을_함께_응답함(ExtractableResponse response, int totalDistance) { 112 | PathResponse pathResponse = response.as(PathResponse.class); 113 | assertThat(pathResponse.getDistance()).isEqualTo(totalDistance); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /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.StationResponse; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.MediaType; 12 | 13 | import java.util.Arrays; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.stream.Collectors; 18 | 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | 21 | @DisplayName("지하철역 관련 기능") 22 | public class StationAcceptanceTest extends AcceptanceTest { 23 | private static final String 강남역 = "강남역"; 24 | private static final String 역삼역 = "역삼역"; 25 | 26 | @DisplayName("지하철역을 생성한다.") 27 | @Test 28 | void createStation() { 29 | // when 30 | ExtractableResponse response = 지하철역_생성_요청(강남역); 31 | 32 | // then 33 | 지하철역_생성됨(response); 34 | } 35 | 36 | @DisplayName("기존에 존재하는 지하철역 이름으로 지하철역을 생성한다.") 37 | @Test 38 | void createStationWithDuplicateName() { 39 | //given 40 | 지하철역_등록되어_있음(강남역); 41 | 42 | // when 43 | ExtractableResponse response = 지하철역_생성_요청(강남역); 44 | 45 | // then 46 | 지하철역_생성_실패됨(response); 47 | } 48 | 49 | @DisplayName("지하철역을 조회한다.") 50 | @Test 51 | void getStations() { 52 | // given 53 | ExtractableResponse createResponse1 = 지하철역_등록되어_있음(강남역); 54 | ExtractableResponse createResponse2 = 지하철역_등록되어_있음(역삼역); 55 | 56 | // when 57 | ExtractableResponse response = 지하철역_목록_조회_요청(); 58 | 59 | // then 60 | 지하철역_목록_응답됨(response); 61 | 지하철역_목록_포함됨(response, Arrays.asList(createResponse1, createResponse2)); 62 | } 63 | 64 | @DisplayName("지하철역을 제거한다.") 65 | @Test 66 | void deleteStation() { 67 | // given 68 | ExtractableResponse createResponse = 지하철역_등록되어_있음(강남역); 69 | 70 | // when 71 | ExtractableResponse response = 지하철역_제거_요청(createResponse); 72 | 73 | // then 74 | 지하철역_삭제됨(response); 75 | } 76 | 77 | public static ExtractableResponse 지하철역_등록되어_있음(String name) { 78 | return 지하철역_생성_요청(name); 79 | } 80 | 81 | public static ExtractableResponse 지하철역_생성_요청(String name) { 82 | Map params = new HashMap<>(); 83 | params.put("name", name); 84 | 85 | return RestAssured.given().log().all(). 86 | body(params). 87 | contentType(MediaType.APPLICATION_JSON_VALUE). 88 | when(). 89 | post("/stations"). 90 | then(). 91 | log().all(). 92 | extract(); 93 | } 94 | 95 | public static ExtractableResponse 지하철역_목록_조회_요청() { 96 | return RestAssured.given().log().all(). 97 | when(). 98 | get("/stations"). 99 | then(). 100 | log().all(). 101 | extract(); 102 | } 103 | 104 | public static ExtractableResponse 지하철역_제거_요청(ExtractableResponse response) { 105 | String uri = response.header("Location"); 106 | 107 | return RestAssured.given().log().all(). 108 | when(). 109 | delete(uri). 110 | then(). 111 | log().all(). 112 | extract(); 113 | } 114 | 115 | public static void 지하철역_생성됨(ExtractableResponse response) { 116 | assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); 117 | assertThat(response.header("Location")).isNotBlank(); 118 | } 119 | 120 | public static void 지하철역_생성_실패됨(ExtractableResponse response) { 121 | assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); 122 | } 123 | 124 | public static void 지하철역_목록_응답됨(ExtractableResponse response) { 125 | assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); 126 | } 127 | 128 | public static void 지하철역_삭제됨(ExtractableResponse response) { 129 | assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); 130 | } 131 | 132 | public static void 지하철역_목록_포함됨(ExtractableResponse response, List> createdResponses) { 133 | List expectedLineIds = createdResponses.stream() 134 | .map(it -> Long.parseLong(it.header("Location").split("/")[2])) 135 | .collect(Collectors.toList()); 136 | 137 | List resultLineIds = response.jsonPath().getList(".", StationResponse.class).stream() 138 | .map(StationResponse::getId) 139 | .collect(Collectors.toList()); 140 | 141 | assertThat(resultLineIds).containsAll(expectedLineIds); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /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 org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.Mock; 13 | import org.mockito.junit.jupiter.MockitoExtension; 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("단위 테스트 - mockito의 MockitoExtension을 활용한 가짜 협력 객체 사용") 21 | @ExtendWith(MockitoExtension.class) 22 | public class MockitoExtensionTest { 23 | @Mock 24 | private LineRepository lineRepository; 25 | @Mock 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.findLineResponses(); 36 | 37 | // then 38 | assertThat(responses).hasSize(1); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /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 org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import java.util.List; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static org.mockito.Mockito.mock; 16 | import static org.mockito.Mockito.when; 17 | 18 | @DisplayName("단위 테스트 - mockito를 활용한 가짜 협력 객체 사용") 19 | public class MockitoTest { 20 | @Test 21 | void findAllLines() { 22 | // given 23 | LineRepository lineRepository = mock(LineRepository.class); 24 | StationService stationService = mock(StationService.class); 25 | 26 | when(lineRepository.findAll()).thenReturn(Lists.newArrayList(new Line())); 27 | LineService lineService = new LineService(lineRepository, stationService); 28 | 29 | // when 30 | List responses = lineService.findLineResponses(); 31 | 32 | // then 33 | assertThat(responses).hasSize(1); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /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 org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.springframework.boot.test.mock.mockito.MockBean; 13 | 14 | import java.util.List; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | import static org.mockito.Mockito.when; 18 | 19 | @DisplayName("단위 테스트 - SpringExtension을 활용한 가짜 협력 객체 사용") 20 | @ExtendWith(org.springframework.test.context.junit.jupiter.SpringExtension.class) 21 | public class SpringExtensionTest { 22 | @MockBean 23 | private LineRepository lineRepository; 24 | @MockBean 25 | private StationService stationService; 26 | 27 | @Test 28 | void findAllLines() { 29 | // given 30 | when(lineRepository.findAll()).thenReturn(Lists.newArrayList(new Line())); 31 | LineService lineService = new LineService(lineRepository, stationService); 32 | 33 | // when 34 | List responses = lineService.findLineResponses(); 35 | 36 | // then 37 | assertThat(responses).hasSize(1); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /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 | @DisplayName("단위 테스트 1") 13 | @Test 14 | void update() { 15 | // given 16 | String newName = "구분당선"; 17 | 18 | Station upStation = new Station("강남역"); 19 | Station downStation = new Station("광교역"); 20 | Line line = new Line("신분당선", "RED", upStation, downStation, 10); 21 | Line newLine = new Line(newName, "GREEN"); 22 | 23 | // when 24 | line.update(newLine); 25 | 26 | // then 27 | assertThat(line.getName()).isEqualTo(newName); 28 | } 29 | } 30 | --------------------------------------------------------------------------------