├── .gitignore ├── appendixA ├── .github │ └── workflows │ │ └── cicd.yml ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ ├── java │ │ └── me │ │ │ └── shinsunyoung │ │ │ ├── Main.java │ │ │ └── springbootdeveloper │ │ │ ├── SpringBootDeveloperApplication.java │ │ │ ├── config │ │ │ ├── TokenAuthenticationFilter.java │ │ │ ├── WebOAuthSecurityConfig.java │ │ │ ├── WebSecurityConfig.java │ │ │ ├── jwt │ │ │ │ ├── JwtProperties.java │ │ │ │ └── TokenProvider.java │ │ │ └── oauth │ │ │ │ ├── OAuth2AuthorizationRequestBasedOnCookieRepository.java │ │ │ │ ├── OAuth2SuccessHandler.java │ │ │ │ └── OAuth2UserCustomService.java │ │ │ ├── controller │ │ │ ├── BlogApiController.java │ │ │ ├── BlogViewController.java │ │ │ ├── ExampleController.java │ │ │ ├── TokenApiController.java │ │ │ ├── UserApiController.java │ │ │ └── UserViewController.java │ │ │ ├── domain │ │ │ ├── Article.java │ │ │ ├── RefreshToken.java │ │ │ └── User.java │ │ │ ├── dto │ │ │ ├── AddArticleRequest.java │ │ │ ├── AddUserRequest.java │ │ │ ├── ArticleListViewResponse.java │ │ │ ├── ArticleResponse.java │ │ │ ├── ArticleViewResponse.java │ │ │ ├── CreateAccessTokenRequest.java │ │ │ ├── CreateAccessTokenResponse.java │ │ │ └── UpdateArticleRequest.java │ │ │ ├── repository │ │ │ ├── BlogRepository.java │ │ │ ├── RefreshTokenRepository.java │ │ │ └── UserRepository.java │ │ │ ├── service │ │ │ ├── BlogService.java │ │ │ ├── RefreshTokenService.java │ │ │ ├── TokenService.java │ │ │ ├── UserDetailService.java │ │ │ └── UserService.java │ │ │ └── util │ │ │ └── CookieUtil.java │ └── resources │ │ ├── application.yml │ │ ├── data.sql │ │ ├── static │ │ ├── img │ │ │ └── google.png │ │ └── js │ │ │ ├── article.js │ │ │ └── token.js │ │ └── templates │ │ ├── article.html │ │ ├── articleList.html │ │ ├── example.html │ │ ├── login.html │ │ ├── newArticle.html │ │ ├── oauthLogin.html │ │ └── signup.html │ └── test │ └── java │ └── me │ └── shinsunyoung │ └── springbootdeveloper │ ├── config │ └── jwt │ │ ├── JwtFactory.java │ │ └── TokenProviderTest.java │ └── controller │ ├── BlogApiControllerTest.java │ └── TokenApiControllerTest.java ├── appendixB ├── .github │ └── workflows │ │ └── cicd.yml ├── .gitignore ├── README.md ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ ├── java │ │ └── me │ │ │ └── shinsunyoung │ │ │ └── springbootdeveloper │ │ │ ├── SpringBootDeveloperApplication.java │ │ │ ├── config │ │ │ ├── TokenAuthenticationFilter.java │ │ │ ├── WebOAuthSecurityConfig.java │ │ │ ├── WebSecurityConfig.java │ │ │ ├── error │ │ │ │ ├── ErrorCode.java │ │ │ │ ├── ErrorResponse.java │ │ │ │ ├── GlobalExceptionHandler.java │ │ │ │ └── exception │ │ │ │ │ ├── ArticleNotFoundException.java │ │ │ │ │ ├── BusinessBaseException.java │ │ │ │ │ └── NotFoundException.java │ │ │ ├── jwt │ │ │ │ ├── JwtProperties.java │ │ │ │ └── TokenProvider.java │ │ │ └── oauth │ │ │ │ ├── OAuth2AuthorizationRequestBasedOnCookieRepository.java │ │ │ │ ├── OAuth2SuccessHandler.java │ │ │ │ └── OAuth2UserCustomService.java │ │ │ ├── controller │ │ │ ├── BlogApiController.java │ │ │ ├── BlogViewController.java │ │ │ ├── ExampleController.java │ │ │ ├── TokenApiController.java │ │ │ ├── UserApiController.java │ │ │ └── UserViewController.java │ │ │ ├── domain │ │ │ ├── Article.java │ │ │ ├── RefreshToken.java │ │ │ └── User.java │ │ │ ├── dto │ │ │ ├── AddArticleRequest.java │ │ │ ├── AddUserRequest.java │ │ │ ├── ArticleListViewResponse.java │ │ │ ├── ArticleResponse.java │ │ │ ├── ArticleViewResponse.java │ │ │ ├── CreateAccessTokenRequest.java │ │ │ ├── CreateAccessTokenResponse.java │ │ │ └── UpdateArticleRequest.java │ │ │ ├── repository │ │ │ ├── BlogRepository.java │ │ │ ├── RefreshTokenRepository.java │ │ │ └── UserRepository.java │ │ │ ├── service │ │ │ ├── BlogService.java │ │ │ ├── RefreshTokenService.java │ │ │ ├── TokenService.java │ │ │ ├── UserDetailService.java │ │ │ └── UserService.java │ │ │ └── util │ │ │ └── CookieUtil.java │ └── resources │ │ ├── application.yml │ │ ├── data.sql │ │ ├── static │ │ ├── img │ │ │ └── google.png │ │ └── js │ │ │ ├── article.js │ │ │ └── token.js │ │ └── templates │ │ ├── article.html │ │ ├── articleList.html │ │ ├── example.html │ │ ├── login.html │ │ ├── newArticle.html │ │ ├── oauthLogin.html │ │ └── signup.html │ └── test │ └── java │ └── me │ └── shinsunyoung │ └── springbootdeveloper │ ├── config │ └── jwt │ │ ├── JwtFactory.java │ │ └── TokenProviderTest.java │ └── controller │ ├── BlogApiControllerTest.java │ └── TokenApiControllerTest.java ├── appendixC ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ ├── java │ │ └── me │ │ │ └── shinsunyoung │ │ │ ├── Main.java │ │ │ └── springbootdeveloper │ │ │ ├── SpringBootDeveloperApplication.java │ │ │ ├── controller │ │ │ └── BlogApiController.java │ │ │ ├── domain │ │ │ └── Article.java │ │ │ ├── dto │ │ │ ├── AddArticleRequest.java │ │ │ ├── ArticleResponse.java │ │ │ └── UpdateArticleRequest.java │ │ │ ├── repository │ │ │ └── BlogRepository.java │ │ │ └── service │ │ │ └── BlogService.java │ └── resources │ │ ├── application.yml │ │ └── data.sql │ └── test │ └── java │ └── me │ └── shinsunyoung │ └── springbootdeveloper │ └── controller │ └── BlogApiControllerTest.java ├── chapter0 ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ └── main │ ├── java │ └── me │ │ └── shinsunyoung │ │ ├── Main.java │ │ └── springbootdeveloper │ │ └── SpringBootDeveloperApplication.java │ └── resources │ └── static │ └── index.html ├── chapter1 ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ └── main │ ├── java │ └── me │ │ └── shinsunyoung │ │ ├── Main.java │ │ └── springbootdeveloper │ │ └── SpringBootDeveloperApplication.java │ └── resources │ └── static │ └── index.html ├── chapter10 ├── README.md ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ ├── java │ │ └── me │ │ │ └── shinsunyoung │ │ │ ├── Main.java │ │ │ └── springbootdeveloper │ │ │ ├── SpringBootDeveloperApplication.java │ │ │ ├── config │ │ │ ├── TokenAuthenticationFilter.java │ │ │ ├── WebOAuthSecurityConfig.java │ │ │ ├── WebSecurityConfig.java │ │ │ ├── jwt │ │ │ │ ├── JwtProperties.java │ │ │ │ └── TokenProvider.java │ │ │ └── oauth │ │ │ │ ├── OAuth2AuthorizationRequestBasedOnCookieRepository.java │ │ │ │ ├── OAuth2SuccessHandler.java │ │ │ │ └── OAuth2UserCustomService.java │ │ │ ├── controller │ │ │ ├── BlogApiController.java │ │ │ ├── BlogViewController.java │ │ │ ├── ExampleController.java │ │ │ ├── TokenApiController.java │ │ │ ├── UserApiController.java │ │ │ └── UserViewController.java │ │ │ ├── domain │ │ │ ├── Article.java │ │ │ ├── RefreshToken.java │ │ │ └── User.java │ │ │ ├── dto │ │ │ ├── AddArticleRequest.java │ │ │ ├── AddUserRequest.java │ │ │ ├── ArticleListViewResponse.java │ │ │ ├── ArticleResponse.java │ │ │ ├── ArticleViewResponse.java │ │ │ ├── CreateAccessTokenRequest.java │ │ │ ├── CreateAccessTokenResponse.java │ │ │ └── UpdateArticleRequest.java │ │ │ ├── repository │ │ │ ├── BlogRepository.java │ │ │ ├── RefreshTokenRepository.java │ │ │ └── UserRepository.java │ │ │ ├── service │ │ │ ├── BlogService.java │ │ │ ├── RefreshTokenService.java │ │ │ ├── TokenService.java │ │ │ ├── UserDetailService.java │ │ │ └── UserService.java │ │ │ └── util │ │ │ └── CookieUtil.java │ └── resources │ │ ├── application.yml │ │ ├── data.sql │ │ ├── static │ │ ├── img │ │ │ └── google.png │ │ └── js │ │ │ ├── article.js │ │ │ └── token.js │ │ └── templates │ │ ├── article.html │ │ ├── articleList.html │ │ ├── example.html │ │ ├── login.html │ │ ├── newArticle.html │ │ ├── oauthLogin.html │ │ └── signup.html │ └── test │ └── java │ └── me │ └── shinsunyoung │ └── springbootdeveloper │ ├── config │ └── jwt │ │ ├── JwtFactory.java │ │ └── TokenProviderTest.java │ └── controller │ ├── BlogApiControllerTest.java │ └── TokenApiControllerTest.java ├── chapter11 ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ ├── java │ │ └── me │ │ │ └── shinsunyoung │ │ │ ├── Main.java │ │ │ └── springbootdeveloper │ │ │ ├── SpringBootDeveloperApplication.java │ │ │ ├── config │ │ │ ├── TokenAuthenticationFilter.java │ │ │ ├── WebOAuthSecurityConfig.java │ │ │ ├── WebSecurityConfig.java │ │ │ ├── jwt │ │ │ │ ├── JwtProperties.java │ │ │ │ └── TokenProvider.java │ │ │ └── oauth │ │ │ │ ├── OAuth2AuthorizationRequestBasedOnCookieRepository.java │ │ │ │ ├── OAuth2SuccessHandler.java │ │ │ │ └── OAuth2UserCustomService.java │ │ │ ├── controller │ │ │ ├── BlogApiController.java │ │ │ ├── BlogViewController.java │ │ │ ├── ExampleController.java │ │ │ ├── TokenApiController.java │ │ │ ├── UserApiController.java │ │ │ └── UserViewController.java │ │ │ ├── domain │ │ │ ├── Article.java │ │ │ ├── RefreshToken.java │ │ │ └── User.java │ │ │ ├── dto │ │ │ ├── AddArticleRequest.java │ │ │ ├── AddUserRequest.java │ │ │ ├── ArticleListViewResponse.java │ │ │ ├── ArticleResponse.java │ │ │ ├── ArticleViewResponse.java │ │ │ ├── CreateAccessTokenRequest.java │ │ │ ├── CreateAccessTokenResponse.java │ │ │ └── UpdateArticleRequest.java │ │ │ ├── repository │ │ │ ├── BlogRepository.java │ │ │ ├── RefreshTokenRepository.java │ │ │ └── UserRepository.java │ │ │ ├── service │ │ │ ├── BlogService.java │ │ │ ├── RefreshTokenService.java │ │ │ ├── TokenService.java │ │ │ ├── UserDetailService.java │ │ │ └── UserService.java │ │ │ └── util │ │ │ └── CookieUtil.java │ └── resources │ │ ├── application.yml │ │ ├── data.sql │ │ ├── static │ │ ├── img │ │ │ └── google.png │ │ └── js │ │ │ ├── article.js │ │ │ └── token.js │ │ └── templates │ │ ├── article.html │ │ ├── articleList.html │ │ ├── example.html │ │ ├── login.html │ │ ├── newArticle.html │ │ ├── oauthLogin.html │ │ └── signup.html │ └── test │ └── java │ └── me │ └── shinsunyoung │ └── springbootdeveloper │ ├── config │ └── jwt │ │ ├── JwtFactory.java │ │ └── TokenProviderTest.java │ └── controller │ ├── BlogApiControllerTest.java │ └── TokenApiControllerTest.java ├── chapter12 ├── .github │ └── workflows │ │ ├── ci.yml │ │ └── cicd.yml ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ ├── java │ │ └── me │ │ │ └── shinsunyoung │ │ │ ├── Main.java │ │ │ └── springbootdeveloper │ │ │ ├── SpringBootDeveloperApplication.java │ │ │ ├── config │ │ │ ├── TokenAuthenticationFilter.java │ │ │ ├── WebOAuthSecurityConfig.java │ │ │ ├── WebSecurityConfig.java │ │ │ ├── jwt │ │ │ │ ├── JwtProperties.java │ │ │ │ └── TokenProvider.java │ │ │ └── oauth │ │ │ │ ├── OAuth2AuthorizationRequestBasedOnCookieRepository.java │ │ │ │ ├── OAuth2SuccessHandler.java │ │ │ │ └── OAuth2UserCustomService.java │ │ │ ├── controller │ │ │ ├── BlogApiController.java │ │ │ ├── BlogViewController.java │ │ │ ├── ExampleController.java │ │ │ ├── TokenApiController.java │ │ │ ├── UserApiController.java │ │ │ └── UserViewController.java │ │ │ ├── domain │ │ │ ├── Article.java │ │ │ ├── RefreshToken.java │ │ │ └── User.java │ │ │ ├── dto │ │ │ ├── AddArticleRequest.java │ │ │ ├── AddUserRequest.java │ │ │ ├── ArticleListViewResponse.java │ │ │ ├── ArticleResponse.java │ │ │ ├── ArticleViewResponse.java │ │ │ ├── CreateAccessTokenRequest.java │ │ │ ├── CreateAccessTokenResponse.java │ │ │ └── UpdateArticleRequest.java │ │ │ ├── repository │ │ │ ├── BlogRepository.java │ │ │ ├── RefreshTokenRepository.java │ │ │ └── UserRepository.java │ │ │ ├── service │ │ │ ├── BlogService.java │ │ │ ├── RefreshTokenService.java │ │ │ ├── TokenService.java │ │ │ ├── UserDetailService.java │ │ │ └── UserService.java │ │ │ └── util │ │ │ └── CookieUtil.java │ └── resources │ │ ├── application.yml │ │ ├── data.sql │ │ ├── static │ │ ├── img │ │ │ └── google.png │ │ └── js │ │ │ ├── article.js │ │ │ └── token.js │ │ └── templates │ │ ├── article.html │ │ ├── articleList.html │ │ ├── example.html │ │ ├── login.html │ │ ├── newArticle.html │ │ ├── oauthLogin.html │ │ └── signup.html │ └── test │ └── java │ └── me │ └── shinsunyoung │ └── springbootdeveloper │ ├── config │ └── jwt │ │ ├── JwtFactory.java │ │ └── TokenProviderTest.java │ └── controller │ ├── BlogApiControllerTest.java │ └── TokenApiControllerTest.java ├── chapter2 ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ └── main │ ├── java │ └── me │ │ └── shinsunyoung │ │ ├── Main.java │ │ └── springbootdeveloper │ │ ├── SpringBootDeveloperApplication.java │ │ └── TestController.java │ └── resources │ └── static │ └── index.html ├── chapter3 ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ └── main │ ├── java │ └── me │ │ └── shinsunyoung │ │ ├── Main.java │ │ └── springbootdeveloper │ │ ├── Member.java │ │ ├── MemberRepository.java │ │ ├── SpringBootDeveloperApplication.java │ │ ├── TestController.java │ │ └── TestService.java │ └── resources │ ├── application.yml │ ├── data.sql │ └── static │ └── index.html ├── chapter4 ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ ├── java │ │ └── me │ │ │ └── shinsunyoung │ │ │ ├── Main.java │ │ │ └── springbootdeveloper │ │ │ ├── Member.java │ │ │ ├── MemberRepository.java │ │ │ ├── QuizController.java │ │ │ ├── SpringBootDeveloperApplication.java │ │ │ ├── TestController.java │ │ │ └── TestService.java │ └── resources │ │ ├── application.yml │ │ ├── data.sql │ │ └── static │ │ └── index.html │ └── test │ └── java │ ├── JUnitCycleTest.java │ ├── JUnitQuiz.java │ ├── JUnitTest.java │ ├── JunitCycleQuiz.java │ └── me │ └── shinsunyoung │ └── springbootdeveloper │ ├── QuizControllerTest.java │ └── TestControllerTest.java ├── chapter5 ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ ├── java │ │ └── me │ │ │ └── shinsunyoung │ │ │ ├── Main.java │ │ │ └── springbootdeveloper │ │ │ ├── Member.java │ │ │ ├── MemberRepository.java │ │ │ ├── QuizController.java │ │ │ ├── SpringBootDeveloperApplication.java │ │ │ ├── TestController.java │ │ │ └── TestService.java │ └── resources │ │ ├── application.yml │ │ ├── data.sql │ │ └── static │ │ └── index.html │ └── test │ ├── java │ ├── JUnitCycleTest.java │ ├── JUnitQuiz.java │ ├── JUnitTest.java │ ├── JunitCycleQuiz.java │ └── me │ │ └── shinsunyoung │ │ └── springbootdeveloper │ │ ├── MemberRepositoryTest.java │ │ ├── QuizControllerTest.java │ │ └── TestControllerTest.java │ └── resources │ ├── application.yml │ └── insert-members.sql ├── chapter6 ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ ├── java │ │ └── me │ │ │ └── shinsunyoung │ │ │ ├── Main.java │ │ │ └── springbootdeveloper │ │ │ ├── SpringBootDeveloperApplication.java │ │ │ ├── controller │ │ │ └── BlogApiController.java │ │ │ ├── domain │ │ │ └── Article.java │ │ │ ├── dto │ │ │ ├── AddArticleRequest.java │ │ │ ├── ArticleResponse.java │ │ │ └── UpdateArticleRequest.java │ │ │ ├── repository │ │ │ └── BlogRepository.java │ │ │ └── service │ │ │ └── BlogService.java │ └── resources │ │ ├── application.yml │ │ └── data.sql │ └── test │ └── java │ └── me │ └── shinsunyoung │ └── springbootdeveloper │ └── controller │ └── BlogApiControllerTest.java ├── chapter7 ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ ├── java │ │ └── me │ │ │ └── shinsunyoung │ │ │ ├── Main.java │ │ │ └── springbootdeveloper │ │ │ ├── SpringBootDeveloperApplication.java │ │ │ ├── controller │ │ │ ├── BlogApiController.java │ │ │ ├── BlogViewController.java │ │ │ └── ExampleController.java │ │ │ ├── domain │ │ │ └── Article.java │ │ │ ├── dto │ │ │ ├── AddArticleRequest.java │ │ │ ├── ArticleListViewResponse.java │ │ │ ├── ArticleResponse.java │ │ │ ├── ArticleViewResponse.java │ │ │ └── UpdateArticleRequest.java │ │ │ ├── repository │ │ │ └── BlogRepository.java │ │ │ └── service │ │ │ └── BlogService.java │ └── resources │ │ ├── application.yml │ │ ├── data.sql │ │ ├── static │ │ └── js │ │ │ └── article.js │ │ └── templates │ │ ├── article.html │ │ ├── articleList.html │ │ ├── example.html │ │ └── newArticle.html │ └── test │ └── java │ └── me │ └── shinsunyoung │ └── springbootdeveloper │ └── controller │ └── BlogApiControllerTest.java ├── chapter8 ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ ├── main │ ├── java │ │ └── me │ │ │ └── shinsunyoung │ │ │ ├── Main.java │ │ │ └── springbootdeveloper │ │ │ ├── SpringBootDeveloperApplication.java │ │ │ ├── config │ │ │ └── WebSecurityConfig.java │ │ │ ├── controller │ │ │ ├── BlogApiController.java │ │ │ ├── BlogViewController.java │ │ │ ├── ExampleController.java │ │ │ ├── UserApiController.java │ │ │ └── UserViewController.java │ │ │ ├── domain │ │ │ ├── Article.java │ │ │ └── User.java │ │ │ ├── dto │ │ │ ├── AddArticleRequest.java │ │ │ ├── AddUserRequest.java │ │ │ ├── ArticleListViewResponse.java │ │ │ ├── ArticleResponse.java │ │ │ ├── ArticleViewResponse.java │ │ │ └── UpdateArticleRequest.java │ │ │ ├── repository │ │ │ ├── BlogRepository.java │ │ │ └── UserRepository.java │ │ │ └── service │ │ │ ├── BlogService.java │ │ │ ├── UserDetailService.java │ │ │ └── UserService.java │ └── resources │ │ ├── application.yml │ │ ├── data.sql │ │ ├── static │ │ └── js │ │ │ └── article.js │ │ └── templates │ │ ├── article.html │ │ ├── articleList.html │ │ ├── example.html │ │ ├── login.html │ │ ├── newArticle.html │ │ └── signup.html │ └── test │ └── java │ └── me │ └── shinsunyoung │ └── springbootdeveloper │ └── controller │ └── BlogApiControllerTest.java └── chapter9 ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── me │ │ └── shinsunyoung │ │ ├── Main.java │ │ └── springbootdeveloper │ │ ├── SpringBootDeveloperApplication.java │ │ ├── config │ │ ├── TokenAuthenticationFilter.java │ │ ├── WebSecurityConfig.java │ │ └── jwt │ │ │ ├── JwtProperties.java │ │ │ └── TokenProvider.java │ │ ├── controller │ │ ├── BlogApiController.java │ │ ├── BlogViewController.java │ │ ├── ExampleController.java │ │ ├── TokenApiController.java │ │ ├── UserApiController.java │ │ └── UserViewController.java │ │ ├── domain │ │ ├── Article.java │ │ ├── RefreshToken.java │ │ └── User.java │ │ ├── dto │ │ ├── AddArticleRequest.java │ │ ├── AddUserRequest.java │ │ ├── ArticleListViewResponse.java │ │ ├── ArticleResponse.java │ │ ├── ArticleViewResponse.java │ │ ├── CreateAccessTokenRequest.java │ │ ├── CreateAccessTokenResponse.java │ │ └── UpdateArticleRequest.java │ │ ├── repository │ │ ├── BlogRepository.java │ │ ├── RefreshTokenRepository.java │ │ └── UserRepository.java │ │ └── service │ │ ├── BlogService.java │ │ ├── RefreshTokenService.java │ │ ├── TokenService.java │ │ ├── UserDetailService.java │ │ └── UserService.java └── resources │ ├── application.yml │ ├── data.sql │ ├── static │ └── js │ │ └── article.js │ └── templates │ ├── article.html │ ├── articleList.html │ ├── example.html │ ├── login.html │ ├── newArticle.html │ └── signup.html └── test └── java └── me └── shinsunyoung └── springbootdeveloper ├── config └── jwt │ ├── JwtFactory.java │ └── TokenProviderTest.java └── controller ├── BlogApiControllerTest.java └── TokenApiControllerTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | .DS_Store -------------------------------------------------------------------------------- /appendixA/.github/workflows/cicd.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - uses: actions/setup-java@v3 15 | with: 16 | distribution: 'corretto' 17 | java-version: '17' 18 | 19 | - name: Grant execute permission for gradlew 20 | run: chmod +x gradlew 21 | 22 | - name: Build with Gradle 23 | run: ./gradlew clean build 24 | 25 | - name: Get current time 26 | uses: josStorer/get-current-time@v2.0.2 27 | id: current-time 28 | with: 29 | format: YYYY-MM-DDTHH-mm-ss 30 | utcOffset: "+09:00" 31 | 32 | - name: Set artifact 33 | run: echo "artifact=$(ls ./build/libs)" >> $GITHUB_ENV 34 | 35 | - name: Beanstalk Deploy 36 | uses: einaregilsson/beanstalk-deploy@v20 37 | with: 38 | aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }} 39 | aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 40 | application_name: spring-boot-developer 41 | environment_name: SpringBootDeveloper-env 42 | version_label: github-action-${{steps.current-time.outputs.formattedTime}} 43 | region: ap-northeast-2 44 | deployment_package: ./build/libs/${{env.artifact}} 45 | -------------------------------------------------------------------------------- /appendixA/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/appendixA/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /appendixA/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 04 17:36:39 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /appendixA/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-developer' 2 | 3 | -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/Main.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world!"); 6 | } 7 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 6 | 7 | @EnableJpaAuditing 8 | @SpringBootApplication 9 | public class SpringBootDeveloperApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 12 | } 13 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/config/jwt/JwtProperties.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.config.jwt; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Setter 9 | @Getter 10 | @Component 11 | @ConfigurationProperties("jwt") 12 | public class JwtProperties { 13 | private String issuer; 14 | private String secretKey; 15 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/controller/ExampleController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.ui.Model; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | 9 | import java.time.LocalDate; 10 | import java.util.List; 11 | 12 | @Controller 13 | public class ExampleController { 14 | @GetMapping("/thymeleaf/example") 15 | public String thymeleafExample(Model model) { 16 | Person examplePerson = new Person(); 17 | examplePerson.setId(1L); 18 | examplePerson.setName("홍길동"); 19 | examplePerson.setAge(11); 20 | examplePerson.setHobbies(List.of("운동", "독서")); 21 | 22 | model.addAttribute("person", examplePerson); 23 | model.addAttribute("today", LocalDate.now()); 24 | 25 | return "example"; 26 | } 27 | @Setter 28 | @Getter 29 | class Person { 30 | private Long id; 31 | private String name; 32 | private int age; 33 | private List hobbies; 34 | } 35 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/controller/TokenApiController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.dto.CreateAccessTokenRequest; 5 | import me.shinsunyoung.springbootdeveloper.dto.CreateAccessTokenResponse; 6 | import me.shinsunyoung.springbootdeveloper.service.TokenService; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | @RequiredArgsConstructor 14 | @RestController 15 | public class TokenApiController { 16 | private final TokenService tokenService; 17 | 18 | @PostMapping("/api/token") 19 | public ResponseEntity createNewAccessToken(@RequestBody CreateAccessTokenRequest request) { 20 | String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken()); 21 | 22 | return ResponseEntity.status(HttpStatus.CREATED) 23 | .body(new CreateAccessTokenResponse(newAccessToken)); 24 | } 25 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/controller/UserViewController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class UserViewController { 8 | @GetMapping("/login") 9 | public String login() { 10 | return "oauthLogin"; 11 | } 12 | 13 | @GetMapping("/signup") 14 | public String signup() { 15 | return "signup"; 16 | } 17 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/domain/RefreshToken.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.domain; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 9 | @Getter 10 | @Entity 11 | public class RefreshToken { 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.IDENTITY) 14 | @Column(name = "id", updatable = false) 15 | private Long id; 16 | 17 | @Column(name = "user_id", nullable = false, unique = true) 18 | private Long userId; 19 | 20 | @Column(name = "refresh_token", nullable = false) 21 | private String refreshToken; 22 | 23 | public RefreshToken(Long userId, String refreshToken) { 24 | this.userId = userId; 25 | this.refreshToken = refreshToken; 26 | } 27 | 28 | public RefreshToken update(String newRefreshToken) { 29 | this.refreshToken = newRefreshToken; 30 | return this; 31 | } 32 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import jakarta.validation.constraints.Size; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import me.shinsunyoung.springbootdeveloper.domain.Article; 9 | 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | @Getter 13 | public class AddArticleRequest { 14 | 15 | @NotNull 16 | @Size(min = 1, max = 10) 17 | private String title; 18 | 19 | @NotNull 20 | private String content; 21 | 22 | public Article toEntity(String author) { 23 | return Article.builder() 24 | .title(title) 25 | .content(content) 26 | .author(author) 27 | .build(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddUserRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class AddUserRequest { 9 | private String email; 10 | private String password; 11 | } 12 | -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleListViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleListViewResponse { 8 | private final Long id; 9 | private final String title; 10 | private final String content; 11 | 12 | public ArticleListViewResponse(Article article) { 13 | this.id = article.getId(); 14 | this.title = article.getTitle(); 15 | this.content = article.getContent(); 16 | } 17 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleResponse { 8 | private final String title; 9 | private final String content; 10 | public ArticleResponse(Article article) { 11 | this.title = article.getTitle(); 12 | this.content = article.getContent(); 13 | } 14 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import me.shinsunyoung.springbootdeveloper.domain.Article; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | @NoArgsConstructor 10 | @Getter 11 | public class ArticleViewResponse { 12 | private Long id; 13 | private String title; 14 | private String content; 15 | private LocalDateTime createdAt; 16 | private String author; 17 | 18 | public ArticleViewResponse(Article article) { 19 | this.id = article.getId(); 20 | this.title = article.getTitle(); 21 | this.content = article.getContent(); 22 | this.createdAt = article.getCreatedAt(); 23 | this.author = article.getAuthor(); 24 | } 25 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/dto/CreateAccessTokenRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class CreateAccessTokenRequest { 9 | private String refreshToken; 10 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/dto/CreateAccessTokenResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @AllArgsConstructor 7 | @Getter 8 | public class CreateAccessTokenResponse { 9 | private String accessToken; 10 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/dto/UpdateArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @NoArgsConstructor 8 | @AllArgsConstructor 9 | @Getter 10 | public class UpdateArticleRequest { 11 | private String title; 12 | private String content; 13 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/repository/BlogRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.Article; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface BlogRepository extends JpaRepository { 7 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/repository/RefreshTokenRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.RefreshToken; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface RefreshTokenRepository extends JpaRepository { 9 | Optional findByUserId(Long userId); 10 | Optional findByRefreshToken(String refreshToken); 11 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface UserRepository extends JpaRepository { 9 | Optional findByEmail(String email); 10 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/service/RefreshTokenService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.RefreshToken; 5 | import me.shinsunyoung.springbootdeveloper.repository.RefreshTokenRepository; 6 | import org.springframework.stereotype.Service; 7 | 8 | @RequiredArgsConstructor 9 | @Service 10 | public class RefreshTokenService { 11 | private final RefreshTokenRepository refreshTokenRepository; 12 | public RefreshToken findByRefreshToken(String refreshToken) { 13 | return refreshTokenRepository.findByRefreshToken(refreshToken) 14 | .orElseThrow(() -> new IllegalArgumentException("Unexpected token")); 15 | } 16 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/service/TokenService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.config.jwt.TokenProvider; 5 | import me.shinsunyoung.springbootdeveloper.domain.User; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.time.Duration; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class TokenService { 13 | private final TokenProvider tokenProvider; 14 | private final RefreshTokenService refreshTokenService; 15 | private final UserService userService; 16 | 17 | public String createNewAccessToken(String refreshToken) { 18 | if(!tokenProvider.validToken(refreshToken)) { 19 | throw new IllegalArgumentException("Unexpected token"); 20 | } 21 | 22 | Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId(); 23 | User user = userService.findById(userId); 24 | 25 | return tokenProvider.generateToken(user, Duration.ofHours(2)); 26 | } 27 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/service/UserDetailService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.User; 5 | import me.shinsunyoung.springbootdeveloper.repository.UserRepository; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class UserDetailService implements UserDetailsService { 12 | private final UserRepository userRepository; 13 | @Override 14 | public User loadUserByUsername(String email) { 15 | return userRepository.findByEmail(email) 16 | .orElseThrow(() -> new IllegalArgumentException((email))); 17 | } 18 | } -------------------------------------------------------------------------------- /appendixA/src/main/java/me/shinsunyoung/springbootdeveloper/service/UserService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.User; 5 | import me.shinsunyoung.springbootdeveloper.dto.AddUserRequest; 6 | import me.shinsunyoung.springbootdeveloper.repository.UserRepository; 7 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 8 | import org.springframework.stereotype.Service; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class UserService { 13 | private final UserRepository userRepository; 14 | 15 | public Long save(AddUserRequest dto) { 16 | BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); 17 | return userRepository.save(User.builder() 18 | .email(dto.getEmail()) 19 | .password(encoder.encode(dto.getPassword())) 20 | .build()).getId(); 21 | } 22 | 23 | public User findById(Long userId) { 24 | return userRepository.findById(userId) 25 | .orElseThrow(() -> new IllegalArgumentException("Unexpected user")); 26 | } 27 | 28 | public User findByEmail(String email) { 29 | return userRepository.findByEmail(email) 30 | .orElseThrow(() -> new IllegalArgumentException("Unexpected user")); 31 | } 32 | } -------------------------------------------------------------------------------- /appendixA/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true 4 | properties: 5 | hibernate: 6 | format_sql: true 7 | defer-datasource-initialization: true 8 | h2: 9 | console: 10 | enabled: true 11 | security: 12 | oauth2: 13 | client: 14 | registration: 15 | google: 16 | client-id: 6636422.. 17 | client-secret: GOCSP.. 18 | scope: 19 | - email 20 | - profile 21 | 22 | 23 | jwt: 24 | issuer: ajufresh@gmail.com 25 | secret_key: study-springboot 26 | -------------------------------------------------------------------------------- /appendixA/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목1', '내용1', 'user1', NOW(), NOW()) 2 | INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목2', '내용2', 'user2', NOW(), NOW()) 3 | INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목3', '내용3', 'user3', NOW(), NOW()) -------------------------------------------------------------------------------- /appendixA/src/main/resources/static/img/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/appendixA/src/main/resources/static/img/google.png -------------------------------------------------------------------------------- /appendixA/src/main/resources/static/js/token.js: -------------------------------------------------------------------------------- 1 | const token = searchParam('token') 2 | 3 | if (token) { 4 | localStorage.setItem("access_token", token) 5 | } 6 | 7 | function searchParam(key) { 8 | return new URLSearchParams(location.search).get(key); 9 | } -------------------------------------------------------------------------------- /appendixA/src/main/resources/templates/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |

