├── .DS_Store ├── .gitignore ├── README.md ├── build.gradle ├── frontend ├── .DS_Store └── vue-search-place │ ├── .babelrc │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── .postcssrc.js │ ├── README.md │ ├── config │ ├── dev.env.js │ ├── index.js │ └── prod.env.js │ ├── index.html │ ├── package.json │ ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ ├── member │ │ │ ├── Join.vue │ │ │ └── Login.vue │ │ └── search │ │ │ ├── Common.vue │ │ │ ├── History.vue │ │ │ ├── Popular.vue │ │ │ ├── Search.vue │ │ │ └── SearchView.vue │ ├── main.js │ ├── router │ │ └── index.js │ ├── service │ │ ├── index.js │ │ ├── joinAPI.js │ │ ├── loginAPI.js │ │ └── searchAPI.js │ └── store │ │ ├── actions.js │ │ ├── getters.js │ │ ├── index.js │ │ ├── mutation_types.js │ │ └── mutations.js │ ├── static │ └── .gitkeep │ └── yarn.lock ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── springboot-webservice-1.0-SNAPSHOT.jar └── src ├── main ├── java │ └── com │ │ └── beingapple │ │ └── webservice │ │ ├── .DS_Store │ │ ├── Application.java │ │ ├── auth │ │ ├── BaseSecurityHandler.java │ │ ├── UserDetailsImpl.java │ │ ├── ajax │ │ │ ├── AjaxAuthenticationProvider.java │ │ │ ├── AjaxUserDetailsService.java │ │ │ ├── ApiLoginOperations.java │ │ │ └── filter │ │ │ │ └── AjaxAuthenticationFilter.java │ │ └── jwt │ │ │ ├── JwtAuthenticationProvider.java │ │ │ ├── JwtAuthenticationToken.java │ │ │ ├── JwtInfo.java │ │ │ ├── JwtUserDetailsService.java │ │ │ ├── filter │ │ │ └── JwtAuthenticationFilter.java │ │ │ └── matcher │ │ │ └── AuthenticationPathRequestMatcher.java │ │ ├── config │ │ ├── AsyncConfig.java │ │ ├── AuditingConfig.java │ │ ├── CommonConfig.java │ │ ├── SpringSecurityConfig.java │ │ ├── SwaggerConfig.java │ │ └── WebConfig.java │ │ ├── domain │ │ ├── BaseTimeEntity.java │ │ ├── History.java │ │ ├── HistoryRequestDTO.java │ │ ├── Member.java │ │ ├── MemberRequestDTO.java │ │ ├── Popular.java │ │ ├── PopularRequestDTO.java │ │ ├── Response.java │ │ └── Search.java │ │ ├── repository │ │ ├── HistoryRepository.java │ │ ├── MemberRepository.java │ │ └── PopularRepository.java │ │ ├── service │ │ ├── HistoryService.java │ │ ├── HistoryServiceImpl.java │ │ ├── MemberService.java │ │ ├── MemberServiceImpl.java │ │ ├── PageService.java │ │ ├── PopularService.java │ │ ├── PopularServiceImpl.java │ │ ├── SearchService.java │ │ └── SearchServiceImpl.java │ │ ├── util │ │ ├── DateUtil.java │ │ ├── JwtUtil.java │ │ └── MemberValidation.java │ │ └── web │ │ ├── CommonController.java │ │ ├── MemberController.java │ │ ├── SearchController.java │ │ └── SearchHistoryController.java └── resources │ ├── application.yml │ └── static │ ├── index.html │ └── static │ ├── css │ ├── app.5d77078c1c180ab742f441e5fffe573a.css │ └── app.5d77078c1c180ab742f441e5fffe573a.css.map │ └── js │ ├── app.43a8422c172b56127065.js │ ├── app.43a8422c172b56127065.js.map │ ├── manifest.2ae2e69a05c33dfc65f8.js │ ├── manifest.2ae2e69a05c33dfc65f8.js.map │ ├── vendor.53dd1aac5feeb4153a2a.js │ └── vendor.53dd1aac5feeb4153a2a.js.map └── test └── java └── com └── beingapple └── webservice └── web ├── MemberControllerTest.java └── SearchAndHistoryControllerTest.java /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BeingApple/java-SpringBoot-searchPlace/cafb5021d7adeedea563819507eb5b0ff9fdfdb3/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | /out 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | 20 | ### NetBeans ### 21 | nbproject/private/ 22 | build/ 23 | nbbuild/ 24 | dist/ 25 | nbdist/ 26 | .nb-gradle/ 27 | 28 | ### yarn ### 29 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 스프링부트 기반 장소 검색 서비스 2 | 3 | Excutable jar 파일은 [여기](https://github.com/BeingApple/searchPlace-SpringBoot/raw/master/springboot-webservice-1.0-SNAPSHOT.jar)에서 다운로드 가능합니다. 4 | 5 | ## 사용한 외부 라이브러리 6 | * java-jwt 7 |
Spring security를 이용한 JWT 토크 기반 로그인 기능 구현에 사용하였습니다.
8 | * lombok 9 |
JPA 및 domain에서 롬복 어노테이션을 통해 손쉬운 함수 생성 및 가독성을 위해 사용하였습니다.
10 | * swagger 11 |
API 명세 페이지 출력을 위해 사용하였으며, /swagger-ui.html 에서 확인 가능합니다. 
12 | * vue.js 13 |
프론트엔드에서 Javascript 기반의 SPA 구현을 위해 사용하였습니다.
14 | * axios 15 |
프론트엔드에서 API서버와 통신하기 위하여 사용하였습니다.
16 | * vue-moment 17 |
프론트엔드에서 검색 이력 페이지의 날짜 포맷을 변경하기 위하여 사용하였습니다.
18 | * Bootstrap 19 |
프론트엔드에서 페이지 디자인을 위하여 사용하였습니다.
20 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext{ 3 | springBootVersion='2.1.6.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | classpath "io.spring.gradle:dependency-management-plugin:1.0.4.RELEASE" 11 | } 12 | } 13 | 14 | apply plugin: 'java' 15 | apply plugin: 'eclipse' 16 | apply plugin: 'org.springframework.boot' 17 | apply plugin: 'io.spring.dependency-management' 18 | 19 | group 'com.beingapple' 20 | version '1.0-SNAPSHOT' 21 | 22 | sourceCompatibility = 1.8 23 | 24 | jar { 25 | manifest { 26 | attributes 'Title': '장소 검색 서비스', 'Main-Class': 'com.beingapple.webservice.Application' 27 | } 28 | from { 29 | configurations.compile.collect {it.isDirectory()? it : zipTree(it)} 30 | } 31 | } 32 | 33 | repositories { 34 | mavenCentral() 35 | } 36 | 37 | //Spring Boot Overriding 38 | ext['hibernate.version'] = '5.2.11.Final' 39 | 40 | dependencies { 41 | compile('org.springframework.boot:spring-boot-starter-actuator') 42 | compile('org.springframework.boot:spring-boot-starter-data-jpa') 43 | compile('org.springframework.boot:spring-boot-starter-web') 44 | compile('org.springframework.boot:spring-boot-starter-security') 45 | compile('com.auth0:java-jwt:3.8.1') 46 | compile('io.springfox:springfox-swagger2:2.9.2') 47 | compile('io.springfox:springfox-swagger-ui:2.9.1') 48 | 49 | runtime('com.h2database:h2') 50 | compileOnly('org.projectlombok:lombok') 51 | testCompile('org.springframework.boot:spring-boot-starter-test') 52 | } 53 | 54 | -------------------------------------------------------------------------------- /frontend/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BeingApple/java-SpringBoot-searchPlace/cafb5021d7adeedea563819507eb5b0ff9fdfdb3/frontend/.DS_Store -------------------------------------------------------------------------------- /frontend/vue-search-place/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"] 12 | } 13 | -------------------------------------------------------------------------------- /frontend/vue-search-place/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /frontend/vue-search-place/.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | -------------------------------------------------------------------------------- /frontend/vue-search-place/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/essential', 15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 16 | 'standard' 17 | ], 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'vue' 21 | ], 22 | // add your custom rules here 23 | rules: { 24 | // allow async-await 25 | 'generator-star-spacing': 'off', 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/vue-search-place/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /frontend/vue-search-place/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/vue-search-place/README.md: -------------------------------------------------------------------------------- 1 | # vue-search-place 2 | 3 | > 장소 검색 서비스 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # build for production and view the bundle analyzer report 18 | npm run build --report 19 | ``` 20 | 21 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 22 | -------------------------------------------------------------------------------- /frontend/vue-search-place/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /frontend/vue-search-place/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | // Use Eslint Loader? 24 | // If true, your code will be linted during bundling and 25 | // linting errors and warnings will be shown in the console. 26 | useEslint: true, 27 | // If true, eslint errors and warnings will also be shown in the error overlay 28 | // in the browser. 29 | showEslintErrorsInOverlay: false, 30 | 31 | /** 32 | * Source Maps 33 | */ 34 | 35 | // https://webpack.js.org/configuration/devtool/#development 36 | devtool: 'cheap-module-eval-source-map', 37 | 38 | // If you have problems debugging vue-files in devtools, 39 | // set this to false - it *may* help 40 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 41 | cacheBusting: true, 42 | 43 | cssSourceMap: true 44 | }, 45 | 46 | build: { 47 | // Template for index.html 48 | index: path.resolve(__dirname, '../../../src/main/resources/static/index.html'), 49 | 50 | // Paths 51 | assetsRoot: path.resolve(__dirname, '../../../src/main/resources/static'), 52 | assetsSubDirectory: 'static', 53 | assetsPublicPath: '/', 54 | 55 | /** 56 | * Source Maps 57 | */ 58 | 59 | productionSourceMap: true, 60 | // https://webpack.js.org/configuration/devtool/#production 61 | devtool: '#source-map', 62 | 63 | // Gzip off by default as many popular static hosts such as 64 | // Surge or Netlify already gzip all static assets for you. 65 | // Before setting to `true`, make sure to: 66 | // npm install --save-dev compression-webpack-plugin 67 | productionGzip: false, 68 | productionGzipExtensions: ['js', 'css'], 69 | 70 | // Run the build command with an extra argument to 71 | // View the bundle analyzer report after build finishes: 72 | // `npm run build --report` 73 | // Set to `true` or `false` to always turn it on or off 74 | bundleAnalyzerReport: process.env.npm_config_report 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /frontend/vue-search-place/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /frontend/vue-search-place/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vue-search-place 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/vue-search-place/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-search-place", 3 | "version": "1.0.0", 4 | "description": "장소 검색 서비스", 5 | "author": "BeingApple", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "lint": "eslint --ext .js,.vue src", 11 | "build": "node build/build.js" 12 | }, 13 | "dependencies": { 14 | "axios": "^0.19.0", 15 | "bootstrap": "^4.3.1", 16 | "bootstrap-vue": "^2.0.0-rc.25", 17 | "vue": "^2.6.10", 18 | "vue-moment": "^4.0.0", 19 | "vue-router": "^3.0.1", 20 | "vuex": "^3.1.1" 21 | }, 22 | "devDependencies": { 23 | "autoprefixer": "^7.1.2", 24 | "babel-core": "^6.22.1", 25 | "babel-eslint": "^8.2.1", 26 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 27 | "babel-loader": "^7.1.1", 28 | "babel-plugin-syntax-jsx": "^6.18.0", 29 | "babel-plugin-transform-runtime": "^6.22.0", 30 | "babel-plugin-transform-vue-jsx": "^3.5.0", 31 | "babel-preset-env": "^1.3.2", 32 | "babel-preset-stage-2": "^6.22.0", 33 | "chalk": "^2.0.1", 34 | "copy-webpack-plugin": "^4.0.1", 35 | "css-loader": "^0.28.0", 36 | "eslint": "^4.15.0", 37 | "eslint-config-standard": "^10.2.1", 38 | "eslint-friendly-formatter": "^3.0.0", 39 | "eslint-loader": "^1.7.1", 40 | "eslint-plugin-import": "^2.7.0", 41 | "eslint-plugin-node": "^5.2.0", 42 | "eslint-plugin-promise": "^3.4.0", 43 | "eslint-plugin-standard": "^3.0.1", 44 | "eslint-plugin-vue": "^4.0.0", 45 | "extract-text-webpack-plugin": "^3.0.0", 46 | "file-loader": "^1.1.4", 47 | "friendly-errors-webpack-plugin": "^1.6.1", 48 | "html-webpack-plugin": "^2.30.1", 49 | "node-notifier": "^5.1.2", 50 | "optimize-css-assets-webpack-plugin": "^3.2.0", 51 | "ora": "^1.2.0", 52 | "portfinder": "^1.0.13", 53 | "postcss-import": "^11.0.0", 54 | "postcss-loader": "^2.0.8", 55 | "postcss-url": "^7.2.1", 56 | "rimraf": "^2.6.0", 57 | "semver": "^5.3.0", 58 | "shelljs": "^0.7.6", 59 | "uglifyjs-webpack-plugin": "^1.1.1", 60 | "url-loader": "^0.5.8", 61 | "vue-loader": "^13.3.0", 62 | "vue-style-loader": "^3.0.1", 63 | "vue-template-compiler": "^2.5.2", 64 | "webpack": "^3.6.0", 65 | "webpack-bundle-analyzer": "^2.9.0", 66 | "webpack-dev-server": "^2.9.1", 67 | "webpack-merge": "^4.1.0" 68 | }, 69 | "engines": { 70 | "node": ">= 6.0.0", 71 | "npm": ">= 3.0.0" 72 | }, 73 | "browserslist": [ 74 | "> 1%", 75 | "last 2 versions", 76 | "not ie <= 8" 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BeingApple/java-SpringBoot-searchPlace/cafb5021d7adeedea563819507eb5b0ff9fdfdb3/frontend/vue-search-place/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/vue-search-place/src/components/member/Join.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 103 | 104 | 146 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/components/member/Login.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 62 | 63 | 105 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/components/search/Common.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 42 | 43 | 61 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/components/search/History.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 97 | 98 | 100 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/components/search/Popular.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/components/search/Search.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 130 | 131 | 133 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/components/search/SearchView.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 73 | 74 | 77 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import BootstrapVue from 'bootstrap-vue' 5 | import moment from 'vue-moment' 6 | import App from './App' 7 | import router from './router' 8 | import store from './store' 9 | 10 | import 'bootstrap/dist/css/bootstrap.css' 11 | import 'bootstrap-vue/dist/bootstrap-vue.css' 12 | 13 | import common from '@/components/search/Common' 14 | 15 | Vue.config.productionTip = false 16 | Vue.use(BootstrapVue) 17 | Vue.use(moment) 18 | 19 | Vue.component('common', common) 20 | Vue.prototype.EventBus = new Vue() 21 | 22 | /* eslint-disable no-new */ 23 | new Vue({ 24 | el: '#app', 25 | router, 26 | store, 27 | components: { App }, 28 | template: '' 29 | }) 30 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import store from '@/store' 3 | import Router from 'vue-router' 4 | import Join from '@/components/member/Join' 5 | import Login from '@/components/member/Login' 6 | 7 | import Search from '@/components/search/Search' 8 | import SearchView from '@/components/search/SearchView' 9 | import History from '@/components/search/History' 10 | import Popular from '@/components/search/Popular' 11 | 12 | Vue.use(Router) 13 | 14 | const requireAuth = () => (from, to, next) => { 15 | if (store.getters.getIsAuth) { return next() } 16 | next('/login') 17 | } 18 | 19 | export default new Router({ 20 | mode: 'history', 21 | routes: [ 22 | { 23 | path: '/', 24 | name: 'Join', 25 | component: Join 26 | }, 27 | { 28 | path: '/login', 29 | name: 'Login', 30 | component: Login 31 | }, 32 | { 33 | path: '/search', 34 | name: 'Search', 35 | component: Search, 36 | beforeEnter: requireAuth() 37 | }, 38 | { 39 | path: '/search/:id', 40 | name: 'SearchView', 41 | component: SearchView, 42 | beforeEnter: requireAuth() 43 | }, 44 | { 45 | path: '/history', 46 | name: 'History', 47 | component: History, 48 | beforeEnter: requireAuth() 49 | }, 50 | { 51 | path: '/popular', 52 | name: 'Popular', 53 | component: Popular, 54 | beforeEnter: requireAuth() 55 | } 56 | ] 57 | }) 58 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/service/index.js: -------------------------------------------------------------------------------- 1 | import loginAPI from './loginAPI' 2 | import joinAPI from './joinAPI' 3 | import searchAPI from './searchAPI' 4 | 5 | export default { 6 | async login (userId, userPassword) { 7 | try { 8 | const loginResponse = await loginAPI.login(userId, userPassword) 9 | return loginResponse 10 | } catch (err) { 11 | console.log(err) 12 | } 13 | }, 14 | async join (userName, userId, userPassword, userPasswordCheck) { 15 | try { 16 | const joinResponse = await joinAPI.join(userName, userId, userPassword, userPasswordCheck) 17 | return joinResponse 18 | } catch (err) { 19 | console.log(err) 20 | } 21 | }, 22 | async search (keyword, page, size) { 23 | try { 24 | const searchResponse = await searchAPI.search(keyword, page, size) 25 | return searchResponse 26 | } catch (err) { 27 | console.log(err) 28 | } 29 | }, 30 | async history (page, size) { 31 | try { 32 | const historyResponse = await searchAPI.history(page, size) 33 | return historyResponse 34 | } catch (err) { 35 | console.log(err) 36 | } 37 | }, 38 | async popular () { 39 | try { 40 | const popularResponse = await searchAPI.popular() 41 | return popularResponse 42 | } catch (err) { 43 | console.log(err) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/service/joinAPI.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const getJoin = (userName, userId, userPassword, userPasswordCheck) => { 4 | return axios.post('//localhost:8080/api/join', { 5 | 'userName': userName, 6 | 'userId': userId, 7 | 'userPassword': userPassword, 8 | 'userPasswordCheck': userPasswordCheck 9 | }) 10 | } 11 | 12 | export default { 13 | async join (userName, userId, userPassword, userPasswordCheck) { 14 | try { 15 | const joinResponse = await getJoin(userName, userId, userPassword, userPasswordCheck).then(function (response) { return response }).catch(function (error) { return error.response }) 16 | return joinResponse 17 | } catch (err) { 18 | console.log(err) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/service/loginAPI.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const getAccessToken = (userId, userPassword) => { 4 | return axios.post('//localhost:8080/api/login', { 5 | 'userId': userId, 6 | 'userPassword': userPassword 7 | }) 8 | } 9 | 10 | export default { 11 | async login (userId, userPassword) { 12 | try { 13 | const accessTokenResponse = await getAccessToken(userId, userPassword).then(function (response) { return response }).catch(function (error) { return error.response }) 14 | if (accessTokenResponse.status === 200) axios.defaults.headers.common['Authorization'] = accessTokenResponse.data.message 15 | return accessTokenResponse 16 | } catch (err) { 17 | console.log(err) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/service/searchAPI.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const getSearchData = (keyword, page, size) => { 4 | return axios.get('//localhost:8080/api/search/place', { 5 | params: { 6 | 'keyword': keyword, 7 | 'page': page, 8 | 'size': size 9 | } 10 | }) 11 | } 12 | 13 | const getHistoryData = (page, size) => { 14 | return axios.get('//localhost:8080/api/history', { 15 | params: { 16 | 'page': page, 17 | 'size': size 18 | } 19 | }) 20 | } 21 | const getPopularData = () => { 22 | return axios.get('//localhost:8080/api/search/popular') 23 | } 24 | 25 | export default { 26 | async search (keyword, page, size) { 27 | try { 28 | const searchData = await getSearchData(keyword, page, size).then(function (response) { return response }).catch(function (error) { return error.response }) 29 | return searchData 30 | } catch (err) { 31 | console.log(err) 32 | } 33 | }, 34 | async history (page, size) { 35 | try { 36 | const historyData = await getHistoryData(page, size).then(function (response) { return response }).catch(function (error) { return error.response }) 37 | return historyData 38 | } catch (err) { 39 | console.log(err) 40 | } 41 | }, 42 | async popular () { 43 | try { 44 | const popularData = await getPopularData().then(function (response) { return response }).catch(function (error) { return error.response }) 45 | return popularData 46 | } catch (err) { 47 | console.log(err) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/store/actions.js: -------------------------------------------------------------------------------- 1 | import {ERROR_STATE, IS_AUTH, ACCESS_TOKEN, SEARCH_DATA, KEYWORD, SEARCH_VIEW_BACK} from './mutation_types' 2 | import api from '../service' 3 | 4 | let setErrorState = ({commit}, data) => { 5 | commit(ERROR_STATE, data) 6 | } 7 | 8 | let setIsAuth = ({commit}, data) => { 9 | commit(IS_AUTH, data) 10 | } 11 | 12 | let setAccessToken = ({commit}, data) => { 13 | commit(ACCESS_TOKEN, data) 14 | } 15 | 16 | let setSearchData = ({commit}, data) => { 17 | commit(SEARCH_DATA, data) 18 | } 19 | 20 | let setKeyword = ({commit}, data) => { 21 | commit(KEYWORD, data) 22 | } 23 | 24 | let setSearchViewBack = ({commit}, data) => { 25 | commit(SEARCH_VIEW_BACK, data) 26 | } 27 | 28 | let processResponse = (store, loginResponse) => { 29 | let status = loginResponse.status 30 | let responseData = loginResponse.data 31 | switch (status) { 32 | case 200 : 33 | setErrorState(store, '') 34 | setIsAuth(store, true) 35 | setAccessToken(store, responseData.message) 36 | break 37 | case 400 : 38 | setErrorState(store, '잘못된 아이디 혹은 비밀번호입니다.') 39 | setIsAuth(store, false) 40 | setAccessToken(store, '') 41 | break 42 | } 43 | } 44 | 45 | let processJoinResponse = (store, joinResponse) => { 46 | let status = joinResponse.status 47 | let data = joinResponse.data 48 | switch (status) { 49 | case 201 : 50 | setErrorState(store, '') 51 | setIsAuth(store, true) 52 | break 53 | case 400 : 54 | setErrorState(store, data.errorMessage) 55 | setIsAuth(store, false) 56 | break 57 | case 409 : 58 | setErrorState(store, data.errorMessage) 59 | setIsAuth(store, false) 60 | break 61 | } 62 | } 63 | 64 | export default { 65 | async login (store, {userId, userPassword}) { 66 | let loginResponse = await api.login(userId, userPassword) 67 | processResponse(store, loginResponse) 68 | return store.getters.getIsAuth 69 | }, 70 | async join (store, {userName, userId, userPassword, userPasswordCheck}) { 71 | let joinResponse = await api.join(userName, userId, userPassword, userPasswordCheck) 72 | processJoinResponse(store, joinResponse) 73 | return store.getters.getIsAuth 74 | }, 75 | async setData (store, data) { 76 | await setSearchData(store, data) 77 | }, 78 | setKeyword (store, keyword) { 79 | setKeyword(store, keyword) 80 | }, 81 | setSearchViewBack (store, searchViewBack) { 82 | setSearchViewBack(store, searchViewBack) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/store/getters.js: -------------------------------------------------------------------------------- 1 | export default { 2 | getErrorState: state => state.errorState, 3 | getIsAuth: state => state.isAuth, 4 | getAccessToken: state => state.accessToken, 5 | getSearchData: state => state.searchData, 6 | getKeyword: state => state.keyword, 7 | getSearchViewBack: state => state.searchViewBack 8 | } 9 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | import actions from './actions' 5 | import mutations from './mutations' 6 | 7 | Vue.use(Vuex) 8 | 9 | const state = { 10 | errorState: '', 11 | isAuth: false, 12 | accessToken: null, 13 | searchData: {}, 14 | keyword: '', 15 | searchViewBack: false 16 | } 17 | 18 | export default new Vuex.Store({ 19 | state, 20 | mutations, 21 | getters, 22 | actions 23 | }) 24 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/store/mutation_types.js: -------------------------------------------------------------------------------- 1 | export const ERROR_STATE = 'ERROR_STATE' 2 | export const IS_AUTH = 'IS_AUTH' 3 | export const ACCESS_TOKEN = 'ACCESS_TOKEN' 4 | export const SEARCH_DATA = 'SEARCH_DATA' 5 | export const KEYWORD = 'KEYWORD' 6 | export const SEARCH_VIEW_BACK = 'SEARCH_VIEW_BACK' 7 | -------------------------------------------------------------------------------- /frontend/vue-search-place/src/store/mutations.js: -------------------------------------------------------------------------------- 1 | import * as types from './mutation_types' 2 | 3 | export default { 4 | [types.ERROR_STATE] (state, errorState) { 5 | state.errorState = errorState 6 | }, 7 | [types.IS_AUTH] (state, isAuth) { 8 | state.isAuth = isAuth 9 | }, 10 | [types.ACCESS_TOKEN] (state, accessToken) { 11 | state.accessToken = accessToken 12 | }, 13 | [types.SEARCH_DATA] (state, searchData) { 14 | state.searchData = searchData 15 | }, 16 | [types.KEYWORD] (state, keyword) { 17 | state.keyword = keyword 18 | }, 19 | [types.SEARCH_VIEW_BACK] (state, searchViewBack) { 20 | state.searchViewBack = searchViewBack 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/vue-search-place/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BeingApple/java-SpringBoot-searchPlace/cafb5021d7adeedea563819507eb5b0ff9fdfdb3/frontend/vue-search-place/static/.gitkeep -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BeingApple/java-SpringBoot-searchPlace/cafb5021d7adeedea563819507eb5b0ff9fdfdb3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 28 17:38:47 KST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-webservice' 2 | 3 | -------------------------------------------------------------------------------- /springboot-webservice-1.0-SNAPSHOT.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BeingApple/java-SpringBoot-searchPlace/cafb5021d7adeedea563819507eb5b0ff9fdfdb3/springboot-webservice-1.0-SNAPSHOT.jar -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BeingApple/java-SpringBoot-searchPlace/cafb5021d7adeedea563819507eb5b0ff9fdfdb3/src/main/java/com/beingapple/webservice/.DS_Store -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/Application.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | public static void main(String arg[]){ 9 | SpringApplication.run(Application.class, arg); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/auth/BaseSecurityHandler.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.auth; 2 | 3 | import com.beingapple.webservice.domain.Response; 4 | import com.beingapple.webservice.util.JwtUtil; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.security.core.AuthenticationException; 11 | import org.springframework.security.core.userdetails.UserDetails; 12 | import org.springframework.security.web.authentication.AuthenticationFailureHandler; 13 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 14 | import org.springframework.stereotype.Component; 15 | 16 | import javax.servlet.http.HttpServletRequest; 17 | import javax.servlet.http.HttpServletResponse; 18 | import java.io.IOException; 19 | import java.io.PrintWriter; 20 | import java.util.ArrayList; 21 | 22 | @Component 23 | public class BaseSecurityHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler { 24 | @Override 25 | public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { 26 | UserDetails userDetails = new UserDetailsImpl(authentication.getPrincipal().toString(), new ArrayList<>(authentication.getAuthorities())); 27 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 28 | 29 | String accessToken = JwtUtil.createToken(userDetails); 30 | Response responseData = new Response( 31 | HttpStatus.OK.toString(), 32 | accessToken, 33 | "", "" 34 | ); 35 | 36 | PrintWriter out = response.getWriter(); 37 | out.print(new ObjectMapper().writeValueAsString(responseData)); 38 | out.flush(); 39 | } 40 | 41 | @Override 42 | public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){ 43 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/auth/UserDetailsImpl.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.auth; 2 | 3 | import com.beingapple.webservice.domain.Member; 4 | import org.springframework.security.core.GrantedAuthority; 5 | import org.springframework.security.core.userdetails.User; 6 | 7 | import java.util.List; 8 | 9 | public class UserDetailsImpl extends User { 10 | public UserDetailsImpl(String userId, List authorities) { 11 | super(userId, "", authorities); 12 | } 13 | 14 | public UserDetailsImpl(Member member, List authorities) { 15 | super(member.getUserId(), member.getUserPassword(), authorities); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/auth/ajax/AjaxAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.auth.ajax; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.NoArgsConstructor; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.authentication.BadCredentialsException; 7 | import org.springframework.security.authentication.InternalAuthenticationServiceException; 8 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 9 | import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; 10 | import org.springframework.security.core.userdetails.UserDetails; 11 | import org.springframework.security.crypto.password.PasswordEncoder; 12 | import org.springframework.stereotype.Component; 13 | 14 | @Component 15 | public class AjaxAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { 16 | @Autowired 17 | private PasswordEncoder passwordEncoder; 18 | 19 | @Autowired 20 | private AjaxUserDetailsService userDetailService; 21 | 22 | @Override 23 | protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) { 24 | if (authentication.getCredentials() == null) { 25 | throw new BadCredentialsException("Bad credentials"); 26 | } 27 | 28 | String presentedPassword = authentication.getCredentials().toString(); 29 | 30 | if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { 31 | throw new BadCredentialsException("Bad credentials"); 32 | } 33 | } 34 | 35 | @Override 36 | protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) { 37 | UserDetails loadedUser = userDetailService.loadUserByUsername(username); 38 | 39 | if (loadedUser == null) { 40 | throw new InternalAuthenticationServiceException( 41 | "UserDetailsService returned null, which is an interface contract violation"); 42 | } 43 | 44 | return loadedUser; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/auth/ajax/AjaxUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.auth.ajax; 2 | 3 | import com.beingapple.webservice.auth.UserDetailsImpl; 4 | import com.beingapple.webservice.domain.Member; 5 | import com.beingapple.webservice.repository.MemberRepository; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.core.authority.AuthorityUtils; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | public class AjaxUserDetailsService implements UserDetailsService { 15 | @Autowired 16 | private MemberRepository memberRepository; 17 | 18 | @Override 19 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 20 | Member member = memberRepository.findFirstByUserId(username); 21 | 22 | if (member == null) { 23 | throw new UsernameNotFoundException(username + "라는 사용자가 없습니다."); 24 | } 25 | return new UserDetailsImpl(member, AuthorityUtils.createAuthorityList(member.getRole())); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/auth/ajax/ApiLoginOperations.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.auth.ajax; 2 | 3 | import com.beingapple.webservice.domain.MemberRequestDTO; 4 | import com.beingapple.webservice.domain.Response; 5 | import com.fasterxml.classmate.TypeResolver; 6 | import com.google.common.collect.Multimap; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.http.HttpMethod; 9 | import springfox.documentation.builders.ApiListingBuilder; 10 | import springfox.documentation.builders.OperationBuilder; 11 | import springfox.documentation.builders.ParameterBuilder; 12 | import springfox.documentation.builders.ResponseMessageBuilder; 13 | import springfox.documentation.schema.ModelRef; 14 | import springfox.documentation.service.ApiDescription; 15 | import springfox.documentation.service.ApiListing; 16 | import springfox.documentation.service.Operation; 17 | import springfox.documentation.service.ResponseMessage; 18 | import springfox.documentation.spring.web.plugins.DocumentationPluginsManager; 19 | import springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator; 20 | import springfox.documentation.spring.web.scanners.ApiDescriptionReader; 21 | import springfox.documentation.spring.web.scanners.ApiListingScanner; 22 | import springfox.documentation.spring.web.scanners.ApiListingScanningContext; 23 | import springfox.documentation.spring.web.scanners.ApiModelReader; 24 | 25 | import java.util.*; 26 | 27 | public class ApiLoginOperations extends ApiListingScanner { 28 | @Autowired 29 | private TypeResolver typeResolver; 30 | 31 | @Autowired 32 | public ApiLoginOperations(ApiDescriptionReader apiDescriptionReader, ApiModelReader apiModelReader, DocumentationPluginsManager pluginsManager){ 33 | super(apiDescriptionReader, apiModelReader, pluginsManager); 34 | } 35 | 36 | @Override 37 | public Multimap scan(ApiListingScanningContext context){ 38 | final Multimap def = super.scan(context); 39 | final List apis = new LinkedList<>(); 40 | final List operations = new ArrayList<>(); 41 | final Set response = new HashSet<>(); 42 | 43 | response.add(new ResponseMessageBuilder().code(200).message("OK").responseModel(new ModelRef("Response")).build()); 44 | response.add(new ResponseMessageBuilder().code(400).message("Bad Request").build()); 45 | operations.add(new OperationBuilder(new CachingOperationNameGenerator()) 46 | .method((HttpMethod.POST)) 47 | .uniqueId("login") 48 | .parameters( 49 | Arrays.asList( 50 | new ParameterBuilder() 51 | .name("dto") 52 | .description("The userId and password") 53 | .parameterType("body") 54 | .type(typeResolver.resolve(MemberRequestDTO.class)) 55 | .modelRef(new ModelRef("MemberRequestDTO")) 56 | .required(true) 57 | .build() 58 | )) 59 | .responseMessages(response) 60 | .summary("Log in") 61 | .notes("Here you can log in get Access token") 62 | .build() 63 | ); 64 | apis.add(new ApiDescription("Authentication","/login", "Authentication Documentation", operations, false)); 65 | def.put("authentication", new ApiListingBuilder(context.getDocumentationContext().getApiDescriptionOrdering()) 66 | .apis(apis) 67 | .description("Custom authentication") 68 | .build() 69 | ); 70 | 71 | return def; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/auth/ajax/filter/AjaxAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.auth.ajax.filter; 2 | 3 | import com.beingapple.webservice.domain.Member; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; 9 | import org.springframework.security.web.util.matcher.RequestMatcher; 10 | 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | import java.io.IOException; 14 | import java.nio.file.AccessDeniedException; 15 | 16 | public class AjaxAuthenticationFilter extends AbstractAuthenticationProcessingFilter { 17 | private final ObjectMapper objectMapper; 18 | 19 | public AjaxAuthenticationFilter(RequestMatcher requestMatcher, ObjectMapper objectMapper) { 20 | super(requestMatcher); 21 | this.objectMapper = objectMapper; 22 | } 23 | 24 | @Override 25 | public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException { 26 | if (isJson(request)) { 27 | Member member = objectMapper.readValue(request.getReader(), Member.class); 28 | UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(member.getUserId(), member.getUserPassword()); 29 | return getAuthenticationManager().authenticate(authentication); 30 | } else { 31 | throw new AccessDeniedException("Don't use content type for " + request.getContentType()); 32 | } 33 | } 34 | 35 | private boolean isJson(HttpServletRequest request) { 36 | return MediaType.APPLICATION_JSON_UTF8_VALUE.equalsIgnoreCase(request.getContentType()) || MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(request.getContentType()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/auth/jwt/JwtAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.auth.jwt; 2 | 3 | import com.beingapple.webservice.util.JwtUtil; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.security.authentication.AuthenticationProvider; 6 | import org.springframework.security.authentication.BadCredentialsException; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | public class JwtAuthenticationProvider implements AuthenticationProvider { 13 | @Autowired 14 | private JwtUserDetailsService userDetailsService; 15 | 16 | @Override 17 | public Authentication authenticate(Authentication authentication) { 18 | if (authentication.getCredentials() == null) { 19 | throw new BadCredentialsException("Bad token"); 20 | } 21 | 22 | String token = authentication.getCredentials().toString(); 23 | 24 | if (JwtUtil.verify(token)) { 25 | UserDetails userDetails = userDetailsService.loadUserByUsername(token); 26 | return new JwtAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities()); 27 | } else { 28 | throw new BadCredentialsException("Bad token"); 29 | } 30 | } 31 | 32 | @Override 33 | public boolean supports(Class authentication) { 34 | return JwtAuthenticationToken.class.isAssignableFrom(authentication); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/auth/jwt/JwtAuthenticationToken.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.auth.jwt; 2 | 3 | import org.springframework.security.authentication.AbstractAuthenticationToken; 4 | import org.springframework.security.core.GrantedAuthority; 5 | import org.springframework.security.core.SpringSecurityCoreVersion; 6 | 7 | import java.util.Collection; 8 | 9 | public class JwtAuthenticationToken extends AbstractAuthenticationToken{ 10 | private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; 11 | 12 | private final Object principal; 13 | private Object credentials; 14 | 15 | public JwtAuthenticationToken(Object credentials) { 16 | super(null); 17 | this.principal = null; 18 | this.credentials = credentials; 19 | setAuthenticated(false); 20 | } 21 | 22 | public JwtAuthenticationToken(Object principal, Object credentials, Collection authorities) { 23 | super(authorities); 24 | this.principal = principal; 25 | this.credentials = credentials; 26 | super.setAuthenticated(true); 27 | } 28 | 29 | public Object getCredentials() { 30 | return this.credentials; 31 | } 32 | 33 | public Object getPrincipal() { 34 | return this.principal; 35 | } 36 | 37 | public void setAuthenticated(boolean isAuthenticated) { 38 | if (isAuthenticated) { 39 | throw new IllegalArgumentException( 40 | "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); 41 | } 42 | 43 | super.setAuthenticated(false); 44 | } 45 | 46 | @Override 47 | public void eraseCredentials() { 48 | super.eraseCredentials(); 49 | credentials = null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/auth/jwt/JwtInfo.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.auth.jwt; 2 | 3 | 4 | import com.auth0.jwt.algorithms.Algorithm; 5 | 6 | public class JwtInfo { 7 | public static final String HEADER_NAME = "Authorization"; 8 | 9 | public static final String ISSUER = "beingapple"; 10 | 11 | public static final String TOKEN_KEY = "com.beingapple.searchplace"; 12 | 13 | public static final long EXPIRES_LIMIT = 3L; 14 | 15 | public static Algorithm getAlgorithm() { 16 | try { 17 | return Algorithm.HMAC256(JwtInfo.TOKEN_KEY); 18 | } catch (IllegalArgumentException e) { 19 | return Algorithm.none(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/auth/jwt/JwtUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.auth.jwt; 2 | 3 | import com.auth0.jwt.interfaces.DecodedJWT; 4 | import com.beingapple.webservice.auth.UserDetailsImpl; 5 | import com.beingapple.webservice.util.JwtUtil; 6 | import org.springframework.security.authentication.BadCredentialsException; 7 | import org.springframework.security.core.authority.AuthorityUtils; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | import org.springframework.stereotype.Component; 11 | 12 | @Component 13 | public class JwtUserDetailsService implements UserDetailsService { 14 | @Override 15 | public UserDetails loadUserByUsername(String token) { 16 | DecodedJWT decodedJWT = JwtUtil.tokenToJwt(token); 17 | 18 | if (decodedJWT == null) { 19 | throw new BadCredentialsException("Not used Token"); 20 | } 21 | 22 | String id = decodedJWT.getClaim("id").asString(); 23 | String role = decodedJWT.getClaim("role").asString(); 24 | 25 | return new UserDetailsImpl(id, AuthorityUtils.createAuthorityList(role)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/auth/jwt/filter/JwtAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.auth.jwt.filter; 2 | 3 | import com.beingapple.webservice.auth.jwt.JwtAuthenticationToken; 4 | import com.beingapple.webservice.auth.jwt.JwtInfo; 5 | import org.springframework.security.access.AccessDeniedException; 6 | import org.springframework.security.core.Authentication; 7 | import org.springframework.security.core.AuthenticationException; 8 | import org.springframework.security.core.context.SecurityContext; 9 | import org.springframework.security.core.context.SecurityContextHolder; 10 | import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; 11 | import org.springframework.security.web.util.matcher.RequestMatcher; 12 | import org.springframework.util.StringUtils; 13 | 14 | import javax.servlet.FilterChain; 15 | import javax.servlet.ServletException; 16 | import javax.servlet.http.HttpServletRequest; 17 | import javax.servlet.http.HttpServletResponse; 18 | import java.io.IOException; 19 | 20 | 21 | public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter { 22 | 23 | public JwtAuthenticationFilter(RequestMatcher requestMatcher) { 24 | super(requestMatcher); 25 | } 26 | 27 | @Override 28 | public Authentication attemptAuthentication(HttpServletRequest request, 29 | HttpServletResponse response){ 30 | String token = request.getHeader(JwtInfo.HEADER_NAME); 31 | 32 | if (StringUtils.isEmpty(token)) { 33 | throw new AccessDeniedException("Not empty Token"); 34 | } else { 35 | return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token)); 36 | } 37 | } 38 | 39 | @Override 40 | protected void successfulAuthentication(HttpServletRequest request, 41 | HttpServletResponse response, 42 | FilterChain chain, 43 | Authentication authResult) throws IOException, ServletException { 44 | SecurityContext context = SecurityContextHolder.createEmptyContext(); 45 | context.setAuthentication(authResult); 46 | SecurityContextHolder.setContext(context); 47 | chain.doFilter(request, response); 48 | } 49 | 50 | @Override 51 | protected void unsuccessfulAuthentication(HttpServletRequest request, 52 | HttpServletResponse response, 53 | AuthenticationException failed) throws IOException, ServletException { 54 | SecurityContextHolder.clearContext(); 55 | getFailureHandler().onAuthenticationFailure(request, response, failed); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/auth/jwt/matcher/AuthenticationPathRequestMatcher.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.auth.jwt.matcher; 2 | 3 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 4 | import org.springframework.security.web.util.matcher.OrRequestMatcher; 5 | import org.springframework.security.web.util.matcher.RequestMatcher; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | 11 | public class AuthenticationPathRequestMatcher implements RequestMatcher { 12 | private OrRequestMatcher skipRequestMatcher; 13 | 14 | public AuthenticationPathRequestMatcher(List skipPathList) { 15 | if(!skipPathList.isEmpty()) { 16 | List requestMatcherList = skipPathList.stream() 17 | .map(AntPathRequestMatcher::new) 18 | .collect(Collectors.toList()); 19 | skipRequestMatcher = new OrRequestMatcher(requestMatcherList); 20 | } 21 | } 22 | 23 | @Override 24 | public boolean matches(HttpServletRequest request) { 25 | return skipRequestMatcher.matches(request); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/config/AsyncConfig.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.scheduling.annotation.AsyncConfigurerSupport; 5 | import org.springframework.scheduling.annotation.EnableAsync; 6 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 7 | 8 | import java.util.concurrent.Executor; 9 | 10 | @Configuration 11 | @EnableAsync 12 | public class AsyncConfig extends AsyncConfigurerSupport { 13 | @Override 14 | public Executor getAsyncExecutor(){ 15 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 16 | executor.setCorePoolSize(2); 17 | executor.setMaxPoolSize(10); 18 | executor.setQueueCapacity(500); 19 | executor.setThreadNamePrefix("beingapple-async-"); 20 | executor.initialize(); 21 | 22 | return executor; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/config/AuditingConfig.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 5 | 6 | @EnableJpaAuditing 7 | @Configuration 8 | public class AuditingConfig { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/config/CommonConfig.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 6 | 7 | @Configuration 8 | public class CommonConfig { 9 | @Bean 10 | public BCryptPasswordEncoder passwordEncoder() { 11 | return new BCryptPasswordEncoder(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/config/SpringSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.config; 2 | 3 | import com.beingapple.webservice.auth.BaseSecurityHandler; 4 | import com.beingapple.webservice.auth.ajax.filter.AjaxAuthenticationFilter; 5 | import com.beingapple.webservice.auth.jwt.JwtAuthenticationProvider; 6 | import com.beingapple.webservice.auth.jwt.filter.JwtAuthenticationFilter; 7 | import com.beingapple.webservice.auth.ajax.AjaxAuthenticationProvider; 8 | import com.beingapple.webservice.auth.jwt.matcher.AuthenticationPathRequestMatcher; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.http.HttpMethod; 14 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 15 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 16 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 17 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 18 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 19 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 20 | import org.springframework.security.config.http.SessionCreationPolicy; 21 | import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; 22 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 23 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 24 | import org.springframework.web.cors.CorsUtils; 25 | 26 | import java.util.ArrayList; 27 | import java.util.Arrays; 28 | import java.util.List; 29 | 30 | 31 | @Configuration 32 | @EnableWebSecurity 33 | @EnableGlobalMethodSecurity(prePostEnabled = true) 34 | public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { 35 | @Autowired 36 | private JwtAuthenticationProvider jwtProvider; 37 | 38 | @Autowired 39 | private AjaxAuthenticationProvider ajaxProvider; 40 | 41 | @Autowired 42 | private BaseSecurityHandler securityHandler; 43 | 44 | @Autowired 45 | private ObjectMapper objectMapper; 46 | 47 | private static final String LOGIN_ENTRY_POINT = "/api/login"; 48 | private static final String SEARCH_ENTRY_POINT = "/api/search/**"; 49 | private static final String HISTORY_ENTRY_POINT = "/api/history"; 50 | private static final String[] SWAGGER_ENTRY_POINT = {"/v2/api-docs", 51 | "/configuration/ui", 52 | "/swagger-resources", 53 | "/swagger-resources/**", 54 | "/configuration/security", 55 | "/swagger-ui.html", 56 | "/webjars/**"}; 57 | 58 | 59 | @Override 60 | public void configure(WebSecurity web){ 61 | web.ignoring().antMatchers("/resources/**") 62 | .antMatchers(SWAGGER_ENTRY_POINT); 63 | } 64 | 65 | @Override 66 | protected void configure(AuthenticationManagerBuilder auth){ 67 | ajaxProvider.setForcePrincipalAsString(true); 68 | 69 | auth.authenticationProvider(ajaxProvider) 70 | .authenticationProvider(jwtProvider); 71 | } 72 | 73 | @Override 74 | protected void configure(HttpSecurity http) throws Exception { 75 | http 76 | .addFilterBefore(ajaxAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) 77 | .addFilterBefore(jwtAuthenticationFilter(), FilterSecurityInterceptor.class) 78 | .csrf().disable() 79 | .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) 80 | .and() 81 | .authorizeRequests() 82 | .antMatchers(SEARCH_ENTRY_POINT).authenticated() 83 | .antMatchers(HISTORY_ENTRY_POINT).authenticated() 84 | .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() 85 | .anyRequest().permitAll() 86 | .and().cors(); 87 | } 88 | 89 | @Bean 90 | public AntPathRequestMatcher antPathRequestMatcher() { 91 | return new AntPathRequestMatcher(LOGIN_ENTRY_POINT, HttpMethod.POST.name()); 92 | } 93 | 94 | @Bean 95 | public AjaxAuthenticationFilter ajaxAuthenticationFilter() throws Exception { 96 | AjaxAuthenticationFilter filter = new AjaxAuthenticationFilter(antPathRequestMatcher(), objectMapper); 97 | filter.setAuthenticationManager(authenticationManager()); 98 | filter.setAuthenticationSuccessHandler(securityHandler); 99 | filter.setAuthenticationFailureHandler(securityHandler); 100 | return filter; 101 | } 102 | 103 | @Bean 104 | public AuthenticationPathRequestMatcher authenticationPathRequestMatcher() { 105 | return new AuthenticationPathRequestMatcher(Arrays.asList(SEARCH_ENTRY_POINT, HISTORY_ENTRY_POINT)); 106 | } 107 | 108 | @Bean 109 | public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception { 110 | JwtAuthenticationFilter filter = new JwtAuthenticationFilter(authenticationPathRequestMatcher()); 111 | filter.setAuthenticationManager(authenticationManager()); 112 | filter.setAuthenticationFailureHandler(securityHandler); 113 | return filter; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.config; 2 | 3 | import com.beingapple.webservice.auth.ajax.ApiLoginOperations; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.Primary; 7 | import springfox.documentation.builders.PathSelectors; 8 | import springfox.documentation.builders.RequestHandlerSelectors; 9 | import springfox.documentation.spi.DocumentationType; 10 | import springfox.documentation.spring.web.plugins.Docket; 11 | import springfox.documentation.spring.web.plugins.DocumentationPluginsManager; 12 | import springfox.documentation.spring.web.scanners.ApiDescriptionReader; 13 | import springfox.documentation.spring.web.scanners.ApiListingScanner; 14 | import springfox.documentation.spring.web.scanners.ApiModelReader; 15 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 16 | 17 | @Configuration 18 | @EnableSwagger2 19 | public class SwaggerConfig { 20 | 21 | @Bean 22 | public Docket api(){ 23 | return new Docket(DocumentationType.SWAGGER_2) 24 | .select() 25 | .apis(RequestHandlerSelectors.basePackage(("com.beingapple.webservice"))) 26 | .build(); 27 | } 28 | 29 | @Primary 30 | @Bean 31 | public ApiListingScanner addExtraOperations(ApiDescriptionReader apiDescriptionReader, ApiModelReader apiModelReader, DocumentationPluginsManager pluginsManager) 32 | { 33 | return new ApiLoginOperations(apiDescriptionReader, apiModelReader, pluginsManager); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 6 | 7 | @Configuration 8 | public class WebConfig implements WebMvcConfigurer { 9 | 10 | @Override 11 | public void addCorsMappings(CorsRegistry registry){ 12 | registry.addMapping("/api/**") 13 | .allowedOrigins("*"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/domain/BaseTimeEntity.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.domain; 2 | 3 | import lombok.Getter; 4 | import org.springframework.data.annotation.CreatedDate; 5 | import org.springframework.data.annotation.LastModifiedDate; 6 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 7 | import javax.persistence.EntityListeners; 8 | import javax.persistence.MappedSuperclass; 9 | import java.time.LocalDateTime; 10 | 11 | @Getter 12 | @MappedSuperclass 13 | @EntityListeners(AuditingEntityListener.class) 14 | public class BaseTimeEntity { 15 | @CreatedDate 16 | private LocalDateTime createdDate; 17 | 18 | @LastModifiedDate 19 | private LocalDateTime modifiedDate; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/domain/History.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.domain; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | import javax.persistence.*; 9 | import java.util.Date; 10 | 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | @Getter 13 | @Entity 14 | public class History extends BaseTimeEntity{ 15 | @Id 16 | @GeneratedValue 17 | private Long id; 18 | 19 | @Column 20 | private Long memberId; 21 | 22 | @Column(length = 200, nullable = false) 23 | private String keyword; 24 | 25 | @Builder 26 | public History(Long memberId, String keyword, Date searchDate){ 27 | this.memberId = memberId; 28 | this.keyword = keyword; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/domain/HistoryRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.domain; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | @NoArgsConstructor 10 | public class HistoryRequestDTO { 11 | private Long memberId; 12 | private String keyword; 13 | 14 | public History toEntity(){ 15 | return History.builder() 16 | .memberId(memberId) 17 | .keyword(keyword) 18 | .build(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/domain/Member.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.domain; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | import javax.persistence.Column; 9 | import javax.persistence.Entity; 10 | import javax.persistence.GeneratedValue; 11 | import javax.persistence.Id; 12 | 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | @Getter 15 | @Entity 16 | public class Member extends BaseTimeEntity{ 17 | @Id 18 | @GeneratedValue 19 | private Long id; 20 | 21 | @Column(length= 50, nullable = false) 22 | private String userName; 23 | 24 | @Column(length= 100, nullable = false) 25 | private String userId; 26 | 27 | @Column(length= 256, nullable = false) 28 | private String userPassword; 29 | 30 | @Column(length = 10, nullable = false) 31 | private String role; 32 | 33 | @Builder 34 | public Member(String userName, String userId, String userPassword, String role){ 35 | this.userName = userName; 36 | this.userId = userId; 37 | this.userPassword = userPassword; 38 | this.role = role; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/domain/MemberRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.domain; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | @NoArgsConstructor 10 | public class MemberRequestDTO { 11 | private String userName; 12 | private String userId; 13 | private String userPassword; 14 | private String userPasswordCheck; 15 | private String role; 16 | 17 | public Member toEntity(){ 18 | return Member.builder() 19 | .userName(userName) 20 | .userId(userId) 21 | .userPassword(userPassword) 22 | .role("USER") 23 | .build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/domain/Popular.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.domain; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | import javax.persistence.Column; 9 | import javax.persistence.Entity; 10 | import javax.persistence.GeneratedValue; 11 | import javax.persistence.Id; 12 | 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | @Getter 15 | @Entity 16 | public class Popular extends BaseTimeEntity{ 17 | @Id 18 | @GeneratedValue 19 | private Long id; 20 | 21 | @Column(length = 200, nullable = false) 22 | private String keyword; 23 | 24 | @Column(nullable = false) 25 | private Integer count; 26 | 27 | @Builder 28 | public Popular(Long id, String keyword, Integer count){ 29 | this.id = id; 30 | this.keyword = keyword; 31 | this.count = count; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/domain/PopularRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.domain; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | @NoArgsConstructor 10 | public class PopularRequestDTO { 11 | private Long id; 12 | private String keyword; 13 | private Integer count; 14 | 15 | public Popular toEntity(){ 16 | return Popular.builder() 17 | .id(id) 18 | .keyword(keyword) 19 | .count(count) 20 | .build(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/domain/Response.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.domain; 2 | 3 | import lombok.*; 4 | 5 | @NoArgsConstructor 6 | @AllArgsConstructor 7 | @Getter 8 | @Setter 9 | @ToString 10 | public class Response { 11 | private String status; 12 | private String message; 13 | private String errorMessage; 14 | private String errorCode; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/domain/Search.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.*; 5 | import lombok.experimental.Accessors; 6 | 7 | import java.util.List; 8 | 9 | @NoArgsConstructor 10 | @AllArgsConstructor 11 | @Getter 12 | @Setter 13 | @ToString 14 | public class Search { 15 | private Meta meta; 16 | private List documents; 17 | 18 | @Getter 19 | @Setter 20 | @ToString 21 | public static class Meta{ 22 | private Integer total_count; 23 | private Integer pageable_count; 24 | @JsonProperty(value="is_end") 25 | private boolean is_end; 26 | private SameName same_name; 27 | } 28 | 29 | @Getter 30 | @Setter 31 | @ToString 32 | public static class SameName{ 33 | private List region; 34 | private String keyword; 35 | private String selected_region; 36 | } 37 | 38 | @Getter 39 | @Setter 40 | @ToString 41 | public static class Document{ 42 | private String id; 43 | private String place_name; 44 | private String category_name; 45 | private String category_group_code; 46 | private String category_group_name; 47 | private String phone; 48 | private String address_name; 49 | private String road_address_name; 50 | private String x; 51 | private String y; 52 | private String place_url; 53 | private String distance; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/repository/HistoryRepository.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.repository; 2 | 3 | import com.beingapple.webservice.domain.History; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | 9 | public interface HistoryRepository extends JpaRepository { 10 | Page findByMemberIdOrderByCreatedDateDesc(Long memberId, Pageable pageable); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/repository/MemberRepository.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.repository; 2 | 3 | import com.beingapple.webservice.domain.Member; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | public interface MemberRepository extends CrudRepository { 7 | Member findFirstByUserId(String userId); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/repository/PopularRepository.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.repository; 2 | 3 | import com.beingapple.webservice.domain.Popular; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | 8 | public interface PopularRepository extends JpaRepository { 9 | Popular findFirstByKeyword(String keyword); 10 | List findFirst10ByOrderByCountDesc(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/service/HistoryService.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.service; 2 | 3 | import com.beingapple.webservice.domain.History; 4 | import com.beingapple.webservice.domain.HistoryRequestDTO; 5 | import org.springframework.data.domain.Page; 6 | 7 | public interface HistoryService extends PageService{ 8 | void saveSearchHistory(History history); 9 | History makeSearchHistoryEntity(Long memberId, String keyword); 10 | Page getSearchHistory(Long memberId); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/service/HistoryServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.service; 2 | 3 | import com.beingapple.webservice.domain.History; 4 | import com.beingapple.webservice.domain.HistoryRequestDTO; 5 | import com.beingapple.webservice.repository.HistoryRepository; 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.PageRequest; 8 | import org.springframework.scheduling.annotation.Async; 9 | import org.springframework.stereotype.Service; 10 | 11 | @Service 12 | public class HistoryServiceImpl implements HistoryService{ 13 | private HistoryRepository historyRepository; 14 | private PageRequest pageRequest; 15 | 16 | public HistoryServiceImpl(HistoryRepository historyRepository){ 17 | this.historyRepository = historyRepository; 18 | } 19 | 20 | @Async 21 | @Override 22 | public void saveSearchHistory(History history) { 23 | historyRepository.save(history); 24 | } 25 | 26 | public History makeSearchHistoryEntity(Long memberId, String keyword) { 27 | HistoryRequestDTO historyRequestDTO = new HistoryRequestDTO(); 28 | historyRequestDTO.setMemberId(memberId); 29 | historyRequestDTO.setKeyword(keyword); 30 | 31 | return historyRequestDTO.toEntity(); 32 | } 33 | 34 | @Override 35 | public Page getSearchHistory(Long memberId) { 36 | return historyRepository.findByMemberIdOrderByCreatedDateDesc(memberId, this.pageRequest); 37 | } 38 | 39 | @Override 40 | public void setPageRequest(Integer page, Integer size) { 41 | if(page <= 0) page = 1; 42 | if(size <= 0) size = 15; 43 | 44 | this.pageRequest = PageRequest.of(page - 1, size); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/service/MemberService.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.service; 2 | 3 | import com.beingapple.webservice.domain.Member; 4 | import com.beingapple.webservice.domain.MemberRequestDTO; 5 | 6 | import java.util.Optional; 7 | 8 | 9 | public interface MemberService { 10 | Member getMemberByUserId(String userId); 11 | boolean isExistMember(String userId); 12 | void saveMember(Member member); 13 | Member makeSaveMemberData(MemberRequestDTO dto); 14 | Optional getAuthenticationMember(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/service/MemberServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.service; 2 | 3 | import com.beingapple.webservice.domain.Member; 4 | import com.beingapple.webservice.repository.MemberRepository; 5 | import com.beingapple.webservice.domain.MemberRequestDTO; 6 | import lombok.AllArgsConstructor; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.core.context.SecurityContextHolder; 9 | import org.springframework.security.crypto.password.PasswordEncoder; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.Optional; 13 | 14 | 15 | @AllArgsConstructor 16 | @Service 17 | public class MemberServiceImpl implements MemberService { 18 | private MemberRepository memberRepository; 19 | private PasswordEncoder passwordEncoder; 20 | 21 | @Override 22 | public boolean isExistMember(String userId) { 23 | Member member = getMemberByUserId(userId); 24 | return Optional.ofNullable(member).isPresent(); 25 | } 26 | 27 | @Override 28 | public Member getMemberByUserId(String userId) { 29 | return memberRepository.findFirstByUserId(userId); 30 | } 31 | 32 | @Override 33 | public void saveMember(Member member){ 34 | memberRepository.save(member); 35 | } 36 | 37 | @Override 38 | public Member makeSaveMemberData(MemberRequestDTO dto) { 39 | String encoded = passwordEncode(dto.getUserPassword()); 40 | dto.setUserPassword(encoded); 41 | 42 | return dto.toEntity(); 43 | } 44 | 45 | public String passwordEncode(String password) { 46 | String encoded = passwordEncoder.encode(password); 47 | return encoded; 48 | } 49 | 50 | @Override 51 | public Optional getAuthenticationMember() { 52 | Optional memberOptional = Optional.empty(); 53 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 54 | 55 | if(authentication != null) { 56 | String userId = authentication.getPrincipal().toString(); 57 | Member authenticationMemberData = memberRepository.findFirstByUserId(userId); 58 | memberOptional = memberOptional.ofNullable(authenticationMemberData); 59 | } 60 | 61 | return memberOptional; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/service/PageService.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.service; 2 | 3 | 4 | public interface PageService { 5 | void setPageRequest (Integer page, Integer size); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/service/PopularService.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.service; 2 | 3 | import com.beingapple.webservice.domain.Popular; 4 | import com.beingapple.webservice.domain.Search; 5 | 6 | import java.util.List; 7 | 8 | public interface PopularService { 9 | void savePopularKeyword(String keyword); 10 | List getPopularList(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/service/PopularServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.service; 2 | 3 | import com.beingapple.webservice.domain.Popular; 4 | import com.beingapple.webservice.domain.PopularRequestDTO; 5 | import com.beingapple.webservice.repository.PopularRepository; 6 | import lombok.AllArgsConstructor; 7 | import org.springframework.scheduling.annotation.Async; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.List; 11 | import java.util.Optional; 12 | 13 | @AllArgsConstructor 14 | @Service 15 | public class PopularServiceImpl implements PopularService { 16 | private PopularRepository popularRepository; 17 | private Optional optionalPopular; 18 | 19 | @Async 20 | @Override 21 | public void savePopularKeyword(String keyword) { 22 | Popular saveData = makeSavePopularData(keyword); 23 | popularRepository.save(saveData); 24 | } 25 | 26 | public Popular makeSavePopularData(String keyword){ 27 | Popular popular = popularRepository.findFirstByKeyword(keyword); 28 | PopularRequestDTO dto = optionalPopular.ofNullable(popular) 29 | .filter(p -> p.getId() > 0) 30 | .map(p -> { 31 | PopularRequestDTO dtoTemp = new PopularRequestDTO(); 32 | dtoTemp.setId(p.getId()); 33 | dtoTemp.setKeyword(p.getKeyword()); 34 | dtoTemp.setCount(p.getCount() + 1); 35 | 36 | return dtoTemp; 37 | }) 38 | .orElseGet(() -> { 39 | PopularRequestDTO dtoTemp = new PopularRequestDTO(); 40 | dtoTemp.setKeyword(keyword); 41 | dtoTemp.setCount(1); 42 | 43 | return dtoTemp; 44 | }); 45 | 46 | return dto.toEntity(); 47 | } 48 | 49 | @Override 50 | public List getPopularList() { 51 | return popularRepository.findFirst10ByOrderByCountDesc(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/service/SearchService.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.service; 2 | 3 | import com.beingapple.webservice.domain.Search; 4 | 5 | public interface SearchService { 6 | Search findByKeyword(String keyword, Integer page, Integer size); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/service/SearchServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.service; 2 | 3 | import com.beingapple.webservice.domain.Search; 4 | import com.beingapple.webservice.repository.HistoryRepository; 5 | import com.beingapple.webservice.repository.PopularRepository; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.boot.web.client.RestTemplateBuilder; 8 | import org.springframework.http.HttpEntity; 9 | import org.springframework.http.HttpHeaders; 10 | import org.springframework.http.HttpMethod; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.web.client.RestTemplate; 14 | import org.springframework.web.util.UriComponents; 15 | import org.springframework.web.util.UriComponentsBuilder; 16 | 17 | @Service 18 | public class SearchServiceImpl implements SearchService { 19 | private final RestTemplate restTemplate; 20 | 21 | private HistoryRepository historyRepository; 22 | private PopularRepository popularRepository; 23 | 24 | @Value("${kakao.api.url}") 25 | private String kakaoApiUrl; 26 | 27 | @Value("${kakao.api.key}") 28 | private String kakaoApiKey; 29 | 30 | public SearchServiceImpl(RestTemplateBuilder restTemplateBuilder) { 31 | this.restTemplate = restTemplateBuilder.build(); 32 | } 33 | 34 | @Override 35 | public Search findByKeyword(String keyword, Integer page, Integer size) { 36 | UriComponents builder = UriComponentsBuilder.fromHttpUrl(kakaoApiUrl+"/v2/local/search/keyword.json") 37 | .queryParam("query", keyword) 38 | .queryParam("page", page) 39 | .queryParam("size", size) 40 | .build(false); 41 | 42 | HttpHeaders httpHeaders = new HttpHeaders(); 43 | httpHeaders.setContentType(MediaType.APPLICATION_JSON); 44 | httpHeaders.set("Authorization", kakaoApiKey); 45 | 46 | HttpEntity httpEntity = new HttpEntity<>(httpHeaders); 47 | return restTemplate.exchange(builder.toUriString(), HttpMethod.GET, httpEntity, Search.class).getBody(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/util/DateUtil.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.util; 2 | 3 | import java.time.LocalDateTime; 4 | import java.time.ZoneOffset; 5 | import java.util.Date; 6 | 7 | public abstract class DateUtil { 8 | public static Date nowToDate() { 9 | return Date.from(LocalDateTime.now().toInstant(ZoneOffset.ofHours(9))); 10 | } 11 | 12 | public static Date nowAfterDaysToDate(Long days) { 13 | return Date.from(LocalDateTime.now().plusDays(days).toInstant(ZoneOffset.ofHours(9))); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/util/JwtUtil.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.util; 2 | 3 | import com.beingapple.webservice.auth.jwt.JwtInfo; 4 | import org.springframework.security.core.userdetails.UserDetails; 5 | 6 | import com.auth0.jwt.JWT; 7 | import com.auth0.jwt.JWTVerifier; 8 | import com.auth0.jwt.exceptions.JWTCreationException; 9 | import com.auth0.jwt.exceptions.JWTDecodeException; 10 | import com.auth0.jwt.exceptions.JWTVerificationException; 11 | import com.auth0.jwt.interfaces.DecodedJWT; 12 | 13 | import java.util.Date; 14 | 15 | public class JwtUtil { 16 | public static String createToken(UserDetails userDetails) { 17 | return createToken(userDetails, DateUtil.nowAfterDaysToDate(JwtInfo.EXPIRES_LIMIT)); 18 | } 19 | 20 | private static String createToken(UserDetails userDetails, Date date) { 21 | try { 22 | return JWT.create() 23 | .withIssuer(JwtInfo.ISSUER) 24 | .withClaim("id", userDetails.getUsername()) 25 | .withClaim("role", userDetails.getAuthorities().toArray()[0].toString()) 26 | .withExpiresAt(date) 27 | .sign(JwtInfo.getAlgorithm()); 28 | } catch (JWTCreationException createEx) { 29 | return null; 30 | } 31 | } 32 | 33 | public static Boolean verify(String token) { 34 | try { 35 | JWTVerifier verifier = JWT.require(JwtInfo.getAlgorithm()).build(); 36 | verifier.verify(token); 37 | 38 | return Boolean.TRUE; 39 | } catch (JWTVerificationException verifyEx) { 40 | return Boolean.FALSE; 41 | } 42 | } 43 | 44 | public static DecodedJWT tokenToJwt(String token) { 45 | try { 46 | return JWT.decode(token); 47 | } catch (JWTDecodeException decodeEx) { 48 | return null; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/util/MemberValidation.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.util; 2 | 3 | import com.beingapple.webservice.domain.MemberRequestDTO; 4 | 5 | public class MemberValidation { 6 | private String userName; 7 | private String userId; 8 | private String userPassword; 9 | private String check; 10 | 11 | public MemberValidation(MemberRequestDTO dto){ 12 | this.userName = dto.getUserName(); 13 | this.userId = dto.getUserId(); 14 | this.userPassword = dto.getUserPassword(); 15 | this.check = dto.getUserPasswordCheck(); 16 | } 17 | 18 | public boolean hasEmptySpace(){ 19 | return (userName == null || "".equals(userName)) || (userId == null || "".equals(userId)) || (userPassword == null || "".equals(userPassword)); 20 | } 21 | 22 | public boolean isPasswordSameWithCheck(){ 23 | return !userPassword.equals(check); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/web/CommonController.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.web; 2 | 3 | import com.beingapple.webservice.domain.Response; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.ResponseEntity; 6 | 7 | public class CommonController { 8 | protected ResponseEntity returnResponseObjectWithHttpStatus(HttpStatus status, String message, String errorMessage, String errorCode){ 9 | Response response = new Response( 10 | status.toString(), 11 | message, 12 | errorMessage, 13 | errorCode); 14 | 15 | return new ResponseEntity<>(response, status); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/web/MemberController.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.web; 2 | 3 | import com.beingapple.webservice.domain.Member; 4 | import com.beingapple.webservice.domain.MemberRequestDTO; 5 | import com.beingapple.webservice.domain.Response; 6 | import com.beingapple.webservice.service.MemberService; 7 | import com.beingapple.webservice.util.MemberValidation; 8 | import lombok.AllArgsConstructor; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.annotation.PostMapping; 12 | import org.springframework.web.bind.annotation.RequestBody; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import java.util.Optional; 16 | 17 | @RestController 18 | @AllArgsConstructor 19 | public class MemberController extends CommonController{ 20 | private MemberService memberService; 21 | 22 | @PostMapping("/api/join") 23 | public ResponseEntity saveMember(@RequestBody MemberRequestDTO dto){ 24 | MemberValidation validation = new MemberValidation(dto); 25 | 26 | if( validation.hasEmptySpace() ){ 27 | return returnResponseObjectWithHttpStatus(HttpStatus.BAD_REQUEST, "", "모두 입력이 필요합니다.", "FORM"); 28 | } 29 | 30 | if( validation.isPasswordSameWithCheck() ){ 31 | return returnResponseObjectWithHttpStatus(HttpStatus.BAD_REQUEST, "", "비밀번호 확인이 불일치 합니다 ", "PASSWORD"); 32 | } 33 | 34 | String userId = Optional.ofNullable(dto).map(MemberRequestDTO::getUserId).orElse(""); 35 | if(!memberService.isExistMember(userId)) { 36 | Member saveData = memberService.makeSaveMemberData(dto); 37 | memberService.saveMember(saveData); 38 | 39 | return new ResponseEntity<>(null, HttpStatus.CREATED); 40 | }else{ 41 | return returnResponseObjectWithHttpStatus(HttpStatus.CONFLICT, "", "중복된 유저 아이디입니다.", "DUP"); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/web/SearchController.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.web; 2 | 3 | import com.beingapple.webservice.domain.*; 4 | import com.beingapple.webservice.service.HistoryService; 5 | import com.beingapple.webservice.service.MemberService; 6 | import com.beingapple.webservice.service.PopularService; 7 | import com.beingapple.webservice.service.SearchService; 8 | import io.swagger.annotations.ApiImplicitParam; 9 | import io.swagger.annotations.ApiImplicitParams; 10 | import lombok.AllArgsConstructor; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.web.bind.annotation.GetMapping; 14 | import org.springframework.web.bind.annotation.RequestParam; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | import java.util.List; 18 | 19 | @RestController 20 | @AllArgsConstructor 21 | public class SearchController extends CommonController{ 22 | private SearchService searchService; 23 | private MemberService memberService; 24 | private HistoryService historyService; 25 | private PopularService popularService; 26 | 27 | @ApiImplicitParams({ 28 | @ApiImplicitParam(name = "Authorization", value = "Authorization header", required = true, 29 | dataType = "string", paramType = "header", defaultValue = "key") 30 | }) 31 | @GetMapping("/api/search/place") 32 | public ResponseEntity searchPlace(@RequestParam("keyword") String keyword, 33 | @RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "size", required = false) Integer size){ 34 | Long memberId = memberService.getAuthenticationMember() 35 | .map(Member::getId) 36 | .orElse(0L); 37 | 38 | if(memberId > 0) { 39 | Search search = searchService.findByKeyword(keyword, page, size); 40 | History saveData = historyService.makeSearchHistoryEntity(memberId, keyword); 41 | 42 | //비동기 처리 43 | historyService.saveSearchHistory(saveData); 44 | popularService.savePopularKeyword(keyword); 45 | 46 | return new ResponseEntity<>(search, HttpStatus.OK); 47 | }else{ 48 | return returnResponseObjectWithHttpStatus(HttpStatus.UNAUTHORIZED, "", "MEMBER", "토큰값에 해당하는 멤버가 존재하지 않습니다."); 49 | } 50 | } 51 | 52 | @ApiImplicitParams({ 53 | @ApiImplicitParam(name = "Authorization", value = "Authorization header", required = true, 54 | dataType = "string", paramType = "header", defaultValue = "key") 55 | }) 56 | @GetMapping("/api/search/popular") 57 | public ResponseEntity getPopular(){ 58 | List popularList = popularService.getPopularList(); 59 | return new ResponseEntity<>(popularList, HttpStatus.OK); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/beingapple/webservice/web/SearchHistoryController.java: -------------------------------------------------------------------------------- 1 | package com.beingapple.webservice.web; 2 | 3 | import com.beingapple.webservice.domain.History; 4 | import com.beingapple.webservice.domain.Member; 5 | import com.beingapple.webservice.domain.Response; 6 | import com.beingapple.webservice.service.HistoryService; 7 | import com.beingapple.webservice.service.MemberService; 8 | import io.swagger.annotations.ApiImplicitParam; 9 | import io.swagger.annotations.ApiImplicitParams; 10 | import lombok.AllArgsConstructor; 11 | import org.springframework.data.domain.Page; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.web.bind.annotation.GetMapping; 15 | import org.springframework.web.bind.annotation.RequestParam; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | @RestController 19 | @AllArgsConstructor 20 | public class SearchHistoryController extends CommonController{ 21 | private MemberService memberService; 22 | HistoryService historyService; 23 | 24 | @ApiImplicitParams({ 25 | @ApiImplicitParam(name = "Authorization", value = "Authorization header", required = true, 26 | dataType = "string", paramType = "header", defaultValue = "key") 27 | }) 28 | @GetMapping("/api/history") 29 | public ResponseEntity getHistory(@RequestParam(value = "page", required = false, defaultValue = "1") Integer page, 30 | @RequestParam(value = "size", required = false, defaultValue = "15") Integer size){ 31 | Long memberId = memberService.getAuthenticationMember() 32 | .map(Member::getId) 33 | .orElse(0L); 34 | 35 | if(memberId > 0) { 36 | historyService.setPageRequest(page, size); 37 | Page historyList = historyService.getSearchHistory(memberId); 38 | 39 | return new ResponseEntity<>(historyList, HttpStatus.OK); 40 | }else{ 41 | return returnResponseObjectWithHttpStatus(HttpStatus.UNAUTHORIZED, "", "MEMBER", "토큰값에 해당하는 멤버가 존재하지 않습니다."); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | h2: 3 | console: 4 | enabled: true 5 | Kakao: 6 | api: 7 | url: https://dapi.kakao.com 8 | key: KakaoAK 540d3097b14050f7e915581474339b2b -------------------------------------------------------------------------------- /src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | vue-search-place
-------------------------------------------------------------------------------- /src/main/resources/static/static/js/app.43a8422c172b56127065.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([1],{"9M+g":function(t,e){},Jmt5:function(t,e){},KIjT:function(t,e){},NHnr:function(t,e,a){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=a("7+uW"),n=a("Tqaz"),s=a("8AgW"),o=a.n(s),i={render:function(){var t=this.$createElement;return(this._self._c||t)("router-view")},staticRenderFns:[]};var c,u=a("VU/8")({name:"App"},i,!1,function(t){a("wZo1")},null,null).exports,p=a("NYxO"),l=a("Xxa5"),v=a.n(l),d=a("exGp"),f=a.n(d),h=a("mtWM"),m=a.n(h),g=function(t,e){return m.a.post("//localhost:8080/api/login",{userId:t,userPassword:e})},w={login:function(t,e){var a=this;return f()(v.a.mark(function r(){var n;return v.a.wrap(function(a){for(;;)switch(a.prev=a.next){case 0:return a.prev=0,a.next=3,g(t,e).then(function(t){return t}).catch(function(t){return t.response});case 3:return 200===(n=a.sent).status&&(m.a.defaults.headers.common.Authorization=n.data.message),a.abrupt("return",n);case 8:a.prev=8,a.t0=a.catch(0),console.log(a.t0);case 11:case"end":return a.stop()}},r,a,[[0,8]])}))()}},_=function(t,e,a,r){return m.a.post("//localhost:8080/api/join",{userName:t,userId:e,userPassword:a,userPasswordCheck:r})},k={join:function(t,e,a,r){var n=this;return f()(v.a.mark(function s(){var o;return v.a.wrap(function(n){for(;;)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,_(t,e,a,r).then(function(t){return t}).catch(function(t){return t.response});case 3:return o=n.sent,n.abrupt("return",o);case 7:n.prev=7,n.t0=n.catch(0),console.log(n.t0);case 10:case"end":return n.stop()}},s,n,[[0,7]])}))()}},b=function(t,e,a){return m.a.get("//localhost:8080/api/search/place",{params:{keyword:t,page:e,size:a}})},C=function(t,e){return m.a.get("//localhost:8080/api/history",{params:{page:t,size:e}})},y={search:function(t,e,a){var r=this;return f()(v.a.mark(function n(){var s;return v.a.wrap(function(r){for(;;)switch(r.prev=r.next){case 0:return r.prev=0,r.next=3,b(t,e,a).then(function(t){return t}).catch(function(t){return t.response});case 3:return s=r.sent,r.abrupt("return",s);case 7:r.prev=7,r.t0=r.catch(0),console.log(r.t0);case 10:case"end":return r.stop()}},n,r,[[0,7]])}))()},history:function(t,e){var a=this;return f()(v.a.mark(function r(){var n;return v.a.wrap(function(a){for(;;)switch(a.prev=a.next){case 0:return a.prev=0,a.next=3,C(t,e).then(function(t){return t}).catch(function(t){return t.response});case 3:return n=a.sent,a.abrupt("return",n);case 7:a.prev=7,a.t0=a.catch(0),console.log(a.t0);case 10:case"end":return a.stop()}},r,a,[[0,7]])}))()},popular:function(){var t=this;return f()(v.a.mark(function e(){var a;return v.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return t.prev=0,t.next=3,m.a.get("//localhost:8080/api/search/popular").then(function(t){return t}).catch(function(t){return t.response});case 3:return a=t.sent,t.abrupt("return",a);case 7:t.prev=7,t.t0=t.catch(0),console.log(t.t0);case 10:case"end":return t.stop()}},e,t,[[0,7]])}))()}},x={login:function(t,e){var a=this;return f()(v.a.mark(function r(){var n;return v.a.wrap(function(a){for(;;)switch(a.prev=a.next){case 0:return a.prev=0,a.next=3,w.login(t,e);case 3:return n=a.sent,a.abrupt("return",n);case 7:a.prev=7,a.t0=a.catch(0),console.log(a.t0);case 10:case"end":return a.stop()}},r,a,[[0,7]])}))()},join:function(t,e,a,r){var n=this;return f()(v.a.mark(function s(){var o;return v.a.wrap(function(n){for(;;)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,k.join(t,e,a,r);case 3:return o=n.sent,n.abrupt("return",o);case 7:n.prev=7,n.t0=n.catch(0),console.log(n.t0);case 10:case"end":return n.stop()}},s,n,[[0,7]])}))()},search:function(t,e,a){var r=this;return f()(v.a.mark(function n(){var s;return v.a.wrap(function(r){for(;;)switch(r.prev=r.next){case 0:return r.prev=0,r.next=3,y.search(t,e,a);case 3:return s=r.sent,r.abrupt("return",s);case 7:r.prev=7,r.t0=r.catch(0),console.log(r.t0);case 10:case"end":return r.stop()}},n,r,[[0,7]])}))()},history:function(t,e){var a=this;return f()(v.a.mark(function r(){var n;return v.a.wrap(function(a){for(;;)switch(a.prev=a.next){case 0:return a.prev=0,a.next=3,y.history(t,e);case 3:return n=a.sent,a.abrupt("return",n);case 7:a.prev=7,a.t0=a.catch(0),console.log(a.t0);case 10:case"end":return a.stop()}},r,a,[[0,7]])}))()},popular:function(){var t=this;return f()(v.a.mark(function e(){var a;return v.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return t.prev=0,t.next=3,y.popular();case 3:return a=t.sent,t.abrupt("return",a);case 7:t.prev=7,t.t0=t.catch(0),console.log(t.t0);case 10:case"end":return t.stop()}},e,t,[[0,7]])}))()}},P=function(t,e){(0,t.commit)("ERROR_STATE",e)},S=function(t,e){(0,t.commit)("IS_AUTH",e)},D=function(t,e){(0,t.commit)("ACCESS_TOKEN",e)},I=function(t,e){(0,t.commit)("SEARCH_DATA",e)},A=function(t,e){var a=e.status,r=e.data;switch(a){case 200:P(t,""),S(t,!0),D(t,r.message);break;case 400:P(t,"잘못된 아이디 혹은 비밀번호입니다."),S(t,!1),D(t,"")}},E=function(t,e){var a=e.status,r=e.data;switch(a){case 201:P(t,""),S(t,!0);break;case 400:case 409:P(t,r.errorMessage),S(t,!1)}},z={login:function(t,e){var a=this,r=e.userId,n=e.userPassword;return f()(v.a.mark(function e(){var s;return v.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,x.login(r,n);case 2:return s=e.sent,A(t,s),e.abrupt("return",t.getters.getIsAuth);case 5:case"end":return e.stop()}},e,a)}))()},join:function(t,e){var a=this,r=e.userName,n=e.userId,s=e.userPassword,o=e.userPasswordCheck;return f()(v.a.mark(function e(){var i;return v.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,x.join(r,n,s,o);case 2:return i=e.sent,E(t,i),e.abrupt("return",t.getters.getIsAuth);case 5:case"end":return e.stop()}},e,a)}))()},setData:function(t,e){var a=this;return f()(v.a.mark(function r(){return v.a.wrap(function(a){for(;;)switch(a.prev=a.next){case 0:return a.next=2,I(t,e);case 2:case"end":return a.stop()}},r,a)}))()},setKeyword:function(t,e){var a;a=e,(0,t.commit)("KEYWORD",a)},setSearchViewBack:function(t,e){var a;a=e,(0,t.commit)("SEARCH_VIEW_BACK",a)}},M=a("bOdI"),N=a.n(M),T=(c={},N()(c,"ERROR_STATE",function(t,e){t.errorState=e}),N()(c,"IS_AUTH",function(t,e){t.isAuth=e}),N()(c,"ACCESS_TOKEN",function(t,e){t.accessToken=e}),N()(c,"SEARCH_DATA",function(t,e){t.searchData=e}),N()(c,"KEYWORD",function(t,e){t.keyword=e}),N()(c,"SEARCH_VIEW_BACK",function(t,e){t.searchViewBack=e}),c);r.default.use(p.a);var L=new p.a.Store({state:{errorState:"",isAuth:!1,accessToken:null,searchData:{},keyword:"",searchViewBack:!1},mutations:T,getters:{getErrorState:function(t){return t.errorState},getIsAuth:function(t){return t.isAuth},getAccessToken:function(t){return t.accessToken},getSearchData:function(t){return t.searchData},getKeyword:function(t){return t.keyword},getSearchViewBack:function(t){return t.searchViewBack}},actions:z}),R=a("/ocq"),V=a("Dd8w"),K=a.n(V),j={name:"Join",data:function(){return{userName:"",userId:"",userPassword:"",userPasswordCheck:""}},methods:K()({},Object(p.b)(["join"]),{onSubmit:function(){var t=this;return f()(v.a.mark(function e(){return v.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.join({userName:t.userName,userId:t.userId,userPassword:t.userPassword,userPasswordCheck:t.userPasswordCheck});case 3:e.sent&&t.goToPages(),e.next=10;break;case 7:e.prev=7,e.t0=e.catch(0),console.log(e.t0);case 10:case"end":return e.stop()}},e,t,[[0,7]])}))()},goToPages:function(){this.$router.push("/login")}}),computed:K()({},Object(p.c)({errorState:"getErrorState"}))},B={render:function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",{staticClass:"wrap"},[a("form",{staticClass:"form-signin",on:{submit:function(e){return e.preventDefault(),t.onSubmit(e)}}},[a("img",{staticClass:"mb-4",attrs:{src:"/docs/4.3/assets/brand/bootstrap-solid.svg",alt:"",width:"72",height:"72"}}),t._v(" "),a("h1",{staticClass:"text-center h3 mb-3 font-weight-normal"},[t._v("회원가입이 필요합니다.")]),t._v(" "),t.errorState?a("div",{staticClass:"alert alert-danger",attrs:{role:"alert"}},[t._v(t._s(t.errorState))]):t._e(),t._v(" "),a("div",{staticClass:"form-group"},[a("label",{attrs:{for:"inputUserName"}},[t._v("이름")]),t._v(" "),a("input",{directives:[{name:"model",rawName:"v-model",value:t.userName,expression:"userName"}],staticClass:"form-control",attrs:{type:"text",id:"inputUserName",name:"userName",placeholder:"이름"},domProps:{value:t.userName},on:{input:function(e){e.target.composing||(t.userName=e.target.value)}}})]),t._v(" "),a("div",{staticClass:"form-group"},[a("label",{attrs:{for:"inputUserId"}},[t._v("아이디")]),t._v(" "),a("input",{directives:[{name:"model",rawName:"v-model",value:t.userId,expression:"userId"}],staticClass:"form-control",attrs:{type:"text",id:"inputUserId",name:"userId",placeholder:"아이디"},domProps:{value:t.userId},on:{input:function(e){e.target.composing||(t.userId=e.target.value)}}})]),t._v(" "),a("div",{staticClass:"form-group"},[a("label",{attrs:{for:"inputUserPassword"}},[t._v("비밀번호")]),t._v(" "),a("input",{directives:[{name:"model",rawName:"v-model",value:t.userPassword,expression:"userPassword"}],staticClass:"form-control",attrs:{type:"password",id:"inputUserPassword",name:"userPassword",placeholder:"비밀번호"},domProps:{value:t.userPassword},on:{input:function(e){e.target.composing||(t.userPassword=e.target.value)}}})]),t._v(" "),a("div",{staticClass:"form-group"},[a("label",{attrs:{for:"inputUserPasswordCheck"}},[t._v("비밀번호 확인")]),t._v(" "),a("input",{directives:[{name:"model",rawName:"v-model",value:t.userPasswordCheck,expression:"userPasswordCheck"}],staticClass:"form-control",attrs:{type:"password",id:"inputUserPasswordCheck",name:"userPasswordCheck",placeholder:"비밀번호 확인"},domProps:{value:t.userPasswordCheck},on:{input:function(e){e.target.composing||(t.userPasswordCheck=e.target.value)}}})]),t._v(" "),a("button",{staticClass:"btn btn-lg btn-primary btn-block",attrs:{type:"submit"}},[t._v("회원가입")]),t._v(" "),a("p",[a("router-link",{attrs:{to:"/login"}},[t._v("이미 회원이신가요?")])],1),t._v(" "),a("p",{staticClass:"text-center mt-5 mb-3 text-muted"},[t._v("©BeingApple")])])])},staticRenderFns:[]};var U=a("VU/8")(j,B,!1,function(t){a("RF0M")},"data-v-61764aae",null).exports,O={name:"Login",data:function(){return{userId:"",userPassword:""}},methods:K()({},Object(p.b)(["login"]),{onSubmit:function(){var t=this;return f()(v.a.mark(function e(){return v.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,t.login({userId:t.userId,userPassword:t.userPassword});case 3:e.sent&&t.goToPages(),e.next=10;break;case 7:e.prev=7,e.t0=e.catch(0),console.log(e.t0);case 10:case"end":return e.stop()}},e,t,[[0,7]])}))()},goToPages:function(){this.$router.push("/search")}}),computed:K()({},Object(p.c)({errorState:"getErrorState"}))},F={render:function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",{staticClass:"wrap text-center"},[a("form",{staticClass:"form-signin",on:{submit:function(e){return e.preventDefault(),t.onSubmit(e)}}},[a("h1",{staticClass:"h3 mb-3 font-weight-normal"},[t._v("로그인이 필요합니다.")]),t._v(" "),t.errorState?a("div",{staticClass:"alert alert-danger",attrs:{role:"alert"}},[t._v(t._s(t.errorState))]):t._e(),t._v(" "),a("label",{staticClass:"sr-only",attrs:{for:"inputId"}},[t._v("아이디")]),t._v(" "),a("input",{directives:[{name:"model",rawName:"v-model",value:t.userId,expression:"userId"}],staticClass:"form-control",attrs:{type:"text",id:"inputId",name:"userId",placeholder:"아이디",required:""},domProps:{value:t.userId},on:{input:function(e){e.target.composing||(t.userId=e.target.value)}}}),t._v(" "),a("label",{staticClass:"sr-only",attrs:{for:"inputPassword"}},[t._v("비밀번호")]),t._v(" "),a("input",{directives:[{name:"model",rawName:"v-model",value:t.userPassword,expression:"userPassword"}],staticClass:"form-control",attrs:{type:"password",id:"inputPassword",name:"userPassword",placeholder:"비밀번호",required:""},domProps:{value:t.userPassword},on:{input:function(e){e.target.composing||(t.userPassword=e.target.value)}}}),t._v(" "),a("button",{staticClass:"btn btn-lg btn-primary btn-block",attrs:{type:"submit"}},[t._v("로그인")]),t._v(" "),a("p",[a("router-link",{attrs:{to:"/"}},[t._v("회원가입")])],1),t._v(" "),a("p",{staticClass:"mt-5 mb-3 text-muted"},[t._v("©BeingApple")])])])},staticRenderFns:[]};var $=a("VU/8")(O,F,!1,function(t){a("fI9g")},"data-v-aba09146",null).exports,H={name:"Search",data:function(){return{keyword:"",nowKeyword:"",page:1,nowPage:1,size:15,perPage:5,searchList:[]}},methods:K()({},Object(p.b)(["setData","setKeyword","setSearchViewBack"]),{search:function(t){var e=this;return f()(v.a.mark(function a(){var r;return v.a.wrap(function(a){for(;;)switch(a.prev=a.next){case 0:if(t&&(e.page=1),e.page===e.nowPage&&e.nowKeyword===e.keyword){a.next=12;break}return a.prev=2,a.next=5,x.search(e.keyword,e.page,e.size);case 5:200===(r=a.sent).status&&(e.nowKeyword=e.keyword,e.nowPage=e.page,e.searchList=r.data),a.next=12;break;case 9:a.prev=9,a.t0=a.catch(2),console.log(a.t0);case 12:case"end":return a.stop()}},a,e,[[2,9]])}))()},pageMove:function(t){this.page=t,this.search(!1)},nowPageActive:function(t){var e=[];return t===this.page&&e.push("active"),e},move:function(t,e){var a=this;return f()(v.a.mark(function r(){return v.a.wrap(function(r){for(;;)switch(r.prev=r.next){case 0:return r.next=2,a.setData(a.searchList.documents[e]);case 2:a.setKeyword(a.keyword),a.$router.push("/search/"+t);case 4:case"end":return r.stop()}},r,a)}))()}}),mounted:function(){""!==this.saveKeyword&&this.getBack&&(this.keyword=this.saveKeyword,this.search(!0)),this.setKeyword(""),this.setSearchViewBack(!1)},computed:K()({pageSize:function(){return Math.ceil(this.searchList.meta.pageable_count/this.size)},pageList:function(){var t=[],e=Math.floor(this.perPage/2);if(this.pageSize>this.perPage)if(this.page<=e+1)for(var a=1;a<=this.perPage;a++)t.push(a);else if(this.page>this.pageSize-e)for(var r=this.pageSize-(this.perPage-1);r<=this.pageSize;r++)t.push(r);else for(var n=this.page-e;n<=this.page+e;n++)t.push(n);else for(var s=1;s<=this.pageSize;s++)t.push(s);return t}},Object(p.c)({saveKeyword:"getKeyword",getBack:"getSearchViewBack"}))},W={render:function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("common",[a("h2",{staticClass:"mt-2"},[t._v("장소 검색")]),t._v(" "),a("p",{staticClass:"lead"},[a("b-input-group",{staticClass:"mt-3",attrs:{prepend:"키워드"}},[a("b-form-input",{on:{keyup:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:t.search(!0)}},model:{value:t.keyword,callback:function(e){t.keyword=e},expression:"keyword"}}),t._v(" "),a("b-input-group-append",[a("b-button",{attrs:{variant:"info"},on:{click:function(e){return e.preventDefault(),t.search(!0)}}},[t._v("검색")])],1)],1),t._v(" "),a("b-list-group",{staticClass:"mt-2"},[t.searchList.documents?t._e():a("b-list-group-item",[t._v("검색 결과가 없습니다.")]),t._v(" "),t._l(t.searchList.documents,function(e,r){return a("b-list-group-item",{key:e.id,on:{click:function(a){return a.preventDefault(),t.move(e.id,r)}}},[t._v("\n "+t._s(e.place_name)+"\n ")])})],2)],1),t._v(" "),t.searchList.documents?a("nav",{attrs:{"aria-label":"Page navigation example"}},[a("ul",{staticClass:"pagination"},[a("li",{staticClass:"page-item"},[a("a",{staticClass:"page-link",attrs:{"aria-label":"Previous"},on:{click:function(e){return e.preventDefault(),t.pageMove(1)}}},[a("span",{attrs:{"aria-hidden":"true"}},[t._v("«")])])]),t._v(" "),t._l(t.pageList,function(e){return a("li",{key:e,staticClass:"page-item",class:t.nowPageActive(e)},[a("a",{staticClass:"page-link",on:{click:function(a){return a.preventDefault(),t.pageMove(e)}}},[t._v(t._s(e))])])}),t._v(" "),a("li",{staticClass:"page-item"},[a("a",{staticClass:"page-link",attrs:{"aria-label":"Next"},on:{click:function(e){return e.preventDefault(),t.pageMove(t.pageSize)}}},[a("span",{attrs:{"aria-hidden":"true"}},[t._v("»")])])])],2)]):t._e()])},staticRenderFns:[]};var J=a("VU/8")(H,W,!1,function(t){a("ztJ8")},"data-v-2fa79fe6",null).exports,Y={name:"SearchView",data:function(){return{detail:L.getters.getSearchData}},methods:K()({},Object(p.b)(["setSearchViewBack"]),{makeLatLng:function(){return new window.daum.maps.LatLng(this.detail.y,this.detail.x)},makeMap:function(t,e){return new window.daum.maps.Map(t,e)},makeMarker:function(t){var e=new window.daum.maps.Marker({position:this.makeLatLng()});e.setMap(t);var a='
'+this.detail.place_name+''+this.detail.address_name+"
";new window.daum.maps.InfoWindow({position:this.makeLatLng(),content:a}).open(t,e)}}),mounted:function(){var t=this;this.setSearchViewBack(!0);var e=this;window.daum.maps.load(function(){var a=document.getElementById("map"),r={center:e.makeLatLng(),level:3},n=e.makeMap(a,r);t.makeMarker(n)})}},Z={render:function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("common",[a("div",{staticClass:"card",staticStyle:{width:"100%"}},[a("div",{staticClass:"map",attrs:{id:"map"}}),t._v(" "),a("div",{staticClass:"card-body"},[a("h5",{staticClass:"card-title"},[t._v(t._s(t.detail.place_name))]),t._v(" "),a("p",{staticClass:"card-text"},[t._v("주소 : "+t._s(t.detail.address_name))]),t._v(" "),a("p",{staticClass:"card-text"},[t._v("전화번호 : "+t._s(t.detail.phone))]),t._v(" "),a("a",{staticClass:"btn btn-primary",attrs:{href:"https://map.kakao.com/link/map/"+t.detail.place_name+","+t.detail.y+","+t.detail.x,target:"_blank"}},[t._v("지도 확인하기")])])])])},staticRenderFns:[]};var q=a("VU/8")(Y,Z,!1,function(t){a("Z3+D")},"data-v-78845e8d",null).exports,G={name:"History",data:function(){return{page:1,size:15,perPage:5,pageSize:0,historyData:{}}},methods:{history:function(){var t=this;return f()(v.a.mark(function e(){var a;return v.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,x.history(t.page,t.size);case 3:200===(a=e.sent).status&&(t.historyData=a.data,t.pageSize=a.data.totalPages),e.next=10;break;case 7:e.prev=7,e.t0=e.catch(0),console.log(e.t0);case 10:case"end":return e.stop()}},e,t,[[0,7]])}))()},pageMove:function(t){this.page=t,this.history()},nowPageActive:function(t){var e=[];return t===this.page&&e.push("active"),e}},created:function(){this.historyData=this.history()},computed:{pageList:function(){var t=[],e=Math.floor(this.perPage/2);if(this.pageSize>this.perPage)if(this.page<=e+1)for(var a=1;a<=this.perPage;a++)t.push(a);else if(this.page>this.pageSize-e)for(var r=this.pageSize-(this.perPage-1);r<=this.pageSize;r++)t.push(r);else for(var n=this.page-e;n<=this.page+e;n++)t.push(n);else for(var s=1;s<=this.pageSize;s++)t.push(s);return t}}},X={render:function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("common",[a("h2",{staticClass:"mt-2"},[t._v("검색 내역")]),t._v(" "),a("p",{staticClass:"lead"},[a("b-list-group",{staticClass:"mt-2"},[!t.historyData.content||t.historyData.content.length<=0?a("b-list-group-item",[t._v("검색 결과가 없습니다.")]):t._e(),t._v(" "),t._l(t.historyData.content,function(e){return a("b-list-group-item",{key:e.id},[a("p",[t._v(t._s(e.keyword))]),t._v(" "),a("span",[t._v(t._s(t._f("moment")(e.createdDate,"YYYY년 M월 d일 H:m:s ")))])])})],2)],1),t._v(" "),t.historyData.content&&t.historyData.content.length>0?a("nav",{attrs:{"aria-label":"Page navigation example"}},[a("ul",{staticClass:"pagination"},[a("li",{staticClass:"page-item"},[a("a",{staticClass:"page-link",attrs:{"aria-label":"Previous"},on:{click:function(e){return e.preventDefault(),t.pageMove(1)}}},[a("span",{attrs:{"aria-hidden":"true"}},[t._v("«")])])]),t._v(" "),t._l(t.pageList,function(e){return a("li",{key:e,staticClass:"page-item",class:t.nowPageActive(e)},[a("a",{staticClass:"page-link",on:{click:function(a){return a.preventDefault(),t.pageMove(e)}}},[t._v(t._s(e))])])}),t._v(" "),a("li",{staticClass:"page-item"},[a("a",{staticClass:"page-link",attrs:{"aria-label":"Next"},on:{click:function(e){return e.preventDefault(),t.pageMove(t.pageSize)}}},[a("span",{attrs:{"aria-hidden":"true"}},[t._v("»")])])])],2)]):t._e()])},staticRenderFns:[]};var Q=a("VU/8")(G,X,!1,function(t){a("n0hZ")},"data-v-66bcfe3e",null).exports,tt={name:"Popular",data:function(){return{popularData:{}}},methods:{popular:function(){var t=this;return f()(v.a.mark(function e(){var a;return v.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,x.popular();case 3:200===(a=e.sent).status&&(t.popularData=a.data),e.next=10;break;case 7:e.prev=7,e.t0=e.catch(0),console.log(e.t0);case 10:case"end":return e.stop()}},e,t,[[0,7]])}))()}},created:function(){this.popularData=this.popular()}},et={render:function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("common",[a("h2",{staticClass:"mt-2"},[t._v("인기 검색")]),t._v(" "),a("p",{staticClass:"lead"},[a("b-list-group",{staticClass:"mt-2"},[!t.popularData||t.popularData.length<=0?a("b-list-group-item",[t._v("검색 결과가 없습니다.")]):t._e(),t._v(" "),t._l(t.popularData,function(e,r){return a("b-list-group-item",{key:e.id},[a("p",[t._v(t._s(r+1)+"위")]),t._v(" "),a("span",[t._v(t._s(e.keyword)+" "+t._s(e.count)+"회")])])})],2)],1)])},staticRenderFns:[]};var at=a("VU/8")(tt,et,!1,function(t){a("KIjT")},"data-v-ade2aafa",null).exports;r.default.use(R.a);var rt=function(){return function(t,e,a){if(L.getters.getIsAuth)return a();a("/login")}},nt=new R.a({mode:"history",routes:[{path:"/",name:"Join",component:U},{path:"/login",name:"Login",component:$},{path:"/search",name:"Search",component:J,beforeEnter:rt()},{path:"/search/:id",name:"SearchView",component:q,beforeEnter:rt()},{path:"/history",name:"History",component:Q,beforeEnter:rt()},{path:"/popular",name:"Popular",component:at,beforeEnter:rt()}]}),st=(a("Jmt5"),a("9M+g"),{render:function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("body",{staticClass:"d-flex flex-column h-100"},[a("header",[a("b-navbar",{attrs:{toggleable:"md",type:"dark",variant:"info"}},[a("b-navbar-brand",{attrs:{to:"/search"}},[t._v("장소 검색 서비스")]),t._v(" "),a("b-navbar-toggle",{attrs:{target:"nav-collapse"}}),t._v(" "),a("b-collapse",{attrs:{id:"nav-collapse","is-nav":""}},[a("b-navbar-nav",[a("router-link",{staticClass:"nav-link",attrs:{to:"/search"}},[t._v("장소 검색")]),t._v(" "),a("router-link",{staticClass:"nav-link",attrs:{to:"/popular"}},[t._v("인기 검색")]),t._v(" "),a("router-link",{staticClass:"nav-link",attrs:{to:"/history"}},[t._v("검색 내역")])],1)],1)],1)],1),t._v(" "),a("main",{staticClass:"flex-shrink-0",attrs:{role:"main"}},[a("div",{staticClass:"container"},[t._t("default")],2)]),t._v(" "),t._m(0)])},staticRenderFns:[function(){var t=this.$createElement,e=this._self._c||t;return e("footer",{staticClass:"footer mt-auto py-3"},[e("div",{staticClass:"container"},[e("span",{staticClass:"text-muted"},[this._v("Place sticky footer content here.")])])])}]});var ot=a("VU/8")({name:"Common",data:function(){return{}}},st,!1,function(t){a("TaFn")},"data-v-a02a16f8",null).exports;r.default.config.productionTip=!1,r.default.use(n.a),r.default.use(o.a),r.default.component("common",ot),r.default.prototype.EventBus=new r.default,new r.default({el:"#app",router:nt,store:L,components:{App:u},template:""})},RF0M:function(t,e){},TaFn:function(t,e){},"Z3+D":function(t,e){},fI9g:function(t,e){},n0hZ:function(t,e){},wZo1:function(t,e){},ztJ8:function(t,e){}},["NHnr"]); 2 | //# sourceMappingURL=app.43a8422c172b56127065.js.map -------------------------------------------------------------------------------- /src/main/resources/static/static/js/manifest.2ae2e69a05c33dfc65f8.js: -------------------------------------------------------------------------------- 1 | !function(r){var n=window.webpackJsonp;window.webpackJsonp=function(e,u,c){for(var f,i,p,a=0,l=[];a