타임리프 익히기

9 |

10 |
11 |

12 |

13 |

취미

14 |
    15 |
  • 16 | (대표 취미) 17 |
18 |
19 | 20 | 글 보기 21 | 22 | -------------------------------------------------------------------------------- /appendixA/src/main/resources/templates/oauthLogin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |

LOGIN

22 |

서비스 사용을 위해 로그인을 해주세요!

23 | 24 |
25 | 26 | 27 | 28 |
29 |
30 |
31 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /appendixB/.github/workflows/cicd.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - uses: actions/setup-java@v3 15 | with: 16 | distribution: 'corretto' 17 | java-version: '17' 18 | 19 | - name: Grant execute permission for gradlew 20 | run: chmod +x gradlew 21 | 22 | - name: Build with Gradle 23 | run: ./gradlew clean build 24 | 25 | - name: Get current time 26 | uses: josStorer/get-current-time@v2.0.2 27 | id: current-time 28 | with: 29 | format: YYYY-MM-DDTHH-mm-ss 30 | utcOffset: "+09:00" 31 | 32 | - name: Set artifact 33 | run: echo "artifact=$(ls ./build/libs)" >> $GITHUB_ENV 34 | 35 | - name: Beanstalk Deploy 36 | uses: einaregilsson/beanstalk-deploy@v20 37 | with: 38 | aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }} 39 | aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 40 | application_name: spring-boot-developer 41 | environment_name: SpringBootDeveloper-env 42 | version_label: github-action-${{steps.current-time.outputs.formattedTime}} 43 | region: ap-northeast-2 44 | deployment_package: ./build/libs/${{env.artifact}} 45 | -------------------------------------------------------------------------------- /appendixB/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | .DS_Store 30 | -------------------------------------------------------------------------------- /appendixB/README.md: -------------------------------------------------------------------------------- 1 | # spring-boot-developer 2 | -------------------------------------------------------------------------------- /appendixB/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/appendixB/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /appendixB/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /appendixB/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'spring-boot-developer' 2 | 3 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 6 | 7 | @EnableJpaAuditing 8 | @SpringBootApplication 9 | public class SpringBootDeveloperApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/config/error/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.config.error; 2 | 3 | import lombok.Getter; 4 | import org.springframework.http.HttpStatus; 5 | 6 | @Getter 7 | public enum ErrorCode { 8 | INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "E1", "올바르지 않은 입력값입니다."), 9 | METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "E2", "잘못된 HTTP 메서드를 호출했습니다."), 10 | INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E3", "서버 에러가 발생했습니다."), 11 | NOT_FOUND(HttpStatus.NOT_FOUND, "E4", "존재하지 않는 엔티티입니다."), 12 | 13 | ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "A1", "존재하지 않는 아티클입니다."); 14 | 15 | private final String message; 16 | 17 | private final String code; 18 | private final HttpStatus status; 19 | 20 | ErrorCode(final HttpStatus status, final String code, final String message) { 21 | this.status = status; 22 | this.code = code; 23 | this.message = message; 24 | } 25 | } -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/config/error/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.config.error; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Getter 8 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 9 | public class ErrorResponse { 10 | 11 | private String message; 12 | private String code; 13 | 14 | 15 | private ErrorResponse(final ErrorCode code) { 16 | this.message = code.getMessage(); 17 | this.code = code.getCode(); 18 | } 19 | 20 | public ErrorResponse(final ErrorCode code, final String message) { 21 | this.message = message; 22 | this.code = code.getCode(); 23 | } 24 | 25 | public static ErrorResponse of(final ErrorCode code) { 26 | return new ErrorResponse(code); 27 | } 28 | 29 | public static ErrorResponse of(final ErrorCode code, final String message) { 30 | return new ErrorResponse(code, message); 31 | } 32 | } -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/config/error/exception/ArticleNotFoundException.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.config.error.exception; 2 | 3 | 4 | import me.shinsunyoung.springbootdeveloper.config.error.ErrorCode; 5 | 6 | public class ArticleNotFoundException extends NotFoundException { 7 | public ArticleNotFoundException() { 8 | super(ErrorCode.ARTICLE_NOT_FOUND); 9 | } 10 | } -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/config/error/exception/BusinessBaseException.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.config.error.exception; 2 | 3 | import me.shinsunyoung.springbootdeveloper.config.error.ErrorCode; 4 | 5 | public class BusinessBaseException extends RuntimeException { 6 | 7 | private final ErrorCode errorCode; 8 | 9 | public BusinessBaseException(String message, ErrorCode errorCode) { 10 | super(message); 11 | this.errorCode = errorCode; 12 | } 13 | 14 | public BusinessBaseException(ErrorCode errorCode) { 15 | super(errorCode.getMessage()); 16 | this.errorCode = errorCode; 17 | } 18 | 19 | public ErrorCode getErrorCode() { 20 | return errorCode; 21 | } 22 | } -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/config/error/exception/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.config.error.exception; 2 | 3 | 4 | import me.shinsunyoung.springbootdeveloper.config.error.ErrorCode; 5 | 6 | public class NotFoundException extends BusinessBaseException { 7 | public NotFoundException(ErrorCode errorCode) { 8 | super(errorCode.getMessage(), errorCode); 9 | } 10 | 11 | public NotFoundException() { 12 | super(ErrorCode.NOT_FOUND); 13 | } 14 | } -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/config/jwt/JwtProperties.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.config.jwt; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Setter 9 | @Getter 10 | @Component 11 | @ConfigurationProperties("jwt") 12 | public class JwtProperties { 13 | 14 | private String issuer; 15 | private String secretKey; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/controller/ExampleController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.ui.Model; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | 9 | import java.time.LocalDate; 10 | import java.util.List; 11 | 12 | @Controller 13 | public class ExampleController { 14 | 15 | @GetMapping("/thymeleaf/example") 16 | public String thymeleafExample(Model model) { 17 | Person examplePerson = new Person(); 18 | examplePerson.setId(1L); 19 | examplePerson.setName("홍길동"); 20 | examplePerson.setAge(11); 21 | examplePerson.setHobbies(List.of("운동", "독서")); 22 | 23 | model.addAttribute("person", examplePerson); 24 | model.addAttribute("today", LocalDate.now()); 25 | 26 | return "example"; 27 | } 28 | 29 | @Setter 30 | @Getter 31 | class Person { 32 | private Long id; 33 | private String name; 34 | private int age; 35 | private List hobbies; 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/controller/TokenApiController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.dto.CreateAccessTokenRequest; 5 | import me.shinsunyoung.springbootdeveloper.dto.CreateAccessTokenResponse; 6 | import me.shinsunyoung.springbootdeveloper.service.TokenService; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | @RequiredArgsConstructor 14 | @RestController 15 | public class TokenApiController { 16 | 17 | private final TokenService tokenService; 18 | 19 | @PostMapping("/api/token") 20 | public ResponseEntity createNewAccessToken(@RequestBody CreateAccessTokenRequest request) { 21 | String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken()); 22 | 23 | return ResponseEntity.status(HttpStatus.CREATED) 24 | .body(new CreateAccessTokenResponse(newAccessToken)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/controller/UserViewController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class UserViewController { 8 | 9 | @GetMapping("/login") 10 | public String login() { 11 | return "oauthLogin"; 12 | } 13 | 14 | @GetMapping("/signup") 15 | public String signup() { 16 | return "signup"; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/domain/RefreshToken.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.domain; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 9 | @Getter 10 | @Entity 11 | public class RefreshToken { 12 | 13 | @Id 14 | @GeneratedValue(strategy = GenerationType.IDENTITY) 15 | @Column(name = "id", updatable = false) 16 | private Long id; 17 | 18 | @Column(name = "user_id", nullable = false, unique = true) 19 | private Long userId; 20 | 21 | @Column(name = "refresh_token", nullable = false) 22 | private String refreshToken; 23 | 24 | public RefreshToken(Long userId, String refreshToken) { 25 | this.userId = userId; 26 | this.refreshToken = refreshToken; 27 | } 28 | 29 | public RefreshToken update(String newRefreshToken) { 30 | this.refreshToken = newRefreshToken; 31 | 32 | return this; 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import me.shinsunyoung.springbootdeveloper.domain.Article; 7 | 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | @Getter 11 | public class AddArticleRequest { 12 | 13 | private String title; 14 | private String content; 15 | 16 | public Article toEntity(String author) { 17 | return Article.builder() 18 | .title(title) 19 | .content(content) 20 | .author(author) 21 | .build(); 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddUserRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | 7 | @NoArgsConstructor 8 | @Getter 9 | public class AddUserRequest { 10 | private String email; 11 | private String password; 12 | } 13 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleListViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleListViewResponse { 8 | 9 | private final Long id; 10 | private final String title; 11 | private final String content; 12 | 13 | public ArticleListViewResponse(Article article) { 14 | this.id = article.getId(); 15 | this.title = article.getTitle(); 16 | this.content = article.getContent(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleResponse { 8 | 9 | private final String title; 10 | private final String content; 11 | 12 | public ArticleResponse(Article article) { 13 | this.title = article.getTitle(); 14 | this.content = article.getContent(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import me.shinsunyoung.springbootdeveloper.domain.Article; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | @NoArgsConstructor 10 | @Getter 11 | public class ArticleViewResponse { 12 | 13 | private Long id; 14 | private String title; 15 | private String content; 16 | private LocalDateTime createdAt; 17 | private String author; 18 | 19 | public ArticleViewResponse(Article article) { 20 | this.id = article.getId(); 21 | this.title = article.getTitle(); 22 | this.content = article.getContent(); 23 | this.createdAt = article.getCreatedAt(); 24 | this.author = article.getAuthor(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/dto/CreateAccessTokenRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class CreateAccessTokenRequest { 9 | private String refreshToken; 10 | } 11 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/dto/CreateAccessTokenResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @AllArgsConstructor 7 | @Getter 8 | public class CreateAccessTokenResponse { 9 | private String accessToken; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/dto/UpdateArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @NoArgsConstructor 8 | @AllArgsConstructor 9 | @Getter 10 | public class UpdateArticleRequest { 11 | private String title; 12 | private String content; 13 | } 14 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/repository/BlogRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.Article; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface BlogRepository extends JpaRepository { 7 | } 8 | 9 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/repository/RefreshTokenRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.RefreshToken; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface RefreshTokenRepository extends JpaRepository { 9 | Optional findByUserId(Long userId); 10 | Optional findByRefreshToken(String refreshToken); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface UserRepository extends JpaRepository { 9 | Optional findByEmail(String email); 10 | } 11 | 12 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/service/RefreshTokenService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.RefreshToken; 5 | import me.shinsunyoung.springbootdeveloper.repository.RefreshTokenRepository; 6 | import org.springframework.stereotype.Service; 7 | 8 | @RequiredArgsConstructor 9 | @Service 10 | public class RefreshTokenService { 11 | private final RefreshTokenRepository refreshTokenRepository; 12 | 13 | public RefreshToken findByRefreshToken(String refreshToken) { 14 | return refreshTokenRepository.findByRefreshToken(refreshToken) 15 | .orElseThrow(() -> new IllegalArgumentException("Unexpected token")); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/service/TokenService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.config.jwt.TokenProvider; 5 | import me.shinsunyoung.springbootdeveloper.domain.User; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.time.Duration; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class TokenService { 13 | 14 | private final TokenProvider tokenProvider; 15 | private final RefreshTokenService refreshTokenService; 16 | private final UserService userService; 17 | 18 | public String createNewAccessToken(String refreshToken) { 19 | // 토큰 유효성 검사에 실패하면 예외 발생 20 | if(!tokenProvider.validToken(refreshToken)) { 21 | throw new IllegalArgumentException("Unexpected token"); 22 | } 23 | 24 | Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId(); 25 | User user = userService.findById(userId); 26 | 27 | return tokenProvider.generateToken(user, Duration.ofHours(2)); 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /appendixB/src/main/java/me/shinsunyoung/springbootdeveloper/service/UserDetailService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.User; 5 | import me.shinsunyoung.springbootdeveloper.repository.UserRepository; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class UserDetailService implements UserDetailsService { 12 | 13 | private final UserRepository userRepository; 14 | 15 | @Override 16 | public User loadUserByUsername(String email) { 17 | return userRepository.findByEmail(email) 18 | .orElseThrow(() -> new IllegalArgumentException((email))); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /appendixB/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true 4 | properties: 5 | hibernate: 6 | format_sql: true 7 | defer-datasource-initialization: true 8 | datasource: 9 | url: jdbc:h2:mem:testdb 10 | username: sa 11 | h2: 12 | console: 13 | enabled: true 14 | security: 15 | oauth2: 16 | client: 17 | registration: 18 | google: 19 | client-id: 6636.. 20 | client-secret: GOCS.. 21 | scope: 22 | - email 23 | - profile 24 | jwt: 25 | issuer: ajufresh@gmail.com 26 | secret_key: study-springboot -------------------------------------------------------------------------------- /appendixB/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목1', '내용1', 'user1', NOW(), NOW()) 2 | INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목2', '내용2', 'user2', NOW(), NOW()) 3 | INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목3', '내용3', 'user3', NOW(), NOW()) 4 | -------------------------------------------------------------------------------- /appendixB/src/main/resources/static/img/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/appendixB/src/main/resources/static/img/google.png -------------------------------------------------------------------------------- /appendixB/src/main/resources/static/js/token.js: -------------------------------------------------------------------------------- 1 | const token = searchParam('token') 2 | 3 | if (token) { 4 | localStorage.setItem("access_token", token) 5 | } 6 | 7 | function searchParam(key) { 8 | return new URLSearchParams(location.search).get(key); 9 | } 10 | -------------------------------------------------------------------------------- /appendixB/src/main/resources/templates/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |

타임리프 익히기

9 |

10 | 11 |
12 |

13 |

14 |

취미

15 |
    16 |
  • 17 | (대표 취미) 18 |
19 |
20 | 21 | 글 보기 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /appendixB/src/main/resources/templates/newArticle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 블로그 글 6 | 7 | 8 | 9 |
10 |

My Blog

11 |

블로그에 오신 것을 환영합니다.

12 |
13 | 14 |
15 |
16 |
17 |
18 | 19 | 20 |
21 | 22 |
23 |
24 | 25 |
26 | 27 | 28 |
29 |
30 |
31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /appendixB/src/main/resources/templates/oauthLogin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |

LOGIN

22 |

서비스 사용을 위해 로그인을 해주세요!

23 | 24 |
25 | 26 | 27 | 28 |
29 |
30 |
31 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /appendixC/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.2.0' 4 | id 'io.spring.dependency-management' version '1.1.0' 5 | } 6 | 7 | group 'me.shinsunyoung' 8 | version '1.0' 9 | sourceCompatibility = '17' 10 | 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | 17 | dependencies { 18 | implementation 'org.springframework.boot:spring-boot-starter-web' 19 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 20 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 21 | runtimeOnly 'com.h2database:h2' 22 | compileOnly 'org.projectlombok:lombok' 23 | annotationProcessor 'org.projectlombok:lombok' 24 | } 25 | 26 | 27 | test { 28 | useJUnitPlatform() 29 | } 30 | -------------------------------------------------------------------------------- /appendixC/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/appendixC/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /appendixC/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 04 17:36:39 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /appendixC/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-developer' 2 | 3 | -------------------------------------------------------------------------------- /appendixC/src/main/java/me/shinsunyoung/Main.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world!"); 6 | } 7 | } -------------------------------------------------------------------------------- /appendixC/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | @SpringBootApplication 6 | public class SpringBootDeveloperApplication { 7 | public static void main(String[] args) { 8 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 9 | } 10 | } -------------------------------------------------------------------------------- /appendixC/src/main/java/me/shinsunyoung/springbootdeveloper/domain/Article.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.domain; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AccessLevel; 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Entity 10 | @Getter 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | public class Article { 13 | @Id 14 | @GeneratedValue(strategy = GenerationType.IDENTITY) 15 | @Column(name = "id", updatable = false) 16 | private Long id; 17 | 18 | @Column(name = "title", nullable = false) 19 | private String title; 20 | 21 | @Column(name = "content", nullable = false) 22 | private String content; 23 | 24 | @Builder 25 | public Article(String title, String content) { 26 | this.title = title; 27 | this.content = content; 28 | } 29 | 30 | public void update(String title, String content) { 31 | this.title = title; 32 | this.content = content; 33 | } 34 | } -------------------------------------------------------------------------------- /appendixC/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import me.shinsunyoung.springbootdeveloper.domain.Article; 7 | 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | @Getter 11 | public class AddArticleRequest { 12 | private String title; 13 | private String content; 14 | public Article toEntity() { 15 | return Article.builder() 16 | .title(title) 17 | .content(content) 18 | .build(); 19 | } 20 | } -------------------------------------------------------------------------------- /appendixC/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleResponse { 8 | private final String title; 9 | private final String content; 10 | public ArticleResponse(Article article) { 11 | this.title = article.getTitle(); 12 | this.content = article.getContent(); 13 | } 14 | } -------------------------------------------------------------------------------- /appendixC/src/main/java/me/shinsunyoung/springbootdeveloper/dto/UpdateArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @NoArgsConstructor 8 | @AllArgsConstructor 9 | @Getter 10 | public class UpdateArticleRequest { 11 | private String title; 12 | private String content; 13 | } -------------------------------------------------------------------------------- /appendixC/src/main/java/me/shinsunyoung/springbootdeveloper/repository/BlogRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.Article; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface BlogRepository extends JpaRepository { 7 | } -------------------------------------------------------------------------------- /appendixC/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true 4 | properties: 5 | hibernate: 6 | format_sql: true 7 | defer-datasource-initialization: true 8 | datasource: 9 | url: jdbc:h2:mem:testdb 10 | h2: 11 | console: 12 | enabled: true -------------------------------------------------------------------------------- /appendixC/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO article (title, content) VALUES ('제목 1', '내용 1') 2 | INSERT INTO article (title, content) VALUES ('제목 2', '내용 2') 3 | INSERT INTO article (title, content) VALUES ('제목 3', '내용 3') -------------------------------------------------------------------------------- /chapter0/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.2.0' 4 | id 'io.spring.dependency-management' version '1.1.0' 5 | } 6 | 7 | group 'me.shinsunyoung' 8 | version '1.0' 9 | sourceCompatibility = '17' 10 | 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | 17 | dependencies { 18 | implementation 'org.springframework.boot:spring-boot-starter-web' 19 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 20 | } 21 | 22 | 23 | test { 24 | useJUnitPlatform() 25 | } 26 | -------------------------------------------------------------------------------- /chapter0/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter0/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 04 17:36:39 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /chapter0/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-developer' 2 | 3 | -------------------------------------------------------------------------------- /chapter0/src/main/java/me/shinsunyoung/Main.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world!"); 6 | } 7 | } -------------------------------------------------------------------------------- /chapter0/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | @SpringBootApplication 6 | public class SpringBootDeveloperApplication { 7 | public static void main(String[] args) { 8 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 9 | } 10 | } -------------------------------------------------------------------------------- /chapter0/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

index.html

9 | 10 | -------------------------------------------------------------------------------- /chapter1/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.2.0' 4 | id 'io.spring.dependency-management' version '1.1.0' 5 | } 6 | 7 | group 'me.shinsunyoung' 8 | version '1.0' 9 | sourceCompatibility = '17' 10 | 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | 17 | dependencies { 18 | implementation 'org.springframework.boot:spring-boot-starter-web' 19 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 20 | } 21 | 22 | 23 | test { 24 | useJUnitPlatform() 25 | } 26 | -------------------------------------------------------------------------------- /chapter1/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter1/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 04 17:36:39 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /chapter1/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-developer' 2 | 3 | -------------------------------------------------------------------------------- /chapter1/src/main/java/me/shinsunyoung/Main.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world!"); 6 | } 7 | } -------------------------------------------------------------------------------- /chapter1/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | @SpringBootApplication 6 | public class SpringBootDeveloperApplication { 7 | public static void main(String[] args) { 8 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 9 | } 10 | } -------------------------------------------------------------------------------- /chapter1/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

index.html

9 | 10 | -------------------------------------------------------------------------------- /chapter10/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter10/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter10/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 04 17:36:39 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /chapter10/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-developer' 2 | 3 | -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/Main.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world!"); 6 | } 7 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 6 | 7 | @EnableJpaAuditing 8 | @SpringBootApplication 9 | public class SpringBootDeveloperApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 12 | } 13 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/config/jwt/JwtProperties.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.config.jwt; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Setter 9 | @Getter 10 | @Component 11 | @ConfigurationProperties("jwt") 12 | public class JwtProperties { 13 | private String issuer; 14 | private String secretKey; 15 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/controller/ExampleController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.ui.Model; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | 9 | import java.time.LocalDate; 10 | import java.util.List; 11 | 12 | @Controller 13 | public class ExampleController { 14 | @GetMapping("/thymeleaf/example") 15 | public String thymeleafExample(Model model) { 16 | Person examplePerson = new Person(); 17 | examplePerson.setId(1L); 18 | examplePerson.setName("홍길동"); 19 | examplePerson.setAge(11); 20 | examplePerson.setHobbies(List.of("운동", "독서")); 21 | 22 | model.addAttribute("person", examplePerson); 23 | model.addAttribute("today", LocalDate.now()); 24 | 25 | return "example"; 26 | } 27 | @Setter 28 | @Getter 29 | class Person { 30 | private Long id; 31 | private String name; 32 | private int age; 33 | private List hobbies; 34 | } 35 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/controller/UserViewController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class UserViewController { 8 | @GetMapping("/login") 9 | public String login() { 10 | return "oauthLogin"; 11 | } 12 | 13 | @GetMapping("/signup") 14 | public String signup() { 15 | return "signup"; 16 | } 17 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/domain/RefreshToken.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.domain; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 9 | @Getter 10 | @Entity 11 | public class RefreshToken { 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.IDENTITY) 14 | @Column(name = "id", updatable = false) 15 | private Long id; 16 | 17 | @Column(name = "user_id", nullable = false, unique = true) 18 | private Long userId; 19 | 20 | @Column(name = "refresh_token", nullable = false) 21 | private String refreshToken; 22 | 23 | public RefreshToken(Long userId, String refreshToken) { 24 | this.userId = userId; 25 | this.refreshToken = refreshToken; 26 | } 27 | 28 | public RefreshToken update(String newRefreshToken) { 29 | this.refreshToken = newRefreshToken; 30 | return this; 31 | } 32 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import me.shinsunyoung.springbootdeveloper.domain.Article; 7 | 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | @Getter 11 | public class AddArticleRequest { 12 | private String title; 13 | private String content; 14 | public Article toEntity(String author) { 15 | return Article.builder() 16 | .title(title) 17 | .content(content) 18 | .author(author) 19 | .build(); 20 | } 21 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddUserRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class AddUserRequest { 9 | private String email; 10 | private String password; 11 | } 12 | -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleListViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleListViewResponse { 8 | private final Long id; 9 | private final String title; 10 | private final String content; 11 | 12 | public ArticleListViewResponse(Article article) { 13 | this.id = article.getId(); 14 | this.title = article.getTitle(); 15 | this.content = article.getContent(); 16 | } 17 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleResponse { 8 | private final String title; 9 | private final String content; 10 | public ArticleResponse(Article article) { 11 | this.title = article.getTitle(); 12 | this.content = article.getContent(); 13 | } 14 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import me.shinsunyoung.springbootdeveloper.domain.Article; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | @NoArgsConstructor 10 | @Getter 11 | public class ArticleViewResponse { 12 | private Long id; 13 | private String title; 14 | private String content; 15 | private LocalDateTime createdAt; 16 | private String author; 17 | 18 | public ArticleViewResponse(Article article) { 19 | this.id = article.getId(); 20 | this.title = article.getTitle(); 21 | this.content = article.getContent(); 22 | this.createdAt = article.getCreatedAt(); 23 | this.author = article.getAuthor(); 24 | } 25 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/dto/CreateAccessTokenRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class CreateAccessTokenRequest { 9 | private String refreshToken; 10 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/dto/CreateAccessTokenResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @AllArgsConstructor 7 | @Getter 8 | public class CreateAccessTokenResponse { 9 | private String accessToken; 10 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/dto/UpdateArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @NoArgsConstructor 8 | @AllArgsConstructor 9 | @Getter 10 | public class UpdateArticleRequest { 11 | private String title; 12 | private String content; 13 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/repository/BlogRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.Article; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface BlogRepository extends JpaRepository { 7 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/repository/RefreshTokenRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.RefreshToken; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface RefreshTokenRepository extends JpaRepository { 9 | Optional findByUserId(Long userId); 10 | Optional findByRefreshToken(String refreshToken); 11 | 12 | void deleteByUserId(Long userId); 13 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface UserRepository extends JpaRepository { 9 | Optional findByEmail(String email); 10 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/service/RefreshTokenService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import jakarta.transaction.Transactional; 4 | import lombok.RequiredArgsConstructor; 5 | import me.shinsunyoung.springbootdeveloper.config.jwt.TokenProvider; 6 | import me.shinsunyoung.springbootdeveloper.domain.RefreshToken; 7 | import me.shinsunyoung.springbootdeveloper.repository.RefreshTokenRepository; 8 | import org.springframework.security.core.context.SecurityContextHolder; 9 | import org.springframework.stereotype.Service; 10 | 11 | @RequiredArgsConstructor 12 | @Service 13 | public class RefreshTokenService { 14 | private final RefreshTokenRepository refreshTokenRepository; 15 | private final TokenProvider tokenProvider; 16 | 17 | public RefreshToken findByRefreshToken(String refreshToken) { 18 | return refreshTokenRepository.findByRefreshToken(refreshToken) 19 | .orElseThrow(() -> new IllegalArgumentException("Unexpected token")); 20 | } 21 | 22 | @Transactional 23 | public void delete() { 24 | String token = SecurityContextHolder.getContext().getAuthentication().getCredentials().toString(); 25 | Long userId = tokenProvider.getUserId(token); 26 | 27 | refreshTokenRepository.deleteByUserId(userId); 28 | } 29 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/service/TokenService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.config.jwt.TokenProvider; 5 | import me.shinsunyoung.springbootdeveloper.domain.User; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.time.Duration; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class TokenService { 13 | private final TokenProvider tokenProvider; 14 | private final RefreshTokenService refreshTokenService; 15 | private final UserService userService; 16 | 17 | public String createNewAccessToken(String refreshToken) { 18 | if(!tokenProvider.validToken(refreshToken)) { 19 | throw new IllegalArgumentException("Unexpected token"); 20 | } 21 | 22 | Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId(); 23 | User user = userService.findById(userId); 24 | 25 | return tokenProvider.generateToken(user, Duration.ofHours(2)); 26 | } 27 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/service/UserDetailService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.User; 5 | import me.shinsunyoung.springbootdeveloper.repository.UserRepository; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class UserDetailService implements UserDetailsService { 12 | private final UserRepository userRepository; 13 | @Override 14 | public User loadUserByUsername(String email) { 15 | return userRepository.findByEmail(email) 16 | .orElseThrow(() -> new IllegalArgumentException((email))); 17 | } 18 | } -------------------------------------------------------------------------------- /chapter10/src/main/java/me/shinsunyoung/springbootdeveloper/service/UserService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.User; 5 | import me.shinsunyoung.springbootdeveloper.dto.AddUserRequest; 6 | import me.shinsunyoung.springbootdeveloper.repository.UserRepository; 7 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 8 | import org.springframework.stereotype.Service; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class UserService { 13 | private final UserRepository userRepository; 14 | 15 | public Long save(AddUserRequest dto) { 16 | BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); 17 | return userRepository.save(User.builder() 18 | .email(dto.getEmail()) 19 | .password(encoder.encode(dto.getPassword())) 20 | .build()).getId(); 21 | } 22 | 23 | public User findById(Long userId) { 24 | return userRepository.findById(userId) 25 | .orElseThrow(() -> new IllegalArgumentException("Unexpected user")); 26 | } 27 | 28 | public User findByEmail(String email) { 29 | return userRepository.findByEmail(email) 30 | .orElseThrow(() -> new IllegalArgumentException("Unexpected user")); 31 | } 32 | } -------------------------------------------------------------------------------- /chapter10/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true 4 | properties: 5 | hibernate: 6 | format_sql: true 7 | defer-datasource-initialization: true 8 | datasource: 9 | url: jdbc:h2:mem:testdb 10 | username: sa 11 | h2: 12 | console: 13 | enabled: true 14 | security: 15 | oauth2: 16 | client: 17 | registration: 18 | google: 19 | client-id: 6636422... 20 | client-secret: GOCSP... 21 | scope: 22 | - email 23 | - profile 24 | jwt: 25 | issuer: ajufresh@gmail.com 26 | secret_key: study-springboot -------------------------------------------------------------------------------- /chapter10/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목1', '내용1', 'user1', NOW(), NOW()) 2 | INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목2', '내용2', 'user2', NOW(), NOW()) 3 | INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목3', '내용3', 'user3', NOW(), NOW()) -------------------------------------------------------------------------------- /chapter10/src/main/resources/static/img/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter10/src/main/resources/static/img/google.png -------------------------------------------------------------------------------- /chapter10/src/main/resources/static/js/token.js: -------------------------------------------------------------------------------- 1 | const token = searchParam('token') 2 | 3 | if (token) { 4 | localStorage.setItem("access_token", token) 5 | } 6 | 7 | function searchParam(key) { 8 | return new URLSearchParams(location.search).get(key); 9 | } -------------------------------------------------------------------------------- /chapter10/src/main/resources/templates/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |

타임리프 익히기

9 |

10 |
11 |

12 |

13 |

취미

14 |
    15 |
  • 16 | (대표 취미) 17 |
18 |
19 | 20 | 글 보기 21 | 22 | -------------------------------------------------------------------------------- /chapter10/src/main/resources/templates/oauthLogin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |

LOGIN

22 |

서비스 사용을 위해 로그인을 해주세요!

23 | 24 |
25 | 26 | 27 | 28 |
29 |
30 |
31 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /chapter11/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter11/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter11/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 04 17:36:39 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /chapter11/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-developer' 2 | 3 | -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/Main.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world!"); 6 | } 7 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 6 | 7 | @EnableJpaAuditing 8 | @SpringBootApplication 9 | public class SpringBootDeveloperApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 12 | } 13 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/config/jwt/JwtProperties.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.config.jwt; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Setter 9 | @Getter 10 | @Component 11 | @ConfigurationProperties("jwt") 12 | public class JwtProperties { 13 | private String issuer; 14 | private String secretKey; 15 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/controller/ExampleController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.ui.Model; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | 9 | import java.time.LocalDate; 10 | import java.util.List; 11 | 12 | @Controller 13 | public class ExampleController { 14 | @GetMapping("/thymeleaf/example") 15 | public String thymeleafExample(Model model) { 16 | Person examplePerson = new Person(); 17 | examplePerson.setId(1L); 18 | examplePerson.setName("홍길동"); 19 | examplePerson.setAge(11); 20 | examplePerson.setHobbies(List.of("운동", "독서")); 21 | 22 | model.addAttribute("person", examplePerson); 23 | model.addAttribute("today", LocalDate.now()); 24 | 25 | return "example"; 26 | } 27 | @Setter 28 | @Getter 29 | class Person { 30 | private Long id; 31 | private String name; 32 | private int age; 33 | private List hobbies; 34 | } 35 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/controller/TokenApiController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.dto.CreateAccessTokenRequest; 5 | import me.shinsunyoung.springbootdeveloper.dto.CreateAccessTokenResponse; 6 | import me.shinsunyoung.springbootdeveloper.service.TokenService; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | @RequiredArgsConstructor 14 | @RestController 15 | public class TokenApiController { 16 | private final TokenService tokenService; 17 | 18 | @PostMapping("/api/token") 19 | public ResponseEntity createNewAccessToken(@RequestBody CreateAccessTokenRequest request) { 20 | String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken()); 21 | 22 | return ResponseEntity.status(HttpStatus.CREATED) 23 | .body(new CreateAccessTokenResponse(newAccessToken)); 24 | } 25 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/controller/UserViewController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class UserViewController { 8 | @GetMapping("/login") 9 | public String login() { 10 | return "oauthLogin"; 11 | } 12 | 13 | @GetMapping("/signup") 14 | public String signup() { 15 | return "signup"; 16 | } 17 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/domain/RefreshToken.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.domain; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 9 | @Getter 10 | @Entity 11 | public class RefreshToken { 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.IDENTITY) 14 | @Column(name = "id", updatable = false) 15 | private Long id; 16 | 17 | @Column(name = "user_id", nullable = false, unique = true) 18 | private Long userId; 19 | 20 | @Column(name = "refresh_token", nullable = false) 21 | private String refreshToken; 22 | 23 | public RefreshToken(Long userId, String refreshToken) { 24 | this.userId = userId; 25 | this.refreshToken = refreshToken; 26 | } 27 | 28 | public RefreshToken update(String newRefreshToken) { 29 | this.refreshToken = newRefreshToken; 30 | return this; 31 | } 32 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import me.shinsunyoung.springbootdeveloper.domain.Article; 7 | 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | @Getter 11 | public class AddArticleRequest { 12 | private String title; 13 | private String content; 14 | public Article toEntity(String author) { 15 | return Article.builder() 16 | .title(title) 17 | .content(content) 18 | .author(author) 19 | .build(); 20 | } 21 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddUserRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class AddUserRequest { 9 | private String email; 10 | private String password; 11 | } 12 | -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleListViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleListViewResponse { 8 | private final Long id; 9 | private final String title; 10 | private final String content; 11 | 12 | public ArticleListViewResponse(Article article) { 13 | this.id = article.getId(); 14 | this.title = article.getTitle(); 15 | this.content = article.getContent(); 16 | } 17 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleResponse { 8 | private final String title; 9 | private final String content; 10 | public ArticleResponse(Article article) { 11 | this.title = article.getTitle(); 12 | this.content = article.getContent(); 13 | } 14 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import me.shinsunyoung.springbootdeveloper.domain.Article; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | @NoArgsConstructor 10 | @Getter 11 | public class ArticleViewResponse { 12 | private Long id; 13 | private String title; 14 | private String content; 15 | private LocalDateTime createdAt; 16 | private String author; 17 | 18 | public ArticleViewResponse(Article article) { 19 | this.id = article.getId(); 20 | this.title = article.getTitle(); 21 | this.content = article.getContent(); 22 | this.createdAt = article.getCreatedAt(); 23 | this.author = article.getAuthor(); 24 | } 25 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/dto/CreateAccessTokenRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class CreateAccessTokenRequest { 9 | private String refreshToken; 10 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/dto/CreateAccessTokenResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @AllArgsConstructor 7 | @Getter 8 | public class CreateAccessTokenResponse { 9 | private String accessToken; 10 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/dto/UpdateArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @NoArgsConstructor 8 | @AllArgsConstructor 9 | @Getter 10 | public class UpdateArticleRequest { 11 | private String title; 12 | private String content; 13 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/repository/BlogRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.Article; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface BlogRepository extends JpaRepository { 7 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/repository/RefreshTokenRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.RefreshToken; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface RefreshTokenRepository extends JpaRepository { 9 | Optional findByUserId(Long userId); 10 | Optional findByRefreshToken(String refreshToken); 11 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface UserRepository extends JpaRepository { 9 | Optional findByEmail(String email); 10 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/service/RefreshTokenService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.RefreshToken; 5 | import me.shinsunyoung.springbootdeveloper.repository.RefreshTokenRepository; 6 | import org.springframework.stereotype.Service; 7 | 8 | @RequiredArgsConstructor 9 | @Service 10 | public class RefreshTokenService { 11 | private final RefreshTokenRepository refreshTokenRepository; 12 | public RefreshToken findByRefreshToken(String refreshToken) { 13 | return refreshTokenRepository.findByRefreshToken(refreshToken) 14 | .orElseThrow(() -> new IllegalArgumentException("Unexpected token")); 15 | } 16 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/service/TokenService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.config.jwt.TokenProvider; 5 | import me.shinsunyoung.springbootdeveloper.domain.User; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.time.Duration; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class TokenService { 13 | private final TokenProvider tokenProvider; 14 | private final RefreshTokenService refreshTokenService; 15 | private final UserService userService; 16 | 17 | public String createNewAccessToken(String refreshToken) { 18 | if(!tokenProvider.validToken(refreshToken)) { 19 | throw new IllegalArgumentException("Unexpected token"); 20 | } 21 | 22 | Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId(); 23 | User user = userService.findById(userId); 24 | 25 | return tokenProvider.generateToken(user, Duration.ofHours(2)); 26 | } 27 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/service/UserDetailService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.User; 5 | import me.shinsunyoung.springbootdeveloper.repository.UserRepository; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class UserDetailService implements UserDetailsService { 12 | private final UserRepository userRepository; 13 | @Override 14 | public User loadUserByUsername(String email) { 15 | return userRepository.findByEmail(email) 16 | .orElseThrow(() -> new IllegalArgumentException((email))); 17 | } 18 | } -------------------------------------------------------------------------------- /chapter11/src/main/java/me/shinsunyoung/springbootdeveloper/service/UserService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.User; 5 | import me.shinsunyoung.springbootdeveloper.dto.AddUserRequest; 6 | import me.shinsunyoung.springbootdeveloper.repository.UserRepository; 7 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 8 | import org.springframework.stereotype.Service; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class UserService { 13 | private final UserRepository userRepository; 14 | 15 | public Long save(AddUserRequest dto) { 16 | BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); 17 | return userRepository.save(User.builder() 18 | .email(dto.getEmail()) 19 | .password(encoder.encode(dto.getPassword())) 20 | .build()).getId(); 21 | } 22 | 23 | public User findById(Long userId) { 24 | return userRepository.findById(userId) 25 | .orElseThrow(() -> new IllegalArgumentException("Unexpected user")); 26 | } 27 | 28 | public User findByEmail(String email) { 29 | return userRepository.findByEmail(email) 30 | .orElseThrow(() -> new IllegalArgumentException("Unexpected user")); 31 | } 32 | } -------------------------------------------------------------------------------- /chapter11/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true 4 | properties: 5 | hibernate: 6 | format_sql: true 7 | defer-datasource-initialization: true 8 | h2: 9 | console: 10 | enabled: true 11 | security: 12 | oauth2: 13 | client: 14 | registration: 15 | google: 16 | client-id: 6636422.. 17 | client-secret: GOCSP.. 18 | scope: 19 | - email 20 | - profile 21 | 22 | 23 | jwt: 24 | issuer: ajufresh@gmail.com 25 | secret_key: study-springboot 26 | -------------------------------------------------------------------------------- /chapter11/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목1', '내용1', 'user1', NOW(), NOW()) 2 | INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목2', '내용2', 'user2', NOW(), NOW()) 3 | INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목3', '내용3', 'user3', NOW(), NOW()) -------------------------------------------------------------------------------- /chapter11/src/main/resources/static/img/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter11/src/main/resources/static/img/google.png -------------------------------------------------------------------------------- /chapter11/src/main/resources/static/js/token.js: -------------------------------------------------------------------------------- 1 | const token = searchParam('token') 2 | 3 | if (token) { 4 | localStorage.setItem("access_token", token) 5 | } 6 | 7 | function searchParam(key) { 8 | return new URLSearchParams(location.search).get(key); 9 | } -------------------------------------------------------------------------------- /chapter11/src/main/resources/templates/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |

타임리프 익히기

9 |

10 |
11 |

12 |

13 |

취미

14 |
    15 |
  • 16 | (대표 취미) 17 |
18 |
19 | 20 | 글 보기 21 | 22 | -------------------------------------------------------------------------------- /chapter11/src/main/resources/templates/oauthLogin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |

LOGIN

22 |

서비스 사용을 위해 로그인을 해주세요!

23 | 24 |
25 | 26 | 27 | 28 |
29 |
30 |
31 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /chapter12/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - uses: actions/setup-java@v3 15 | with: 16 | distribution: 'corretto' 17 | java-version: '17' 18 | -------------------------------------------------------------------------------- /chapter12/.github/workflows/cicd.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - uses: actions/setup-java@v3 15 | with: 16 | distribution: 'corretto' 17 | java-version: '17' 18 | 19 | - name: Grant execute permission for gradlew 20 | run: chmod +x gradlew 21 | 22 | - name: Build with Gradle 23 | run: ./gradlew clean build 24 | 25 | - name: Get current time 26 | uses: josStorer/get-current-time@v2.0.2 27 | id: current-time 28 | with: 29 | format: YYYY-MM-DDTHH-mm-ss 30 | utcOffset: "+09:00" 31 | 32 | - name: Set artifact 33 | run: echo "artifact=$(ls ./build/libs)" >> $GITHUB_ENV 34 | 35 | - name: Beanstalk Deploy 36 | uses: einaregilsson/beanstalk-deploy@v20 37 | with: 38 | aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }} 39 | aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 40 | application_name: springboot-developer 41 | environment_name: Springboot-developer-env 42 | version_label: github-action-${{steps.current-time.outputs.formattedTime}} 43 | region: ap-northeast-2 44 | deployment_package: ./build/libs/${{env.artifact}} 45 | -------------------------------------------------------------------------------- /chapter12/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter12/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter12/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 04 17:36:39 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /chapter12/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-developer' 2 | 3 | -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/Main.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world!"); 6 | } 7 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 6 | 7 | @EnableJpaAuditing 8 | @SpringBootApplication 9 | public class SpringBootDeveloperApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 12 | } 13 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/config/jwt/JwtProperties.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.config.jwt; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Setter 9 | @Getter 10 | @Component 11 | @ConfigurationProperties("jwt") 12 | public class JwtProperties { 13 | private String issuer; 14 | private String secretKey; 15 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/controller/ExampleController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.ui.Model; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | 9 | import java.time.LocalDate; 10 | import java.util.List; 11 | 12 | @Controller 13 | public class ExampleController { 14 | @GetMapping("/thymeleaf/example") 15 | public String thymeleafExample(Model model) { 16 | Person examplePerson = new Person(); 17 | examplePerson.setId(1L); 18 | examplePerson.setName("홍길동"); 19 | examplePerson.setAge(11); 20 | examplePerson.setHobbies(List.of("운동", "독서")); 21 | 22 | model.addAttribute("person", examplePerson); 23 | model.addAttribute("today", LocalDate.now()); 24 | 25 | return "example"; 26 | } 27 | @Setter 28 | @Getter 29 | class Person { 30 | private Long id; 31 | private String name; 32 | private int age; 33 | private List hobbies; 34 | } 35 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/controller/TokenApiController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.dto.CreateAccessTokenRequest; 5 | import me.shinsunyoung.springbootdeveloper.dto.CreateAccessTokenResponse; 6 | import me.shinsunyoung.springbootdeveloper.service.TokenService; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | @RequiredArgsConstructor 14 | @RestController 15 | public class TokenApiController { 16 | private final TokenService tokenService; 17 | 18 | @PostMapping("/api/token") 19 | public ResponseEntity createNewAccessToken(@RequestBody CreateAccessTokenRequest request) { 20 | String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken()); 21 | 22 | return ResponseEntity.status(HttpStatus.CREATED) 23 | .body(new CreateAccessTokenResponse(newAccessToken)); 24 | } 25 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/controller/UserViewController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class UserViewController { 8 | @GetMapping("/login") 9 | public String login() { 10 | return "oauthLogin"; 11 | } 12 | 13 | @GetMapping("/signup") 14 | public String signup() { 15 | return "signup"; 16 | } 17 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/domain/RefreshToken.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.domain; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 9 | @Getter 10 | @Entity 11 | public class RefreshToken { 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.IDENTITY) 14 | @Column(name = "id", updatable = false) 15 | private Long id; 16 | 17 | @Column(name = "user_id", nullable = false, unique = true) 18 | private Long userId; 19 | 20 | @Column(name = "refresh_token", nullable = false) 21 | private String refreshToken; 22 | 23 | public RefreshToken(Long userId, String refreshToken) { 24 | this.userId = userId; 25 | this.refreshToken = refreshToken; 26 | } 27 | 28 | public RefreshToken update(String newRefreshToken) { 29 | this.refreshToken = newRefreshToken; 30 | return this; 31 | } 32 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import me.shinsunyoung.springbootdeveloper.domain.Article; 7 | 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | @Getter 11 | public class AddArticleRequest { 12 | private String title; 13 | private String content; 14 | public Article toEntity(String author) { 15 | return Article.builder() 16 | .title(title) 17 | .content(content) 18 | .author(author) 19 | .build(); 20 | } 21 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddUserRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class AddUserRequest { 9 | private String email; 10 | private String password; 11 | } 12 | -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleListViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleListViewResponse { 8 | private final Long id; 9 | private final String title; 10 | private final String content; 11 | 12 | public ArticleListViewResponse(Article article) { 13 | this.id = article.getId(); 14 | this.title = article.getTitle(); 15 | this.content = article.getContent(); 16 | } 17 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleResponse { 8 | private final String title; 9 | private final String content; 10 | public ArticleResponse(Article article) { 11 | this.title = article.getTitle(); 12 | this.content = article.getContent(); 13 | } 14 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import me.shinsunyoung.springbootdeveloper.domain.Article; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | @NoArgsConstructor 10 | @Getter 11 | public class ArticleViewResponse { 12 | private Long id; 13 | private String title; 14 | private String content; 15 | private LocalDateTime createdAt; 16 | private String author; 17 | 18 | public ArticleViewResponse(Article article) { 19 | this.id = article.getId(); 20 | this.title = article.getTitle(); 21 | this.content = article.getContent(); 22 | this.createdAt = article.getCreatedAt(); 23 | this.author = article.getAuthor(); 24 | } 25 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/dto/CreateAccessTokenRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class CreateAccessTokenRequest { 9 | private String refreshToken; 10 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/dto/CreateAccessTokenResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @AllArgsConstructor 7 | @Getter 8 | public class CreateAccessTokenResponse { 9 | private String accessToken; 10 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/dto/UpdateArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @NoArgsConstructor 8 | @AllArgsConstructor 9 | @Getter 10 | public class UpdateArticleRequest { 11 | private String title; 12 | private String content; 13 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/repository/BlogRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.Article; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface BlogRepository extends JpaRepository { 7 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/repository/RefreshTokenRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.RefreshToken; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface RefreshTokenRepository extends JpaRepository { 9 | Optional findByUserId(Long userId); 10 | Optional findByRefreshToken(String refreshToken); 11 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface UserRepository extends JpaRepository { 9 | Optional findByEmail(String email); 10 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/service/RefreshTokenService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.RefreshToken; 5 | import me.shinsunyoung.springbootdeveloper.repository.RefreshTokenRepository; 6 | import org.springframework.stereotype.Service; 7 | 8 | @RequiredArgsConstructor 9 | @Service 10 | public class RefreshTokenService { 11 | private final RefreshTokenRepository refreshTokenRepository; 12 | public RefreshToken findByRefreshToken(String refreshToken) { 13 | return refreshTokenRepository.findByRefreshToken(refreshToken) 14 | .orElseThrow(() -> new IllegalArgumentException("Unexpected token")); 15 | } 16 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/service/TokenService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.config.jwt.TokenProvider; 5 | import me.shinsunyoung.springbootdeveloper.domain.User; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.time.Duration; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class TokenService { 13 | private final TokenProvider tokenProvider; 14 | private final RefreshTokenService refreshTokenService; 15 | private final UserService userService; 16 | 17 | public String createNewAccessToken(String refreshToken) { 18 | if(!tokenProvider.validToken(refreshToken)) { 19 | throw new IllegalArgumentException("Unexpected token"); 20 | } 21 | 22 | Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId(); 23 | User user = userService.findById(userId); 24 | 25 | return tokenProvider.generateToken(user, Duration.ofHours(2)); 26 | } 27 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/service/UserDetailService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.User; 5 | import me.shinsunyoung.springbootdeveloper.repository.UserRepository; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class UserDetailService implements UserDetailsService { 12 | private final UserRepository userRepository; 13 | @Override 14 | public User loadUserByUsername(String email) { 15 | return userRepository.findByEmail(email) 16 | .orElseThrow(() -> new IllegalArgumentException((email))); 17 | } 18 | } -------------------------------------------------------------------------------- /chapter12/src/main/java/me/shinsunyoung/springbootdeveloper/service/UserService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.User; 5 | import me.shinsunyoung.springbootdeveloper.dto.AddUserRequest; 6 | import me.shinsunyoung.springbootdeveloper.repository.UserRepository; 7 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 8 | import org.springframework.stereotype.Service; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class UserService { 13 | private final UserRepository userRepository; 14 | 15 | public Long save(AddUserRequest dto) { 16 | BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); 17 | return userRepository.save(User.builder() 18 | .email(dto.getEmail()) 19 | .password(encoder.encode(dto.getPassword())) 20 | .build()).getId(); 21 | } 22 | 23 | public User findById(Long userId) { 24 | return userRepository.findById(userId) 25 | .orElseThrow(() -> new IllegalArgumentException("Unexpected user")); 26 | } 27 | 28 | public User findByEmail(String email) { 29 | return userRepository.findByEmail(email) 30 | .orElseThrow(() -> new IllegalArgumentException("Unexpected user")); 31 | } 32 | } -------------------------------------------------------------------------------- /chapter12/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true 4 | properties: 5 | hibernate: 6 | format_sql: true 7 | defer-datasource-initialization: true 8 | h2: 9 | console: 10 | enabled: true 11 | security: 12 | oauth2: 13 | client: 14 | registration: 15 | google: 16 | client-id: 6636422.. 17 | client-secret: GOCSP.. 18 | scope: 19 | - email 20 | - profile 21 | 22 | 23 | jwt: 24 | issuer: ajufresh@gmail.com 25 | secret_key: study-springboot 26 | -------------------------------------------------------------------------------- /chapter12/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목1', '내용1', 'user1', NOW(), NOW()) 2 | INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목2', '내용2', 'user2', NOW(), NOW()) 3 | INSERT INTO article (title, content, author, created_at, updated_at) VALUES ('제목3', '내용3', 'user3', NOW(), NOW()) -------------------------------------------------------------------------------- /chapter12/src/main/resources/static/img/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter12/src/main/resources/static/img/google.png -------------------------------------------------------------------------------- /chapter12/src/main/resources/static/js/token.js: -------------------------------------------------------------------------------- 1 | const token = searchParam('token') 2 | 3 | if (token) { 4 | localStorage.setItem("access_token", token) 5 | } 6 | 7 | function searchParam(key) { 8 | return new URLSearchParams(location.search).get(key); 9 | } -------------------------------------------------------------------------------- /chapter12/src/main/resources/templates/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |

타임리프 익히기

9 |

10 |
11 |

12 |

13 |

취미

14 |
    15 |
  • 16 | (대표 취미) 17 |
18 |
19 | 20 | 글 보기 21 | 22 | -------------------------------------------------------------------------------- /chapter2/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.2.0' 4 | id 'io.spring.dependency-management' version '1.1.0' 5 | } 6 | 7 | group 'me.shinsunyoung' 8 | version '1.0' 9 | sourceCompatibility = '17' 10 | 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | 17 | dependencies { 18 | implementation 'org.springframework.boot:spring-boot-starter-web' 19 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 20 | } 21 | 22 | 23 | test { 24 | useJUnitPlatform() 25 | } 26 | -------------------------------------------------------------------------------- /chapter2/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter2/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 04 17:36:39 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /chapter2/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-developer' 2 | 3 | -------------------------------------------------------------------------------- /chapter2/src/main/java/me/shinsunyoung/Main.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world!"); 6 | } 7 | } -------------------------------------------------------------------------------- /chapter2/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | @SpringBootApplication 6 | public class SpringBootDeveloperApplication { 7 | public static void main(String[] args) { 8 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 9 | } 10 | } -------------------------------------------------------------------------------- /chapter2/src/main/java/me/shinsunyoung/springbootdeveloper/TestController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | @RestController 7 | public class TestController { 8 | @GetMapping("/test") 9 | public String test() { 10 | return "Hello, world!"; 11 | } 12 | } -------------------------------------------------------------------------------- /chapter2/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

index.html

9 | 10 | -------------------------------------------------------------------------------- /chapter3/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.2.0' 4 | id 'io.spring.dependency-management' version '1.1.0' 5 | } 6 | 7 | group 'me.shinsunyoung' 8 | version '1.0' 9 | sourceCompatibility = '17' 10 | 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | 17 | dependencies { 18 | implementation 'org.springframework.boot:spring-boot-starter-web' 19 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 20 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 21 | runtimeOnly 'com.h2database:h2' 22 | compileOnly 'org.projectlombok:lombok' 23 | annotationProcessor 'org.projectlombok:lombok' 24 | } 25 | 26 | 27 | test { 28 | useJUnitPlatform() 29 | } 30 | -------------------------------------------------------------------------------- /chapter3/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter3/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 04 17:36:39 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /chapter3/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-developer' 2 | 3 | -------------------------------------------------------------------------------- /chapter3/src/main/java/me/shinsunyoung/Main.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world!"); 6 | } 7 | } -------------------------------------------------------------------------------- /chapter3/src/main/java/me/shinsunyoung/springbootdeveloper/Member.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AccessLevel; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 10 | @AllArgsConstructor 11 | @Getter 12 | @Entity 13 | public class Member { 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.IDENTITY) 16 | @Column(name = "id", updatable = false) 17 | private Long id; 18 | @Column(name = "name", nullable = false) 19 | private String name; 20 | } 21 | -------------------------------------------------------------------------------- /chapter3/src/main/java/me/shinsunyoung/springbootdeveloper/MemberRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @Repository 7 | public interface MemberRepository extends JpaRepository { 8 | } -------------------------------------------------------------------------------- /chapter3/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | @SpringBootApplication 6 | public class SpringBootDeveloperApplication { 7 | public static void main(String[] args) { 8 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 9 | } 10 | } -------------------------------------------------------------------------------- /chapter3/src/main/java/me/shinsunyoung/springbootdeveloper/TestController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | import java.util.List; 8 | 9 | @RestController 10 | public class TestController { 11 | @Autowired 12 | TestService testService; 13 | @GetMapping("/test") 14 | public List getAllMembers() { 15 | List members = testService.getAllMembers(); 16 | return members; 17 | } 18 | } -------------------------------------------------------------------------------- /chapter3/src/main/java/me/shinsunyoung/springbootdeveloper/TestService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | 6 | import java.util.List; 7 | 8 | @Service 9 | public class TestService { 10 | 11 | @Autowired 12 | MemberRepository memberRepository; 13 | 14 | public List getAllMembers() { 15 | return memberRepository.findAll(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chapter3/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true 4 | properties: 5 | hibernate: 6 | format_sql: true 7 | defer-datasource-initialization: true -------------------------------------------------------------------------------- /chapter3/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO member (id, name) VALUES (1, 'name 1') 2 | INSERT INTO member (id, name) VALUES (2, 'name 2') 3 | INSERT INTO member (id, name) VALUES (3, 'name 3') -------------------------------------------------------------------------------- /chapter3/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

index.html

9 | 10 | -------------------------------------------------------------------------------- /chapter4/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.2.0' 4 | id 'io.spring.dependency-management' version '1.1.0' 5 | } 6 | 7 | group 'me.shinsunyoung' 8 | version '1.0' 9 | sourceCompatibility = '17' 10 | 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | 17 | dependencies { 18 | implementation 'org.springframework.boot:spring-boot-starter-web' 19 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 20 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 21 | runtimeOnly 'com.h2database:h2' 22 | compileOnly 'org.projectlombok:lombok' 23 | annotationProcessor 'org.projectlombok:lombok' 24 | } 25 | 26 | 27 | test { 28 | useJUnitPlatform() 29 | } 30 | -------------------------------------------------------------------------------- /chapter4/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter4/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 04 17:36:39 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /chapter4/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-developer' 2 | 3 | -------------------------------------------------------------------------------- /chapter4/src/main/java/me/shinsunyoung/Main.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world!"); 6 | } 7 | } -------------------------------------------------------------------------------- /chapter4/src/main/java/me/shinsunyoung/springbootdeveloper/Member.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AccessLevel; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @AllArgsConstructor 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | @Getter 12 | @Entity 13 | public class Member { 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.IDENTITY) 16 | @Column(name = "id", updatable = false) 17 | private Long id; 18 | 19 | @Column(name = "name", nullable = false) 20 | private String name; 21 | } 22 | -------------------------------------------------------------------------------- /chapter4/src/main/java/me/shinsunyoung/springbootdeveloper/MemberRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @Repository 7 | public interface MemberRepository extends JpaRepository { 8 | } -------------------------------------------------------------------------------- /chapter4/src/main/java/me/shinsunyoung/springbootdeveloper/QuizController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.http.ResponseEntity; 4 | import org.springframework.web.bind.annotation.*; 5 | 6 | @RestController 7 | public class QuizController { 8 | 9 | 10 | @GetMapping("/quiz") 11 | public ResponseEntity quiz(@RequestParam("code") int code) { 12 | switch (code) { 13 | case 1: 14 | return ResponseEntity.created(null).body("Created!"); 15 | case 2: 16 | return ResponseEntity.badRequest().body("Bad Request!"); 17 | default: 18 | return ResponseEntity.ok().body("OK!"); 19 | } 20 | } 21 | 22 | 23 | @PostMapping("/quiz") 24 | public ResponseEntity quiz2(@RequestBody Code code) { 25 | 26 | 27 | switch (code.value()) { 28 | case 1: 29 | return ResponseEntity.status(403).body("Forbidden!"); 30 | default: 31 | return ResponseEntity.ok().body("OK!"); 32 | } 33 | } 34 | } 35 | 36 | 37 | record Code(int value) {} 38 | -------------------------------------------------------------------------------- /chapter4/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | @SpringBootApplication 6 | public class SpringBootDeveloperApplication { 7 | public static void main(String[] args) { 8 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 9 | } 10 | } -------------------------------------------------------------------------------- /chapter4/src/main/java/me/shinsunyoung/springbootdeveloper/TestController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | import java.util.List; 8 | 9 | @RestController 10 | public class TestController { 11 | @Autowired 12 | TestService testService; 13 | @GetMapping("/test") 14 | public List getAllMembers() { 15 | List members = testService.getAllMembers(); 16 | return members; 17 | } 18 | } -------------------------------------------------------------------------------- /chapter4/src/main/java/me/shinsunyoung/springbootdeveloper/TestService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | 6 | import java.util.List; 7 | 8 | @Service 9 | public class TestService { 10 | 11 | @Autowired 12 | MemberRepository memberRepository; 13 | 14 | public List getAllMembers() { 15 | return memberRepository.findAll(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chapter4/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true 4 | properties: 5 | hibernate: 6 | format_sql: true 7 | defer-datasource-initialization: true -------------------------------------------------------------------------------- /chapter4/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO member (id, name) VALUES (1, 'name 1') 2 | INSERT INTO member (id, name) VALUES (2, 'name 2') 3 | INSERT INTO member (id, name) VALUES (3, 'name 3') -------------------------------------------------------------------------------- /chapter4/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

index.html

9 | 10 | -------------------------------------------------------------------------------- /chapter4/src/test/java/JUnitCycleTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.*; 2 | 3 | public class JUnitCycleTest { 4 | @BeforeAll 5 | static void beforeAll() { 6 | System.out.println("@BeforeAll"); 7 | } 8 | 9 | @BeforeEach 10 | public void beforeEach() { 11 | System.out.println("@BeforeEach"); 12 | } 13 | 14 | @Test 15 | public void test1() { 16 | System.out.println("test1"); 17 | } 18 | 19 | @Test 20 | public void test2() { 21 | System.out.println("test2"); 22 | } 23 | 24 | @Test 25 | public void test3() { 26 | System.out.println("test3"); 27 | } 28 | 29 | @AfterAll 30 | static void afterAll() { 31 | System.out.println("@AfterAll"); 32 | } 33 | 34 | @AfterEach 35 | public void afterEach() { 36 | System.out.println("@AfterEach"); 37 | } 38 | } -------------------------------------------------------------------------------- /chapter4/src/test/java/JUnitQuiz.java: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.Test; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | public class JUnitQuiz { 6 | 7 | @Test 8 | public void junitQuiz1() { 9 | String name1 = "홍길동"; 10 | String name2 = "홍길동"; 11 | String name3 = "홍길은"; 12 | 13 | 14 | // ❶ 모든 변수가 null이 아닌지 확인 15 | assertThat(name1).isNotNull(); 16 | assertThat(name2).isNotNull(); 17 | assertThat(name3).isNotNull(); 18 | 19 | 20 | // ❷ name1과 name2가 같은지 확인 21 | assertThat(name1).isEqualTo(name2); 22 | 23 | 24 | // ❸ name1과 name3이 다른지 확인 25 | assertThat(name1).isNotEqualTo(name3); 26 | } 27 | 28 | @Test 29 | public void junitQuiz2() { 30 | int number1 = 15; 31 | int number2 = 0; 32 | int number3 = -5; 33 | 34 | 35 | // ❶ number1은 양수인지 확인 36 | assertThat(number1).isPositive(); 37 | 38 | 39 | // ❷ number2은 0인지 확인 40 | assertThat(number2).isZero(); 41 | 42 | 43 | // ❸ number3은 음수인지 확인 44 | assertThat(number3).isNegative(); 45 | 46 | 47 | // ❹ number1은 number2보다 큰지 확인 48 | assertThat(number1).isGreaterThan(number2); 49 | 50 | 51 | // ❺ number3은 number2보다 작은지 확인 52 | assertThat(number3).isLessThan(number2); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /chapter4/src/test/java/JUnitTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.Assertions; 2 | import org.junit.jupiter.api.DisplayName; 3 | import org.junit.jupiter.api.Test; 4 | 5 | public class JUnitTest { 6 | @DisplayName("1 + 2는 3이다") 7 | @Test 8 | public void junitTest() { 9 | int a = 1; 10 | int b = 2; 11 | int sum = 3; 12 | Assertions.assertEquals(sum, a + b); 13 | } 14 | 15 | // @DisplayName("1 + 3는 4이다.") 16 | // @Test 17 | // public void junitFailedTest() { 18 | // int a = 1; 19 | // int b = 3; 20 | // int sum = 3; 21 | // Assertions.assertEquals(sum, a + b); 22 | // } 23 | } -------------------------------------------------------------------------------- /chapter4/src/test/java/JunitCycleQuiz.java: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.AfterAll; 2 | import org.junit.jupiter.api.BeforeEach; 3 | import org.junit.jupiter.api.Test; 4 | 5 | public class JunitCycleQuiz { 6 | 7 | @BeforeEach 8 | public void beforeEach() { 9 | System.out.println("Hello!"); 10 | } 11 | 12 | 13 | @AfterAll 14 | public static void afterAll() { 15 | System.out.println("Bye!"); 16 | } 17 | 18 | 19 | @Test 20 | public void junitQuiz3() { 21 | System.out.println("This is first test"); 22 | } 23 | 24 | 25 | @Test 26 | public void junitQuiz4() { 27 | System.out.println("This is second test"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /chapter5/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.2.0' 4 | id 'io.spring.dependency-management' version '1.1.0' 5 | } 6 | 7 | group 'me.shinsunyoung' 8 | version '1.0' 9 | sourceCompatibility = '17' 10 | 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | 17 | dependencies { 18 | implementation 'org.springframework.boot:spring-boot-starter-web' 19 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 20 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 21 | runtimeOnly 'com.h2database:h2' 22 | compileOnly 'org.projectlombok:lombok' 23 | annotationProcessor 'org.projectlombok:lombok' 24 | } 25 | 26 | 27 | test { 28 | useJUnitPlatform() 29 | } 30 | -------------------------------------------------------------------------------- /chapter5/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter5/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 04 17:36:39 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /chapter5/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-developer' 2 | 3 | -------------------------------------------------------------------------------- /chapter5/src/main/java/me/shinsunyoung/Main.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world!"); 6 | } 7 | } -------------------------------------------------------------------------------- /chapter5/src/main/java/me/shinsunyoung/springbootdeveloper/Member.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AccessLevel; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @AllArgsConstructor 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | @Getter 12 | @Entity 13 | public class Member { 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.IDENTITY) 16 | @Column(name = "id", updatable = false) 17 | private Long id; 18 | 19 | @Column(name = "name", nullable = false) 20 | private String name; 21 | 22 | public void changeName(String name) { 23 | this.name = name; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /chapter5/src/main/java/me/shinsunyoung/springbootdeveloper/MemberRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Query; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.Optional; 8 | 9 | @Repository 10 | public interface MemberRepository extends JpaRepository { 11 | Optional findByName(String name); 12 | 13 | @Query("select m from Member m where m.name = ?1") 14 | Optional findByNameQuery(String name); 15 | } -------------------------------------------------------------------------------- /chapter5/src/main/java/me/shinsunyoung/springbootdeveloper/QuizController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.http.ResponseEntity; 4 | import org.springframework.web.bind.annotation.*; 5 | 6 | @RestController 7 | public class QuizController { 8 | 9 | 10 | @GetMapping("/quiz") 11 | public ResponseEntity quiz(@RequestParam("code") int code) { 12 | switch (code) { 13 | case 1: 14 | return ResponseEntity.created(null).body("Created!"); 15 | case 2: 16 | return ResponseEntity.badRequest().body("Bad Request!"); 17 | default: 18 | return ResponseEntity.ok().body("OK!"); 19 | } 20 | } 21 | 22 | 23 | @PostMapping("/quiz") 24 | public ResponseEntity quiz2(@RequestBody Code code) { 25 | 26 | 27 | switch (code.value()) { 28 | case 1: 29 | return ResponseEntity.status(403).body("Forbidden!"); 30 | default: 31 | return ResponseEntity.ok().body("OK!"); 32 | } 33 | } 34 | } 35 | 36 | 37 | record Code(int value) {} 38 | -------------------------------------------------------------------------------- /chapter5/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | @SpringBootApplication 6 | public class SpringBootDeveloperApplication { 7 | public static void main(String[] args) { 8 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 9 | } 10 | } -------------------------------------------------------------------------------- /chapter5/src/main/java/me/shinsunyoung/springbootdeveloper/TestController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | import java.util.List; 8 | 9 | @RestController 10 | public class TestController { 11 | @Autowired 12 | TestService testService; 13 | @GetMapping("/test") 14 | public List getAllMembers() { 15 | List members = testService.getAllMembers(); 16 | return members; 17 | } 18 | } -------------------------------------------------------------------------------- /chapter5/src/main/java/me/shinsunyoung/springbootdeveloper/TestService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | 6 | import java.util.List; 7 | 8 | @Service 9 | public class TestService { 10 | 11 | @Autowired 12 | MemberRepository memberRepository; 13 | 14 | public List getAllMembers() { 15 | return memberRepository.findAll(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /chapter5/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true 4 | properties: 5 | hibernate: 6 | format_sql: true 7 | defer-datasource-initialization: true -------------------------------------------------------------------------------- /chapter5/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO member (id, name) VALUES (1, 'name 1') 2 | INSERT INTO member (id, name) VALUES (2, 'name 2') 3 | INSERT INTO member (id, name) VALUES (3, 'name 3') -------------------------------------------------------------------------------- /chapter5/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

index.html

9 | 10 | -------------------------------------------------------------------------------- /chapter5/src/test/java/JUnitCycleTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.*; 2 | 3 | public class JUnitCycleTest { 4 | @BeforeAll 5 | static void beforeAll() { 6 | System.out.println("@BeforeAll"); 7 | } 8 | 9 | @BeforeEach 10 | public void beforeEach() { 11 | System.out.println("@BeforeEach"); 12 | } 13 | 14 | @Test 15 | public void test1() { 16 | System.out.println("test1"); 17 | } 18 | 19 | @Test 20 | public void test2() { 21 | System.out.println("test2"); 22 | } 23 | 24 | @Test 25 | public void test3() { 26 | System.out.println("test3"); 27 | } 28 | 29 | @AfterAll 30 | static void afterAll() { 31 | System.out.println("@AfterAll"); 32 | } 33 | 34 | @AfterEach 35 | public void afterEach() { 36 | System.out.println("@AfterEach"); 37 | } 38 | } -------------------------------------------------------------------------------- /chapter5/src/test/java/JUnitQuiz.java: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.Test; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | public class JUnitQuiz { 6 | 7 | @Test 8 | public void junitQuiz1() { 9 | String name1 = "홍길동"; 10 | String name2 = "홍길동"; 11 | String name3 = "홍길은"; 12 | 13 | 14 | // ❶ 모든 변수가 null이 아닌지 확인 15 | assertThat(name1).isNotNull(); 16 | assertThat(name2).isNotNull(); 17 | assertThat(name3).isNotNull(); 18 | 19 | 20 | // ❷ name1과 name2가 같은지 확인 21 | assertThat(name1).isEqualTo(name2); 22 | 23 | 24 | // ❸ name1과 name3이 다른지 확인 25 | assertThat(name1).isNotEqualTo(name3); 26 | } 27 | 28 | @Test 29 | public void junitQuiz2() { 30 | int number1 = 15; 31 | int number2 = 0; 32 | int number3 = -5; 33 | 34 | 35 | // ❶ number1은 양수인지 확인 36 | assertThat(number1).isPositive(); 37 | 38 | 39 | // ❷ number2은 0인지 확인 40 | assertThat(number2).isZero(); 41 | 42 | 43 | // ❸ number3은 음수인지 확인 44 | assertThat(number3).isNegative(); 45 | 46 | 47 | // ❹ number1은 number2보다 큰지 확인 48 | assertThat(number1).isGreaterThan(number2); 49 | 50 | 51 | // ❺ number3은 number2보다 작은지 확인 52 | assertThat(number3).isLessThan(number2); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /chapter5/src/test/java/JUnitTest.java: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.Assertions; 2 | import org.junit.jupiter.api.DisplayName; 3 | import org.junit.jupiter.api.Test; 4 | 5 | public class JUnitTest { 6 | @DisplayName("1 + 2는 3이다") 7 | @Test 8 | public void junitTest() { 9 | int a = 1; 10 | int b = 2; 11 | int sum = 3; 12 | Assertions.assertEquals(sum, a + b); 13 | } 14 | 15 | // @DisplayName("1 + 3는 4이다.") 16 | // @Test 17 | // public void junitFailedTest() { 18 | // int a = 1; 19 | // int b = 3; 20 | // int sum = 3; 21 | // Assertions.assertEquals(sum, a + b); 22 | // } 23 | } -------------------------------------------------------------------------------- /chapter5/src/test/java/JunitCycleQuiz.java: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.AfterAll; 2 | import org.junit.jupiter.api.BeforeEach; 3 | import org.junit.jupiter.api.Test; 4 | 5 | public class JunitCycleQuiz { 6 | 7 | @BeforeEach 8 | public void beforeEach() { 9 | System.out.println("Hello!"); 10 | } 11 | 12 | 13 | @AfterAll 14 | public static void afterAll() { 15 | System.out.println("Bye!"); 16 | } 17 | 18 | 19 | @Test 20 | public void junitQuiz3() { 21 | System.out.println("This is first test"); 22 | } 23 | 24 | 25 | @Test 26 | public void junitQuiz4() { 27 | System.out.println("This is second test"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /chapter5/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | sql: 3 | init: 4 | mode: never -------------------------------------------------------------------------------- /chapter5/src/test/resources/insert-members.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO member (id, name) VALUES (1, 'A') 2 | INSERT INTO member (id, name) VALUES (2, 'B') 3 | INSERT INTO member (id, name) VALUES (3, 'C') -------------------------------------------------------------------------------- /chapter6/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.2.0' 4 | id 'io.spring.dependency-management' version '1.1.0' 5 | } 6 | 7 | group 'me.shinsunyoung' 8 | version '1.0' 9 | sourceCompatibility = '17' 10 | 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | 17 | dependencies { 18 | implementation 'org.springframework.boot:spring-boot-starter-web' 19 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 20 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 21 | runtimeOnly 'com.h2database:h2' 22 | compileOnly 'org.projectlombok:lombok' 23 | annotationProcessor 'org.projectlombok:lombok' 24 | } 25 | 26 | 27 | test { 28 | useJUnitPlatform() 29 | } 30 | -------------------------------------------------------------------------------- /chapter6/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter6/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 04 17:36:39 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /chapter6/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-developer' 2 | 3 | -------------------------------------------------------------------------------- /chapter6/src/main/java/me/shinsunyoung/Main.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world!"); 6 | } 7 | } -------------------------------------------------------------------------------- /chapter6/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | @SpringBootApplication 6 | public class SpringBootDeveloperApplication { 7 | public static void main(String[] args) { 8 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 9 | } 10 | } -------------------------------------------------------------------------------- /chapter6/src/main/java/me/shinsunyoung/springbootdeveloper/domain/Article.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.domain; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AccessLevel; 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Entity 10 | @Getter 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | public class Article { 13 | @Id 14 | @GeneratedValue(strategy = GenerationType.IDENTITY) 15 | @Column(name = "id", updatable = false) 16 | private Long id; 17 | 18 | @Column(name = "title", nullable = false) 19 | private String title; 20 | 21 | @Column(name = "content", nullable = false) 22 | private String content; 23 | 24 | @Builder 25 | public Article(String title, String content) { 26 | this.title = title; 27 | this.content = content; 28 | } 29 | 30 | public void update(String title, String content) { 31 | this.title = title; 32 | this.content = content; 33 | } 34 | } -------------------------------------------------------------------------------- /chapter6/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import me.shinsunyoung.springbootdeveloper.domain.Article; 7 | 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | @Getter 11 | public class AddArticleRequest { 12 | private String title; 13 | private String content; 14 | public Article toEntity() { 15 | return Article.builder() 16 | .title(title) 17 | .content(content) 18 | .build(); 19 | } 20 | } -------------------------------------------------------------------------------- /chapter6/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleResponse { 8 | private final String title; 9 | private final String content; 10 | public ArticleResponse(Article article) { 11 | this.title = article.getTitle(); 12 | this.content = article.getContent(); 13 | } 14 | } -------------------------------------------------------------------------------- /chapter6/src/main/java/me/shinsunyoung/springbootdeveloper/dto/UpdateArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @NoArgsConstructor 8 | @AllArgsConstructor 9 | @Getter 10 | public class UpdateArticleRequest { 11 | private String title; 12 | private String content; 13 | } -------------------------------------------------------------------------------- /chapter6/src/main/java/me/shinsunyoung/springbootdeveloper/repository/BlogRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.Article; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface BlogRepository extends JpaRepository { 7 | } -------------------------------------------------------------------------------- /chapter6/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true 4 | properties: 5 | hibernate: 6 | format_sql: true 7 | defer-datasource-initialization: true 8 | datasource: 9 | url: jdbc:h2:mem:testdb 10 | h2: 11 | console: 12 | enabled: true -------------------------------------------------------------------------------- /chapter6/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO article (title, content) VALUES ('제목 1', '내용 1') 2 | INSERT INTO article (title, content) VALUES ('제목 2', '내용 2') 3 | INSERT INTO article (title, content) VALUES ('제목 3', '내용 3') -------------------------------------------------------------------------------- /chapter7/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.2.0' 4 | id 'io.spring.dependency-management' version '1.1.0' 5 | } 6 | 7 | group 'me.shinsunyoung' 8 | version '1.0' 9 | sourceCompatibility = '17' 10 | 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | 17 | dependencies { 18 | implementation 'org.springframework.boot:spring-boot-starter-web' 19 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 20 | implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' 21 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 22 | runtimeOnly 'com.h2database:h2' 23 | compileOnly 'org.projectlombok:lombok' 24 | annotationProcessor 'org.projectlombok:lombok' 25 | } 26 | 27 | 28 | test { 29 | useJUnitPlatform() 30 | } 31 | -------------------------------------------------------------------------------- /chapter7/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter7/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 04 17:36:39 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /chapter7/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-developer' 2 | 3 | -------------------------------------------------------------------------------- /chapter7/src/main/java/me/shinsunyoung/Main.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world!"); 6 | } 7 | } -------------------------------------------------------------------------------- /chapter7/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 6 | 7 | @EnableJpaAuditing 8 | @SpringBootApplication 9 | public class SpringBootDeveloperApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 12 | } 13 | } -------------------------------------------------------------------------------- /chapter7/src/main/java/me/shinsunyoung/springbootdeveloper/controller/ExampleController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.ui.Model; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | 9 | import java.time.LocalDate; 10 | import java.util.List; 11 | 12 | @Controller 13 | public class ExampleController { 14 | @GetMapping("/thymeleaf/example") 15 | public String thymeleafExample(Model model) { 16 | Person examplePerson = new Person(); 17 | examplePerson.setId(1L); 18 | examplePerson.setName("홍길동"); 19 | examplePerson.setAge(11); 20 | examplePerson.setHobbies(List.of("운동", "독서")); 21 | 22 | model.addAttribute("person", examplePerson); 23 | model.addAttribute("today", LocalDate.now()); 24 | 25 | return "example"; 26 | } 27 | @Setter 28 | @Getter 29 | class Person { 30 | private Long id; 31 | private String name; 32 | private int age; 33 | private List hobbies; 34 | } 35 | } -------------------------------------------------------------------------------- /chapter7/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import me.shinsunyoung.springbootdeveloper.domain.Article; 7 | 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | @Getter 11 | public class AddArticleRequest { 12 | private String title; 13 | private String content; 14 | public Article toEntity() { 15 | return Article.builder() 16 | .title(title) 17 | .content(content) 18 | .build(); 19 | } 20 | } -------------------------------------------------------------------------------- /chapter7/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleListViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleListViewResponse { 8 | private final Long id; 9 | private final String title; 10 | private final String content; 11 | 12 | public ArticleListViewResponse(Article article) { 13 | this.id = article.getId(); 14 | this.title = article.getTitle(); 15 | this.content = article.getContent(); 16 | } 17 | } -------------------------------------------------------------------------------- /chapter7/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleResponse { 8 | private final String title; 9 | private final String content; 10 | public ArticleResponse(Article article) { 11 | this.title = article.getTitle(); 12 | this.content = article.getContent(); 13 | } 14 | } -------------------------------------------------------------------------------- /chapter7/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import me.shinsunyoung.springbootdeveloper.domain.Article; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | @NoArgsConstructor 10 | @Getter 11 | public class ArticleViewResponse { 12 | private Long id; 13 | private String title; 14 | private String content; 15 | private LocalDateTime createdAt; 16 | 17 | public ArticleViewResponse(Article article) { 18 | this.id = article.getId(); 19 | this.title = article.getTitle(); 20 | this.content = article.getContent(); 21 | this.createdAt = article.getCreatedAt(); 22 | } 23 | } -------------------------------------------------------------------------------- /chapter7/src/main/java/me/shinsunyoung/springbootdeveloper/dto/UpdateArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @NoArgsConstructor 8 | @AllArgsConstructor 9 | @Getter 10 | public class UpdateArticleRequest { 11 | private String title; 12 | private String content; 13 | } -------------------------------------------------------------------------------- /chapter7/src/main/java/me/shinsunyoung/springbootdeveloper/repository/BlogRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.Article; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface BlogRepository extends JpaRepository { 7 | } -------------------------------------------------------------------------------- /chapter7/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true 4 | properties: 5 | hibernate: 6 | format_sql: true 7 | defer-datasource-initialization: true 8 | datasource: 9 | url: jdbc:h2:mem:testdb 10 | h2: 11 | console: 12 | enabled: true -------------------------------------------------------------------------------- /chapter7/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO article (title, content, created_at, updated_at) VALUES ('제목 1', '내용 1', NOW(), NOW()) 2 | INSERT INTO article (title, content, created_at, updated_at) VALUES ('제목 2', '내용 2', NOW(), NOW()) 3 | INSERT INTO article (title, content, created_at, updated_at) VALUES ('제목 3', '내용 3', NOW(), NOW()) -------------------------------------------------------------------------------- /chapter7/src/main/resources/templates/articleList.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 블로그 글 목록 6 | 7 | 8 | 9 |
10 |

My Blog

11 |

블로그에 오신 것을 환영합니다.

12 |
13 | 14 |
15 | 18 |
19 |
20 |
21 |
22 |
23 |
24 |

25 | 보러가기 26 |
27 |
28 |
29 |
30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /chapter7/src/main/resources/templates/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |

타임리프 익히기

9 |

10 |
11 |

12 |

13 |

취미

14 |
    15 |
  • 16 | (대표 취미) 17 |
18 |
19 | 20 | 글 보기 21 | 22 | -------------------------------------------------------------------------------- /chapter8/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.2.0' 4 | id 'io.spring.dependency-management' version '1.1.0' 5 | } 6 | 7 | group 'me.shinsunyoung' 8 | version '1.0' 9 | sourceCompatibility = '17' 10 | 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | 17 | dependencies { 18 | implementation 'org.springframework.boot:spring-boot-starter-web' 19 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 20 | implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' 21 | implementation 'org.springframework.boot:spring-boot-starter-security' 22 | implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' 23 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 24 | testImplementation 'org.springframework.security:spring-security-test' 25 | runtimeOnly 'com.h2database:h2' 26 | compileOnly 'org.projectlombok:lombok' 27 | annotationProcessor 'org.projectlombok:lombok' 28 | } 29 | 30 | 31 | test { 32 | useJUnitPlatform() 33 | } 34 | -------------------------------------------------------------------------------- /chapter8/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter8/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 04 17:36:39 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /chapter8/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-developer' 2 | 3 | -------------------------------------------------------------------------------- /chapter8/src/main/java/me/shinsunyoung/Main.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world!"); 6 | } 7 | } -------------------------------------------------------------------------------- /chapter8/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 6 | 7 | @EnableJpaAuditing 8 | @SpringBootApplication 9 | public class SpringBootDeveloperApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 12 | } 13 | } -------------------------------------------------------------------------------- /chapter8/src/main/java/me/shinsunyoung/springbootdeveloper/controller/ExampleController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.ui.Model; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | 9 | import java.time.LocalDate; 10 | import java.util.List; 11 | 12 | @Controller 13 | public class ExampleController { 14 | @GetMapping("/thymeleaf/example") 15 | public String thymeleafExample(Model model) { 16 | Person examplePerson = new Person(); 17 | examplePerson.setId(1L); 18 | examplePerson.setName("홍길동"); 19 | examplePerson.setAge(11); 20 | examplePerson.setHobbies(List.of("운동", "독서")); 21 | 22 | model.addAttribute("person", examplePerson); 23 | model.addAttribute("today", LocalDate.now()); 24 | 25 | return "example"; 26 | } 27 | @Setter 28 | @Getter 29 | class Person { 30 | private Long id; 31 | private String name; 32 | private int age; 33 | private List hobbies; 34 | } 35 | } -------------------------------------------------------------------------------- /chapter8/src/main/java/me/shinsunyoung/springbootdeveloper/controller/UserViewController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class UserViewController { 8 | @GetMapping("/login") 9 | public String login() { 10 | return "login"; 11 | } 12 | 13 | @GetMapping("/signup") 14 | public String signup() { 15 | return "signup"; 16 | } 17 | } -------------------------------------------------------------------------------- /chapter8/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import me.shinsunyoung.springbootdeveloper.domain.Article; 7 | 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | @Getter 11 | public class AddArticleRequest { 12 | private String title; 13 | private String content; 14 | public Article toEntity() { 15 | return Article.builder() 16 | .title(title) 17 | .content(content) 18 | .build(); 19 | } 20 | } -------------------------------------------------------------------------------- /chapter8/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddUserRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class AddUserRequest { 9 | private String email; 10 | private String password; 11 | } 12 | -------------------------------------------------------------------------------- /chapter8/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleListViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleListViewResponse { 8 | private final Long id; 9 | private final String title; 10 | private final String content; 11 | 12 | public ArticleListViewResponse(Article article) { 13 | this.id = article.getId(); 14 | this.title = article.getTitle(); 15 | this.content = article.getContent(); 16 | } 17 | } -------------------------------------------------------------------------------- /chapter8/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleResponse { 8 | private final String title; 9 | private final String content; 10 | public ArticleResponse(Article article) { 11 | this.title = article.getTitle(); 12 | this.content = article.getContent(); 13 | } 14 | } -------------------------------------------------------------------------------- /chapter8/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import me.shinsunyoung.springbootdeveloper.domain.Article; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | @NoArgsConstructor 10 | @Getter 11 | public class ArticleViewResponse { 12 | private Long id; 13 | private String title; 14 | private String content; 15 | private LocalDateTime createdAt; 16 | 17 | public ArticleViewResponse(Article article) { 18 | this.id = article.getId(); 19 | this.title = article.getTitle(); 20 | this.content = article.getContent(); 21 | this.createdAt = article.getCreatedAt(); 22 | } 23 | } -------------------------------------------------------------------------------- /chapter8/src/main/java/me/shinsunyoung/springbootdeveloper/dto/UpdateArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @NoArgsConstructor 8 | @AllArgsConstructor 9 | @Getter 10 | public class UpdateArticleRequest { 11 | private String title; 12 | private String content; 13 | } -------------------------------------------------------------------------------- /chapter8/src/main/java/me/shinsunyoung/springbootdeveloper/repository/BlogRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.Article; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface BlogRepository extends JpaRepository { 7 | } -------------------------------------------------------------------------------- /chapter8/src/main/java/me/shinsunyoung/springbootdeveloper/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface UserRepository extends JpaRepository { 9 | Optional findByEmail(String email); 10 | } -------------------------------------------------------------------------------- /chapter8/src/main/java/me/shinsunyoung/springbootdeveloper/service/UserDetailService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.User; 5 | import me.shinsunyoung.springbootdeveloper.repository.UserRepository; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class UserDetailService implements UserDetailsService { 12 | private final UserRepository userRepository; 13 | @Override 14 | public User loadUserByUsername(String email) { 15 | return userRepository.findByEmail(email) 16 | .orElseThrow(() -> new IllegalArgumentException((email))); 17 | } 18 | } -------------------------------------------------------------------------------- /chapter8/src/main/java/me/shinsunyoung/springbootdeveloper/service/UserService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.User; 5 | import me.shinsunyoung.springbootdeveloper.dto.AddUserRequest; 6 | import me.shinsunyoung.springbootdeveloper.repository.UserRepository; 7 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 8 | import org.springframework.stereotype.Service; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class UserService { 13 | private final UserRepository userRepository; 14 | private final BCryptPasswordEncoder bCryptPasswordEncoder; 15 | public Long save(AddUserRequest dto) { 16 | return userRepository.save(User.builder() 17 | .email(dto.getEmail()) 18 | .password(bCryptPasswordEncoder.encode(dto.getPassword())) 19 | .build()).getId(); 20 | } 21 | } -------------------------------------------------------------------------------- /chapter8/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true 4 | properties: 5 | hibernate: 6 | format_sql: true 7 | defer-datasource-initialization: true 8 | datasource: 9 | url: jdbc:h2:mem:testdb 10 | username: sa 11 | h2: 12 | console: 13 | enabled: true -------------------------------------------------------------------------------- /chapter8/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO article (title, content, created_at, updated_at) VALUES ('제목 1', '내용 1', NOW(), NOW()) 2 | INSERT INTO article (title, content, created_at, updated_at) VALUES ('제목 2', '내용 2', NOW(), NOW()) 3 | INSERT INTO article (title, content, created_at, updated_at) VALUES ('제목 3', '내용 3', NOW(), NOW()) -------------------------------------------------------------------------------- /chapter8/src/main/resources/templates/articleList.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 블로그 글 목록 6 | 7 | 8 | 9 |
10 |

My Blog

11 |

블로그에 오신 것을 환영합니다.

12 |
13 | 14 |
15 | 18 |
19 |
20 |
21 |
22 |
23 |
24 |

25 | 보러가기 26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /chapter8/src/main/resources/templates/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |

타임리프 익히기

9 |

10 |
11 |

12 |

13 |

취미

14 |
    15 |
  • 16 | (대표 취미) 17 |
18 |
19 | 20 | 글 보기 21 | 22 | -------------------------------------------------------------------------------- /chapter9/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.2.0' 4 | id 'io.spring.dependency-management' version '1.1.0' 5 | } 6 | 7 | group 'me.shinsunyoung' 8 | version '1.0' 9 | sourceCompatibility = '17' 10 | 11 | 12 | repositories { 13 | mavenCentral() 14 | } 15 | 16 | 17 | dependencies { 18 | implementation 'org.springframework.boot:spring-boot-starter-web' 19 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 20 | implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' 21 | implementation 'org.springframework.boot:spring-boot-starter-security' 22 | implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' 23 | implementation 'io.jsonwebtoken:jjwt:0.9.1' 24 | implementation 'javax.xml.bind:jaxb-api:2.3.1' 25 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 26 | testImplementation 'org.springframework.security:spring-security-test' 27 | testImplementation 'org.projectlombok:lombok' 28 | runtimeOnly 'com.h2database:h2' 29 | compileOnly 'org.projectlombok:lombok' 30 | annotationProcessor 'org.projectlombok:lombok' 31 | testAnnotationProcessor 'org.projectlombok:lombok' 32 | 33 | } 34 | 35 | 36 | test { 37 | useJUnitPlatform() 38 | } 39 | -------------------------------------------------------------------------------- /chapter9/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shinsunyoung/springboot-developer-2rd/86be064b12668b946127dd9f36ffc1d609bafe91/chapter9/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /chapter9/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Nov 04 17:36:39 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /chapter9/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'springboot-developer' 2 | 3 | -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/Main.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world!"); 6 | } 7 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/SpringBootDeveloperApplication.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 6 | 7 | @EnableJpaAuditing 8 | @SpringBootApplication 9 | public class SpringBootDeveloperApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(SpringBootDeveloperApplication.class, args); 12 | } 13 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/config/jwt/JwtProperties.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.config.jwt; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Setter 9 | @Getter 10 | @Component 11 | @ConfigurationProperties("jwt") 12 | public class JwtProperties { 13 | private String issuer; 14 | private String secretKey; 15 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/controller/ExampleController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.ui.Model; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | 9 | import java.time.LocalDate; 10 | import java.util.List; 11 | 12 | @Controller 13 | public class ExampleController { 14 | @GetMapping("/thymeleaf/example") 15 | public String thymeleafExample(Model model) { 16 | Person examplePerson = new Person(); 17 | examplePerson.setId(1L); 18 | examplePerson.setName("홍길동"); 19 | examplePerson.setAge(11); 20 | examplePerson.setHobbies(List.of("운동", "독서")); 21 | 22 | model.addAttribute("person", examplePerson); 23 | model.addAttribute("today", LocalDate.now()); 24 | 25 | return "example"; 26 | } 27 | @Setter 28 | @Getter 29 | class Person { 30 | private Long id; 31 | private String name; 32 | private int age; 33 | private List hobbies; 34 | } 35 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/controller/TokenApiController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.dto.CreateAccessTokenRequest; 5 | import me.shinsunyoung.springbootdeveloper.dto.CreateAccessTokenResponse; 6 | import me.shinsunyoung.springbootdeveloper.service.TokenService; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | @RequiredArgsConstructor 14 | @RestController 15 | public class TokenApiController { 16 | private final TokenService tokenService; 17 | 18 | @PostMapping("/api/token") 19 | public ResponseEntity createNewAccessToken(@RequestBody CreateAccessTokenRequest request) { 20 | String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken()); 21 | 22 | return ResponseEntity.status(HttpStatus.CREATED) 23 | .body(new CreateAccessTokenResponse(newAccessToken)); 24 | } 25 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/controller/UserViewController.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class UserViewController { 8 | @GetMapping("/login") 9 | public String login() { 10 | return "login"; 11 | } 12 | 13 | @GetMapping("/signup") 14 | public String signup() { 15 | return "signup"; 16 | } 17 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/domain/RefreshToken.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.domain; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 9 | @Getter 10 | @Entity 11 | public class RefreshToken { 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.IDENTITY) 14 | @Column(name = "id", updatable = false) 15 | private Long id; 16 | 17 | @Column(name = "user_id", nullable = false, unique = true) 18 | private Long userId; 19 | 20 | @Column(name = "refresh_token", nullable = false) 21 | private String refreshToken; 22 | 23 | public RefreshToken(Long userId, String refreshToken) { 24 | this.userId = userId; 25 | this.refreshToken = refreshToken; 26 | } 27 | 28 | public RefreshToken update(String newRefreshToken) { 29 | this.refreshToken = newRefreshToken; 30 | return this; 31 | } 32 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import me.shinsunyoung.springbootdeveloper.domain.Article; 7 | 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | @Getter 11 | public class AddArticleRequest { 12 | private String title; 13 | private String content; 14 | public Article toEntity() { 15 | return Article.builder() 16 | .title(title) 17 | .content(content) 18 | .build(); 19 | } 20 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/dto/AddUserRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class AddUserRequest { 9 | private String email; 10 | private String password; 11 | } 12 | -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleListViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleListViewResponse { 8 | private final Long id; 9 | private final String title; 10 | private final String content; 11 | 12 | public ArticleListViewResponse(Article article) { 13 | this.id = article.getId(); 14 | this.title = article.getTitle(); 15 | this.content = article.getContent(); 16 | } 17 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import me.shinsunyoung.springbootdeveloper.domain.Article; 5 | 6 | @Getter 7 | public class ArticleResponse { 8 | private final String title; 9 | private final String content; 10 | public ArticleResponse(Article article) { 11 | this.title = article.getTitle(); 12 | this.content = article.getContent(); 13 | } 14 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/dto/ArticleViewResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import me.shinsunyoung.springbootdeveloper.domain.Article; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | @NoArgsConstructor 10 | @Getter 11 | public class ArticleViewResponse { 12 | private Long id; 13 | private String title; 14 | private String content; 15 | private LocalDateTime createdAt; 16 | 17 | public ArticleViewResponse(Article article) { 18 | this.id = article.getId(); 19 | this.title = article.getTitle(); 20 | this.content = article.getContent(); 21 | this.createdAt = article.getCreatedAt(); 22 | } 23 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/dto/CreateAccessTokenRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class CreateAccessTokenRequest { 9 | private String refreshToken; 10 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/dto/CreateAccessTokenResponse.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @AllArgsConstructor 7 | @Getter 8 | public class CreateAccessTokenResponse { 9 | private String accessToken; 10 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/dto/UpdateArticleRequest.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @NoArgsConstructor 8 | @AllArgsConstructor 9 | @Getter 10 | public class UpdateArticleRequest { 11 | private String title; 12 | private String content; 13 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/repository/BlogRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.Article; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface BlogRepository extends JpaRepository { 7 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/repository/RefreshTokenRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.RefreshToken; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface RefreshTokenRepository extends JpaRepository { 9 | Optional findByUserId(Long userId); 10 | Optional findByRefreshToken(String refreshToken); 11 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.repository; 2 | 3 | import me.shinsunyoung.springbootdeveloper.domain.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface UserRepository extends JpaRepository { 9 | Optional findByEmail(String email); 10 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/service/RefreshTokenService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.RefreshToken; 5 | import me.shinsunyoung.springbootdeveloper.repository.RefreshTokenRepository; 6 | import org.springframework.stereotype.Service; 7 | 8 | @RequiredArgsConstructor 9 | @Service 10 | public class RefreshTokenService { 11 | private final RefreshTokenRepository refreshTokenRepository; 12 | public RefreshToken findByRefreshToken(String refreshToken) { 13 | return refreshTokenRepository.findByRefreshToken(refreshToken) 14 | .orElseThrow(() -> new IllegalArgumentException("Unexpected token")); 15 | } 16 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/service/TokenService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.config.jwt.TokenProvider; 5 | import me.shinsunyoung.springbootdeveloper.domain.User; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.time.Duration; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class TokenService { 13 | private final TokenProvider tokenProvider; 14 | private final RefreshTokenService refreshTokenService; 15 | private final UserService userService; 16 | 17 | public String createNewAccessToken(String refreshToken) { 18 | if(!tokenProvider.validToken(refreshToken)) { 19 | throw new IllegalArgumentException("Unexpected token"); 20 | } 21 | 22 | Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId(); 23 | User user = userService.findById(userId); 24 | 25 | return tokenProvider.generateToken(user, Duration.ofHours(2)); 26 | } 27 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/service/UserDetailService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.User; 5 | import me.shinsunyoung.springbootdeveloper.repository.UserRepository; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.stereotype.Service; 8 | 9 | @RequiredArgsConstructor 10 | @Service 11 | public class UserDetailService implements UserDetailsService { 12 | private final UserRepository userRepository; 13 | @Override 14 | public User loadUserByUsername(String email) { 15 | return userRepository.findByEmail(email) 16 | .orElseThrow(() -> new IllegalArgumentException((email))); 17 | } 18 | } -------------------------------------------------------------------------------- /chapter9/src/main/java/me/shinsunyoung/springbootdeveloper/service/UserService.java: -------------------------------------------------------------------------------- 1 | package me.shinsunyoung.springbootdeveloper.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import me.shinsunyoung.springbootdeveloper.domain.User; 5 | import me.shinsunyoung.springbootdeveloper.dto.AddUserRequest; 6 | import me.shinsunyoung.springbootdeveloper.repository.UserRepository; 7 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 8 | import org.springframework.stereotype.Service; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class UserService { 13 | private final UserRepository userRepository; 14 | private final BCryptPasswordEncoder bCryptPasswordEncoder; 15 | public Long save(AddUserRequest dto) { 16 | return userRepository.save(User.builder() 17 | .email(dto.getEmail()) 18 | .password(bCryptPasswordEncoder.encode(dto.getPassword())) 19 | .build()).getId(); 20 | } 21 | 22 | public User findById(Long userId) { 23 | return userRepository.findById(userId) 24 | .orElseThrow(() -> new IllegalArgumentException("Unexpected user")); 25 | } 26 | } -------------------------------------------------------------------------------- /chapter9/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true 4 | properties: 5 | hibernate: 6 | format_sql: true 7 | defer-datasource-initialization: true 8 | datasource: 9 | url: jdbc:h2:mem:testdb 10 | username: sa 11 | h2: 12 | console: 13 | enabled: true 14 | jwt: 15 | issuer: ajufresh@gmail.com 16 | secret_key: study-springboot -------------------------------------------------------------------------------- /chapter9/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO article (title, content, created_at, updated_at) VALUES ('제목 1', '내용 1', NOW(), NOW()) 2 | INSERT INTO article (title, content, created_at, updated_at) VALUES ('제목 2', '내용 2', NOW(), NOW()) 3 | INSERT INTO article (title, content, created_at, updated_at) VALUES ('제목 3', '내용 3', NOW(), NOW()) -------------------------------------------------------------------------------- /chapter9/src/main/resources/templates/articleList.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 블로그 글 목록 6 | 7 | 8 | 9 |
10 |

My Blog

11 |

블로그에 오신 것을 환영합니다.

12 |
13 | 14 |
15 | 18 |
19 |
20 |
21 |
22 |
23 |
24 |

25 | 보러가기 26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /chapter9/src/main/resources/templates/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |

타임리프 익히기

9 |

10 |
11 |

12 |

13 |

취미

14 |
    15 |
  • 16 | (대표 취미) 17 |
18 |
19 | 20 | 글 보기 21 | 22 | --------------------------------------------------------------------------------