From 9407b7ae75e80781ed6d6026bea418f35154cf7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Tue, 22 Apr 2025 19:05:34 +0900 Subject: [PATCH 01/45] =?UTF-8?q?refactor=20:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=AA=85=EC=97=90=EC=84=9C=20boa?= =?UTF-8?q?rd=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/{board => }/BoardApplication.java | 2 +- .../controller/BlogApiController.java | 14 +++---- .../dto/request/ArticleCreateRequest.java | 2 +- .../dto/request/ArticleUpdateRequest.java | 2 +- .../dto/response/ArticleResponse.java | 4 +- .../{board => }/entity/ArticleEntity.java | 17 ++++++--- .../board/member/service/MemberService.java | 19 ---------- .../repository/BlogRepository.java | 4 +- .../{board => }/service/BlogService.java | 16 ++++---- .../{board => }/config/DataInitializer.java | 6 +-- .../com/{board => }/config/WebConfig.java | 7 ++-- .../config/auth/AuthConstants.java | 2 +- .../com/{board => }/config/auth/AuthUtil.java | 6 +-- .../AuthenticatedMemberArgumentResolver.java | 6 +-- .../auth/annotation/AuthenticatedMember.java | 2 +- .../config/filter/FilterConfig.java | 8 ++-- .../{board => }/config/jwt/JwtAuthFilter.java | 6 +-- .../{board => }/config/jwt/JwtProperties.java | 2 +- .../com/{board => }/config/jwt/JwtUtil.java | 2 +- .../config/jwt/TokenWithExpiration.java | 2 +- .../exception/CustomException.java | 2 +- .../{board => }/exception/ErrorCodeType.java | 2 +- .../{board => }/exception/ErrorResponse.java | 2 +- .../com/{board => }/exception/ErrorType.java | 2 +- .../exception/GlobalExceptionHandler.java | 4 +- .../custom/DifferentOwnerException.java | 6 +-- .../custom/EmailNotFoundException.java | 6 +-- .../custom/MyEntityNotFoundException.java | 6 +-- .../exception/custom/ServerException.java | 6 +-- .../exception/custom/SignUpException.java | 6 +-- .../member/controller/MemberController.java | 12 +++--- .../com/{board => }/member/domain/Member.java | 6 +-- .../member/dto/request/LoginRequest.java | 2 +- .../dto/request/MemberSignUpRequest.java | 2 +- .../member/dto/response/LoginResponse.java | 2 +- .../dto/response/MemberSignUpResponse.java | 4 +- .../member/entity/MemberEntity.java | 15 +++++--- .../member/message/ErrorMessage.java | 2 +- .../member/repository/MemberRepository.java | 7 ++-- .../com/member/service/MemberService.java | 19 ++++++++++ .../member/service/MemberServiceImpl.java | 28 +++++++------- .../{board => }/BoardApplicationTests.java | 2 +- .../BlogApiControllerIntegrationTest.java | 20 +++++----- .../controller/BlogApiControllerTest.java | 38 ++++++++++--------- .../{board => }/service/BlogServiceTest.java | 16 ++++---- .../MemberControllerIntegrationTest.java | 12 +++--- .../controller/MemberControllerTest.java | 26 ++++++------- .../member/service/MemberServiceImplTest.java | 20 +++++----- 48 files changed, 208 insertions(+), 196 deletions(-) rename src/main/java/com/{board => }/BoardApplication.java (95%) rename src/main/java/com/board/{board => }/controller/BlogApiController.java (89%) rename src/main/java/com/board/{board => }/dto/request/ArticleCreateRequest.java (93%) rename src/main/java/com/board/{board => }/dto/request/ArticleUpdateRequest.java (91%) rename src/main/java/com/board/{board => }/dto/response/ArticleResponse.java (85%) rename src/main/java/com/board/{board => }/entity/ArticleEntity.java (75%) delete mode 100644 src/main/java/com/board/member/service/MemberService.java rename src/main/java/com/board/{board => }/repository/BlogRepository.java (63%) rename src/main/java/com/board/{board => }/service/BlogService.java (82%) rename src/main/java/com/{board => }/config/DataInitializer.java (93%) rename src/main/java/com/{board => }/config/WebConfig.java (87%) rename src/main/java/com/{board => }/config/auth/AuthConstants.java (77%) rename src/main/java/com/{board => }/config/auth/AuthUtil.java (87%) rename src/main/java/com/{board => }/config/auth/AuthenticatedMemberArgumentResolver.java (91%) rename src/main/java/com/{board => }/config/auth/annotation/AuthenticatedMember.java (89%) rename src/main/java/com/{board => }/config/filter/FilterConfig.java (85%) rename src/main/java/com/{board => }/config/jwt/JwtAuthFilter.java (94%) rename src/main/java/com/{board => }/config/jwt/JwtProperties.java (93%) rename src/main/java/com/{board => }/config/jwt/JwtUtil.java (99%) rename src/main/java/com/{board => }/config/jwt/TokenWithExpiration.java (86%) rename src/main/java/com/{board => }/exception/CustomException.java (93%) rename src/main/java/com/{board => }/exception/ErrorCodeType.java (97%) rename src/main/java/com/{board => }/exception/ErrorResponse.java (97%) rename src/main/java/com/{board => }/exception/ErrorType.java (84%) rename src/main/java/com/{board => }/exception/GlobalExceptionHandler.java (94%) rename src/main/java/com/{board => }/exception/custom/DifferentOwnerException.java (81%) rename src/main/java/com/{board => }/exception/custom/EmailNotFoundException.java (80%) rename src/main/java/com/{board => }/exception/custom/MyEntityNotFoundException.java (81%) rename src/main/java/com/{board => }/exception/custom/ServerException.java (68%) rename src/main/java/com/{board => }/exception/custom/SignUpException.java (81%) rename src/main/java/com/{board => }/member/controller/MemberController.java (82%) rename src/main/java/com/{board => }/member/domain/Member.java (82%) rename src/main/java/com/{board => }/member/dto/request/LoginRequest.java (93%) rename src/main/java/com/{board => }/member/dto/request/MemberSignUpRequest.java (94%) rename src/main/java/com/{board => }/member/dto/response/LoginResponse.java (84%) rename src/main/java/com/{board => }/member/dto/response/MemberSignUpResponse.java (83%) rename src/main/java/com/{board => }/member/entity/MemberEntity.java (80%) rename src/main/java/com/{board => }/member/message/ErrorMessage.java (90%) rename src/main/java/com/{board => }/member/repository/MemberRepository.java (77%) create mode 100644 src/main/java/com/member/service/MemberService.java rename src/main/java/com/{board => }/member/service/MemberServiceImpl.java (76%) rename src/test/java/com/{board => }/BoardApplicationTests.java (90%) rename src/test/java/com/board/{board => }/controller/BlogApiControllerIntegrationTest.java (94%) rename src/test/java/com/board/{board => }/controller/BlogApiControllerTest.java (89%) rename src/test/java/com/board/{board => }/service/BlogServiceTest.java (93%) rename src/test/java/com/{board => }/member/controller/MemberControllerIntegrationTest.java (93%) rename src/test/java/com/{board => }/member/controller/MemberControllerTest.java (87%) rename src/test/java/com/{board => }/member/service/MemberServiceImplTest.java (89%) diff --git a/src/main/java/com/board/BoardApplication.java b/src/main/java/com/BoardApplication.java similarity index 95% rename from src/main/java/com/board/BoardApplication.java rename to src/main/java/com/BoardApplication.java index fc2d3d9..f6dc92e 100644 --- a/src/main/java/com/board/BoardApplication.java +++ b/src/main/java/com/BoardApplication.java @@ -1,4 +1,4 @@ -package com.board; +package com; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/src/main/java/com/board/board/controller/BlogApiController.java b/src/main/java/com/board/controller/BlogApiController.java similarity index 89% rename from src/main/java/com/board/board/controller/BlogApiController.java rename to src/main/java/com/board/controller/BlogApiController.java index 7dc0c3d..b21b4ff 100644 --- a/src/main/java/com/board/board/controller/BlogApiController.java +++ b/src/main/java/com/board/controller/BlogApiController.java @@ -1,11 +1,11 @@ -package com.board.board.controller; +package com.board.controller; -import com.board.board.dto.request.ArticleCreateRequest; -import com.board.board.dto.request.ArticleUpdateRequest; -import com.board.board.dto.response.ArticleResponse; -import com.board.board.entity.ArticleEntity; -import com.board.board.service.BlogService; -import com.board.config.auth.annotation.AuthenticatedMember; +import com.board.dto.request.ArticleCreateRequest; +import com.board.dto.request.ArticleUpdateRequest; +import com.board.dto.response.ArticleResponse; +import com.board.entity.ArticleEntity; +import com.board.service.BlogService; +import com.config.auth.annotation.AuthenticatedMember; import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/board/board/dto/request/ArticleCreateRequest.java b/src/main/java/com/board/dto/request/ArticleCreateRequest.java similarity index 93% rename from src/main/java/com/board/board/dto/request/ArticleCreateRequest.java rename to src/main/java/com/board/dto/request/ArticleCreateRequest.java index f12951a..c46365f 100644 --- a/src/main/java/com/board/board/dto/request/ArticleCreateRequest.java +++ b/src/main/java/com/board/dto/request/ArticleCreateRequest.java @@ -1,4 +1,4 @@ -package com.board.board.dto.request; +package com.board.dto.request; import jakarta.validation.constraints.NotBlank; import lombok.Builder; diff --git a/src/main/java/com/board/board/dto/request/ArticleUpdateRequest.java b/src/main/java/com/board/dto/request/ArticleUpdateRequest.java similarity index 91% rename from src/main/java/com/board/board/dto/request/ArticleUpdateRequest.java rename to src/main/java/com/board/dto/request/ArticleUpdateRequest.java index 62a1a2b..8d8b066 100644 --- a/src/main/java/com/board/board/dto/request/ArticleUpdateRequest.java +++ b/src/main/java/com/board/dto/request/ArticleUpdateRequest.java @@ -1,4 +1,4 @@ -package com.board.board.dto.request; +package com.board.dto.request; import jakarta.validation.constraints.NotBlank; import lombok.Getter; diff --git a/src/main/java/com/board/board/dto/response/ArticleResponse.java b/src/main/java/com/board/dto/response/ArticleResponse.java similarity index 85% rename from src/main/java/com/board/board/dto/response/ArticleResponse.java rename to src/main/java/com/board/dto/response/ArticleResponse.java index 499c760..cdbb7d9 100644 --- a/src/main/java/com/board/board/dto/response/ArticleResponse.java +++ b/src/main/java/com/board/dto/response/ArticleResponse.java @@ -1,6 +1,6 @@ -package com.board.board.dto.response; +package com.board.dto.response; -import com.board.board.entity.ArticleEntity; +import com.board.entity.ArticleEntity; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/board/board/entity/ArticleEntity.java b/src/main/java/com/board/entity/ArticleEntity.java similarity index 75% rename from src/main/java/com/board/board/entity/ArticleEntity.java rename to src/main/java/com/board/entity/ArticleEntity.java index 9a7ea4a..19df016 100644 --- a/src/main/java/com/board/board/entity/ArticleEntity.java +++ b/src/main/java/com/board/entity/ArticleEntity.java @@ -1,8 +1,15 @@ -package com.board.board.entity; - -import com.board.exception.custom.DifferentOwnerException; -import com.board.member.entity.MemberEntity; -import jakarta.persistence.*; +package com.board.entity; + +import com.exception.custom.DifferentOwnerException; +import com.member.entity.MemberEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/board/member/service/MemberService.java b/src/main/java/com/board/member/service/MemberService.java deleted file mode 100644 index 5b18938..0000000 --- a/src/main/java/com/board/member/service/MemberService.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.board.member.service; - -import com.board.member.dto.request.LoginRequest; -import com.board.member.dto.request.MemberSignUpRequest; -import com.board.member.dto.response.LoginResponse; -import com.board.member.dto.response.MemberSignUpResponse; -import com.board.member.entity.MemberEntity; - -public interface MemberService { - - MemberSignUpResponse signUp(MemberSignUpRequest request); - - LoginResponse login(LoginRequest request); - - MemberEntity findByEmail(String email); - - MemberEntity findById(Long id); - -} diff --git a/src/main/java/com/board/board/repository/BlogRepository.java b/src/main/java/com/board/repository/BlogRepository.java similarity index 63% rename from src/main/java/com/board/board/repository/BlogRepository.java rename to src/main/java/com/board/repository/BlogRepository.java index 7d92f5c..0ed3ab7 100644 --- a/src/main/java/com/board/board/repository/BlogRepository.java +++ b/src/main/java/com/board/repository/BlogRepository.java @@ -1,6 +1,6 @@ -package com.board.board.repository; +package com.board.repository; -import com.board.board.entity.ArticleEntity; +import com.board.entity.ArticleEntity; import org.springframework.data.jpa.repository.JpaRepository; public interface BlogRepository extends JpaRepository { diff --git a/src/main/java/com/board/board/service/BlogService.java b/src/main/java/com/board/service/BlogService.java similarity index 82% rename from src/main/java/com/board/board/service/BlogService.java rename to src/main/java/com/board/service/BlogService.java index 67fd16b..4ac1cf1 100644 --- a/src/main/java/com/board/board/service/BlogService.java +++ b/src/main/java/com/board/service/BlogService.java @@ -1,12 +1,12 @@ -package com.board.board.service; +package com.board.service; -import com.board.board.dto.request.ArticleCreateRequest; -import com.board.board.dto.request.ArticleUpdateRequest; -import com.board.board.entity.ArticleEntity; -import com.board.board.repository.BlogRepository; -import com.board.exception.custom.MyEntityNotFoundException; -import com.board.member.entity.MemberEntity; -import com.board.member.service.MemberService; +import com.board.dto.request.ArticleCreateRequest; +import com.board.dto.request.ArticleUpdateRequest; +import com.board.entity.ArticleEntity; +import com.board.repository.BlogRepository; +import com.exception.custom.MyEntityNotFoundException; +import com.member.entity.MemberEntity; +import com.member.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/com/board/config/DataInitializer.java b/src/main/java/com/config/DataInitializer.java similarity index 93% rename from src/main/java/com/board/config/DataInitializer.java rename to src/main/java/com/config/DataInitializer.java index 8dc89bf..84e8eb0 100644 --- a/src/main/java/com/board/config/DataInitializer.java +++ b/src/main/java/com/config/DataInitializer.java @@ -1,7 +1,7 @@ -package com.board.config; +package com.config; -import com.board.board.repository.BlogRepository; -import com.board.member.repository.MemberRepository; +import com.board.repository.BlogRepository; +import com.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/board/config/WebConfig.java b/src/main/java/com/config/WebConfig.java similarity index 87% rename from src/main/java/com/board/config/WebConfig.java rename to src/main/java/com/config/WebConfig.java index 3b82b18..2b50f5f 100644 --- a/src/main/java/com/board/config/WebConfig.java +++ b/src/main/java/com/config/WebConfig.java @@ -1,13 +1,12 @@ -package com.board.config; +package com.config; -import com.board.config.auth.AuthenticatedMemberArgumentResolver; +import com.config.auth.AuthenticatedMemberArgumentResolver; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import java.util.List; - @Configuration @RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { diff --git a/src/main/java/com/board/config/auth/AuthConstants.java b/src/main/java/com/config/auth/AuthConstants.java similarity index 77% rename from src/main/java/com/board/config/auth/AuthConstants.java rename to src/main/java/com/config/auth/AuthConstants.java index f43b2cd..871aeae 100644 --- a/src/main/java/com/board/config/auth/AuthConstants.java +++ b/src/main/java/com/config/auth/AuthConstants.java @@ -1,4 +1,4 @@ -package com.board.config.auth; +package com.config.auth; public class AuthConstants { public static final String AUTHENTICATED_USER = "authenticatedUser"; diff --git a/src/main/java/com/board/config/auth/AuthUtil.java b/src/main/java/com/config/auth/AuthUtil.java similarity index 87% rename from src/main/java/com/board/config/auth/AuthUtil.java rename to src/main/java/com/config/auth/AuthUtil.java index e741637..d283211 100644 --- a/src/main/java/com/board/config/auth/AuthUtil.java +++ b/src/main/java/com/config/auth/AuthUtil.java @@ -1,9 +1,9 @@ -package com.board.config.auth; +package com.config.auth; -import static com.board.config.auth.AuthConstants.AUTHENTICATED_USER; +import static com.config.auth.AuthConstants.AUTHENTICATED_USER; -import com.board.exception.custom.ServerException; +import com.exception.custom.ServerException; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; diff --git a/src/main/java/com/board/config/auth/AuthenticatedMemberArgumentResolver.java b/src/main/java/com/config/auth/AuthenticatedMemberArgumentResolver.java similarity index 91% rename from src/main/java/com/board/config/auth/AuthenticatedMemberArgumentResolver.java rename to src/main/java/com/config/auth/AuthenticatedMemberArgumentResolver.java index 4f68cb6..d5f04bb 100644 --- a/src/main/java/com/board/config/auth/AuthenticatedMemberArgumentResolver.java +++ b/src/main/java/com/config/auth/AuthenticatedMemberArgumentResolver.java @@ -1,7 +1,7 @@ -package com.board.config.auth; +package com.config.auth; -import com.board.config.auth.annotation.AuthenticatedMember; -import com.board.member.service.MemberService; +import com.config.auth.annotation.AuthenticatedMember; +import com.member.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/board/config/auth/annotation/AuthenticatedMember.java b/src/main/java/com/config/auth/annotation/AuthenticatedMember.java similarity index 89% rename from src/main/java/com/board/config/auth/annotation/AuthenticatedMember.java rename to src/main/java/com/config/auth/annotation/AuthenticatedMember.java index d29e476..a3a90f7 100644 --- a/src/main/java/com/board/config/auth/annotation/AuthenticatedMember.java +++ b/src/main/java/com/config/auth/annotation/AuthenticatedMember.java @@ -1,4 +1,4 @@ -package com.board.config.auth.annotation; +package com.config.auth.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/src/main/java/com/board/config/filter/FilterConfig.java b/src/main/java/com/config/filter/FilterConfig.java similarity index 85% rename from src/main/java/com/board/config/filter/FilterConfig.java rename to src/main/java/com/config/filter/FilterConfig.java index 3f0c46f..8726626 100644 --- a/src/main/java/com/board/config/filter/FilterConfig.java +++ b/src/main/java/com/config/filter/FilterConfig.java @@ -1,8 +1,8 @@ -package com.board.config.filter; +package com.config.filter; -import com.board.config.auth.AuthUtil; -import com.board.config.jwt.JwtAuthFilter; -import com.board.config.jwt.JwtUtil; +import com.config.auth.AuthUtil; +import com.config.jwt.JwtAuthFilter; +import com.config.jwt.JwtUtil; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/board/config/jwt/JwtAuthFilter.java b/src/main/java/com/config/jwt/JwtAuthFilter.java similarity index 94% rename from src/main/java/com/board/config/jwt/JwtAuthFilter.java rename to src/main/java/com/config/jwt/JwtAuthFilter.java index 022a605..cfe0406 100644 --- a/src/main/java/com/board/config/jwt/JwtAuthFilter.java +++ b/src/main/java/com/config/jwt/JwtAuthFilter.java @@ -1,6 +1,6 @@ -package com.board.config.jwt; +package com.config.jwt; -import com.board.config.auth.AuthUtil; +import com.config.auth.AuthUtil; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -15,7 +15,7 @@ public class JwtAuthFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final AuthUtil authUtil; - + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { diff --git a/src/main/java/com/board/config/jwt/JwtProperties.java b/src/main/java/com/config/jwt/JwtProperties.java similarity index 93% rename from src/main/java/com/board/config/jwt/JwtProperties.java rename to src/main/java/com/config/jwt/JwtProperties.java index 93577d9..46bb902 100644 --- a/src/main/java/com/board/config/jwt/JwtProperties.java +++ b/src/main/java/com/config/jwt/JwtProperties.java @@ -1,4 +1,4 @@ -package com.board.config.jwt; +package com.config.jwt; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/board/config/jwt/JwtUtil.java b/src/main/java/com/config/jwt/JwtUtil.java similarity index 99% rename from src/main/java/com/board/config/jwt/JwtUtil.java rename to src/main/java/com/config/jwt/JwtUtil.java index e61f443..a8e5319 100644 --- a/src/main/java/com/board/config/jwt/JwtUtil.java +++ b/src/main/java/com/config/jwt/JwtUtil.java @@ -1,4 +1,4 @@ -package com.board.config.jwt; +package com.config.jwt; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; diff --git a/src/main/java/com/board/config/jwt/TokenWithExpiration.java b/src/main/java/com/config/jwt/TokenWithExpiration.java similarity index 86% rename from src/main/java/com/board/config/jwt/TokenWithExpiration.java rename to src/main/java/com/config/jwt/TokenWithExpiration.java index 919c80a..73441e4 100644 --- a/src/main/java/com/board/config/jwt/TokenWithExpiration.java +++ b/src/main/java/com/config/jwt/TokenWithExpiration.java @@ -1,4 +1,4 @@ -package com.board.config.jwt; +package com.config.jwt; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/board/exception/CustomException.java b/src/main/java/com/exception/CustomException.java similarity index 93% rename from src/main/java/com/board/exception/CustomException.java rename to src/main/java/com/exception/CustomException.java index a2b60ae..974cb82 100644 --- a/src/main/java/com/board/exception/CustomException.java +++ b/src/main/java/com/exception/CustomException.java @@ -1,4 +1,4 @@ -package com.board.exception; +package com.exception; import java.util.Map; import lombok.Getter; diff --git a/src/main/java/com/board/exception/ErrorCodeType.java b/src/main/java/com/exception/ErrorCodeType.java similarity index 97% rename from src/main/java/com/board/exception/ErrorCodeType.java rename to src/main/java/com/exception/ErrorCodeType.java index 582dcb8..8925408 100644 --- a/src/main/java/com/board/exception/ErrorCodeType.java +++ b/src/main/java/com/exception/ErrorCodeType.java @@ -1,4 +1,4 @@ -package com.board.exception; +package com.exception; import lombok.Getter; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/board/exception/ErrorResponse.java b/src/main/java/com/exception/ErrorResponse.java similarity index 97% rename from src/main/java/com/board/exception/ErrorResponse.java rename to src/main/java/com/exception/ErrorResponse.java index a4b5073..2ad5a93 100644 --- a/src/main/java/com/board/exception/ErrorResponse.java +++ b/src/main/java/com/exception/ErrorResponse.java @@ -1,4 +1,4 @@ -package com.board.exception; +package com.exception; import java.time.LocalDateTime; import java.util.HashMap; diff --git a/src/main/java/com/board/exception/ErrorType.java b/src/main/java/com/exception/ErrorType.java similarity index 84% rename from src/main/java/com/board/exception/ErrorType.java rename to src/main/java/com/exception/ErrorType.java index b024d11..128edc2 100644 --- a/src/main/java/com/board/exception/ErrorType.java +++ b/src/main/java/com/exception/ErrorType.java @@ -1,4 +1,4 @@ -package com.board.exception; +package com.exception; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/board/exception/GlobalExceptionHandler.java b/src/main/java/com/exception/GlobalExceptionHandler.java similarity index 94% rename from src/main/java/com/board/exception/GlobalExceptionHandler.java rename to src/main/java/com/exception/GlobalExceptionHandler.java index 16bd24b..dc4f9e2 100644 --- a/src/main/java/com/board/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/exception/GlobalExceptionHandler.java @@ -1,6 +1,6 @@ -package com.board.exception; +package com.exception; -import static com.board.exception.ErrorCodeType.VALIDATION_ERROR; +import static com.exception.ErrorCodeType.VALIDATION_ERROR; import java.util.List; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/board/exception/custom/DifferentOwnerException.java b/src/main/java/com/exception/custom/DifferentOwnerException.java similarity index 81% rename from src/main/java/com/board/exception/custom/DifferentOwnerException.java rename to src/main/java/com/exception/custom/DifferentOwnerException.java index fbbadf9..4ae3cec 100644 --- a/src/main/java/com/board/exception/custom/DifferentOwnerException.java +++ b/src/main/java/com/exception/custom/DifferentOwnerException.java @@ -1,7 +1,7 @@ -package com.board.exception.custom; +package com.exception.custom; -import com.board.exception.CustomException; -import com.board.exception.ErrorCodeType; +import com.exception.CustomException; +import com.exception.ErrorCodeType; import java.util.Map; import lombok.Getter; diff --git a/src/main/java/com/board/exception/custom/EmailNotFoundException.java b/src/main/java/com/exception/custom/EmailNotFoundException.java similarity index 80% rename from src/main/java/com/board/exception/custom/EmailNotFoundException.java rename to src/main/java/com/exception/custom/EmailNotFoundException.java index 702c6ed..8308eab 100644 --- a/src/main/java/com/board/exception/custom/EmailNotFoundException.java +++ b/src/main/java/com/exception/custom/EmailNotFoundException.java @@ -1,7 +1,7 @@ -package com.board.exception.custom; +package com.exception.custom; -import com.board.exception.CustomException; -import com.board.exception.ErrorCodeType; +import com.exception.CustomException; +import com.exception.ErrorCodeType; import java.util.Map; import lombok.Getter; diff --git a/src/main/java/com/board/exception/custom/MyEntityNotFoundException.java b/src/main/java/com/exception/custom/MyEntityNotFoundException.java similarity index 81% rename from src/main/java/com/board/exception/custom/MyEntityNotFoundException.java rename to src/main/java/com/exception/custom/MyEntityNotFoundException.java index 6322f1a..d2c15a7 100644 --- a/src/main/java/com/board/exception/custom/MyEntityNotFoundException.java +++ b/src/main/java/com/exception/custom/MyEntityNotFoundException.java @@ -1,7 +1,7 @@ -package com.board.exception.custom; +package com.exception.custom; -import com.board.exception.CustomException; -import com.board.exception.ErrorCodeType; +import com.exception.CustomException; +import com.exception.ErrorCodeType; import java.util.Map; import lombok.Getter; diff --git a/src/main/java/com/board/exception/custom/ServerException.java b/src/main/java/com/exception/custom/ServerException.java similarity index 68% rename from src/main/java/com/board/exception/custom/ServerException.java rename to src/main/java/com/exception/custom/ServerException.java index 7457044..f37f2a5 100644 --- a/src/main/java/com/board/exception/custom/ServerException.java +++ b/src/main/java/com/exception/custom/ServerException.java @@ -1,7 +1,7 @@ -package com.board.exception.custom; +package com.exception.custom; -import com.board.exception.CustomException; -import com.board.exception.ErrorCodeType; +import com.exception.CustomException; +import com.exception.ErrorCodeType; import lombok.Getter; @Getter diff --git a/src/main/java/com/board/exception/custom/SignUpException.java b/src/main/java/com/exception/custom/SignUpException.java similarity index 81% rename from src/main/java/com/board/exception/custom/SignUpException.java rename to src/main/java/com/exception/custom/SignUpException.java index dac9b61..fe5a48e 100644 --- a/src/main/java/com/board/exception/custom/SignUpException.java +++ b/src/main/java/com/exception/custom/SignUpException.java @@ -1,7 +1,7 @@ -package com.board.exception.custom; +package com.exception.custom; -import com.board.exception.CustomException; -import com.board.exception.ErrorCodeType; +import com.exception.CustomException; +import com.exception.ErrorCodeType; import java.util.Map; import lombok.Getter; diff --git a/src/main/java/com/board/member/controller/MemberController.java b/src/main/java/com/member/controller/MemberController.java similarity index 82% rename from src/main/java/com/board/member/controller/MemberController.java rename to src/main/java/com/member/controller/MemberController.java index 40fae65..bada9c7 100644 --- a/src/main/java/com/board/member/controller/MemberController.java +++ b/src/main/java/com/member/controller/MemberController.java @@ -1,10 +1,10 @@ -package com.board.member.controller; +package com.member.controller; -import com.board.member.dto.request.LoginRequest; -import com.board.member.dto.request.MemberSignUpRequest; -import com.board.member.dto.response.LoginResponse; -import com.board.member.dto.response.MemberSignUpResponse; -import com.board.member.service.MemberService; +import com.member.dto.request.LoginRequest; +import com.member.dto.request.MemberSignUpRequest; +import com.member.dto.response.LoginResponse; +import com.member.dto.response.MemberSignUpResponse; +import com.member.service.MemberService; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; diff --git a/src/main/java/com/board/member/domain/Member.java b/src/main/java/com/member/domain/Member.java similarity index 82% rename from src/main/java/com/board/member/domain/Member.java rename to src/main/java/com/member/domain/Member.java index 82dd3ad..f60124c 100644 --- a/src/main/java/com/board/member/domain/Member.java +++ b/src/main/java/com/member/domain/Member.java @@ -1,7 +1,7 @@ -package com.board.member.domain; +package com.member.domain; -import com.board.member.entity.MemberEntity; -import com.board.member.message.ErrorMessage; +import com.member.entity.MemberEntity; +import com.member.message.ErrorMessage; import lombok.Getter; @Getter diff --git a/src/main/java/com/board/member/dto/request/LoginRequest.java b/src/main/java/com/member/dto/request/LoginRequest.java similarity index 93% rename from src/main/java/com/board/member/dto/request/LoginRequest.java rename to src/main/java/com/member/dto/request/LoginRequest.java index 17ff91f..555d008 100644 --- a/src/main/java/com/board/member/dto/request/LoginRequest.java +++ b/src/main/java/com/member/dto/request/LoginRequest.java @@ -1,4 +1,4 @@ -package com.board.member.dto.request; +package com.member.dto.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/com/board/member/dto/request/MemberSignUpRequest.java b/src/main/java/com/member/dto/request/MemberSignUpRequest.java similarity index 94% rename from src/main/java/com/board/member/dto/request/MemberSignUpRequest.java rename to src/main/java/com/member/dto/request/MemberSignUpRequest.java index ee26eb2..96a783f 100644 --- a/src/main/java/com/board/member/dto/request/MemberSignUpRequest.java +++ b/src/main/java/com/member/dto/request/MemberSignUpRequest.java @@ -1,4 +1,4 @@ -package com.board.member.dto.request; +package com.member.dto.request; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/com/board/member/dto/response/LoginResponse.java b/src/main/java/com/member/dto/response/LoginResponse.java similarity index 84% rename from src/main/java/com/board/member/dto/response/LoginResponse.java rename to src/main/java/com/member/dto/response/LoginResponse.java index ae649c2..982180f 100644 --- a/src/main/java/com/board/member/dto/response/LoginResponse.java +++ b/src/main/java/com/member/dto/response/LoginResponse.java @@ -1,4 +1,4 @@ -package com.board.member.dto.response; +package com.member.dto.response; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/board/member/dto/response/MemberSignUpResponse.java b/src/main/java/com/member/dto/response/MemberSignUpResponse.java similarity index 83% rename from src/main/java/com/board/member/dto/response/MemberSignUpResponse.java rename to src/main/java/com/member/dto/response/MemberSignUpResponse.java index a245c16..8087993 100644 --- a/src/main/java/com/board/member/dto/response/MemberSignUpResponse.java +++ b/src/main/java/com/member/dto/response/MemberSignUpResponse.java @@ -1,6 +1,6 @@ -package com.board.member.dto.response; +package com.member.dto.response; -import com.board.member.entity.MemberEntity; +import com.member.entity.MemberEntity; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/board/member/entity/MemberEntity.java b/src/main/java/com/member/entity/MemberEntity.java similarity index 80% rename from src/main/java/com/board/member/entity/MemberEntity.java rename to src/main/java/com/member/entity/MemberEntity.java index 5cd74d5..671b626 100644 --- a/src/main/java/com/board/member/entity/MemberEntity.java +++ b/src/main/java/com/member/entity/MemberEntity.java @@ -1,13 +1,18 @@ -package com.board.member.entity; - -import jakarta.persistence.*; +package com.member.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; - @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter diff --git a/src/main/java/com/board/member/message/ErrorMessage.java b/src/main/java/com/member/message/ErrorMessage.java similarity index 90% rename from src/main/java/com/board/member/message/ErrorMessage.java rename to src/main/java/com/member/message/ErrorMessage.java index e20e7ee..2f02ec0 100644 --- a/src/main/java/com/board/member/message/ErrorMessage.java +++ b/src/main/java/com/member/message/ErrorMessage.java @@ -1,4 +1,4 @@ -package com.board.member.message; +package com.member.message; public class ErrorMessage { public static final String EMAIL_DUPLICATE = "이미 가입된 이메일입니다."; diff --git a/src/main/java/com/board/member/repository/MemberRepository.java b/src/main/java/com/member/repository/MemberRepository.java similarity index 77% rename from src/main/java/com/board/member/repository/MemberRepository.java rename to src/main/java/com/member/repository/MemberRepository.java index 7d7831b..76451ef 100644 --- a/src/main/java/com/board/member/repository/MemberRepository.java +++ b/src/main/java/com/member/repository/MemberRepository.java @@ -1,9 +1,8 @@ -package com.board.member.repository; - -import com.board.member.entity.MemberEntity; -import org.springframework.data.jpa.repository.JpaRepository; +package com.member.repository; +import com.member.entity.MemberEntity; import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { diff --git a/src/main/java/com/member/service/MemberService.java b/src/main/java/com/member/service/MemberService.java new file mode 100644 index 0000000..c228def --- /dev/null +++ b/src/main/java/com/member/service/MemberService.java @@ -0,0 +1,19 @@ +package com.member.service; + +import com.member.dto.request.LoginRequest; +import com.member.dto.request.MemberSignUpRequest; +import com.member.dto.response.LoginResponse; +import com.member.dto.response.MemberSignUpResponse; +import com.member.entity.MemberEntity; + +public interface MemberService { + + MemberSignUpResponse signUp(MemberSignUpRequest request); + + LoginResponse login(LoginRequest request); + + MemberEntity findByEmail(String email); + + MemberEntity findById(Long id); + +} diff --git a/src/main/java/com/board/member/service/MemberServiceImpl.java b/src/main/java/com/member/service/MemberServiceImpl.java similarity index 76% rename from src/main/java/com/board/member/service/MemberServiceImpl.java rename to src/main/java/com/member/service/MemberServiceImpl.java index 67e80e1..0212528 100644 --- a/src/main/java/com/board/member/service/MemberServiceImpl.java +++ b/src/main/java/com/member/service/MemberServiceImpl.java @@ -1,18 +1,18 @@ -package com.board.member.service; +package com.member.service; -import com.board.config.jwt.JwtUtil; -import com.board.config.jwt.TokenWithExpiration; -import com.board.exception.custom.EmailNotFoundException; -import com.board.exception.custom.MyEntityNotFoundException; -import com.board.exception.custom.SignUpException; -import com.board.member.domain.Member; -import com.board.member.dto.request.LoginRequest; -import com.board.member.dto.request.MemberSignUpRequest; -import com.board.member.dto.response.LoginResponse; -import com.board.member.dto.response.MemberSignUpResponse; -import com.board.member.entity.MemberEntity; -import com.board.member.message.ErrorMessage; -import com.board.member.repository.MemberRepository; +import com.config.jwt.JwtUtil; +import com.config.jwt.TokenWithExpiration; +import com.exception.custom.EmailNotFoundException; +import com.exception.custom.MyEntityNotFoundException; +import com.exception.custom.SignUpException; +import com.member.domain.Member; +import com.member.dto.request.LoginRequest; +import com.member.dto.request.MemberSignUpRequest; +import com.member.dto.response.LoginResponse; +import com.member.dto.response.MemberSignUpResponse; +import com.member.entity.MemberEntity; +import com.member.message.ErrorMessage; +import com.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/test/java/com/board/BoardApplicationTests.java b/src/test/java/com/BoardApplicationTests.java similarity index 90% rename from src/test/java/com/board/BoardApplicationTests.java rename to src/test/java/com/BoardApplicationTests.java index 7d12c37..ff6b29d 100644 --- a/src/test/java/com/board/BoardApplicationTests.java +++ b/src/test/java/com/BoardApplicationTests.java @@ -1,4 +1,4 @@ -package com.board; +package com; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; diff --git a/src/test/java/com/board/board/controller/BlogApiControllerIntegrationTest.java b/src/test/java/com/board/controller/BlogApiControllerIntegrationTest.java similarity index 94% rename from src/test/java/com/board/board/controller/BlogApiControllerIntegrationTest.java rename to src/test/java/com/board/controller/BlogApiControllerIntegrationTest.java index c4b9508..caf6632 100644 --- a/src/test/java/com/board/board/controller/BlogApiControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/BlogApiControllerIntegrationTest.java @@ -1,4 +1,4 @@ -package com.board.board.controller; +package com.board.controller; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -11,17 +11,17 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.board.board.dto.request.ArticleCreateRequest; -import com.board.board.dto.request.ArticleUpdateRequest; -import com.board.board.entity.ArticleEntity; -import com.board.board.repository.BlogRepository; -import com.board.config.jwt.JwtUtil; -import com.board.member.dto.request.LoginRequest; -import com.board.member.dto.request.MemberSignUpRequest; -import com.board.member.entity.MemberEntity; -import com.board.member.repository.MemberRepository; +import com.board.dto.request.ArticleCreateRequest; +import com.board.dto.request.ArticleUpdateRequest; +import com.board.entity.ArticleEntity; +import com.board.repository.BlogRepository; +import com.config.jwt.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; +import com.member.dto.request.LoginRequest; +import com.member.dto.request.MemberSignUpRequest; +import com.member.entity.MemberEntity; +import com.member.repository.MemberRepository; import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/com/board/board/controller/BlogApiControllerTest.java b/src/test/java/com/board/controller/BlogApiControllerTest.java similarity index 89% rename from src/test/java/com/board/board/controller/BlogApiControllerTest.java rename to src/test/java/com/board/controller/BlogApiControllerTest.java index e88e4f3..23d4ef5 100644 --- a/src/test/java/com/board/board/controller/BlogApiControllerTest.java +++ b/src/test/java/com/board/controller/BlogApiControllerTest.java @@ -1,14 +1,24 @@ -package com.board.board.controller; - -import com.board.board.dto.request.ArticleCreateRequest; -import com.board.board.dto.request.ArticleUpdateRequest; -import com.board.board.dto.response.ArticleResponse; -import com.board.board.entity.ArticleEntity; -import com.board.board.service.BlogService; -import com.board.config.auth.AuthenticatedMemberArgumentResolver; -import com.board.member.entity.MemberEntity; -import com.board.member.service.MemberService; +package com.board.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.board.dto.request.ArticleCreateRequest; +import com.board.dto.request.ArticleUpdateRequest; +import com.board.dto.response.ArticleResponse; +import com.board.entity.ArticleEntity; +import com.board.service.BlogService; +import com.config.auth.AuthenticatedMemberArgumentResolver; import com.fasterxml.jackson.databind.ObjectMapper; +import com.member.entity.MemberEntity; +import com.member.service.MemberService; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -18,14 +28,6 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -import java.util.List; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @WebMvcTest(BlogApiController.class) class BlogApiControllerTest { diff --git a/src/test/java/com/board/board/service/BlogServiceTest.java b/src/test/java/com/board/service/BlogServiceTest.java similarity index 93% rename from src/test/java/com/board/board/service/BlogServiceTest.java rename to src/test/java/com/board/service/BlogServiceTest.java index 9d1a5bc..4d0fec2 100644 --- a/src/test/java/com/board/board/service/BlogServiceTest.java +++ b/src/test/java/com/board/service/BlogServiceTest.java @@ -1,4 +1,4 @@ -package com.board.board.service; +package com.board.service; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -11,13 +11,13 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.board.board.dto.request.ArticleCreateRequest; -import com.board.board.entity.ArticleEntity; -import com.board.board.repository.BlogRepository; -import com.board.exception.custom.DifferentOwnerException; -import com.board.exception.custom.MyEntityNotFoundException; -import com.board.member.entity.MemberEntity; -import com.board.member.service.MemberService; +import com.board.dto.request.ArticleCreateRequest; +import com.board.entity.ArticleEntity; +import com.board.repository.BlogRepository; +import com.exception.custom.DifferentOwnerException; +import com.exception.custom.MyEntityNotFoundException; +import com.member.entity.MemberEntity; +import com.member.service.MemberService; import java.util.Collections; import java.util.Optional; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/com/board/member/controller/MemberControllerIntegrationTest.java b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java similarity index 93% rename from src/test/java/com/board/member/controller/MemberControllerIntegrationTest.java rename to src/test/java/com/member/controller/MemberControllerIntegrationTest.java index 8c1cab9..5309362 100644 --- a/src/test/java/com/board/member/controller/MemberControllerIntegrationTest.java +++ b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java @@ -1,4 +1,4 @@ -package com.board.member.controller; +package com.member.controller; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -6,12 +6,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.board.config.jwt.JwtUtil; -import com.board.member.dto.request.LoginRequest; -import com.board.member.dto.request.MemberSignUpRequest; -import com.board.member.entity.MemberEntity; -import com.board.member.repository.MemberRepository; +import com.config.jwt.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; +import com.member.dto.request.LoginRequest; +import com.member.dto.request.MemberSignUpRequest; +import com.member.entity.MemberEntity; +import com.member.repository.MemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/board/member/controller/MemberControllerTest.java b/src/test/java/com/member/controller/MemberControllerTest.java similarity index 87% rename from src/test/java/com/board/member/controller/MemberControllerTest.java rename to src/test/java/com/member/controller/MemberControllerTest.java index 7e56e09..145c516 100644 --- a/src/test/java/com/board/member/controller/MemberControllerTest.java +++ b/src/test/java/com/member/controller/MemberControllerTest.java @@ -1,12 +1,18 @@ -package com.board.member.controller; +package com.member.controller; -import com.board.config.auth.AuthUtil; -import com.board.member.dto.request.LoginRequest; -import com.board.member.dto.request.MemberSignUpRequest; -import com.board.member.dto.response.LoginResponse; -import com.board.member.dto.response.MemberSignUpResponse; -import com.board.member.service.MemberService; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.config.auth.AuthUtil; import com.fasterxml.jackson.databind.ObjectMapper; +import com.member.dto.request.LoginRequest; +import com.member.dto.request.MemberSignUpRequest; +import com.member.dto.response.LoginResponse; +import com.member.dto.response.MemberSignUpResponse; +import com.member.service.MemberService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -14,12 +20,6 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @WebMvcTest(MemberController.class) class MemberControllerTest { diff --git a/src/test/java/com/board/member/service/MemberServiceImplTest.java b/src/test/java/com/member/service/MemberServiceImplTest.java similarity index 89% rename from src/test/java/com/board/member/service/MemberServiceImplTest.java rename to src/test/java/com/member/service/MemberServiceImplTest.java index 41efcd7..e900598 100644 --- a/src/test/java/com/board/member/service/MemberServiceImplTest.java +++ b/src/test/java/com/member/service/MemberServiceImplTest.java @@ -1,4 +1,4 @@ -package com.board.member.service; +package com.member.service; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -6,15 +6,15 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -import com.board.config.jwt.JwtUtil; -import com.board.config.jwt.TokenWithExpiration; -import com.board.exception.custom.SignUpException; -import com.board.member.dto.request.LoginRequest; -import com.board.member.dto.request.MemberSignUpRequest; -import com.board.member.dto.response.LoginResponse; -import com.board.member.dto.response.MemberSignUpResponse; -import com.board.member.entity.MemberEntity; -import com.board.member.repository.MemberRepository; +import com.config.jwt.JwtUtil; +import com.config.jwt.TokenWithExpiration; +import com.exception.custom.SignUpException; +import com.member.dto.request.LoginRequest; +import com.member.dto.request.MemberSignUpRequest; +import com.member.dto.response.LoginResponse; +import com.member.dto.response.MemberSignUpResponse; +import com.member.entity.MemberEntity; +import com.member.repository.MemberRepository; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; From e6443270d29e73ea7adcd58a28916e023d68f073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Tue, 22 Apr 2025 21:03:43 +0900 Subject: [PATCH 02/45] =?UTF-8?q?refactor=20:=20ArticleEntity=20=EC=97=90?= =?UTF-8?q?=20createdAt=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/board/entity/ArticleEntity.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/board/entity/ArticleEntity.java b/src/main/java/com/board/entity/ArticleEntity.java index 19df016..ea17f8d 100644 --- a/src/main/java/com/board/entity/ArticleEntity.java +++ b/src/main/java/com/board/entity/ArticleEntity.java @@ -10,6 +10,8 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.PrePersist; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -35,6 +37,8 @@ public class ArticleEntity { @JoinColumn(name = "member_id") private MemberEntity member; + @Column(nullable = false) + private LocalDateTime createdAt; public ArticleEntity(String title, String content, MemberEntity member) { this.title = title; @@ -60,4 +64,9 @@ public void validateOwner(MemberEntity member) { throw DifferentOwnerException.from(this.member.getEmail()); } } + + @PrePersist + public void setCreatedAtNow() { + this.createdAt = LocalDateTime.now(); + } } From 68ed2a94d38562b8a2efedec815d74a76ffd2b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Tue, 22 Apr 2025 21:07:02 +0900 Subject: [PATCH 03/45] =?UTF-8?q?feat=20:=20=EB=8C=93=EA=B8=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B4=80=EB=A0=A8=20Entity=20/=20Service=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/CommentCreateRequest.java | 21 ++++++ .../dto/request/CommentUpdateRequest.java | 21 ++++++ .../java/com/board/entity/CommentEntity.java | 66 +++++++++++++++++++ .../board/repository/CommentRepository.java | 7 ++ .../com/board/service/CommentService.java | 55 ++++++++++++++++ 5 files changed, 170 insertions(+) create mode 100644 src/main/java/com/board/dto/request/CommentCreateRequest.java create mode 100644 src/main/java/com/board/dto/request/CommentUpdateRequest.java create mode 100644 src/main/java/com/board/entity/CommentEntity.java create mode 100644 src/main/java/com/board/repository/CommentRepository.java create mode 100644 src/main/java/com/board/service/CommentService.java diff --git a/src/main/java/com/board/dto/request/CommentCreateRequest.java b/src/main/java/com/board/dto/request/CommentCreateRequest.java new file mode 100644 index 0000000..72e166a --- /dev/null +++ b/src/main/java/com/board/dto/request/CommentCreateRequest.java @@ -0,0 +1,21 @@ +package com.board.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class CommentCreateRequest { + + @NotBlank(message = "내용을 입력해주세요") + private String content; + private Long articleId; + + @Builder + public CommentCreateRequest(String content, Long articleId) { + this.content = content; + this.articleId = articleId; + } +} diff --git a/src/main/java/com/board/dto/request/CommentUpdateRequest.java b/src/main/java/com/board/dto/request/CommentUpdateRequest.java new file mode 100644 index 0000000..df64598 --- /dev/null +++ b/src/main/java/com/board/dto/request/CommentUpdateRequest.java @@ -0,0 +1,21 @@ +package com.board.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class CommentUpdateRequest { + + @NotBlank(message = "내용을 입력해주세요") + private String content; + private Long commentId; + + @Builder + public CommentUpdateRequest(String content, Long commentId) { + this.content = content; + this.commentId = commentId; + } +} diff --git a/src/main/java/com/board/entity/CommentEntity.java b/src/main/java/com/board/entity/CommentEntity.java new file mode 100644 index 0000000..65d8976 --- /dev/null +++ b/src/main/java/com/board/entity/CommentEntity.java @@ -0,0 +1,66 @@ +package com.board.entity; + +import com.exception.custom.DifferentOwnerException; +import com.member.entity.MemberEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.PrePersist; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class CommentEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", updatable = false) + private Long id; + + @Column(name = "content", nullable = false) + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "article_id", nullable = false) + private ArticleEntity article; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private MemberEntity member; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @Column + private boolean deleted; + + public CommentEntity(String content, ArticleEntity article, MemberEntity member) { + this.content = content; + this.article = article; + this.member = member; + } + + public void validateOwner(MemberEntity member) { + if (!this.member.equals(member)) { + throw DifferentOwnerException.from(this.member.getEmail()); + } + } + + public void update(String content) { + this.content = content; + } + + @PrePersist + public void setCreatedAtNow() { + this.createdAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/board/repository/CommentRepository.java b/src/main/java/com/board/repository/CommentRepository.java new file mode 100644 index 0000000..f3e7dee --- /dev/null +++ b/src/main/java/com/board/repository/CommentRepository.java @@ -0,0 +1,7 @@ +package com.board.repository; + +import com.board.entity.CommentEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository { +} diff --git a/src/main/java/com/board/service/CommentService.java b/src/main/java/com/board/service/CommentService.java new file mode 100644 index 0000000..d8fd650 --- /dev/null +++ b/src/main/java/com/board/service/CommentService.java @@ -0,0 +1,55 @@ +package com.board.service; + +import com.board.dto.request.CommentCreateRequest; +import com.board.dto.request.CommentUpdateRequest; +import com.board.entity.ArticleEntity; +import com.board.entity.CommentEntity; +import com.board.repository.CommentRepository; +import com.exception.custom.MyEntityNotFoundException; +import com.member.entity.MemberEntity; +import com.member.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +public class CommentService { + + private final CommentRepository commentRepository; + private final BlogService blogService; + private final MemberService memberService; + + @Transactional + public CommentEntity createComment(CommentCreateRequest request, Long memberId) { + ArticleEntity article = blogService.findById(request.getArticleId()); + MemberEntity member = memberService.findById(memberId); + CommentEntity comment = new CommentEntity(request.getContent(), article, member); + return commentRepository.save(comment); + } + + @Transactional + public CommentEntity updateComment(CommentUpdateRequest request, Long memberId) { + CommentEntity comment = compareAuthors(request.getCommentId(), memberService.findById(memberId)); + comment.update(request.getContent()); + return comment; + } + + @Transactional + public void deleteComment(long commentId, Long memberId) { + compareAuthors(commentId, memberService.findById(memberId)); + commentRepository.deleteById(commentId); + } + + private CommentEntity compareAuthors(long commentId, MemberEntity member) { + CommentEntity comment = findComment(commentId); + comment.validateOwner(member); + return comment; + } + + private CommentEntity findComment(long id) { + return commentRepository.findById(id) + .orElseThrow(() -> MyEntityNotFoundException.from(id)); + } +} From 41f37e49236a9a4a2f24789eb71cc90d6af7d457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Wed, 23 Apr 2025 20:55:42 +0900 Subject: [PATCH 04/45] =?UTF-8?q?feat=20:=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81(=EC=83=81=ED=83=9Ctrue,=20false=EB=A1=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/board/entity/CommentEntity.java | 4 ++++ src/main/java/com/board/repository/CommentRepository.java | 4 ++++ src/main/java/com/board/service/CommentService.java | 6 +++--- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/board/entity/CommentEntity.java b/src/main/java/com/board/entity/CommentEntity.java index 65d8976..40e76f7 100644 --- a/src/main/java/com/board/entity/CommentEntity.java +++ b/src/main/java/com/board/entity/CommentEntity.java @@ -59,6 +59,10 @@ public void update(String content) { this.content = content; } + public void delete() { + this.deleted = true; + } + @PrePersist public void setCreatedAtNow() { this.createdAt = LocalDateTime.now(); diff --git a/src/main/java/com/board/repository/CommentRepository.java b/src/main/java/com/board/repository/CommentRepository.java index f3e7dee..989c16a 100644 --- a/src/main/java/com/board/repository/CommentRepository.java +++ b/src/main/java/com/board/repository/CommentRepository.java @@ -1,7 +1,11 @@ package com.board.repository; import com.board.entity.CommentEntity; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface CommentRepository extends JpaRepository { + + Optional findByIdAndDeletedFalse(Long id); + } diff --git a/src/main/java/com/board/service/CommentService.java b/src/main/java/com/board/service/CommentService.java index d8fd650..0c015a5 100644 --- a/src/main/java/com/board/service/CommentService.java +++ b/src/main/java/com/board/service/CommentService.java @@ -38,8 +38,8 @@ public CommentEntity updateComment(CommentUpdateRequest request, Long memberId) @Transactional public void deleteComment(long commentId, Long memberId) { - compareAuthors(commentId, memberService.findById(memberId)); - commentRepository.deleteById(commentId); + CommentEntity comment = compareAuthors(commentId, memberService.findById(memberId)); + comment.delete(); } private CommentEntity compareAuthors(long commentId, MemberEntity member) { @@ -49,7 +49,7 @@ private CommentEntity compareAuthors(long commentId, MemberEntity member) { } private CommentEntity findComment(long id) { - return commentRepository.findById(id) + return commentRepository.findByIdAndDeletedFalse(id) .orElseThrow(() -> MyEntityNotFoundException.from(id)); } } From 03df24832f9f8e60189f33206d705705bd1d25fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Wed, 23 Apr 2025 20:55:59 +0900 Subject: [PATCH 05/45] =?UTF-8?q?feat=20:=20=EB=8C=93=EA=B8=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20service=20test=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/board/service/CommentServiceTest.java | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/test/java/com/board/service/CommentServiceTest.java diff --git a/src/test/java/com/board/service/CommentServiceTest.java b/src/test/java/com/board/service/CommentServiceTest.java new file mode 100644 index 0000000..d7c2a99 --- /dev/null +++ b/src/test/java/com/board/service/CommentServiceTest.java @@ -0,0 +1,134 @@ +package com.board.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.board.dto.request.CommentCreateRequest; +import com.board.dto.request.CommentUpdateRequest; +import com.board.entity.ArticleEntity; +import com.board.entity.CommentEntity; +import com.board.repository.CommentRepository; +import com.exception.custom.DifferentOwnerException; +import com.member.entity.MemberEntity; +import com.member.service.MemberService; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CommentServiceTest { + + @InjectMocks + private CommentService commentService; + @Mock + private CommentRepository commentRepository; + @Mock + private BlogService blogService; + @Mock + private MemberService memberService; + + @Test + @DisplayName("댓글 생성 성공") + void 댓글_생성_성공() { + // Given + Long memberId = 1L; + Long articleId = 2L; + String content = "댓글 내용"; + CommentCreateRequest request = new CommentCreateRequest(content, articleId); + + MemberEntity member = new MemberEntity("test@example.com", "password", "nickname"); + ArticleEntity article = new ArticleEntity("제목", "내용", member); + CommentEntity comment = new CommentEntity(content, article, member); + + when(blogService.findById(articleId)).thenReturn(article); + when(memberService.findById(memberId)).thenReturn(member); + when(commentRepository.save(any(CommentEntity.class))).thenReturn(comment); + + // When + CommentEntity createdComment = commentService.createComment(request, memberId); + + // Then + assertNotNull(createdComment); + assertEquals(content, createdComment.getContent()); + assertEquals(article, createdComment.getArticle()); + assertEquals(member, createdComment.getMember()); + } + + @Test + @DisplayName("댓글_수정_성공") + void 댓글_수정_성공() { + // Given + Long memberId = 1L; + Long commentId = 3L; + String updatedContent = "수정된 댓글 내용"; + CommentUpdateRequest request = new CommentUpdateRequest(updatedContent, commentId); + + MemberEntity member = new MemberEntity("test@example.com", "password", "nickname"); + ArticleEntity article = new ArticleEntity("제목", "내용", member); + CommentEntity comment = new CommentEntity("기존 댓글 내용", article, member); + + when(commentRepository.findByIdAndDeletedFalse(commentId)).thenReturn(Optional.of(comment)); + when(memberService.findById(memberId)).thenReturn(member); + + // When + CommentEntity updatedComment = commentService.updateComment(request, memberId); + + // Then + assertNotNull(updatedComment); + assertEquals(updatedContent, updatedComment.getContent()); + } + + + @Nested + @DisplayName("삭제 테스트") + class deleteTest { + @Test + @DisplayName("댓글_삭제_성공") + void 댓글_삭제_성공() { + // Given + Long memberId = 1L; + Long commentId = 3L; + + MemberEntity member = new MemberEntity("test@example.com", "password", "nickname"); + ArticleEntity article = new ArticleEntity("제목", "내용", member); + CommentEntity comment = new CommentEntity("기존 댓글 내용", article, member); + + when(commentRepository.findByIdAndDeletedFalse(commentId)).thenReturn(Optional.of(comment)); + when(memberService.findById(memberId)).thenReturn(member); + + // When + commentService.deleteComment(commentId, memberId); + + // Then + assertEquals(true, comment.isDeleted()); + } + + @Test + @DisplayName("작성자 다를 경우 삭제 실패") + void 작성자_다르면_댓글_삭제_실패() { + // Given + Long memberId = 1L; + Long commentId = 3L; + + MemberEntity member = new MemberEntity("test@example.com", "password", "nickname"); + MemberEntity anotherMember = new MemberEntity("another@example.com", "anotherPw", "another"); + ArticleEntity article = new ArticleEntity("제목", "내용", member); + CommentEntity comment = new CommentEntity("기존 댓글 내용", article, anotherMember); + + when(commentRepository.findByIdAndDeletedFalse(commentId)).thenReturn(Optional.of(comment)); + when(memberService.findById(memberId)).thenReturn(member); + + // When & Then + assertThrows(DifferentOwnerException.class, () -> commentService.deleteComment(commentId, memberId)); + } + } + +} From 171ea5f37ac0cc2b8ed58d0bd0cabf05a5c37883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Thu, 24 Apr 2025 16:12:34 +0900 Subject: [PATCH 06/45] =?UTF-8?q?feat=20:=20=EB=8C=93=EA=B8=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80(controller,=20test)=20-=20servic?= =?UTF-8?q?e=EC=97=90=20=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/CommentController.java | 70 ++++++++++ .../dto/request/CommentUpdateRequest.java | 2 + .../board/dto/response/CommentResponse.java | 27 ++++ .../board/repository/CommentRepository.java | 4 + .../com/board/service/CommentService.java | 6 + .../controller/CommentControllerTest.java | 123 ++++++++++++++++++ .../com/board/service/CommentServiceTest.java | 40 +++++- 7 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/board/controller/CommentController.java create mode 100644 src/main/java/com/board/dto/response/CommentResponse.java create mode 100644 src/test/java/com/board/controller/CommentControllerTest.java diff --git a/src/main/java/com/board/controller/CommentController.java b/src/main/java/com/board/controller/CommentController.java new file mode 100644 index 0000000..ced82e3 --- /dev/null +++ b/src/main/java/com/board/controller/CommentController.java @@ -0,0 +1,70 @@ +package com.board.controller; + +import com.board.dto.request.CommentCreateRequest; +import com.board.dto.request.CommentUpdateRequest; +import com.board.dto.response.CommentResponse; +import com.board.entity.CommentEntity; +import com.board.service.CommentService; +import com.config.auth.annotation.AuthenticatedMember; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/comments") +public class CommentController { + + private final CommentService commentService; + + @GetMapping + public ResponseEntity> findAllComments(@RequestParam Long articleId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + Pageable pageable = PageRequest.of(page, size); + Page commentResponses = commentService.findAllComments(articleId, pageable) + .map(CommentResponse::new); + + return ResponseEntity.ok(commentResponses); + } + + @PostMapping + public ResponseEntity addComment(@Valid @RequestBody CommentCreateRequest request, + @AuthenticatedMember Long memberId) { + CommentEntity savedComment = commentService.createComment(request, memberId); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(new CommentResponse(savedComment)); + } + + @PatchMapping("/{commentId}") + public ResponseEntity updateComment(@PathVariable long commentId, + @Valid @RequestBody CommentUpdateRequest request, + @AuthenticatedMember Long memberId) { + request.setCommentId(commentId); + CommentEntity updatedComment = commentService.updateComment(request, memberId); + return ResponseEntity.ok() + .body(new CommentResponse(updatedComment)); + } + + @DeleteMapping("/{commentId}") + public ResponseEntity deleteComment(@PathVariable long commentId, + @AuthenticatedMember Long memberId) { + commentService.deleteComment(commentId, memberId); + return ResponseEntity.noContent() + .build(); + } +} diff --git a/src/main/java/com/board/dto/request/CommentUpdateRequest.java b/src/main/java/com/board/dto/request/CommentUpdateRequest.java index df64598..6e67637 100644 --- a/src/main/java/com/board/dto/request/CommentUpdateRequest.java +++ b/src/main/java/com/board/dto/request/CommentUpdateRequest.java @@ -4,8 +4,10 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; @Getter +@Setter @NoArgsConstructor public class CommentUpdateRequest { diff --git a/src/main/java/com/board/dto/response/CommentResponse.java b/src/main/java/com/board/dto/response/CommentResponse.java new file mode 100644 index 0000000..d595d9b --- /dev/null +++ b/src/main/java/com/board/dto/response/CommentResponse.java @@ -0,0 +1,27 @@ +package com.board.dto.response; + +import com.board.entity.CommentEntity; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class CommentResponse { + + private final Long id; + private final String content; + private final Long authorId; + private final String authorName; + private final LocalDateTime createdAt; + private final boolean deleted; + + public CommentResponse(CommentEntity comment) { + this.id = comment.getId(); + this.content = comment.getContent(); + this.authorId = comment.getMember().getId(); + this.authorName = comment.getMember().getNickName(); + this.createdAt = comment.getCreatedAt(); + this.deleted = comment.isDeleted(); + } +} diff --git a/src/main/java/com/board/repository/CommentRepository.java b/src/main/java/com/board/repository/CommentRepository.java index 989c16a..e9a9ac1 100644 --- a/src/main/java/com/board/repository/CommentRepository.java +++ b/src/main/java/com/board/repository/CommentRepository.java @@ -2,10 +2,14 @@ import com.board.entity.CommentEntity; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface CommentRepository extends JpaRepository { Optional findByIdAndDeletedFalse(Long id); + Page findByArticleIdAndDeletedFalseOrderByCreatedAtDesc(Long articleId, Pageable pageable); + } diff --git a/src/main/java/com/board/service/CommentService.java b/src/main/java/com/board/service/CommentService.java index 0c015a5..922488d 100644 --- a/src/main/java/com/board/service/CommentService.java +++ b/src/main/java/com/board/service/CommentService.java @@ -9,6 +9,8 @@ import com.member.entity.MemberEntity; import com.member.service.MemberService; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,6 +23,10 @@ public class CommentService { private final BlogService blogService; private final MemberService memberService; + public Page findAllComments(Long articleId, Pageable pageable) { + return commentRepository.findByArticleIdAndDeletedFalseOrderByCreatedAtDesc(articleId, pageable); + } + @Transactional public CommentEntity createComment(CommentCreateRequest request, Long memberId) { ArticleEntity article = blogService.findById(request.getArticleId()); diff --git a/src/test/java/com/board/controller/CommentControllerTest.java b/src/test/java/com/board/controller/CommentControllerTest.java new file mode 100644 index 0000000..d92bee7 --- /dev/null +++ b/src/test/java/com/board/controller/CommentControllerTest.java @@ -0,0 +1,123 @@ +package com.board.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.board.dto.request.CommentCreateRequest; +import com.board.dto.request.CommentUpdateRequest; +import com.board.entity.ArticleEntity; +import com.board.entity.CommentEntity; +import com.board.service.CommentService; +import com.config.auth.AuthenticatedMemberArgumentResolver; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.member.entity.MemberEntity; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(CommentController.class) +class CommentControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private CommentService commentService; + + @MockitoBean + private AuthenticatedMemberArgumentResolver authenticatedMemberArgumentResolver; + + @Autowired + private ObjectMapper objectMapper; + + @BeforeEach + void setup() throws Exception { + when(authenticatedMemberArgumentResolver.supportsParameter(any())).thenReturn(true); + when(authenticatedMemberArgumentResolver.resolveArgument(any(), any(), any(), any())) + .thenReturn(1L); // Long memberId 주입 + } + + @Test + @DisplayName("댓글 생성 성공") + void 댓글_생성() throws Exception { + CommentCreateRequest request = new CommentCreateRequest("댓글 내용", 1L); + MemberEntity member = new MemberEntity("이메일", "패스워드", "닉네임"); + CommentEntity comment = new CommentEntity("댓글 내용", null, member); + when(commentService.createComment(any(CommentCreateRequest.class), anyLong())).thenReturn(comment); + + mockMvc.perform(post("/comments") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.content").value("댓글 내용")) + .andExpect(jsonPath("$.authorName").value("닉네임")); + } + + + @Test + @DisplayName("댓글_전체_조회_성공") + void 댓글_전체_조회_성공() throws Exception { + // Given + MemberEntity member = new MemberEntity("email", "password", "nickName"); + ArticleEntity article = new ArticleEntity("title", "content", member); + + List responses = List.of( + new CommentEntity("내용1", article, member), + new CommentEntity("내용2", article, member)); + Page pageResult = new PageImpl<>(responses); + + when(commentService.findAllComments(anyLong(), any())).thenReturn(pageResult); + + // When & Then + mockMvc.perform(get("/comments") + .param("articleId", "1") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content[0].content").value("내용1")) + .andExpect(jsonPath("$.content[1].authorName").value("nickName")); + } + + @Test + @DisplayName("댓글 수정 성공") + void 댓글_수정_성공() throws Exception { + // Given + CommentUpdateRequest request = new CommentUpdateRequest("수정된 댓글 내용", 1L); + MemberEntity member = new MemberEntity("email", "password", "nickName"); + CommentEntity updatedComment = new CommentEntity("수정된 댓글 내용", null, member); + + when(commentService.updateComment(any(CommentUpdateRequest.class), anyLong())).thenReturn(updatedComment); + + // When & Then + mockMvc.perform(patch("/comments/1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").value("수정된 댓글 내용")) + .andExpect(jsonPath("$.authorName").value("nickName")); + } + + @Test + @DisplayName("댓글 삭제 성공") + void 댓글_삭제_성공() throws Exception { + // When & Then + mockMvc.perform(delete("/comments/1")) + .andExpect(status().isNoContent()); + } +} diff --git a/src/test/java/com/board/service/CommentServiceTest.java b/src/test/java/com/board/service/CommentServiceTest.java index d7c2a99..6cad6ed 100644 --- a/src/test/java/com/board/service/CommentServiceTest.java +++ b/src/test/java/com/board/service/CommentServiceTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.when; import com.board.dto.request.CommentCreateRequest; @@ -14,6 +15,7 @@ import com.exception.custom.DifferentOwnerException; import com.member.entity.MemberEntity; import com.member.service.MemberService; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -22,6 +24,10 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; @ExtendWith(MockitoExtension.class) class CommentServiceTest { @@ -35,6 +41,37 @@ class CommentServiceTest { @Mock private MemberService memberService; + @Test + @DisplayName("게시글의 댓글들 조회") + void 댓글_조회_성공() { + // Given + MemberEntity member = new MemberEntity("test@example.com", "password", "nickname"); + MemberEntity member2 = new MemberEntity("member2@example.com", "password", "member2"); + MemberEntity member3 = new MemberEntity("member3@example.com", "password", "member3"); + ArticleEntity article = new ArticleEntity("제목", "내용", member); + + CommentEntity comment = new CommentEntity("댓글내용1", article, member); + CommentEntity comment2 = new CommentEntity("댓글내용2", article, member2); + CommentEntity comment3 = new CommentEntity("댓글내용3", article, member3); + CommentEntity comment4 = new CommentEntity("댓글내용4", article, member3); + + Pageable pageable = PageRequest.of(0, 10); + + List commentEntityList = List.of(comment, comment2, comment3, comment4); + Page commentPage = new PageImpl<>(commentEntityList, pageable, commentEntityList.size()); + + when(commentRepository.findByArticleIdAndDeletedFalseOrderByCreatedAtDesc(anyLong(), any(Pageable.class))) + .thenReturn(commentPage); + + // When + Page returnCommentsList = commentService.findAllComments(1L, pageable); + + // then + assertEquals(4, returnCommentsList.getContent().size()); + assertEquals("댓글내용1", returnCommentsList.getContent().get(0).getContent()); + assertEquals(member3, returnCommentsList.getContent().get(3).getMember()); + } + @Test @DisplayName("댓글 생성 성공") void 댓글_생성_성공() { @@ -88,7 +125,7 @@ class CommentServiceTest { @Nested - @DisplayName("삭제 테스트") + @DisplayName("댓글 삭제 테스트") class deleteTest { @Test @DisplayName("댓글_삭제_성공") @@ -130,5 +167,4 @@ class deleteTest { assertThrows(DifferentOwnerException.class, () -> commentService.deleteComment(commentId, memberId)); } } - } From e5ff8ecee92e1d14427e0d841239f63eb7a75b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Mon, 28 Apr 2025 19:39:03 +0900 Subject: [PATCH 07/45] =?UTF-8?q?test=20:=20=EB=8C=93=EA=B8=80=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommentControllerIntegrationTest.java | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/test/java/com/board/controller/CommentControllerIntegrationTest.java diff --git a/src/test/java/com/board/controller/CommentControllerIntegrationTest.java b/src/test/java/com/board/controller/CommentControllerIntegrationTest.java new file mode 100644 index 0000000..5bfc09a --- /dev/null +++ b/src/test/java/com/board/controller/CommentControllerIntegrationTest.java @@ -0,0 +1,127 @@ +package com.board.controller; + +import com.board.dto.request.CommentCreateRequest; +import com.board.dto.request.CommentUpdateRequest; +import com.board.entity.ArticleEntity; +import com.board.entity.CommentEntity; +import com.board.repository.BlogRepository; +import com.board.repository.CommentRepository; +import com.config.jwt.JwtUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.member.entity.MemberEntity; +import com.member.repository.MemberRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@ActiveProfiles("test") +class CommentControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private BlogRepository blogRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private JwtUtil jwtUtil; + + private MemberEntity member; + private ArticleEntity article; + private String jwtToken; + private static final String AUTHORIZATION_HEADER = "Authorization"; + + @BeforeEach + void setup() { + member = memberRepository.save(new MemberEntity("test@example.com", "password", "nickname")); + article = blogRepository.save(new ArticleEntity("title", "content", member)); + jwtToken = "Bearer " + jwtUtil.generateToken(member.getEmail()); + } + + @Test + @DisplayName("댓글 생성 성공") + void 댓글_생성_성공() throws Exception { + // Given + CommentCreateRequest request = new CommentCreateRequest("댓글 내용", article.getId()); + + // When & Then + mockMvc.perform(post("/comments") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(AUTHORIZATION_HEADER, jwtToken)) // 가짜 인증 헤더 + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.content").value("댓글 내용")) + .andExpect(jsonPath("$.authorName").value("nickname")); + } + + @Test + @DisplayName("댓글 조회 성공") + void 댓글_조회_성공() throws Exception { + // Given + CommentEntity comment1 = commentRepository.save(new CommentEntity("댓글 내용1", article, member)); + CommentEntity comment2 = commentRepository.save(new CommentEntity("댓글 내용2", article, member)); + + // When & Then + mockMvc.perform(get("/comments") + .param("articleId", String.valueOf(article.getId())) + .param("page", "0") + .param("size", "10") + .header(AUTHORIZATION_HEADER, jwtToken)) // JWT 인증 헤더 추가 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content[0].content").value("댓글 내용2")) + .andExpect(jsonPath("$.content[1].content").value("댓글 내용1")); + } + + @Test + @DisplayName("댓글 수정 성공") + void 댓글_수정_성공() throws Exception { + // Given + CommentEntity comment = commentRepository.save(new CommentEntity("댓글 내용", article, member)); + CommentUpdateRequest request = new CommentUpdateRequest("수정된 댓글 내용", comment.getId()); + + // When & Then + mockMvc.perform(patch("/comments/" + comment.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(AUTHORIZATION_HEADER, jwtToken)) // JWT 인증 헤더 추가 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").value("수정된 댓글 내용")) + .andExpect(jsonPath("$.authorName").value(member.getNickName())); + } + + @Test + @DisplayName("댓글 삭제 성공") + void 댓글_삭제_성공() throws Exception { + // Given + CommentEntity comment = commentRepository.save(new CommentEntity("댓글 내용", article, member)); + + // When & Then + mockMvc.perform(delete("/comments/" + comment.getId()) + .header(AUTHORIZATION_HEADER, jwtToken)) // JWT 인증 헤더 추가 + .andExpect(status().isNoContent()); + } +} From 46653fddafb519479082496cb1a5ef7f5b005be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Mon, 28 Apr 2025 19:39:43 +0900 Subject: [PATCH 08/45] =?UTF-8?q?refactor=20:=20jwt=20=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80(=EB=8C=93=EA=B8=80=20url)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/config/auth/AuthenticatedMemberArgumentResolver.java | 2 +- src/main/java/com/config/filter/FilterConfig.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/config/auth/AuthenticatedMemberArgumentResolver.java b/src/main/java/com/config/auth/AuthenticatedMemberArgumentResolver.java index d5f04bb..e82a2c3 100644 --- a/src/main/java/com/config/auth/AuthenticatedMemberArgumentResolver.java +++ b/src/main/java/com/config/auth/AuthenticatedMemberArgumentResolver.java @@ -19,7 +19,7 @@ public class AuthenticatedMemberArgumentResolver implements HandlerMethodArgumen @Override public boolean supportsParameter(MethodParameter parameter) { - // @AuthenticatedMember 가 붙어있고 MemberEntity 타입이면 처리 + // @AuthenticatedMember 가 붙어있고 Long 타입이면 처리 return parameter.hasParameterAnnotation(AuthenticatedMember.class) && parameter.getParameterType().equals(Long.class); } diff --git a/src/main/java/com/config/filter/FilterConfig.java b/src/main/java/com/config/filter/FilterConfig.java index 8726626..7f0f45e 100644 --- a/src/main/java/com/config/filter/FilterConfig.java +++ b/src/main/java/com/config/filter/FilterConfig.java @@ -19,7 +19,8 @@ public JwtAuthFilter jwtAuthFilter(JwtUtil jwtUtil, AuthUtil authUtil) { public FilterRegistrationBean jwtAuthFilterRegistration(JwtAuthFilter jwtAuthFilter) { FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(jwtAuthFilter); - registrationBean.addUrlPatterns("/articles", "/articles/*"); // 특정 URL 패턴에만 필터 적용 + registrationBean.addUrlPatterns("/articles", "/articles/*", + "/comments", "/comments/*"); // 특정 URL 패턴에만 필터 적용 registrationBean.setOrder(1); // 우선순위 설정 (낮을수록 먼저 실행) return registrationBean; } From fd7d7a2423f13ef85a30e079337599401cab26cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Mon, 28 Apr 2025 20:20:22 +0900 Subject: [PATCH 09/45] =?UTF-8?q?refactor=20:=20=EA=B0=9C=EB=B3=84=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=A1=B0=ED=9A=8C=EB=95=8C?= =?UTF-8?q?=EB=A7=8C=20=EB=82=B4=EC=9A=A9=20=EB=B3=B4=EC=9D=B4=EA=B2=8C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=20-=20=EC=A0=84=EC=B2=B4=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20=EC=95=88=EB=B3=B4=EC=9E=84(=EC=83=81=EC=84=B8?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=ED=95=B4=EC=95=BC=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EB=B3=B4=EC=9E=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/BlogApiController.java | 15 ++------ .../board/dto/response/ArticleResponse.java | 13 ++++++- .../controller/BlogApiControllerTest.java | 37 +++++++++++-------- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/board/controller/BlogApiController.java b/src/main/java/com/board/controller/BlogApiController.java index b21b4ff..55e78ba 100644 --- a/src/main/java/com/board/controller/BlogApiController.java +++ b/src/main/java/com/board/controller/BlogApiController.java @@ -7,21 +7,14 @@ import com.board.service.BlogService; import com.config.auth.annotation.AuthenticatedMember; import jakarta.validation.Valid; -import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.List; @RequiredArgsConstructor @RestController @@ -46,7 +39,7 @@ public ResponseEntity> findAllArticles(@RequestParam(defau List articles = blogService.findAll(pageable) .stream() - .map(ArticleResponse::new) + .map(ArticleResponse::withoutContent) .toList(); return ResponseEntity.ok() diff --git a/src/main/java/com/board/dto/response/ArticleResponse.java b/src/main/java/com/board/dto/response/ArticleResponse.java index cdbb7d9..b5d7beb 100644 --- a/src/main/java/com/board/dto/response/ArticleResponse.java +++ b/src/main/java/com/board/dto/response/ArticleResponse.java @@ -1,16 +1,18 @@ package com.board.dto.response; import com.board.entity.ArticleEntity; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Getter +@AllArgsConstructor public class ArticleResponse { private final Long id; private final String title; - private final String content; + private String content; private final Long memberId; public ArticleResponse(ArticleEntity article) { @@ -19,4 +21,13 @@ public ArticleResponse(ArticleEntity article) { this.content = article.getContent(); this.memberId = article.getMember().getId(); } + + public static ArticleResponse withoutContent(ArticleEntity article) { + return new ArticleResponse( + article.getId(), + article.getTitle(), + null, // content를 포함하지 않음 + article.getMember().getId() + ); + } } diff --git a/src/test/java/com/board/controller/BlogApiControllerTest.java b/src/test/java/com/board/controller/BlogApiControllerTest.java index 23d4ef5..5e6943e 100644 --- a/src/test/java/com/board/controller/BlogApiControllerTest.java +++ b/src/test/java/com/board/controller/BlogApiControllerTest.java @@ -1,14 +1,5 @@ package com.board.controller; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.board.dto.request.ArticleCreateRequest; import com.board.dto.request.ArticleUpdateRequest; import com.board.dto.response.ArticleResponse; @@ -18,8 +9,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.member.entity.MemberEntity; import com.member.service.MemberService; -import java.util.List; +import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -28,6 +20,14 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @WebMvcTest(BlogApiController.class) class BlogApiControllerTest { @@ -54,6 +54,7 @@ void setup() throws Exception { } @Test + @DisplayName("게시글 생성 성공") void addArticle_Success() throws Exception { // Given ArticleCreateRequest request = new ArticleCreateRequest("Title", "Content"); @@ -83,11 +84,12 @@ void addArticle_Success() throws Exception { } @Test - void findAllArticles_Success() throws Exception { + @DisplayName("전체 게시글 조회 성공 - 내용 미포함") + void 전체_게시글_조회_성공() throws Exception { // Given List responses = List.of( - new ArticleResponse(1L, "Title1", "Content1", 1L), - new ArticleResponse(2L, "Title2", "Content2", 1L) + new ArticleResponse(1L, "Title1", null, 1L), + new ArticleResponse(2L, "Title2", null, 1L) ); MemberEntity member = MemberEntity.builder() .email("abc@example.com") @@ -96,6 +98,7 @@ void findAllArticles_Success() throws Exception { .build(); when(blogService.findAll(any())).thenReturn( new PageImpl<>(responses.stream().map(response -> ArticleEntity.builder() + .id(response.getId()) .title(response.getTitle()) .content(response.getContent()) .member(member) @@ -106,11 +109,13 @@ void findAllArticles_Success() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.length()").value(2)) .andExpect(jsonPath("$[0].title").value("Title1")) - .andExpect(jsonPath("$[1].content").value("Content2")); + .andExpect(jsonPath("$[0].content").value(Matchers.nullValue())) + .andExpect(jsonPath("$[1].id").value(2L)); } @Test - void findArticle_Success() throws Exception { + @DisplayName("개별 게시글 조회 성공 - 내용 포함") + void 개별_게시글_조회_성공_내용_포함() throws Exception { // Given ArticleResponse response = new ArticleResponse(1L, "Title", "Content", 1L); MemberEntity member = MemberEntity.builder() @@ -134,6 +139,7 @@ void findArticle_Success() throws Exception { @Test + @DisplayName("게시글 삭제 성공") void deleteArticle_Success() throws Exception { // When & Then mockMvc.perform(delete("/articles/1")) @@ -141,6 +147,7 @@ void deleteArticle_Success() throws Exception { } @Test + @DisplayName("게시글 수정 성공") void updateArticle_Success() throws Exception { // Given ArticleUpdateRequest request = new ArticleUpdateRequest("Updated Title", "Updated Content"); From 8d35c9ef642be0194154d6060083342221493c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Mon, 28 Apr 2025 21:21:35 +0900 Subject: [PATCH 10/45] =?UTF-8?q?feat=20:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=88=98=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=20-=20=EC=83=9D=EC=84=B1=EC=9E=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80/=EB=B3=80=EA=B2=BD=EC=9C=BC=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/BlogApiController.java | 2 +- .../board/dto/response/ArticleResponse.java | 5 ++- .../java/com/board/entity/ArticleEntity.java | 23 ++++++------- .../java/com/board/service/BlogService.java | 7 ++++ .../BlogApiControllerIntegrationTest.java | 33 ++++++++++++------- .../controller/BlogApiControllerTest.java | 12 ++++--- 6 files changed, 54 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/board/controller/BlogApiController.java b/src/main/java/com/board/controller/BlogApiController.java index 55e78ba..36763e1 100644 --- a/src/main/java/com/board/controller/BlogApiController.java +++ b/src/main/java/com/board/controller/BlogApiController.java @@ -48,7 +48,7 @@ public ResponseEntity> findAllArticles(@RequestParam(defau @GetMapping("/{id}") public ResponseEntity findArticle(@PathVariable long id) { - ArticleEntity article = blogService.findById(id); + ArticleEntity article = blogService.findByIdAndIncreaseViewCount(id); return ResponseEntity.ok() .body(new ArticleResponse(article)); diff --git a/src/main/java/com/board/dto/response/ArticleResponse.java b/src/main/java/com/board/dto/response/ArticleResponse.java index b5d7beb..588ebc9 100644 --- a/src/main/java/com/board/dto/response/ArticleResponse.java +++ b/src/main/java/com/board/dto/response/ArticleResponse.java @@ -14,12 +14,14 @@ public class ArticleResponse { private final String title; private String content; private final Long memberId; + private final long viewCount; public ArticleResponse(ArticleEntity article) { this.id = article.getId(); this.title = article.getTitle(); this.content = article.getContent(); this.memberId = article.getMember().getId(); + this.viewCount = article.getViewCount(); } public static ArticleResponse withoutContent(ArticleEntity article) { @@ -27,7 +29,8 @@ public static ArticleResponse withoutContent(ArticleEntity article) { article.getId(), article.getTitle(), null, // content를 포함하지 않음 - article.getMember().getId() + article.getMember().getId(), + article.getViewCount() ); } } diff --git a/src/main/java/com/board/entity/ArticleEntity.java b/src/main/java/com/board/entity/ArticleEntity.java index ea17f8d..b8ba17b 100644 --- a/src/main/java/com/board/entity/ArticleEntity.java +++ b/src/main/java/com/board/entity/ArticleEntity.java @@ -2,21 +2,14 @@ import com.exception.custom.DifferentOwnerException; import com.member.entity.MemberEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.PrePersist; -import java.time.LocalDateTime; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @@ -40,6 +33,9 @@ public class ArticleEntity { @Column(nullable = false) private LocalDateTime createdAt; + @Column(nullable = false) + private long viewCount = 0; + public ArticleEntity(String title, String content, MemberEntity member) { this.title = title; this.content = content; @@ -47,11 +43,16 @@ public ArticleEntity(String title, String content, MemberEntity member) { } @Builder - public ArticleEntity(Long id, String title, String content, MemberEntity member) { + public ArticleEntity(Long id, String title, String content, MemberEntity member, long viewCount) { this.id = id; this.title = title; this.content = content; this.member = member; + this.viewCount = viewCount; + } + + public void increaseViewCount() { + this.viewCount++; } public void update(String title, String content) { diff --git a/src/main/java/com/board/service/BlogService.java b/src/main/java/com/board/service/BlogService.java index 4ac1cf1..71c3c2e 100644 --- a/src/main/java/com/board/service/BlogService.java +++ b/src/main/java/com/board/service/BlogService.java @@ -38,6 +38,13 @@ public ArticleEntity findById(long id) { return findArticle(id); } + @Transactional + public ArticleEntity findByIdAndIncreaseViewCount(long id) { + ArticleEntity article = findArticle(id); + article.increaseViewCount(); + return article; + } + @Transactional public void delete(long id, Long memberId) { compareAuthors(id, memberService.findById(memberId)); diff --git a/src/test/java/com/board/controller/BlogApiControllerIntegrationTest.java b/src/test/java/com/board/controller/BlogApiControllerIntegrationTest.java index caf6632..805e713 100644 --- a/src/test/java/com/board/controller/BlogApiControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/BlogApiControllerIntegrationTest.java @@ -1,16 +1,5 @@ package com.board.controller; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.board.dto.request.ArticleCreateRequest; import com.board.dto.request.ArticleUpdateRequest; import com.board.entity.ArticleEntity; @@ -37,6 +26,12 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + @SpringBootTest @AutoConfigureMockMvc @Transactional @@ -147,6 +142,22 @@ void findArticleTest() throws Exception { .andExpect(jsonPath("$.content").value("Content 1")); } + @Test + @DisplayName("글 상세조회시 조회수가 정상적으로 상승하면 성공") + void 글_상세조회_조회수_상승() throws Exception { + MemberEntity member = createAndSaveMember("bb@aa.com", "nickname"); + ArticleEntity article = blogRepository.save(new ArticleEntity("Title 1", "Content 1", member)); + + mockMvc.perform(get("/articles/" + article.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.title").value("Title 1")) + .andExpect(jsonPath("$.content").value("Content 1")) + .andExpect(jsonPath("$.viewCount").value(1)); + + mockMvc.perform(get("/articles/" + article.getId())) + .andExpect(jsonPath("$.viewCount").value(2)); + } + @Test @DisplayName("로그인 없이 글 삭제 시 401 에러 발생") void notLoginDeleteArticleTest() throws Exception { diff --git a/src/test/java/com/board/controller/BlogApiControllerTest.java b/src/test/java/com/board/controller/BlogApiControllerTest.java index 5e6943e..47c456d 100644 --- a/src/test/java/com/board/controller/BlogApiControllerTest.java +++ b/src/test/java/com/board/controller/BlogApiControllerTest.java @@ -117,18 +117,22 @@ void addArticle_Success() throws Exception { @DisplayName("개별 게시글 조회 성공 - 내용 포함") void 개별_게시글_조회_성공_내용_포함() throws Exception { // Given - ArticleResponse response = new ArticleResponse(1L, "Title", "Content", 1L); + ArticleResponse response = new ArticleResponse(1L, "Title", "Content", 1L, 0); MemberEntity member = MemberEntity.builder() .email("abc@example.com") .password("abc") .nickName("abc") .build(); - when(blogService.findById(1L)).thenReturn(ArticleEntity.builder() + ArticleEntity article = ArticleEntity.builder() + .id(1L) .title(response.getTitle()) .content(response.getContent()) .member(member) - .build()); + .viewCount(0L) // 초기 조회수 설정 + .build(); + + when(blogService.findByIdAndIncreaseViewCount(1L)).thenReturn(article); // When & Then mockMvc.perform(get("/articles/1")) @@ -151,7 +155,7 @@ void deleteArticle_Success() throws Exception { void updateArticle_Success() throws Exception { // Given ArticleUpdateRequest request = new ArticleUpdateRequest("Updated Title", "Updated Content"); - ArticleResponse response = new ArticleResponse(1L, "Updated Title", "Updated Content", 1L); + ArticleResponse response = new ArticleResponse(1L, "Updated Title", "Updated Content", 1L, 0); MemberEntity member = MemberEntity.builder() .email("abc@example.com") From d7fc582fa97399aeedf1a890ae9506b33d1e2c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Tue, 29 Apr 2025 21:43:13 +0900 Subject: [PATCH 11/45] =?UTF-8?q?refactor=20:=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EB=AA=85=20=EB=B0=8F=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?=EB=93=B1=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=EC=A7=84?= =?UTF-8?q?=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/board/service/BlogService.java | 24 +++++++++---------- .../com/board/service/CommentService.java | 12 ++++++---- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/board/service/BlogService.java b/src/main/java/com/board/service/BlogService.java index 71c3c2e..ee1f27e 100644 --- a/src/main/java/com/board/service/BlogService.java +++ b/src/main/java/com/board/service/BlogService.java @@ -26,7 +26,7 @@ public ArticleEntity save(ArticleCreateRequest request, Long memberId) { return blogRepository.save(ArticleEntity.builder() .title(request.getTitle()) .content(request.getContent()) - .member(memberService.findById(memberId)) + .member(findMemberById(memberId)) .build()); } @@ -34,38 +34,38 @@ public Page findAll(Pageable pageable) { return blogRepository.findAll(pageable); } - public ArticleEntity findById(long id) { - return findArticle(id); - } - @Transactional public ArticleEntity findByIdAndIncreaseViewCount(long id) { - ArticleEntity article = findArticle(id); + ArticleEntity article = findById(id); article.increaseViewCount(); return article; } @Transactional public void delete(long id, Long memberId) { - compareAuthors(id, memberService.findById(memberId)); + compareAuthors(id, findMemberById(memberId)); blogRepository.deleteById(id); } @Transactional public ArticleEntity update(long id, Long memberId, ArticleUpdateRequest request) { - ArticleEntity article = compareAuthors(id, memberService.findById(memberId)); + ArticleEntity article = findById(id); + compareAuthors(id, findMemberById(memberId)); article.update(request.getTitle(), request.getContent()); return article; } - private ArticleEntity compareAuthors(long articleId, MemberEntity member) { - ArticleEntity article = findArticle(articleId); + private void compareAuthors(long articleId, MemberEntity member) { + ArticleEntity article = findById(articleId); article.validateOwner(member); - return article; } - private ArticleEntity findArticle(long id) { + public ArticleEntity findById(long id) { return blogRepository.findById(id) .orElseThrow(() -> MyEntityNotFoundException.from(id)); } + + private MemberEntity findMemberById(Long memberId) { + return memberService.findById(memberId); + } } diff --git a/src/main/java/com/board/service/CommentService.java b/src/main/java/com/board/service/CommentService.java index 922488d..ffb55ee 100644 --- a/src/main/java/com/board/service/CommentService.java +++ b/src/main/java/com/board/service/CommentService.java @@ -30,25 +30,25 @@ public Page findAllComments(Long articleId, Pageable pageable) { @Transactional public CommentEntity createComment(CommentCreateRequest request, Long memberId) { ArticleEntity article = blogService.findById(request.getArticleId()); - MemberEntity member = memberService.findById(memberId); + MemberEntity member = findMemberById(memberId); CommentEntity comment = new CommentEntity(request.getContent(), article, member); return commentRepository.save(comment); } @Transactional public CommentEntity updateComment(CommentUpdateRequest request, Long memberId) { - CommentEntity comment = compareAuthors(request.getCommentId(), memberService.findById(memberId)); + CommentEntity comment = findCommentAndValidateOwner(request.getCommentId(), findMemberById(memberId)); comment.update(request.getContent()); return comment; } @Transactional public void deleteComment(long commentId, Long memberId) { - CommentEntity comment = compareAuthors(commentId, memberService.findById(memberId)); + CommentEntity comment = findCommentAndValidateOwner(commentId, findMemberById(memberId)); comment.delete(); } - private CommentEntity compareAuthors(long commentId, MemberEntity member) { + private CommentEntity findCommentAndValidateOwner(long commentId, MemberEntity member) { CommentEntity comment = findComment(commentId); comment.validateOwner(member); return comment; @@ -58,4 +58,8 @@ private CommentEntity findComment(long id) { return commentRepository.findByIdAndDeletedFalse(id) .orElseThrow(() -> MyEntityNotFoundException.from(id)); } + + private MemberEntity findMemberById(Long memberId) { + return memberService.findById(memberId); + } } From db1869af43590aacf5f38015b31be8d237c7ca3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Thu, 1 May 2025 16:18:33 +0900 Subject: [PATCH 12/45] =?UTF-8?q?refactor=20:=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EB=AA=85=20=EB=8D=94=20=EC=95=8C=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20-=20boardApiController?= =?UTF-8?q?=20>=20ArticleController=20=EB=93=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor : 클래스명 더 알맞게 리팩토링 - boardApiController > ArticleController 등 --- ...Controller.java => ArticleController.java} | 16 ++--- ...Repository.java => ArticleRepository.java} | 2 +- .../{BlogService.java => ArticleService.java} | 14 ++--- .../com/board/service/CommentService.java | 4 +- src/main/java/com/config/DataInitializer.java | 4 +- .../member/controller/MemberController.java | 10 ++-- ...rSignUpRequest.java => SignUpRequest.java} | 4 +- ...ignUpResponse.java => SignUpResponse.java} | 4 +- .../com/member/service/MemberService.java | 6 +- .../com/member/service/MemberServiceImpl.java | 10 ++-- ... => ArticleControllerIntegrationTest.java} | 24 ++++---- ...erTest.java => ArticleControllerTest.java} | 16 ++--- .../CommentControllerIntegrationTest.java | 6 +- ...rviceTest.java => ArticleServiceTest.java} | 59 +++++++++---------- .../com/board/service/CommentServiceTest.java | 21 ++++--- .../MemberControllerIntegrationTest.java | 14 ++--- .../controller/MemberControllerTest.java | 22 +++---- .../member/service/MemberServiceImplTest.java | 23 ++++---- 18 files changed, 125 insertions(+), 134 deletions(-) rename src/main/java/com/board/controller/{BlogApiController.java => ArticleController.java} (83%) rename src/main/java/com/board/repository/{BlogRepository.java => ArticleRepository.java} (62%) rename src/main/java/com/board/service/{BlogService.java => ArticleService.java} (85%) rename src/main/java/com/member/dto/request/{MemberSignUpRequest.java => SignUpRequest.java} (84%) rename src/main/java/com/member/dto/response/{MemberSignUpResponse.java => SignUpResponse.java} (80%) rename src/test/java/com/board/controller/{BlogApiControllerIntegrationTest.java => ArticleControllerIntegrationTest.java} (89%) rename src/test/java/com/board/controller/{BlogApiControllerTest.java => ArticleControllerTest.java} (92%) rename src/test/java/com/board/service/{BlogServiceTest.java => ArticleServiceTest.java} (73%) diff --git a/src/main/java/com/board/controller/BlogApiController.java b/src/main/java/com/board/controller/ArticleController.java similarity index 83% rename from src/main/java/com/board/controller/BlogApiController.java rename to src/main/java/com/board/controller/ArticleController.java index 36763e1..55906bc 100644 --- a/src/main/java/com/board/controller/BlogApiController.java +++ b/src/main/java/com/board/controller/ArticleController.java @@ -4,7 +4,7 @@ import com.board.dto.request.ArticleUpdateRequest; import com.board.dto.response.ArticleResponse; import com.board.entity.ArticleEntity; -import com.board.service.BlogService; +import com.board.service.ArticleService; import com.config.auth.annotation.AuthenticatedMember; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -19,14 +19,14 @@ @RequiredArgsConstructor @RestController @RequestMapping("/articles") -public class BlogApiController { +public class ArticleController { - private final BlogService blogService; + private final ArticleService articleService; @PostMapping("") public ResponseEntity addArticle(@Valid @RequestBody ArticleCreateRequest request, @AuthenticatedMember Long memberId) { - ArticleEntity savedArticle = blogService.save(request, memberId); + ArticleEntity savedArticle = articleService.save(request, memberId); return ResponseEntity.status(HttpStatus.CREATED) .body(new ArticleResponse(savedArticle)); @@ -37,7 +37,7 @@ public ResponseEntity> findAllArticles(@RequestParam(defau @RequestParam(defaultValue = "10") int size) { Pageable pageable = PageRequest.of(page, size); - List articles = blogService.findAll(pageable) + List articles = articleService.findAll(pageable) .stream() .map(ArticleResponse::withoutContent) .toList(); @@ -48,7 +48,7 @@ public ResponseEntity> findAllArticles(@RequestParam(defau @GetMapping("/{id}") public ResponseEntity findArticle(@PathVariable long id) { - ArticleEntity article = blogService.findByIdAndIncreaseViewCount(id); + ArticleEntity article = articleService.findByIdAndIncreaseViewCount(id); return ResponseEntity.ok() .body(new ArticleResponse(article)); @@ -57,7 +57,7 @@ public ResponseEntity findArticle(@PathVariable long id) { @DeleteMapping("/{id}") public ResponseEntity deleteArticle(@PathVariable long id, @AuthenticatedMember Long memberId) { - blogService.delete(id, memberId); + articleService.delete(id, memberId); return ResponseEntity.noContent() .build(); @@ -67,7 +67,7 @@ public ResponseEntity deleteArticle(@PathVariable long id, public ResponseEntity updateArticle(@PathVariable long id, @AuthenticatedMember Long memberId, @Valid @RequestBody ArticleUpdateRequest request) { - ArticleEntity updateArticle = blogService.update(id, memberId, request); + ArticleEntity updateArticle = articleService.update(id, memberId, request); return ResponseEntity.ok() .body(new ArticleResponse(updateArticle)); diff --git a/src/main/java/com/board/repository/BlogRepository.java b/src/main/java/com/board/repository/ArticleRepository.java similarity index 62% rename from src/main/java/com/board/repository/BlogRepository.java rename to src/main/java/com/board/repository/ArticleRepository.java index 0ed3ab7..8d19b27 100644 --- a/src/main/java/com/board/repository/BlogRepository.java +++ b/src/main/java/com/board/repository/ArticleRepository.java @@ -3,5 +3,5 @@ import com.board.entity.ArticleEntity; import org.springframework.data.jpa.repository.JpaRepository; -public interface BlogRepository extends JpaRepository { +public interface ArticleRepository extends JpaRepository { } diff --git a/src/main/java/com/board/service/BlogService.java b/src/main/java/com/board/service/ArticleService.java similarity index 85% rename from src/main/java/com/board/service/BlogService.java rename to src/main/java/com/board/service/ArticleService.java index ee1f27e..282032a 100644 --- a/src/main/java/com/board/service/BlogService.java +++ b/src/main/java/com/board/service/ArticleService.java @@ -3,7 +3,7 @@ import com.board.dto.request.ArticleCreateRequest; import com.board.dto.request.ArticleUpdateRequest; import com.board.entity.ArticleEntity; -import com.board.repository.BlogRepository; +import com.board.repository.ArticleRepository; import com.exception.custom.MyEntityNotFoundException; import com.member.entity.MemberEntity; import com.member.service.MemberService; @@ -16,14 +16,14 @@ @RequiredArgsConstructor @Service @Transactional(readOnly = true) -public class BlogService { +public class ArticleService { - private final BlogRepository blogRepository; + private final ArticleRepository articleRepository; private final MemberService memberService; @Transactional public ArticleEntity save(ArticleCreateRequest request, Long memberId) { - return blogRepository.save(ArticleEntity.builder() + return articleRepository.save(ArticleEntity.builder() .title(request.getTitle()) .content(request.getContent()) .member(findMemberById(memberId)) @@ -31,7 +31,7 @@ public ArticleEntity save(ArticleCreateRequest request, Long memberId) { } public Page findAll(Pageable pageable) { - return blogRepository.findAll(pageable); + return articleRepository.findAll(pageable); } @Transactional @@ -44,7 +44,7 @@ public ArticleEntity findByIdAndIncreaseViewCount(long id) { @Transactional public void delete(long id, Long memberId) { compareAuthors(id, findMemberById(memberId)); - blogRepository.deleteById(id); + articleRepository.deleteById(id); } @Transactional @@ -61,7 +61,7 @@ private void compareAuthors(long articleId, MemberEntity member) { } public ArticleEntity findById(long id) { - return blogRepository.findById(id) + return articleRepository.findById(id) .orElseThrow(() -> MyEntityNotFoundException.from(id)); } diff --git a/src/main/java/com/board/service/CommentService.java b/src/main/java/com/board/service/CommentService.java index ffb55ee..98f7502 100644 --- a/src/main/java/com/board/service/CommentService.java +++ b/src/main/java/com/board/service/CommentService.java @@ -20,7 +20,7 @@ public class CommentService { private final CommentRepository commentRepository; - private final BlogService blogService; + private final ArticleService articleService; private final MemberService memberService; public Page findAllComments(Long articleId, Pageable pageable) { @@ -29,7 +29,7 @@ public Page findAllComments(Long articleId, Pageable pageable) { @Transactional public CommentEntity createComment(CommentCreateRequest request, Long memberId) { - ArticleEntity article = blogService.findById(request.getArticleId()); + ArticleEntity article = articleService.findById(request.getArticleId()); MemberEntity member = findMemberById(memberId); CommentEntity comment = new CommentEntity(request.getContent(), article, member); return commentRepository.save(comment); diff --git a/src/main/java/com/config/DataInitializer.java b/src/main/java/com/config/DataInitializer.java index 84e8eb0..94ab22f 100644 --- a/src/main/java/com/config/DataInitializer.java +++ b/src/main/java/com/config/DataInitializer.java @@ -1,6 +1,6 @@ package com.config; -import com.board.repository.BlogRepository; +import com.board.repository.ArticleRepository; import com.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; @@ -10,7 +10,7 @@ public class DataInitializer { private final MemberRepository memberRepository; - private final BlogRepository blogRepository; + private final ArticleRepository articleRepository; /* 테스트에 방해되서 주석처리 @Bean public CommandLineRunner initData() { diff --git a/src/main/java/com/member/controller/MemberController.java b/src/main/java/com/member/controller/MemberController.java index bada9c7..745f19b 100644 --- a/src/main/java/com/member/controller/MemberController.java +++ b/src/main/java/com/member/controller/MemberController.java @@ -1,9 +1,9 @@ package com.member.controller; import com.member.dto.request.LoginRequest; -import com.member.dto.request.MemberSignUpRequest; +import com.member.dto.request.SignUpRequest; import com.member.dto.response.LoginResponse; -import com.member.dto.response.MemberSignUpResponse; +import com.member.dto.response.SignUpResponse; import com.member.service.MemberService; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; @@ -23,9 +23,9 @@ public class MemberController { private final MemberService memberService; @PostMapping("/signup") - public ResponseEntity signUp(@Valid @RequestBody MemberSignUpRequest request) { - MemberSignUpResponse memberSignUpResponse = memberService.signUp(request); - return ResponseEntity.ok(memberSignUpResponse); + public ResponseEntity signUp(@Valid @RequestBody SignUpRequest request) { + SignUpResponse signUpResponse = memberService.signUp(request); + return ResponseEntity.ok(signUpResponse); } @PostMapping("/login") diff --git a/src/main/java/com/member/dto/request/MemberSignUpRequest.java b/src/main/java/com/member/dto/request/SignUpRequest.java similarity index 84% rename from src/main/java/com/member/dto/request/MemberSignUpRequest.java rename to src/main/java/com/member/dto/request/SignUpRequest.java index 96a783f..f0a7e4c 100644 --- a/src/main/java/com/member/dto/request/MemberSignUpRequest.java +++ b/src/main/java/com/member/dto/request/SignUpRequest.java @@ -6,7 +6,7 @@ import lombok.Getter; @Getter -public class MemberSignUpRequest { +public class SignUpRequest { @NotBlank(message = "이메일은 필수 입력값이에용") @Email @@ -19,7 +19,7 @@ public class MemberSignUpRequest { private final String password; @Builder - public MemberSignUpRequest(String email, String nickName, String password) { + public SignUpRequest(String email, String nickName, String password) { this.email = email; this.nickName = nickName; this.password = password; diff --git a/src/main/java/com/member/dto/response/MemberSignUpResponse.java b/src/main/java/com/member/dto/response/SignUpResponse.java similarity index 80% rename from src/main/java/com/member/dto/response/MemberSignUpResponse.java rename to src/main/java/com/member/dto/response/SignUpResponse.java index 8087993..295b1dc 100644 --- a/src/main/java/com/member/dto/response/MemberSignUpResponse.java +++ b/src/main/java/com/member/dto/response/SignUpResponse.java @@ -6,13 +6,13 @@ @Getter @RequiredArgsConstructor -public class MemberSignUpResponse { +public class SignUpResponse { private final Long id; private final String email; private final String nickName; - public MemberSignUpResponse(MemberEntity memberEntity) { + public SignUpResponse(MemberEntity memberEntity) { this.id = memberEntity.getId(); this.email = memberEntity.getEmail(); this.nickName = memberEntity.getNickName(); diff --git a/src/main/java/com/member/service/MemberService.java b/src/main/java/com/member/service/MemberService.java index c228def..7269fac 100644 --- a/src/main/java/com/member/service/MemberService.java +++ b/src/main/java/com/member/service/MemberService.java @@ -1,14 +1,14 @@ package com.member.service; import com.member.dto.request.LoginRequest; -import com.member.dto.request.MemberSignUpRequest; +import com.member.dto.request.SignUpRequest; import com.member.dto.response.LoginResponse; -import com.member.dto.response.MemberSignUpResponse; +import com.member.dto.response.SignUpResponse; import com.member.entity.MemberEntity; public interface MemberService { - MemberSignUpResponse signUp(MemberSignUpRequest request); + SignUpResponse signUp(SignUpRequest request); LoginResponse login(LoginRequest request); diff --git a/src/main/java/com/member/service/MemberServiceImpl.java b/src/main/java/com/member/service/MemberServiceImpl.java index 0212528..2a94a97 100644 --- a/src/main/java/com/member/service/MemberServiceImpl.java +++ b/src/main/java/com/member/service/MemberServiceImpl.java @@ -7,9 +7,9 @@ import com.exception.custom.SignUpException; import com.member.domain.Member; import com.member.dto.request.LoginRequest; -import com.member.dto.request.MemberSignUpRequest; +import com.member.dto.request.SignUpRequest; import com.member.dto.response.LoginResponse; -import com.member.dto.response.MemberSignUpResponse; +import com.member.dto.response.SignUpResponse; import com.member.entity.MemberEntity; import com.member.message.ErrorMessage; import com.member.repository.MemberRepository; @@ -27,7 +27,7 @@ public class MemberServiceImpl implements MemberService { @Override @Transactional - public MemberSignUpResponse signUp(MemberSignUpRequest request) { + public SignUpResponse signUp(SignUpRequest request) { validateDuplicate(request); @@ -38,10 +38,10 @@ public MemberSignUpResponse signUp(MemberSignUpRequest request) { .build(); MemberEntity savedMember = memberRepository.save(member); - return new MemberSignUpResponse(savedMember); + return new SignUpResponse(savedMember); } - private void validateDuplicate(MemberSignUpRequest request) { + private void validateDuplicate(SignUpRequest request) { if (memberRepository.findByEmail(request.getEmail()).isPresent()) { throw SignUpException.from(ErrorMessage.EMAIL_DUPLICATE); } diff --git a/src/test/java/com/board/controller/BlogApiControllerIntegrationTest.java b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java similarity index 89% rename from src/test/java/com/board/controller/BlogApiControllerIntegrationTest.java rename to src/test/java/com/board/controller/ArticleControllerIntegrationTest.java index 805e713..07e78ce 100644 --- a/src/test/java/com/board/controller/BlogApiControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java @@ -3,12 +3,12 @@ import com.board.dto.request.ArticleCreateRequest; import com.board.dto.request.ArticleUpdateRequest; import com.board.entity.ArticleEntity; -import com.board.repository.BlogRepository; +import com.board.repository.ArticleRepository; import com.config.jwt.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; import com.member.dto.request.LoginRequest; -import com.member.dto.request.MemberSignUpRequest; +import com.member.dto.request.SignUpRequest; import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; import jakarta.servlet.http.Cookie; @@ -36,7 +36,7 @@ @AutoConfigureMockMvc @Transactional @ActiveProfiles("test") -class BlogApiControllerIntegrationTest { +class ArticleControllerIntegrationTest { @Autowired private MockMvc mockMvc; @@ -45,7 +45,7 @@ class BlogApiControllerIntegrationTest { private ObjectMapper objectMapper; @Autowired - private BlogRepository blogRepository; + private ArticleRepository articleRepository; @Autowired private MemberRepository memberRepository; @@ -74,7 +74,7 @@ void testJwtSecretKey() { } private void signUpAndLogin() throws Exception { - sendPostRequest("/members/signup", new MemberSignUpRequest(MEMBER_EMAIL, MEMBER_NICKNAME, MEMBER_PASSWORD)) + sendPostRequest("/members/signup", new SignUpRequest(MEMBER_EMAIL, MEMBER_NICKNAME, MEMBER_PASSWORD)) .andExpect(status().isOk()) .andExpect(jsonPath("$.email").value(MEMBER_EMAIL)) .andExpect(jsonPath("$.nickName").value(MEMBER_NICKNAME)); @@ -120,8 +120,8 @@ void findAllArticleTest() throws Exception { MemberEntity member1 = createAndSaveMember("bb@aa.com", "nickname1"); MemberEntity member2 = createAndSaveMember("cc@aa.com", "nickname2"); - blogRepository.save(new ArticleEntity("Title 1", "Content 1", member1)); - blogRepository.save(new ArticleEntity("Title 2", "Content 2", member2)); + articleRepository.save(new ArticleEntity("Title 1", "Content 1", member1)); + articleRepository.save(new ArticleEntity("Title 2", "Content 2", member2)); mockMvc.perform(get("/articles").param("page", "0").param("size", "10")) .andExpect(status().isOk()) @@ -134,7 +134,7 @@ void findAllArticleTest() throws Exception { @DisplayName("개별 조회 테스트") void findArticleTest() throws Exception { MemberEntity member = createAndSaveMember("bb@aa.com", "nickname"); - ArticleEntity article = blogRepository.save(new ArticleEntity("Title 1", "Content 1", member)); + ArticleEntity article = articleRepository.save(new ArticleEntity("Title 1", "Content 1", member)); mockMvc.perform(get("/articles/" + article.getId())) .andExpect(status().isOk()) @@ -146,7 +146,7 @@ void findArticleTest() throws Exception { @DisplayName("글 상세조회시 조회수가 정상적으로 상승하면 성공") void 글_상세조회_조회수_상승() throws Exception { MemberEntity member = createAndSaveMember("bb@aa.com", "nickname"); - ArticleEntity article = blogRepository.save(new ArticleEntity("Title 1", "Content 1", member)); + ArticleEntity article = articleRepository.save(new ArticleEntity("Title 1", "Content 1", member)); mockMvc.perform(get("/articles/" + article.getId())) .andExpect(status().isOk()) @@ -166,7 +166,7 @@ void notLoginDeleteArticleTest() throws Exception { mockMvc.perform(delete("/articles/" + article.getId())) .andExpect(status().isUnauthorized()); - assertTrue(blogRepository.findById(article.getId()).isPresent()); + assertTrue(articleRepository.findById(article.getId()).isPresent()); } @Test @@ -177,7 +177,7 @@ void deleteArticleTest() throws Exception { mockMvc.perform(delete("/articles/" + articleId).cookie(new Cookie("token", tokenCookie))) .andExpect(status().isNoContent()); - assertFalse(blogRepository.findById(articleId).isPresent()); + assertFalse(articleRepository.findById(articleId).isPresent()); } @Test @@ -203,7 +203,7 @@ private MemberEntity createAndSaveMember(String email, String nickname) { } private ArticleEntity createAndSaveArticle(String title, String content) { - return blogRepository.save(new ArticleEntity(title, content, createAndSaveMember("bb@aa.com", "nickname"))); + return articleRepository.save(new ArticleEntity(title, content, createAndSaveMember("bb@aa.com", "nickname"))); } private Long createArticleAndGetId(String title, String content) throws Exception { diff --git a/src/test/java/com/board/controller/BlogApiControllerTest.java b/src/test/java/com/board/controller/ArticleControllerTest.java similarity index 92% rename from src/test/java/com/board/controller/BlogApiControllerTest.java rename to src/test/java/com/board/controller/ArticleControllerTest.java index 47c456d..6ba4d44 100644 --- a/src/test/java/com/board/controller/BlogApiControllerTest.java +++ b/src/test/java/com/board/controller/ArticleControllerTest.java @@ -4,7 +4,7 @@ import com.board.dto.request.ArticleUpdateRequest; import com.board.dto.response.ArticleResponse; import com.board.entity.ArticleEntity; -import com.board.service.BlogService; +import com.board.service.ArticleService; import com.config.auth.AuthenticatedMemberArgumentResolver; import com.fasterxml.jackson.databind.ObjectMapper; import com.member.entity.MemberEntity; @@ -28,14 +28,14 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(BlogApiController.class) -class BlogApiControllerTest { +@WebMvcTest(ArticleController.class) +class ArticleControllerTest { @Autowired private MockMvc mockMvc; @MockitoBean - private BlogService blogService; + private ArticleService articleService; @MockitoBean private MemberService memberService; @@ -72,7 +72,7 @@ void addArticle_Success() throws Exception { .build(); when(memberService.findById(any())).thenReturn(member); - when(blogService.save(any(ArticleCreateRequest.class), any(Long.class))).thenReturn(article); + when(articleService.save(any(ArticleCreateRequest.class), any(Long.class))).thenReturn(article); // When & Then mockMvc.perform(post("/articles") @@ -96,7 +96,7 @@ void addArticle_Success() throws Exception { .password("abc") .nickName("abc") .build(); - when(blogService.findAll(any())).thenReturn( + when(articleService.findAll(any())).thenReturn( new PageImpl<>(responses.stream().map(response -> ArticleEntity.builder() .id(response.getId()) .title(response.getTitle()) @@ -132,7 +132,7 @@ void addArticle_Success() throws Exception { .viewCount(0L) // 초기 조회수 설정 .build(); - when(blogService.findByIdAndIncreaseViewCount(1L)).thenReturn(article); + when(articleService.findByIdAndIncreaseViewCount(1L)).thenReturn(article); // When & Then mockMvc.perform(get("/articles/1")) @@ -162,7 +162,7 @@ void updateArticle_Success() throws Exception { .password("abc") .nickName("abc") .build(); - when(blogService.update(any(Long.class), any(Long.class), any(ArticleUpdateRequest.class))) + when(articleService.update(any(Long.class), any(Long.class), any(ArticleUpdateRequest.class))) .thenReturn(ArticleEntity.builder() .title(response.getTitle()) .content(response.getContent()) diff --git a/src/test/java/com/board/controller/CommentControllerIntegrationTest.java b/src/test/java/com/board/controller/CommentControllerIntegrationTest.java index 5bfc09a..4c28a47 100644 --- a/src/test/java/com/board/controller/CommentControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/CommentControllerIntegrationTest.java @@ -4,7 +4,7 @@ import com.board.dto.request.CommentUpdateRequest; import com.board.entity.ArticleEntity; import com.board.entity.CommentEntity; -import com.board.repository.BlogRepository; +import com.board.repository.ArticleRepository; import com.board.repository.CommentRepository; import com.config.jwt.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; @@ -41,7 +41,7 @@ class CommentControllerIntegrationTest { private CommentRepository commentRepository; @Autowired - private BlogRepository blogRepository; + private ArticleRepository articleRepository; @Autowired private MemberRepository memberRepository; @@ -57,7 +57,7 @@ class CommentControllerIntegrationTest { @BeforeEach void setup() { member = memberRepository.save(new MemberEntity("test@example.com", "password", "nickname")); - article = blogRepository.save(new ArticleEntity("title", "content", member)); + article = articleRepository.save(new ArticleEntity("title", "content", member)); jwtToken = "Bearer " + jwtUtil.generateToken(member.getEmail()); } diff --git a/src/test/java/com/board/service/BlogServiceTest.java b/src/test/java/com/board/service/ArticleServiceTest.java similarity index 73% rename from src/test/java/com/board/service/BlogServiceTest.java rename to src/test/java/com/board/service/ArticleServiceTest.java index 4d0fec2..d284413 100644 --- a/src/test/java/com/board/service/BlogServiceTest.java +++ b/src/test/java/com/board/service/ArticleServiceTest.java @@ -1,25 +1,12 @@ package com.board.service; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - import com.board.dto.request.ArticleCreateRequest; import com.board.entity.ArticleEntity; -import com.board.repository.BlogRepository; +import com.board.repository.ArticleRepository; import com.exception.custom.DifferentOwnerException; import com.exception.custom.MyEntityNotFoundException; import com.member.entity.MemberEntity; import com.member.service.MemberService; -import java.util.Collections; -import java.util.Optional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -31,14 +18,22 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import java.util.Collections; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + @ExtendWith(MockitoExtension.class) -class BlogServiceTest { +class ArticleServiceTest { @InjectMocks - private BlogService blogService; + private ArticleService articleService; @Mock - private BlogRepository blogRepository; + private ArticleRepository articleRepository; @Mock private MemberService memberService; @@ -66,10 +61,10 @@ void saveArticle_Success() { .build(); when(memberService.findById(any(Long.class))).thenReturn(member); - when(blogRepository.save(any(ArticleEntity.class))).thenReturn(article); + when(articleRepository.save(any(ArticleEntity.class))).thenReturn(article); // When - ArticleEntity savedArticle = blogService.save(request, memberId); + ArticleEntity savedArticle = articleService.save(request, memberId); // Then assertNotNull(savedArticle); @@ -85,10 +80,10 @@ void findAllArticles_Success() { Pageable pageable = PageRequest.of(0, 10); Page mockPage = new PageImpl<>(Collections.emptyList()); - when(blogRepository.findAll(pageable)).thenReturn(mockPage); + when(articleRepository.findAll(pageable)).thenReturn(mockPage); // When - Page result = blogService.findAll(pageable); + Page result = articleService.findAll(pageable); // Then assertNotNull(result); @@ -101,10 +96,10 @@ void findById_ArticleExists() { // Given MemberEntity member = new MemberEntity("test@example.com", "password", "testUser"); ArticleEntity article = new ArticleEntity("title", "content", member); - when(blogRepository.findById(anyLong())).thenReturn(Optional.of(article)); - when(blogRepository.findById(1L)).thenReturn(Optional.of(article)); + when(articleRepository.findById(anyLong())).thenReturn(Optional.of(article)); + when(articleRepository.findById(1L)).thenReturn(Optional.of(article)); // When - ArticleEntity foundArticle = blogService.findById(1L); + ArticleEntity foundArticle = articleService.findById(1L); // Then assertNotNull(foundArticle); @@ -117,10 +112,10 @@ void findById_ArticleExists() { @DisplayName("Serivce - 없는 정보 조회 시 에러 발생") void findById_ArticleNotFound() { // Given - when(blogRepository.findById(1L)).thenReturn(Optional.empty()); + when(articleRepository.findById(1L)).thenReturn(Optional.empty()); // When & Then - assertThrows(MyEntityNotFoundException.class, () -> blogService.findById(1L)); + assertThrows(MyEntityNotFoundException.class, () -> articleService.findById(1L)); } @Test @@ -134,15 +129,15 @@ void deleteArticle_Success() { ArticleEntity article = new ArticleEntity("title", "content", member); // ID 없이 생성 when(memberService.findById(any(Long.class))).thenReturn(member); - when(blogRepository.findById(articleId)).thenReturn(Optional.of(article)); + when(articleRepository.findById(articleId)).thenReturn(Optional.of(article)); - doNothing().when(blogRepository).deleteById(articleId); + doNothing().when(articleRepository).deleteById(articleId); // When - blogService.delete(articleId, memberId); + articleService.delete(articleId, memberId); // Then - verify(blogRepository, times(1)).deleteById(anyLong()); + verify(articleRepository, times(1)).deleteById(anyLong()); } @@ -158,11 +153,11 @@ void deleteArticle_NotAuthor_ThrowsException() { // Mock 설정 when(memberService.findById(memberId)).thenReturn(requestingMember); - when(blogRepository.findById(articleId)).thenReturn(Optional.of(article)); + when(articleRepository.findById(articleId)).thenReturn(Optional.of(article)); // When & Then DifferentOwnerException exception = assertThrows(DifferentOwnerException.class, - () -> blogService.delete(articleId, memberId)); + () -> articleService.delete(articleId, memberId)); assertEquals("권한 없음", exception.getMessage()); } diff --git a/src/test/java/com/board/service/CommentServiceTest.java b/src/test/java/com/board/service/CommentServiceTest.java index 6cad6ed..5204dbc 100644 --- a/src/test/java/com/board/service/CommentServiceTest.java +++ b/src/test/java/com/board/service/CommentServiceTest.java @@ -1,12 +1,5 @@ package com.board.service; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.when; - import com.board.dto.request.CommentCreateRequest; import com.board.dto.request.CommentUpdateRequest; import com.board.entity.ArticleEntity; @@ -15,8 +8,6 @@ import com.exception.custom.DifferentOwnerException; import com.member.entity.MemberEntity; import com.member.service.MemberService; -import java.util.List; -import java.util.Optional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -29,6 +20,14 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class CommentServiceTest { @@ -37,7 +36,7 @@ class CommentServiceTest { @Mock private CommentRepository commentRepository; @Mock - private BlogService blogService; + private ArticleService articleService; @Mock private MemberService memberService; @@ -85,7 +84,7 @@ class CommentServiceTest { ArticleEntity article = new ArticleEntity("제목", "내용", member); CommentEntity comment = new CommentEntity(content, article, member); - when(blogService.findById(articleId)).thenReturn(article); + when(articleService.findById(articleId)).thenReturn(article); when(memberService.findById(memberId)).thenReturn(member); when(commentRepository.save(any(CommentEntity.class))).thenReturn(comment); diff --git a/src/test/java/com/member/controller/MemberControllerIntegrationTest.java b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java index 5309362..d4fc1e3 100644 --- a/src/test/java/com/member/controller/MemberControllerIntegrationTest.java +++ b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java @@ -1,15 +1,9 @@ package com.member.controller; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.config.jwt.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.member.dto.request.LoginRequest; -import com.member.dto.request.MemberSignUpRequest; +import com.member.dto.request.SignUpRequest; import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; import org.junit.jupiter.api.BeforeEach; @@ -24,6 +18,10 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.transaction.annotation.Transactional; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + @SpringBootTest @AutoConfigureMockMvc @Transactional @@ -97,7 +95,7 @@ void signUpTest() throws Exception { final String testNickName = "test-nickname"; final String testPassword = "password123"; - MemberSignUpRequest signUpRequest = new MemberSignUpRequest(testEmail, testNickName, testPassword); + SignUpRequest signUpRequest = new SignUpRequest(testEmail, testNickName, testPassword); String signUpJson = objectMapper.writeValueAsString(signUpRequest); mockMvc.perform(post("/members/signup") diff --git a/src/test/java/com/member/controller/MemberControllerTest.java b/src/test/java/com/member/controller/MemberControllerTest.java index 145c516..7d42c03 100644 --- a/src/test/java/com/member/controller/MemberControllerTest.java +++ b/src/test/java/com/member/controller/MemberControllerTest.java @@ -1,17 +1,11 @@ package com.member.controller; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.config.auth.AuthUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.member.dto.request.LoginRequest; -import com.member.dto.request.MemberSignUpRequest; +import com.member.dto.request.SignUpRequest; import com.member.dto.response.LoginResponse; -import com.member.dto.response.MemberSignUpResponse; +import com.member.dto.response.SignUpResponse; import com.member.service.MemberService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -20,6 +14,12 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @WebMvcTest(MemberController.class) class MemberControllerTest { @@ -35,10 +35,10 @@ class MemberControllerTest { @Test void signUp_Success() throws Exception { // Given - MemberSignUpRequest request = new MemberSignUpRequest("test@example.com", "nickname", "password123"); - MemberSignUpResponse response = new MemberSignUpResponse(1L, "test@example.com", "nickname"); + SignUpRequest request = new SignUpRequest("test@example.com", "nickname", "password123"); + SignUpResponse response = new SignUpResponse(1L, "test@example.com", "nickname"); - when(memberService.signUp(any(MemberSignUpRequest.class))).thenReturn(response); + when(memberService.signUp(any(SignUpRequest.class))).thenReturn(response); // When & Then mockMvc.perform(post("/members/signup") diff --git a/src/test/java/com/member/service/MemberServiceImplTest.java b/src/test/java/com/member/service/MemberServiceImplTest.java index e900598..ac929af 100644 --- a/src/test/java/com/member/service/MemberServiceImplTest.java +++ b/src/test/java/com/member/service/MemberServiceImplTest.java @@ -1,21 +1,14 @@ package com.member.service; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - import com.config.jwt.JwtUtil; import com.config.jwt.TokenWithExpiration; import com.exception.custom.SignUpException; import com.member.dto.request.LoginRequest; -import com.member.dto.request.MemberSignUpRequest; +import com.member.dto.request.SignUpRequest; import com.member.dto.response.LoginResponse; -import com.member.dto.response.MemberSignUpResponse; +import com.member.dto.response.SignUpResponse; import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -25,6 +18,12 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class MemberServiceImplTest { @@ -56,11 +55,11 @@ class SignUpTests { @DisplayName("회원가입 성공") void signUp_Success() { // Given - MemberSignUpRequest request = new MemberSignUpRequest("test@example.com", "1234", "testUser"); + SignUpRequest request = new SignUpRequest("test@example.com", "1234", "testUser"); when(memberRepository.save(any(MemberEntity.class))).thenReturn(member); // When - MemberSignUpResponse response = memberService.signUp(request); + SignUpResponse response = memberService.signUp(request); // Then assertNotNull(response); @@ -72,7 +71,7 @@ void signUp_Success() { @DisplayName("회원가입 중복 이메일 에러") void signUp_DuplicateEmail_ThrowsException() { // Given - MemberSignUpRequest request = new MemberSignUpRequest("test@example.com", "1234", "testUser"); + SignUpRequest request = new SignUpRequest("test@example.com", "1234", "testUser"); when(memberRepository.findByEmail(request.getEmail())).thenReturn(Optional.of(member)); // When & Then From ab235c4c97b1fa4eaf6461c9c4bc6561883c76fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Thu, 1 May 2025 17:11:16 +0900 Subject: [PATCH 13/45] =?UTF-8?q?refactor=20:=20findAll(=EC=97=AC=EB=9F=AC?= =?UTF-8?q?=EA=B0=9C)=20=EB=B0=98=ED=99=98=20=EC=8B=9C=20Page=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20DTO=EB=A1=9C=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20=20-=20=EA=B8=B0=EC=A1=B4=20Li?= =?UTF-8?q?st=20=EB=B0=98=ED=99=98=EC=97=90=EC=84=9C=20Page=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20DTO=EB=A1=9C=20=EB=B0=98=ED=99=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/ArticleController.java | 17 +++++------ .../board/controller/CommentController.java | 21 +++++--------- .../com/board/dto/response/PageResponse.java | 29 +++++++++++++++++++ .../ArticleControllerIntegrationTest.java | 6 ++-- .../controller/ArticleControllerTest.java | 8 ++--- 5 files changed, 50 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/board/dto/response/PageResponse.java diff --git a/src/main/java/com/board/controller/ArticleController.java b/src/main/java/com/board/controller/ArticleController.java index 55906bc..0f1f39d 100644 --- a/src/main/java/com/board/controller/ArticleController.java +++ b/src/main/java/com/board/controller/ArticleController.java @@ -3,19 +3,19 @@ import com.board.dto.request.ArticleCreateRequest; import com.board.dto.request.ArticleUpdateRequest; import com.board.dto.response.ArticleResponse; +import com.board.dto.response.PageResponse; import com.board.entity.ArticleEntity; import com.board.service.ArticleService; import com.config.auth.annotation.AuthenticatedMember; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RequiredArgsConstructor @RestController @RequestMapping("/articles") @@ -33,17 +33,14 @@ public ResponseEntity addArticle(@Valid @RequestBody ArticleCre } @GetMapping("") - public ResponseEntity> findAllArticles(@RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size) { + public ResponseEntity> findAllArticles(@RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { Pageable pageable = PageRequest.of(page, size); - - List articles = articleService.findAll(pageable) - .stream() - .map(ArticleResponse::withoutContent) - .toList(); + Page articlePage = articleService.findAll(pageable) + .map(ArticleResponse::withoutContent); return ResponseEntity.ok() - .body(articles); + .body(PageResponse.from(articlePage)); } @GetMapping("/{id}") diff --git a/src/main/java/com/board/controller/CommentController.java b/src/main/java/com/board/controller/CommentController.java index ced82e3..3bdbf52 100644 --- a/src/main/java/com/board/controller/CommentController.java +++ b/src/main/java/com/board/controller/CommentController.java @@ -3,6 +3,7 @@ import com.board.dto.request.CommentCreateRequest; import com.board.dto.request.CommentUpdateRequest; import com.board.dto.response.CommentResponse; +import com.board.dto.response.PageResponse; import com.board.entity.CommentEntity; import com.board.service.CommentService; import com.config.auth.annotation.AuthenticatedMember; @@ -13,15 +14,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @RestController @@ -31,14 +24,14 @@ public class CommentController { private final CommentService commentService; @GetMapping - public ResponseEntity> findAllComments(@RequestParam Long articleId, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size) { + public ResponseEntity> findAllComments(@RequestParam Long articleId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { Pageable pageable = PageRequest.of(page, size); - Page commentResponses = commentService.findAllComments(articleId, pageable) + Page commentPage = commentService.findAllComments(articleId, pageable) .map(CommentResponse::new); - return ResponseEntity.ok(commentResponses); + return ResponseEntity.ok(PageResponse.from(commentPage)); } @PostMapping diff --git a/src/main/java/com/board/dto/response/PageResponse.java b/src/main/java/com/board/dto/response/PageResponse.java new file mode 100644 index 0000000..ca67c47 --- /dev/null +++ b/src/main/java/com/board/dto/response/PageResponse.java @@ -0,0 +1,29 @@ +package com.board.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.data.domain.Page; + +import java.util.List; + +@AllArgsConstructor +@Getter +public class PageResponse { + private List content; + private int page; + private int size; + private long totalElements; + private int totalPages; + private boolean last; + + public static PageResponse from(Page page) { + return new PageResponse<>( + page.getContent(), + page.getNumber(), + page.getSize(), + page.getTotalElements(), + page.getTotalPages(), + page.isLast() + ); + } +} diff --git a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java index 07e78ce..9b4d061 100644 --- a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java @@ -125,9 +125,9 @@ void findAllArticleTest() throws Exception { mockMvc.perform(get("/articles").param("page", "0").param("size", "10")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.size()").value(2)) - .andExpect(jsonPath("$[0].title").value("Title 1")) - .andExpect(jsonPath("$[1].title").value("Title 2")); + .andExpect(jsonPath("$.content.size()").value(2)) + .andExpect(jsonPath("$.content[0].title").value("Title 1")) + .andExpect(jsonPath("$.content[1].title").value("Title 2")); } @Test diff --git a/src/test/java/com/board/controller/ArticleControllerTest.java b/src/test/java/com/board/controller/ArticleControllerTest.java index 6ba4d44..a5a9e81 100644 --- a/src/test/java/com/board/controller/ArticleControllerTest.java +++ b/src/test/java/com/board/controller/ArticleControllerTest.java @@ -107,10 +107,10 @@ void addArticle_Success() throws Exception { // When & Then mockMvc.perform(get("/articles")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(2)) - .andExpect(jsonPath("$[0].title").value("Title1")) - .andExpect(jsonPath("$[0].content").value(Matchers.nullValue())) - .andExpect(jsonPath("$[1].id").value(2L)); + .andExpect(jsonPath("$.content.length()").value(2)) + .andExpect(jsonPath("$.content[0].title").value("Title1")) + .andExpect(jsonPath("$.content[0].content").value(Matchers.nullValue())) + .andExpect(jsonPath("$.content[1].id").value(2L)); } @Test From 9ce6cf6acb6b9697e615e08b68a6ce8c84906f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Thu, 1 May 2025 17:23:11 +0900 Subject: [PATCH 14/45] =?UTF-8?q?refactor=20:=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=ED=8F=AC=ED=95=A8=20=EC=95=88=ED=95=98=EB=8A=94=20?= =?UTF-8?q?response?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 기본 생성자에서 builder 방식으로 변경 - Builder 방식을 통해 좀 더 명확하게 게시글 부분에 null이 들어가는것을 표현 --- .../board/dto/response/ArticleResponse.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/board/dto/response/ArticleResponse.java b/src/main/java/com/board/dto/response/ArticleResponse.java index 588ebc9..047d64f 100644 --- a/src/main/java/com/board/dto/response/ArticleResponse.java +++ b/src/main/java/com/board/dto/response/ArticleResponse.java @@ -1,13 +1,12 @@ package com.board.dto.response; import com.board.entity.ArticleEntity; -import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Getter -@AllArgsConstructor public class ArticleResponse { private final Long id; @@ -16,6 +15,15 @@ public class ArticleResponse { private final Long memberId; private final long viewCount; + @Builder + public ArticleResponse(Long id, String title, String content, Long memberId, long viewCount) { + this.id = id; + this.title = title; + this.content = content; + this.memberId = memberId; + this.viewCount = viewCount; + } + public ArticleResponse(ArticleEntity article) { this.id = article.getId(); this.title = article.getTitle(); @@ -25,12 +33,12 @@ public ArticleResponse(ArticleEntity article) { } public static ArticleResponse withoutContent(ArticleEntity article) { - return new ArticleResponse( - article.getId(), - article.getTitle(), - null, // content를 포함하지 않음 - article.getMember().getId(), - article.getViewCount() - ); + return ArticleResponse.builder() + .id(article.getId()) + .title(article.getTitle()) + .content(null) // content 포함 안함 + .memberId(article.getMember().getId()) + .viewCount(article.getViewCount()) + .build(); } } From 533e1600b57cacb16fc0a00b561799e5ce63db23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Thu, 1 May 2025 19:33:27 +0900 Subject: [PATCH 15/45] =?UTF-8?q?refactor=20:=20sort=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=EA=B0=92(=EC=B5=9C=EC=8B=A0=EC=88=9C=20=EC=A0=95=EB=A0=AC)?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=B4=EC=9D=B4=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/board/controller/ArticleController.java | 9 ++++++--- .../java/com/board/controller/CommentController.java | 8 +++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/board/controller/ArticleController.java b/src/main/java/com/board/controller/ArticleController.java index 0f1f39d..349d7e0 100644 --- a/src/main/java/com/board/controller/ArticleController.java +++ b/src/main/java/com/board/controller/ArticleController.java @@ -3,15 +3,16 @@ import com.board.dto.request.ArticleCreateRequest; import com.board.dto.request.ArticleUpdateRequest; import com.board.dto.response.ArticleResponse; -import com.board.dto.response.PageResponse; import com.board.entity.ArticleEntity; import com.board.service.ArticleService; import com.config.auth.annotation.AuthenticatedMember; +import com.util.page.PageResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -34,8 +35,10 @@ public ResponseEntity addArticle(@Valid @RequestBody ArticleCre @GetMapping("") public ResponseEntity> findAllArticles(@RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size) { - Pageable pageable = PageRequest.of(page, size); + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "createdAt") String sort) { + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, sort)); + Page articlePage = articleService.findAll(pageable) .map(ArticleResponse::withoutContent); diff --git a/src/main/java/com/board/controller/CommentController.java b/src/main/java/com/board/controller/CommentController.java index 3bdbf52..07f1756 100644 --- a/src/main/java/com/board/controller/CommentController.java +++ b/src/main/java/com/board/controller/CommentController.java @@ -3,15 +3,16 @@ import com.board.dto.request.CommentCreateRequest; import com.board.dto.request.CommentUpdateRequest; import com.board.dto.response.CommentResponse; -import com.board.dto.response.PageResponse; import com.board.entity.CommentEntity; import com.board.service.CommentService; import com.config.auth.annotation.AuthenticatedMember; +import com.util.page.PageResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -26,8 +27,9 @@ public class CommentController { @GetMapping public ResponseEntity> findAllComments(@RequestParam Long articleId, @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size) { - Pageable pageable = PageRequest.of(page, size); + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "createdAt") String sort) { + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, sort)); Page commentPage = commentService.findAllComments(articleId, pageable) .map(CommentResponse::new); From a61a1cc5f68ad616316db3367ecdae7456fe6960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Thu, 1 May 2025 19:34:58 +0900 Subject: [PATCH 16/45] =?UTF-8?q?refactor=20:=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80(soft=20delete)=20-=20?= =?UTF-8?q?=EC=8B=A4=EC=A0=9C=20=EC=82=AD=EC=A0=9C=EA=B0=80=20=EC=95=84?= =?UTF-8?q?=EB=8B=8C=20deleted=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20boolean=20?= =?UTF-8?q?=EA=B0=92=EC=9C=BC=EB=A1=9C=20soft=20delete=20=EC=83=81?= =?UTF-8?q?=ED=83=9C/=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/board/entity/ArticleEntity.java | 7 +++++ .../java/com/board/entity/CommentEntity.java | 17 ++++------- .../board/repository/ArticleRepository.java | 3 ++ .../board/repository/CommentRepository.java | 5 ++-- .../com/board/service/ArticleService.java | 12 ++++---- .../com/board/service/CommentService.java | 4 +-- .../ArticleControllerIntegrationTest.java | 29 ++++++++++++++++--- .../com/board/service/ArticleServiceTest.java | 12 ++++---- .../com/board/service/CommentServiceTest.java | 2 +- 9 files changed, 57 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/board/entity/ArticleEntity.java b/src/main/java/com/board/entity/ArticleEntity.java index b8ba17b..f512e07 100644 --- a/src/main/java/com/board/entity/ArticleEntity.java +++ b/src/main/java/com/board/entity/ArticleEntity.java @@ -36,6 +36,9 @@ public class ArticleEntity { @Column(nullable = false) private long viewCount = 0; + @Column(nullable = false) + private boolean deleted = false; + public ArticleEntity(String title, String content, MemberEntity member) { this.title = title; this.content = content; @@ -66,6 +69,10 @@ public void validateOwner(MemberEntity member) { } } + public void softDelete() { + this.deleted = true; + } + @PrePersist public void setCreatedAtNow() { this.createdAt = LocalDateTime.now(); diff --git a/src/main/java/com/board/entity/CommentEntity.java b/src/main/java/com/board/entity/CommentEntity.java index 40e76f7..e626828 100644 --- a/src/main/java/com/board/entity/CommentEntity.java +++ b/src/main/java/com/board/entity/CommentEntity.java @@ -2,20 +2,13 @@ import com.exception.custom.DifferentOwnerException; import com.member.entity.MemberEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.PrePersist; -import java.time.LocalDateTime; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @@ -41,7 +34,7 @@ public class CommentEntity { private LocalDateTime createdAt; @Column - private boolean deleted; + private boolean deleted = false; public CommentEntity(String content, ArticleEntity article, MemberEntity member) { this.content = content; @@ -59,7 +52,7 @@ public void update(String content) { this.content = content; } - public void delete() { + public void softDelete() { this.deleted = true; } diff --git a/src/main/java/com/board/repository/ArticleRepository.java b/src/main/java/com/board/repository/ArticleRepository.java index 8d19b27..ca16640 100644 --- a/src/main/java/com/board/repository/ArticleRepository.java +++ b/src/main/java/com/board/repository/ArticleRepository.java @@ -1,7 +1,10 @@ package com.board.repository; import com.board.entity.ArticleEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface ArticleRepository extends JpaRepository { + Page findAllByDeletedFalse(Pageable pageable); } diff --git a/src/main/java/com/board/repository/CommentRepository.java b/src/main/java/com/board/repository/CommentRepository.java index e9a9ac1..87be38f 100644 --- a/src/main/java/com/board/repository/CommentRepository.java +++ b/src/main/java/com/board/repository/CommentRepository.java @@ -1,15 +1,16 @@ package com.board.repository; import com.board.entity.CommentEntity; -import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface CommentRepository extends JpaRepository { Optional findByIdAndDeletedFalse(Long id); - Page findByArticleIdAndDeletedFalseOrderByCreatedAtDesc(Long articleId, Pageable pageable); + Page findByArticleIdAndDeletedFalse(Long articleId, Pageable pageable); } diff --git a/src/main/java/com/board/service/ArticleService.java b/src/main/java/com/board/service/ArticleService.java index 282032a..16d9bf2 100644 --- a/src/main/java/com/board/service/ArticleService.java +++ b/src/main/java/com/board/service/ArticleService.java @@ -31,7 +31,7 @@ public ArticleEntity save(ArticleCreateRequest request, Long memberId) { } public Page findAll(Pageable pageable) { - return articleRepository.findAll(pageable); + return articleRepository.findAllByDeletedFalse(pageable); } @Transactional @@ -43,21 +43,21 @@ public ArticleEntity findByIdAndIncreaseViewCount(long id) { @Transactional public void delete(long id, Long memberId) { - compareAuthors(id, findMemberById(memberId)); - articleRepository.deleteById(id); + getOwnedArticle(id, memberId).softDelete(); } @Transactional public ArticleEntity update(long id, Long memberId, ArticleUpdateRequest request) { - ArticleEntity article = findById(id); - compareAuthors(id, findMemberById(memberId)); + ArticleEntity article = getOwnedArticle(id, memberId); article.update(request.getTitle(), request.getContent()); return article; } - private void compareAuthors(long articleId, MemberEntity member) { + private ArticleEntity getOwnedArticle(long articleId, Long memberId) { ArticleEntity article = findById(articleId); + MemberEntity member = findMemberById(memberId); article.validateOwner(member); + return article; } public ArticleEntity findById(long id) { diff --git a/src/main/java/com/board/service/CommentService.java b/src/main/java/com/board/service/CommentService.java index 98f7502..ad3ca8d 100644 --- a/src/main/java/com/board/service/CommentService.java +++ b/src/main/java/com/board/service/CommentService.java @@ -24,7 +24,7 @@ public class CommentService { private final MemberService memberService; public Page findAllComments(Long articleId, Pageable pageable) { - return commentRepository.findByArticleIdAndDeletedFalseOrderByCreatedAtDesc(articleId, pageable); + return commentRepository.findByArticleIdAndDeletedFalse(articleId, pageable); } @Transactional @@ -45,7 +45,7 @@ public CommentEntity updateComment(CommentUpdateRequest request, Long memberId) @Transactional public void deleteComment(long commentId, Long memberId) { CommentEntity comment = findCommentAndValidateOwner(commentId, findMemberById(memberId)); - comment.delete(); + comment.softDelete(); } private CommentEntity findCommentAndValidateOwner(long commentId, MemberEntity member) { diff --git a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java index 9b4d061..2b6ba79 100644 --- a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java @@ -27,7 +27,6 @@ import org.springframework.transaction.annotation.Transactional; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -126,8 +125,28 @@ void findAllArticleTest() throws Exception { mockMvc.perform(get("/articles").param("page", "0").param("size", "10")) .andExpect(status().isOk()) .andExpect(jsonPath("$.content.size()").value(2)) - .andExpect(jsonPath("$.content[0].title").value("Title 1")) - .andExpect(jsonPath("$.content[1].title").value("Title 2")); + .andExpect(jsonPath("$.content[0].title").value("Title 2")) + .andExpect(jsonPath("$.content[1].title").value("Title 1")); + } + + @Test + @DisplayName("삭제된 게시물을 제외하고 전체 조회 테스트") + void findAllExcludingDeletedArticlesTest() throws Exception { + // Given: 게시물 3개 생성 + MemberEntity member = createAndSaveMember("bb@aa.com", "nickname"); + ArticleEntity article1 = articleRepository.save(new ArticleEntity("Title 1", "Content 1", member)); + ArticleEntity article2 = articleRepository.save(new ArticleEntity("Title 2", "Content 2", member)); + ArticleEntity article3 = articleRepository.save(new ArticleEntity("Title 3", "Content 3", member)); + + // 게시물 1개 삭제 + article3.softDelete(); + + // When: 전체 조회 요청 + mockMvc.perform(get("/articles").param("page", "0").param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content.size()").value(2)) // 삭제된 게시물 제외 + .andExpect(jsonPath("$.content[0].title").value("Title 2")) + .andExpect(jsonPath("$.content[1].title").value("Title 1")); } @Test @@ -177,7 +196,9 @@ void deleteArticleTest() throws Exception { mockMvc.perform(delete("/articles/" + articleId).cookie(new Cookie("token", tokenCookie))) .andExpect(status().isNoContent()); - assertFalse(articleRepository.findById(articleId).isPresent()); + ArticleEntity deletedArticle = articleRepository.findById(articleId) + .orElseThrow(() -> new AssertionError("삭제된 글을 찾을 수 없습니다.")); + assertTrue(deletedArticle.isDeleted()); } @Test diff --git a/src/test/java/com/board/service/ArticleServiceTest.java b/src/test/java/com/board/service/ArticleServiceTest.java index d284413..5af647b 100644 --- a/src/test/java/com/board/service/ArticleServiceTest.java +++ b/src/test/java/com/board/service/ArticleServiceTest.java @@ -24,7 +24,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class ArticleServiceTest { @@ -80,7 +80,7 @@ void findAllArticles_Success() { Pageable pageable = PageRequest.of(0, 10); Page mockPage = new PageImpl<>(Collections.emptyList()); - when(articleRepository.findAll(pageable)).thenReturn(mockPage); + when(articleRepository.findAllByDeletedFalse(pageable)).thenReturn(mockPage); // When Page result = articleService.findAll(pageable); @@ -125,19 +125,17 @@ void deleteArticle_Success() { Long memberId = 3L; Long articleId = 1L; String email = "test@example.com"; - MemberEntity member = new MemberEntity(email, "testUser", "nickName"); // ID 없이 생성 - ArticleEntity article = new ArticleEntity("title", "content", member); // ID 없이 생성 + MemberEntity member = new MemberEntity(email, "testUser", "nickName"); + ArticleEntity article = new ArticleEntity("title", "content", member); when(memberService.findById(any(Long.class))).thenReturn(member); when(articleRepository.findById(articleId)).thenReturn(Optional.of(article)); - doNothing().when(articleRepository).deleteById(articleId); - // When articleService.delete(articleId, memberId); // Then - verify(articleRepository, times(1)).deleteById(anyLong()); + assertTrue(article.isDeleted()); // softDelete() 호출 후 상태 확인 } diff --git a/src/test/java/com/board/service/CommentServiceTest.java b/src/test/java/com/board/service/CommentServiceTest.java index 5204dbc..dfe85d0 100644 --- a/src/test/java/com/board/service/CommentServiceTest.java +++ b/src/test/java/com/board/service/CommentServiceTest.java @@ -59,7 +59,7 @@ class CommentServiceTest { List commentEntityList = List.of(comment, comment2, comment3, comment4); Page commentPage = new PageImpl<>(commentEntityList, pageable, commentEntityList.size()); - when(commentRepository.findByArticleIdAndDeletedFalseOrderByCreatedAtDesc(anyLong(), any(Pageable.class))) + when(commentRepository.findByArticleIdAndDeletedFalse(anyLong(), any(Pageable.class))) .thenReturn(commentPage); // When From 08d082ef182cbcdef1cf19b0d1d732ab60e26468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Fri, 2 May 2025 09:47:25 +0900 Subject: [PATCH 17/45] =?UTF-8?q?refactor=20:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98(=ED=8C=A8=ED=82=A4=EC=A7=80)=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/{board/dto/response => util/page}/PageResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/com/{board/dto/response => util/page}/PageResponse.java (95%) diff --git a/src/main/java/com/board/dto/response/PageResponse.java b/src/main/java/com/util/page/PageResponse.java similarity index 95% rename from src/main/java/com/board/dto/response/PageResponse.java rename to src/main/java/com/util/page/PageResponse.java index ca67c47..13fdfd6 100644 --- a/src/main/java/com/board/dto/response/PageResponse.java +++ b/src/main/java/com/util/page/PageResponse.java @@ -1,4 +1,4 @@ -package com.board.dto.response; +package com.util.page; import lombok.AllArgsConstructor; import lombok.Getter; From f5f6699e45b79cd9ea3a548bf496d2e8a1b04de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Fri, 2 May 2025 09:50:22 +0900 Subject: [PATCH 18/45] =?UTF-8?q?feat=20:=20SortUtils=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1(=EB=82=B4=EC=9A=A9=20=EC=95=84?= =?UTF-8?q?=EC=A7=81=20=EB=AF=B8=EC=99=84=EC=84=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/util/sort/SortUtils.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/com/util/sort/SortUtils.java diff --git a/src/main/java/com/util/sort/SortUtils.java b/src/main/java/com/util/sort/SortUtils.java new file mode 100644 index 0000000..e0a635e --- /dev/null +++ b/src/main/java/com/util/sort/SortUtils.java @@ -0,0 +1,8 @@ +package com.util.sort; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SortUtils { + +} From e83c64e28574f65a497db4fc963d5bcb31b6c3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Fri, 2 May 2025 11:00:39 +0900 Subject: [PATCH 19/45] =?UTF-8?q?feat=20:=20=EA=B3=B5=ED=86=B5=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20util=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=99=84?= =?UTF-8?q?=EC=84=B1=20=20-=20=ED=98=84=EC=9E=AC=20=20=20-=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20:=20[=EC=B5=9C=EC=8B=A0=EC=88=9C,=20?= =?UTF-8?q?=EC=98=A4=EB=9E=98=EB=90=9C=EC=88=9C,=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=88=9C]=20=EC=A0=95=EB=A0=AC=20=EA=B0=80=EB=8A=A5=20=20=20-?= =?UTF-8?q?=20=EB=8C=93=EA=B8=80=20:=20[=EC=B5=9C=EC=8B=A0=EC=88=9C,=20?= =?UTF-8?q?=EC=98=A4=EB=9E=98=EB=90=9C=EC=88=9C]=20=EC=A0=95=EB=A0=AC=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/ArticleController.java | 16 +++- .../board/controller/CommentController.java | 16 +++- src/main/java/com/util/sort/SortUtils.java | 34 ++++++++- .../java/com/util/sort/SortUtilsTest.java | 75 +++++++++++++++++++ 4 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 src/test/java/com/util/sort/SortUtilsTest.java diff --git a/src/main/java/com/board/controller/ArticleController.java b/src/main/java/com/board/controller/ArticleController.java index 349d7e0..3458b63 100644 --- a/src/main/java/com/board/controller/ArticleController.java +++ b/src/main/java/com/board/controller/ArticleController.java @@ -7,15 +7,23 @@ import com.board.service.ArticleService; import com.config.auth.annotation.AuthenticatedMember; import com.util.page.PageResponse; +import com.util.sort.SortUtils; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @RestController @@ -36,8 +44,8 @@ public ResponseEntity addArticle(@Valid @RequestBody ArticleCre @GetMapping("") public ResponseEntity> findAllArticles(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, - @RequestParam(defaultValue = "createdAt") String sort) { - Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, sort)); + @RequestParam(defaultValue = "latest") String sort) { + Pageable pageable = PageRequest.of(page, size, SortUtils.getArticleSort(sort)); Page articlePage = articleService.findAll(pageable) .map(ArticleResponse::withoutContent); diff --git a/src/main/java/com/board/controller/CommentController.java b/src/main/java/com/board/controller/CommentController.java index 07f1756..f3f9dcc 100644 --- a/src/main/java/com/board/controller/CommentController.java +++ b/src/main/java/com/board/controller/CommentController.java @@ -7,15 +7,23 @@ import com.board.service.CommentService; import com.config.auth.annotation.AuthenticatedMember; import com.util.page.PageResponse; +import com.util.sort.SortUtils; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @RestController @@ -28,8 +36,8 @@ public class CommentController { public ResponseEntity> findAllComments(@RequestParam Long articleId, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, - @RequestParam(defaultValue = "createdAt") String sort) { - Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, sort)); + @RequestParam(defaultValue = "latest") String sort) { + Pageable pageable = PageRequest.of(page, size, SortUtils.getCommentSort(sort)); Page commentPage = commentService.findAllComments(articleId, pageable) .map(CommentResponse::new); diff --git a/src/main/java/com/util/sort/SortUtils.java b/src/main/java/com/util/sort/SortUtils.java index e0a635e..9fd98e4 100644 --- a/src/main/java/com/util/sort/SortUtils.java +++ b/src/main/java/com/util/sort/SortUtils.java @@ -1,8 +1,40 @@ package com.util.sort; +import java.util.Map; +import java.util.Set; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; @Slf4j public class SortUtils { - + + private static final Map SORT_ALIAS_MAP = Map.of( + "latest", Order.desc("createdAt"), + "oldest", Order.asc("createdAt"), + "views", Order.desc("viewCount") + ); + + private static final Set ARTICLE_SORT_FIELDS = Set.of("createdAt", "viewCount"); + private static final Set COMMENT_SORT_FIELDS = Set.of("createdAt"); + + public static Sort getArticleSort(String sortParam) { + return createSort(sortParam, ARTICLE_SORT_FIELDS, Order.desc("createdAt")); + } + + public static Sort getCommentSort(String sortParam) { + return createSort(sortParam, COMMENT_SORT_FIELDS, Order.desc("createdAt")); + } + + private static Sort createSort(String sortParam, Set allowedFields, Order defaultOrder) { + Order mappedOrder = SORT_ALIAS_MAP.getOrDefault(sortParam, defaultOrder); + + if (!allowedFields.contains(mappedOrder.getProperty())) { + log.warn("Unsupported sort field '{}'. Falling back to default '{}'", sortParam, + defaultOrder.getProperty()); + mappedOrder = defaultOrder; + } + + return Sort.by(mappedOrder); + } } diff --git a/src/test/java/com/util/sort/SortUtilsTest.java b/src/test/java/com/util/sort/SortUtilsTest.java new file mode 100644 index 0000000..33056aa --- /dev/null +++ b/src/test/java/com/util/sort/SortUtilsTest.java @@ -0,0 +1,75 @@ +package com.util.sort; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; + +class SortUtilsTest { + + @Nested + @DisplayName("게시글 정렬 테스트") + class ArticleSortTest { + + @Test + @DisplayName("정상적인_정렬_파라미터_입력_시_정상_반환") + void 정상적인_정렬_파라미터_입력_시_정상_반환() { + Sort latestSort = SortUtils.getArticleSort("latest"); + Sort viewsSort = SortUtils.getArticleSort("views"); + + assertAll( + () -> { + Order order = latestSort.getOrderFor("createdAt"); + assertThat(order).isNotNull(); + assertThat(order.getDirection()).isEqualTo(Sort.Direction.DESC); + }, + () -> { + Order order = viewsSort.getOrderFor("viewCount"); + assertThat(order).isNotNull(); + assertThat(order.getDirection()).isEqualTo(Sort.Direction.DESC); + } + ); + } + + @Test + @DisplayName("잘못된_정렬_파라미터_입력_시_createdAt_DESC_기본값_반환") + void 잘못된_정렬_파라미터_입력_시_default_반환() { + Sort sort = SortUtils.getArticleSort("invalidKey"); + + Order order = sort.getOrderFor("createdAt"); + + assertThat(order).isNotNull(); + assertThat(order.getDirection()).isEqualTo(Sort.Direction.DESC); + } + } + + @Nested + @DisplayName("댓글 정렬 테스트") + class CommentSortTest { + + @Test + @DisplayName("최신순_정렬_입력_시_createdAt_DESC_반환") + void 최신순_정렬_입력_시_createdAt_DESC_반환() { + Sort sort = SortUtils.getCommentSort("latest"); + Order order = sort.getOrderFor("createdAt"); + + assertThat(order).isNotNull(); + assertThat(order.getDirection()).isEqualTo(Sort.Direction.DESC); + } + + @Test + @DisplayName("지원되지_않는_정렬_입력_시_createdAt_DESC_기본값_반환") + void 지원되지_않는_정렬_입력_시_createdAt_DESC_반환() { + Sort sort = SortUtils.getCommentSort("views"); // 댓글은 viewCount 허용 안됨 + + Order order = sort.getOrderFor("createdAt"); + + assertThat(order).isNotNull(); + assertThat(order.getDirection()).isEqualTo(Sort.Direction.DESC); + } + } +} From 2145079a642311535834421f90d56d9ea48b2013 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Fri, 2 May 2025 11:19:05 +0900 Subject: [PATCH 20/45] =?UTF-8?q?refactor=20:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20=EB=8D=94=20?= =?UTF-8?q?=EC=9E=90=EC=84=B8=ED=95=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/exception/custom/LoginException.java | 20 +++++++++++++++ .../com/member/service/MemberServiceImpl.java | 3 ++- .../member/service/MemberServiceImplTest.java | 25 ++++++++++++++----- 3 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/exception/custom/LoginException.java diff --git a/src/main/java/com/exception/custom/LoginException.java b/src/main/java/com/exception/custom/LoginException.java new file mode 100644 index 0000000..e28c29e --- /dev/null +++ b/src/main/java/com/exception/custom/LoginException.java @@ -0,0 +1,20 @@ +package com.exception.custom; + +import com.exception.CustomException; +import com.exception.ErrorCodeType; +import lombok.Getter; + +@Getter +public class LoginException extends CustomException { + + private final String errorMessage; + + private LoginException(String errorMessage) { + super(ErrorCodeType.ENTITY_NOT_FOUND); + this.errorMessage = errorMessage; + } + + public static LoginException from(String errorMessage) { + return new LoginException(errorMessage); + } +} diff --git a/src/main/java/com/member/service/MemberServiceImpl.java b/src/main/java/com/member/service/MemberServiceImpl.java index 2a94a97..2fbc32b 100644 --- a/src/main/java/com/member/service/MemberServiceImpl.java +++ b/src/main/java/com/member/service/MemberServiceImpl.java @@ -3,6 +3,7 @@ import com.config.jwt.JwtUtil; import com.config.jwt.TokenWithExpiration; import com.exception.custom.EmailNotFoundException; +import com.exception.custom.LoginException; import com.exception.custom.MyEntityNotFoundException; import com.exception.custom.SignUpException; import com.member.domain.Member; @@ -53,7 +54,7 @@ private void validateDuplicate(SignUpRequest request) { @Override public LoginResponse login(LoginRequest request) { MemberEntity memberEntity = memberRepository.findByEmail(request.getEmail()) - .orElseThrow(() -> new IllegalArgumentException(ErrorMessage.NOT_CORRECT_LOGIN)); + .orElseThrow(() -> LoginException.from(ErrorMessage.NOT_CORRECT_LOGIN)); Member member = new Member(memberEntity); member.checkPassword(request.getPassword()); diff --git a/src/test/java/com/member/service/MemberServiceImplTest.java b/src/test/java/com/member/service/MemberServiceImplTest.java index ac929af..cbe145b 100644 --- a/src/test/java/com/member/service/MemberServiceImplTest.java +++ b/src/test/java/com/member/service/MemberServiceImplTest.java @@ -1,7 +1,14 @@ package com.member.service; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + import com.config.jwt.JwtUtil; import com.config.jwt.TokenWithExpiration; +import com.exception.custom.LoginException; import com.exception.custom.SignUpException; import com.member.dto.request.LoginRequest; import com.member.dto.request.SignUpRequest; @@ -9,6 +16,7 @@ import com.member.dto.response.SignUpResponse; import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -18,12 +26,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) class MemberServiceImplTest { @@ -109,6 +111,17 @@ void login_Fail_WrongPassword() { // When & Then assertThrows(IllegalArgumentException.class, () -> memberService.login(request)); } + + @Test + @DisplayName("로그인 실패 - 이메일이 존재하지 않음") + void login_Fail_EmailNotFound() { + // Given + LoginRequest request = new LoginRequest("nonexistent@example.com", "1234"); + when(memberRepository.findByEmail(request.getEmail())).thenReturn(Optional.empty()); + + // When & Then + assertThrows(LoginException.class, () -> memberService.login(request)); + } } From 9410b16b473b5d8c410e67445afa9e77ea974e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Fri, 2 May 2025 16:54:44 +0900 Subject: [PATCH 21/45] =?UTF-8?q?feat=20:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20/=20=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=20-=20soft=20delete?= =?UTF-8?q?=EB=A5=BC=20=ED=86=B5=ED=95=B4=20deleted=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EB=A1=9C=20=EA=B4=80=EB=A6=AC=20=20-=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=ED=83=88=ED=87=B4=20=EA=B2=BD=EC=9A=B0(soft=20delete)=20?= =?UTF-8?q?=EA=B0=92=EC=9D=80=20db=EC=97=90=20=EB=B3=B4=EA=B4=80=EB=90=98?= =?UTF-8?q?=EA=B8=B0=20=EB=95=8C=EB=AC=B8=EC=97=90=20=ED=95=B4=EB=8B=B9=20?= =?UTF-8?q?id/nickName=EC=99=80=20=EC=A4=91=EB=B3=B5=EB=90=9C=20=EA=B0=92?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=EB=8A=94=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=B6=88=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/config/filter/FilterConfig.java | 6 +- .../com/exception/custom/LoginException.java | 6 ++ .../member/controller/MemberController.java | 30 +++++- .../java/com/member/entity/MemberEntity.java | 7 ++ .../member/repository/MemberRepository.java | 2 + .../com/member/service/MemberService.java | 4 + .../com/member/service/MemberServiceImpl.java | 12 ++- .../java/com/util/cookie/CookieUtils.java | 22 +++++ .../controller/ArticleControllerTest.java | 18 ++-- .../MemberControllerIntegrationTest.java | 98 ++++++++++++++++++- .../controller/MemberControllerTest.java | 55 +++++++++-- .../member/service/MemberServiceImplTest.java | 6 +- 12 files changed, 238 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/util/cookie/CookieUtils.java diff --git a/src/main/java/com/config/filter/FilterConfig.java b/src/main/java/com/config/filter/FilterConfig.java index 7f0f45e..c89f810 100644 --- a/src/main/java/com/config/filter/FilterConfig.java +++ b/src/main/java/com/config/filter/FilterConfig.java @@ -19,8 +19,10 @@ public JwtAuthFilter jwtAuthFilter(JwtUtil jwtUtil, AuthUtil authUtil) { public FilterRegistrationBean jwtAuthFilterRegistration(JwtAuthFilter jwtAuthFilter) { FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(jwtAuthFilter); - registrationBean.addUrlPatterns("/articles", "/articles/*", - "/comments", "/comments/*"); // 특정 URL 패턴에만 필터 적용 + registrationBean.addUrlPatterns( + "/articles", "/articles/*", + "/comments", "/comments/*", + "/members/logout", "/members/withdraw"); // 특정 URL 패턴에만 필터 적용 registrationBean.setOrder(1); // 우선순위 설정 (낮을수록 먼저 실행) return registrationBean; } diff --git a/src/main/java/com/exception/custom/LoginException.java b/src/main/java/com/exception/custom/LoginException.java index e28c29e..2251a8a 100644 --- a/src/main/java/com/exception/custom/LoginException.java +++ b/src/main/java/com/exception/custom/LoginException.java @@ -2,6 +2,7 @@ import com.exception.CustomException; import com.exception.ErrorCodeType; +import java.util.Map; import lombok.Getter; @Getter @@ -14,6 +15,11 @@ private LoginException(String errorMessage) { this.errorMessage = errorMessage; } + @Override + public Map getAdditionalDetails() { + return Map.of("errorMessage", errorMessage); + } + public static LoginException from(String errorMessage) { return new LoginException(errorMessage); } diff --git a/src/main/java/com/member/controller/MemberController.java b/src/main/java/com/member/controller/MemberController.java index 745f19b..d1b42ae 100644 --- a/src/main/java/com/member/controller/MemberController.java +++ b/src/main/java/com/member/controller/MemberController.java @@ -1,15 +1,18 @@ package com.member.controller; +import com.config.auth.annotation.AuthenticatedMember; import com.member.dto.request.LoginRequest; import com.member.dto.request.SignUpRequest; import com.member.dto.response.LoginResponse; import com.member.dto.response.SignUpResponse; import com.member.service.MemberService; +import com.util.cookie.CookieUtils; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -33,12 +36,31 @@ public ResponseEntity login(@Valid @RequestBody LoginRequest logi HttpServletResponse response) { LoginResponse loginResponse = memberService.login(loginRequest); - Cookie cookie = new Cookie("token", loginResponse.getAccessToken()); - cookie.setHttpOnly(true); - cookie.setPath("/"); - cookie.setMaxAge((int) loginResponse.getExpirationTime()); + Cookie cookie = CookieUtils.createCookie("token", + loginResponse.getAccessToken(), + (int) loginResponse.getExpirationTime()); response.addCookie(cookie); return ResponseEntity.ok(loginResponse); } + + @PostMapping("/logout") + public ResponseEntity logout(@AuthenticatedMember Long memberId, + HttpServletResponse response) { + memberService.logout(memberId); + + Cookie cookie = CookieUtils.invalidateCookie("token"); + response.addCookie(cookie); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/withdraw") + public ResponseEntity withdraw(@AuthenticatedMember Long memberId, + HttpServletResponse response) { + memberService.withdraw(memberId); + + Cookie cookie = CookieUtils.invalidateCookie("token"); + response.addCookie(cookie); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/com/member/entity/MemberEntity.java b/src/main/java/com/member/entity/MemberEntity.java index 671b626..58b377e 100644 --- a/src/main/java/com/member/entity/MemberEntity.java +++ b/src/main/java/com/member/entity/MemberEntity.java @@ -36,6 +36,9 @@ public class MemberEntity { @Column(nullable = false) private LocalDateTime createdAt; + @Column(nullable = false) + private boolean deleted = false; + @Builder public MemberEntity(String email, String password, String nickName) { this.email = email; @@ -50,6 +53,10 @@ public MemberEntity(Long id, String email, String password, String nickName) { this.nickName = nickName; } + public void softDelete() { + this.deleted = true; + } + @PrePersist public void setCreatedAtNow() { this.createdAt = LocalDateTime.now(); diff --git a/src/main/java/com/member/repository/MemberRepository.java b/src/main/java/com/member/repository/MemberRepository.java index 76451ef..bf3641f 100644 --- a/src/main/java/com/member/repository/MemberRepository.java +++ b/src/main/java/com/member/repository/MemberRepository.java @@ -9,4 +9,6 @@ public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); Optional findByNickName(String nickName); + + Optional findByEmailAndDeletedFalse(String email); } diff --git a/src/main/java/com/member/service/MemberService.java b/src/main/java/com/member/service/MemberService.java index 7269fac..0395a9a 100644 --- a/src/main/java/com/member/service/MemberService.java +++ b/src/main/java/com/member/service/MemberService.java @@ -16,4 +16,8 @@ public interface MemberService { MemberEntity findById(Long id); + void logout(Long memberId); + + void withdraw(Long memberId); + } diff --git a/src/main/java/com/member/service/MemberServiceImpl.java b/src/main/java/com/member/service/MemberServiceImpl.java index 2fbc32b..bf7fe03 100644 --- a/src/main/java/com/member/service/MemberServiceImpl.java +++ b/src/main/java/com/member/service/MemberServiceImpl.java @@ -15,11 +15,13 @@ import com.member.message.ErrorMessage; import com.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service +@Slf4j @Transactional(readOnly = true) public class MemberServiceImpl implements MemberService { @@ -53,7 +55,7 @@ private void validateDuplicate(SignUpRequest request) { @Override public LoginResponse login(LoginRequest request) { - MemberEntity memberEntity = memberRepository.findByEmail(request.getEmail()) + MemberEntity memberEntity = memberRepository.findByEmailAndDeletedFalse(request.getEmail()) .orElseThrow(() -> LoginException.from(ErrorMessage.NOT_CORRECT_LOGIN)); Member member = new Member(memberEntity); @@ -76,5 +78,13 @@ public MemberEntity findById(Long id) { .orElseThrow(() -> MyEntityNotFoundException.from(id)); } + public void logout(Long memberId) { + log.info("회원 {} 로그아웃함", memberId); + } + @Transactional + public void withdraw(Long memberId) { + MemberEntity member = findById(memberId); + member.softDelete(); + } } diff --git a/src/main/java/com/util/cookie/CookieUtils.java b/src/main/java/com/util/cookie/CookieUtils.java new file mode 100644 index 0000000..4acdee1 --- /dev/null +++ b/src/main/java/com/util/cookie/CookieUtils.java @@ -0,0 +1,22 @@ +package com.util.cookie; + +import jakarta.servlet.http.Cookie; + +public class CookieUtils { + + public static Cookie createCookie(String name, String value, int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setHttpOnly(true); + cookie.setPath("/"); + cookie.setMaxAge(maxAge); + return cookie; + } + + public static Cookie invalidateCookie(String name) { + Cookie cookie = new Cookie(name, null); + cookie.setHttpOnly(true); + cookie.setPath("/"); + cookie.setMaxAge(0); // 쿠키 삭제 + return cookie; + } +} diff --git a/src/test/java/com/board/controller/ArticleControllerTest.java b/src/test/java/com/board/controller/ArticleControllerTest.java index a5a9e81..7a6399e 100644 --- a/src/test/java/com/board/controller/ArticleControllerTest.java +++ b/src/test/java/com/board/controller/ArticleControllerTest.java @@ -1,5 +1,14 @@ package com.board.controller; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.board.dto.request.ArticleCreateRequest; import com.board.dto.request.ArticleUpdateRequest; import com.board.dto.response.ArticleResponse; @@ -9,6 +18,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.member.entity.MemberEntity; import com.member.service.MemberService; +import java.util.List; import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -20,14 +30,6 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -import java.util.List; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @WebMvcTest(ArticleController.class) class ArticleControllerTest { diff --git a/src/test/java/com/member/controller/MemberControllerIntegrationTest.java b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java index d4fc1e3..3656c4f 100644 --- a/src/test/java/com/member/controller/MemberControllerIntegrationTest.java +++ b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java @@ -1,11 +1,20 @@ package com.member.controller; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.board.dto.request.ArticleCreateRequest; import com.config.jwt.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.member.dto.request.LoginRequest; import com.member.dto.request.SignUpRequest; import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; +import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -18,10 +27,6 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.transaction.annotation.Transactional; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - @SpringBootTest @AutoConfigureMockMvc @Transactional @@ -106,4 +111,89 @@ void signUpTest() throws Exception { .andExpect(jsonPath("$.nickName").value(testNickName)); } + @Test + @DisplayName("로그아웃_요청시_쿠키가_만료된다") + void 로그아웃_요청시_쿠키가_만료된다() throws Exception { + // Given + String token = getAccessToken(); + + // When: 로그아웃 요청 + mockMvc.perform(post("/members/logout") + .header("Authorization", "Bearer " + token)) + .andExpect(status().isNoContent()) + .andExpect(cookie().maxAge("token", 0)); // Then: 쿠키 만료 확인 + } + + @Test + @DisplayName("회원탈퇴_요청시_쿠키가_만료되고_재로그인이_불가능하다") + void 회원탈퇴_후_재로그인_불가() throws Exception { + // Given + String token = getAccessToken(); + + // When: 회원 탈퇴 요청 + mockMvc.perform(delete("/members/withdraw") + .cookie(new Cookie("token", token))) + .andExpect(status().isNoContent()) + .andExpect(cookie().maxAge("token", 0)); + + // Then: 로그인 실패 + LoginRequest loginRequest = new LoginRequest(setUpMemberEmail, setUpMemberPassword); + String loginJson = objectMapper.writeValueAsString(loginRequest); + + mockMvc.perform(post("/members/login") + .contentType(MediaType.APPLICATION_JSON) + .content(loginJson)) + .andExpect(jsonPath("$.statusCode").value(404)); + } + + @Test + @DisplayName("로그아웃_후_인증이_필요한_요청시_거부된다") + void 로그아웃_후_인증요청_실패() throws Exception { + // Given + String token = getAccessToken(); + + // 로그아웃 + mockMvc.perform(post("/members/logout") + .cookie(new Cookie("token", token))) + .andExpect(status().isNoContent()) + .andExpect(cookie().maxAge("token", 0)); // 쿠키가 만료되었는지 확인 + + // 이후 요청 시 → 쿠키 없음 (즉, 인증 실패 유도) + mockMvc.perform(post("/articles") // 인증 필요한 API + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleCreateRequest("제목", "내용")))) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("회원탈퇴_후_인증이_필요한_요청시_거부된다") + void 회원탈퇴_후_인증요청_실패() throws Exception { + // Given: 로그인 후 JWT 토큰 발급 + String token = getAccessToken(); + + // When: 회원 탈퇴 요청 + mockMvc.perform(delete("/members/withdraw") + .cookie(new Cookie("token", token))) + .andExpect(status().isNoContent()); + + // Then: 인증이 필요한 요청 시 실패 + mockMvc.perform(post("/articles") // 인증 필요한 API + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleCreateRequest("제목", "내용")))) + .andExpect(status().isUnauthorized()); + } + + private String getAccessToken() throws Exception { + LoginRequest loginRequest = new LoginRequest(setUpMemberEmail, setUpMemberPassword); + String loginJson = objectMapper.writeValueAsString(loginRequest); + + MvcResult result = mockMvc.perform(post("/members/login") + .contentType(MediaType.APPLICATION_JSON) + .content(loginJson)) + .andExpect(status().isOk()) + .andReturn(); + + return result.getResponse().getCookie("token").getValue(); // JWT 토큰 쿠키 값 반환 + } + } diff --git a/src/test/java/com/member/controller/MemberControllerTest.java b/src/test/java/com/member/controller/MemberControllerTest.java index 7d42c03..865d179 100644 --- a/src/test/java/com/member/controller/MemberControllerTest.java +++ b/src/test/java/com/member/controller/MemberControllerTest.java @@ -1,12 +1,22 @@ package com.member.controller; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.config.auth.AuthUtil; +import com.config.auth.AuthenticatedMemberArgumentResolver; import com.fasterxml.jackson.databind.ObjectMapper; import com.member.dto.request.LoginRequest; import com.member.dto.request.SignUpRequest; import com.member.dto.response.LoginResponse; import com.member.dto.response.SignUpResponse; import com.member.service.MemberService; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -14,12 +24,6 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @WebMvcTest(MemberController.class) class MemberControllerTest { @@ -32,6 +36,19 @@ class MemberControllerTest { @MockitoBean private MemberService memberService; + @MockitoBean + private AuthenticatedMemberArgumentResolver authenticatedMemberArgumentResolver; + + @Autowired + private ObjectMapper objectMapper; + + @BeforeEach + void setup() throws Exception { + when(authenticatedMemberArgumentResolver.supportsParameter(any())).thenReturn(true); + when(authenticatedMemberArgumentResolver.resolveArgument(any(), any(), any(), any())) + .thenReturn(1L); // Long memberId 주입 + } + @Test void signUp_Success() throws Exception { // Given @@ -65,4 +82,30 @@ void login_Success() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.accessToken").value("mockAccessToken")); } + + @Test + public void logout_ShouldCallMemberServiceLogout() throws Exception { + // Given + Long memberId = 1L; + + // When: 로그아웃 요청 + mockMvc.perform(post("/members/logout")) + .andExpect(status().isNoContent()); + + // Then: MemberService의 logout 호출 확인 + verify(memberService).logout(memberId); + } + + @Test + public void withdraw_ShouldCallMemberServiceWithdraw() throws Exception { + // Given + Long memberId = 1L; + + // When: 회원탈퇴 요청 + mockMvc.perform(delete("/members/withdraw")) + .andExpect(status().isNoContent()); + + // Then: MemberService의 withdraw 호출 확인 + verify(memberService).withdraw(memberId); + } } diff --git a/src/test/java/com/member/service/MemberServiceImplTest.java b/src/test/java/com/member/service/MemberServiceImplTest.java index cbe145b..3dae8aa 100644 --- a/src/test/java/com/member/service/MemberServiceImplTest.java +++ b/src/test/java/com/member/service/MemberServiceImplTest.java @@ -89,7 +89,7 @@ class LoginTests { void login_Success() { // Given LoginRequest request = new LoginRequest("test@example.com", "1234"); - when(memberRepository.findByEmail(request.getEmail())).thenReturn(Optional.of(member)); + when(memberRepository.findByEmailAndDeletedFalse(request.getEmail())).thenReturn(Optional.of(member)); when(jwtUtil.generateTokenWithExpiration(any(String.class))) .thenReturn(new TokenWithExpiration("token", 3600000L)); @@ -106,7 +106,7 @@ void login_Success() { void login_Fail_WrongPassword() { // Given LoginRequest request = new LoginRequest("test@example.com", "wrongPassword"); - when(memberRepository.findByEmail(request.getEmail())).thenReturn(Optional.of(member)); + when(memberRepository.findByEmailAndDeletedFalse(request.getEmail())).thenReturn(Optional.of(member)); // When & Then assertThrows(IllegalArgumentException.class, () -> memberService.login(request)); @@ -117,7 +117,7 @@ void login_Fail_WrongPassword() { void login_Fail_EmailNotFound() { // Given LoginRequest request = new LoginRequest("nonexistent@example.com", "1234"); - when(memberRepository.findByEmail(request.getEmail())).thenReturn(Optional.empty()); + when(memberRepository.findByEmailAndDeletedFalse(request.getEmail())).thenReturn(Optional.empty()); // When & Then assertThrows(LoginException.class, () -> memberService.login(request)); From 4c253f58fcadd53d2ea19f93c868bf00c5590219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Wed, 7 May 2025 21:13:45 +0900 Subject: [PATCH 22/45] =?UTF-8?q?refactor=20:=20null=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EC=97=90=20=EB=AA=85=EC=8B=9C=ED=95=9C=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/board/dto/response/ArticleResponse.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/board/dto/response/ArticleResponse.java b/src/main/java/com/board/dto/response/ArticleResponse.java index 047d64f..ca42d8d 100644 --- a/src/main/java/com/board/dto/response/ArticleResponse.java +++ b/src/main/java/com/board/dto/response/ArticleResponse.java @@ -33,12 +33,9 @@ public ArticleResponse(ArticleEntity article) { } public static ArticleResponse withoutContent(ArticleEntity article) { - return ArticleResponse.builder() - .id(article.getId()) - .title(article.getTitle()) - .content(null) // content 포함 안함 - .memberId(article.getMember().getId()) - .viewCount(article.getViewCount()) - .build(); + return new ArticleResponse(article.getId(), + article.getTitle(), + article.getMember().getId(), + article.getViewCount()); } } From 0a33f9f7591b4b3a5cf0a2bd58647b5c2c9c141d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Wed, 7 May 2025 21:17:29 +0900 Subject: [PATCH 23/45] =?UTF-8?q?refactor=20:=20boolean=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=EB=AA=85=20=EC=95=9E=EC=97=90=20is=20=EC=A0=91?= =?UTF-8?q?=EB=91=90=EC=82=AC=20=EB=B6=99=EC=97=AC=20=EB=AA=85=EC=8B=9C?= =?UTF-8?q?=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/board/dto/response/CommentResponse.java | 7 ++++--- src/main/java/com/board/entity/ArticleEntity.java | 4 ++-- src/main/java/com/board/entity/CommentEntity.java | 4 ++-- src/main/java/com/member/entity/MemberEntity.java | 15 +++++---------- src/main/java/com/util/page/PageResponse.java | 2 +- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/board/dto/response/CommentResponse.java b/src/main/java/com/board/dto/response/CommentResponse.java index d595d9b..56606a0 100644 --- a/src/main/java/com/board/dto/response/CommentResponse.java +++ b/src/main/java/com/board/dto/response/CommentResponse.java @@ -1,10 +1,11 @@ package com.board.dto.response; import com.board.entity.CommentEntity; -import java.time.LocalDateTime; import lombok.Getter; import lombok.RequiredArgsConstructor; +import java.time.LocalDateTime; + @RequiredArgsConstructor @Getter public class CommentResponse { @@ -14,7 +15,7 @@ public class CommentResponse { private final Long authorId; private final String authorName; private final LocalDateTime createdAt; - private final boolean deleted; + private final boolean isDeleted; public CommentResponse(CommentEntity comment) { this.id = comment.getId(); @@ -22,6 +23,6 @@ public CommentResponse(CommentEntity comment) { this.authorId = comment.getMember().getId(); this.authorName = comment.getMember().getNickName(); this.createdAt = comment.getCreatedAt(); - this.deleted = comment.isDeleted(); + this.isDeleted = comment.isDeleted(); } } diff --git a/src/main/java/com/board/entity/ArticleEntity.java b/src/main/java/com/board/entity/ArticleEntity.java index f512e07..e1a9615 100644 --- a/src/main/java/com/board/entity/ArticleEntity.java +++ b/src/main/java/com/board/entity/ArticleEntity.java @@ -37,7 +37,7 @@ public class ArticleEntity { private long viewCount = 0; @Column(nullable = false) - private boolean deleted = false; + private boolean isDeleted = false; public ArticleEntity(String title, String content, MemberEntity member) { this.title = title; @@ -70,7 +70,7 @@ public void validateOwner(MemberEntity member) { } public void softDelete() { - this.deleted = true; + this.isDeleted = true; } @PrePersist diff --git a/src/main/java/com/board/entity/CommentEntity.java b/src/main/java/com/board/entity/CommentEntity.java index e626828..c34fd87 100644 --- a/src/main/java/com/board/entity/CommentEntity.java +++ b/src/main/java/com/board/entity/CommentEntity.java @@ -34,7 +34,7 @@ public class CommentEntity { private LocalDateTime createdAt; @Column - private boolean deleted = false; + private boolean isDeleted = false; public CommentEntity(String content, ArticleEntity article, MemberEntity member) { this.content = content; @@ -53,7 +53,7 @@ public void update(String content) { } public void softDelete() { - this.deleted = true; + this.isDeleted = true; } @PrePersist diff --git a/src/main/java/com/member/entity/MemberEntity.java b/src/main/java/com/member/entity/MemberEntity.java index 58b377e..5a9ca24 100644 --- a/src/main/java/com/member/entity/MemberEntity.java +++ b/src/main/java/com/member/entity/MemberEntity.java @@ -1,18 +1,13 @@ package com.member.entity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.PrePersist; -import jakarta.persistence.Table; -import java.time.LocalDateTime; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @@ -37,7 +32,7 @@ public class MemberEntity { private LocalDateTime createdAt; @Column(nullable = false) - private boolean deleted = false; + private boolean isDeleted = false; @Builder public MemberEntity(String email, String password, String nickName) { @@ -54,7 +49,7 @@ public MemberEntity(Long id, String email, String password, String nickName) { } public void softDelete() { - this.deleted = true; + this.isDeleted = true; } @PrePersist diff --git a/src/main/java/com/util/page/PageResponse.java b/src/main/java/com/util/page/PageResponse.java index 13fdfd6..34290b6 100644 --- a/src/main/java/com/util/page/PageResponse.java +++ b/src/main/java/com/util/page/PageResponse.java @@ -14,7 +14,7 @@ public class PageResponse { private int size; private long totalElements; private int totalPages; - private boolean last; + private boolean isLast; public static PageResponse from(Page page) { return new PageResponse<>( From 0eccefbe3a8ec95c3e711e91aaf7bf983197924f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Thu, 8 May 2025 19:50:36 +0900 Subject: [PATCH 24/45] =?UTF-8?q?refactor=20:=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85=20deleted=20>=20isDeleted=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=96=88=EC=9C=BC=EB=AF=80=EB=A1=9C=20repository=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EB=AA=85=EB=8F=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/repository/ArticleRepository.java | 2 +- .../board/repository/CommentRepository.java | 4 ++-- .../com/board/service/ArticleService.java | 2 +- .../com/board/service/CommentService.java | 4 ++-- .../member/repository/MemberRepository.java | 2 +- .../com/member/service/MemberServiceImpl.java | 2 +- .../com/board/service/ArticleServiceTest.java | 2 +- .../com/board/service/CommentServiceTest.java | 8 ++++---- .../member/service/MemberServiceImplTest.java | 19 +++++++++---------- 9 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/board/repository/ArticleRepository.java b/src/main/java/com/board/repository/ArticleRepository.java index ca16640..d282152 100644 --- a/src/main/java/com/board/repository/ArticleRepository.java +++ b/src/main/java/com/board/repository/ArticleRepository.java @@ -6,5 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface ArticleRepository extends JpaRepository { - Page findAllByDeletedFalse(Pageable pageable); + Page findAllByIsDeletedFalse(Pageable pageable); } diff --git a/src/main/java/com/board/repository/CommentRepository.java b/src/main/java/com/board/repository/CommentRepository.java index 87be38f..5c4f7fb 100644 --- a/src/main/java/com/board/repository/CommentRepository.java +++ b/src/main/java/com/board/repository/CommentRepository.java @@ -9,8 +9,8 @@ public interface CommentRepository extends JpaRepository { - Optional findByIdAndDeletedFalse(Long id); + Optional findByIdAndIsDeletedFalse(Long id); - Page findByArticleIdAndDeletedFalse(Long articleId, Pageable pageable); + Page findByArticleIdAndIsDeletedFalse(Long articleId, Pageable pageable); } diff --git a/src/main/java/com/board/service/ArticleService.java b/src/main/java/com/board/service/ArticleService.java index 16d9bf2..b016f39 100644 --- a/src/main/java/com/board/service/ArticleService.java +++ b/src/main/java/com/board/service/ArticleService.java @@ -31,7 +31,7 @@ public ArticleEntity save(ArticleCreateRequest request, Long memberId) { } public Page findAll(Pageable pageable) { - return articleRepository.findAllByDeletedFalse(pageable); + return articleRepository.findAllByIsDeletedFalse(pageable); } @Transactional diff --git a/src/main/java/com/board/service/CommentService.java b/src/main/java/com/board/service/CommentService.java index ad3ca8d..25ac69b 100644 --- a/src/main/java/com/board/service/CommentService.java +++ b/src/main/java/com/board/service/CommentService.java @@ -24,7 +24,7 @@ public class CommentService { private final MemberService memberService; public Page findAllComments(Long articleId, Pageable pageable) { - return commentRepository.findByArticleIdAndDeletedFalse(articleId, pageable); + return commentRepository.findByArticleIdAndIsDeletedFalse(articleId, pageable); } @Transactional @@ -55,7 +55,7 @@ private CommentEntity findCommentAndValidateOwner(long commentId, MemberEntity m } private CommentEntity findComment(long id) { - return commentRepository.findByIdAndDeletedFalse(id) + return commentRepository.findByIdAndIsDeletedFalse(id) .orElseThrow(() -> MyEntityNotFoundException.from(id)); } diff --git a/src/main/java/com/member/repository/MemberRepository.java b/src/main/java/com/member/repository/MemberRepository.java index bf3641f..90a4ad1 100644 --- a/src/main/java/com/member/repository/MemberRepository.java +++ b/src/main/java/com/member/repository/MemberRepository.java @@ -10,5 +10,5 @@ public interface MemberRepository extends JpaRepository { Optional findByNickName(String nickName); - Optional findByEmailAndDeletedFalse(String email); + Optional findByEmailAndIsDeletedFalse(String email); } diff --git a/src/main/java/com/member/service/MemberServiceImpl.java b/src/main/java/com/member/service/MemberServiceImpl.java index bf7fe03..cff28ee 100644 --- a/src/main/java/com/member/service/MemberServiceImpl.java +++ b/src/main/java/com/member/service/MemberServiceImpl.java @@ -55,7 +55,7 @@ private void validateDuplicate(SignUpRequest request) { @Override public LoginResponse login(LoginRequest request) { - MemberEntity memberEntity = memberRepository.findByEmailAndDeletedFalse(request.getEmail()) + MemberEntity memberEntity = memberRepository.findByEmailAndIsDeletedFalse(request.getEmail()) .orElseThrow(() -> LoginException.from(ErrorMessage.NOT_CORRECT_LOGIN)); Member member = new Member(memberEntity); diff --git a/src/test/java/com/board/service/ArticleServiceTest.java b/src/test/java/com/board/service/ArticleServiceTest.java index 5af647b..270280b 100644 --- a/src/test/java/com/board/service/ArticleServiceTest.java +++ b/src/test/java/com/board/service/ArticleServiceTest.java @@ -80,7 +80,7 @@ void findAllArticles_Success() { Pageable pageable = PageRequest.of(0, 10); Page mockPage = new PageImpl<>(Collections.emptyList()); - when(articleRepository.findAllByDeletedFalse(pageable)).thenReturn(mockPage); + when(articleRepository.findAllByIsDeletedFalse(pageable)).thenReturn(mockPage); // When Page result = articleService.findAll(pageable); diff --git a/src/test/java/com/board/service/CommentServiceTest.java b/src/test/java/com/board/service/CommentServiceTest.java index dfe85d0..bc1bba8 100644 --- a/src/test/java/com/board/service/CommentServiceTest.java +++ b/src/test/java/com/board/service/CommentServiceTest.java @@ -59,7 +59,7 @@ class CommentServiceTest { List commentEntityList = List.of(comment, comment2, comment3, comment4); Page commentPage = new PageImpl<>(commentEntityList, pageable, commentEntityList.size()); - when(commentRepository.findByArticleIdAndDeletedFalse(anyLong(), any(Pageable.class))) + when(commentRepository.findByArticleIdAndIsDeletedFalse(anyLong(), any(Pageable.class))) .thenReturn(commentPage); // When @@ -111,7 +111,7 @@ class CommentServiceTest { ArticleEntity article = new ArticleEntity("제목", "내용", member); CommentEntity comment = new CommentEntity("기존 댓글 내용", article, member); - when(commentRepository.findByIdAndDeletedFalse(commentId)).thenReturn(Optional.of(comment)); + when(commentRepository.findByIdAndIsDeletedFalse(commentId)).thenReturn(Optional.of(comment)); when(memberService.findById(memberId)).thenReturn(member); // When @@ -137,7 +137,7 @@ class deleteTest { ArticleEntity article = new ArticleEntity("제목", "내용", member); CommentEntity comment = new CommentEntity("기존 댓글 내용", article, member); - when(commentRepository.findByIdAndDeletedFalse(commentId)).thenReturn(Optional.of(comment)); + when(commentRepository.findByIdAndIsDeletedFalse(commentId)).thenReturn(Optional.of(comment)); when(memberService.findById(memberId)).thenReturn(member); // When @@ -159,7 +159,7 @@ class deleteTest { ArticleEntity article = new ArticleEntity("제목", "내용", member); CommentEntity comment = new CommentEntity("기존 댓글 내용", article, anotherMember); - when(commentRepository.findByIdAndDeletedFalse(commentId)).thenReturn(Optional.of(comment)); + when(commentRepository.findByIdAndIsDeletedFalse(commentId)).thenReturn(Optional.of(comment)); when(memberService.findById(memberId)).thenReturn(member); // When & Then diff --git a/src/test/java/com/member/service/MemberServiceImplTest.java b/src/test/java/com/member/service/MemberServiceImplTest.java index 3dae8aa..ebc50bd 100644 --- a/src/test/java/com/member/service/MemberServiceImplTest.java +++ b/src/test/java/com/member/service/MemberServiceImplTest.java @@ -1,11 +1,5 @@ package com.member.service; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - import com.config.jwt.JwtUtil; import com.config.jwt.TokenWithExpiration; import com.exception.custom.LoginException; @@ -16,7 +10,6 @@ import com.member.dto.response.SignUpResponse; import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -26,6 +19,12 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class MemberServiceImplTest { @@ -89,7 +88,7 @@ class LoginTests { void login_Success() { // Given LoginRequest request = new LoginRequest("test@example.com", "1234"); - when(memberRepository.findByEmailAndDeletedFalse(request.getEmail())).thenReturn(Optional.of(member)); + when(memberRepository.findByEmailAndIsDeletedFalse(request.getEmail())).thenReturn(Optional.of(member)); when(jwtUtil.generateTokenWithExpiration(any(String.class))) .thenReturn(new TokenWithExpiration("token", 3600000L)); @@ -106,7 +105,7 @@ void login_Success() { void login_Fail_WrongPassword() { // Given LoginRequest request = new LoginRequest("test@example.com", "wrongPassword"); - when(memberRepository.findByEmailAndDeletedFalse(request.getEmail())).thenReturn(Optional.of(member)); + when(memberRepository.findByEmailAndIsDeletedFalse(request.getEmail())).thenReturn(Optional.of(member)); // When & Then assertThrows(IllegalArgumentException.class, () -> memberService.login(request)); @@ -117,7 +116,7 @@ void login_Fail_WrongPassword() { void login_Fail_EmailNotFound() { // Given LoginRequest request = new LoginRequest("nonexistent@example.com", "1234"); - when(memberRepository.findByEmailAndDeletedFalse(request.getEmail())).thenReturn(Optional.empty()); + when(memberRepository.findByEmailAndIsDeletedFalse(request.getEmail())).thenReturn(Optional.empty()); // When & Then assertThrows(LoginException.class, () -> memberService.login(request)); From 4baafc9bad21080b405b9760fc07b6b8eb32803e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Thu, 8 May 2025 19:58:57 +0900 Subject: [PATCH 25/45] =?UTF-8?q?refactor=20:=20long=20vs=20Long=20?= =?UTF-8?q?=EA=B5=AC=EB=B6=84=ED=95=B4=EC=84=9C=20=EB=8D=94=20=EC=A0=81?= =?UTF-8?q?=EC=A0=88=ED=95=9C=EA=B2=83=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/board/service/ArticleService.java | 10 +++++----- src/main/java/com/board/service/CommentService.java | 6 +++--- .../exception/custom/MyEntityNotFoundException.java | 9 +++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/board/service/ArticleService.java b/src/main/java/com/board/service/ArticleService.java index b016f39..8d1d078 100644 --- a/src/main/java/com/board/service/ArticleService.java +++ b/src/main/java/com/board/service/ArticleService.java @@ -35,32 +35,32 @@ public Page findAll(Pageable pageable) { } @Transactional - public ArticleEntity findByIdAndIncreaseViewCount(long id) { + public ArticleEntity findByIdAndIncreaseViewCount(Long id) { ArticleEntity article = findById(id); article.increaseViewCount(); return article; } @Transactional - public void delete(long id, Long memberId) { + public void delete(Long id, Long memberId) { getOwnedArticle(id, memberId).softDelete(); } @Transactional - public ArticleEntity update(long id, Long memberId, ArticleUpdateRequest request) { + public ArticleEntity update(Long id, Long memberId, ArticleUpdateRequest request) { ArticleEntity article = getOwnedArticle(id, memberId); article.update(request.getTitle(), request.getContent()); return article; } - private ArticleEntity getOwnedArticle(long articleId, Long memberId) { + private ArticleEntity getOwnedArticle(Long articleId, Long memberId) { ArticleEntity article = findById(articleId); MemberEntity member = findMemberById(memberId); article.validateOwner(member); return article; } - public ArticleEntity findById(long id) { + public ArticleEntity findById(Long id) { return articleRepository.findById(id) .orElseThrow(() -> MyEntityNotFoundException.from(id)); } diff --git a/src/main/java/com/board/service/CommentService.java b/src/main/java/com/board/service/CommentService.java index 25ac69b..a8851b7 100644 --- a/src/main/java/com/board/service/CommentService.java +++ b/src/main/java/com/board/service/CommentService.java @@ -43,18 +43,18 @@ public CommentEntity updateComment(CommentUpdateRequest request, Long memberId) } @Transactional - public void deleteComment(long commentId, Long memberId) { + public void deleteComment(Long commentId, Long memberId) { CommentEntity comment = findCommentAndValidateOwner(commentId, findMemberById(memberId)); comment.softDelete(); } - private CommentEntity findCommentAndValidateOwner(long commentId, MemberEntity member) { + private CommentEntity findCommentAndValidateOwner(Long commentId, MemberEntity member) { CommentEntity comment = findComment(commentId); comment.validateOwner(member); return comment; } - private CommentEntity findComment(long id) { + private CommentEntity findComment(Long id) { return commentRepository.findByIdAndIsDeletedFalse(id) .orElseThrow(() -> MyEntityNotFoundException.from(id)); } diff --git a/src/main/java/com/exception/custom/MyEntityNotFoundException.java b/src/main/java/com/exception/custom/MyEntityNotFoundException.java index d2c15a7..d55679f 100644 --- a/src/main/java/com/exception/custom/MyEntityNotFoundException.java +++ b/src/main/java/com/exception/custom/MyEntityNotFoundException.java @@ -2,14 +2,15 @@ import com.exception.CustomException; import com.exception.ErrorCodeType; -import java.util.Map; import lombok.Getter; +import java.util.Map; + @Getter public class MyEntityNotFoundException extends CustomException { - private final long entityId; + private final Long entityId; - private MyEntityNotFoundException(long entityId) { + private MyEntityNotFoundException(Long entityId) { super(ErrorCodeType.ENTITY_NOT_FOUND); this.entityId = entityId; } @@ -19,7 +20,7 @@ public Map getAdditionalDetails() { return Map.of("entityId", entityId); } - public static MyEntityNotFoundException from(long entityId) { + public static MyEntityNotFoundException from(Long entityId) { return new MyEntityNotFoundException(entityId); } } From 7e08bc7de719f6471f53bf944b6e449c41cc3ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Thu, 8 May 2025 20:08:06 +0900 Subject: [PATCH 26/45] =?UTF-8?q?refactor=20:=20requestDTO=EC=9D=98=20Long?= =?UTF-8?q?=20=EB=B3=80=EC=88=98=EC=97=90=EB=8F=84=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EC=B2=B4=ED=81=AC=20=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/board/dto/request/CommentCreateRequest.java | 4 ++++ src/main/java/com/board/dto/request/CommentUpdateRequest.java | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/com/board/dto/request/CommentCreateRequest.java b/src/main/java/com/board/dto/request/CommentCreateRequest.java index 72e166a..efc06ea 100644 --- a/src/main/java/com/board/dto/request/CommentCreateRequest.java +++ b/src/main/java/com/board/dto/request/CommentCreateRequest.java @@ -1,6 +1,8 @@ package com.board.dto.request; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -11,6 +13,8 @@ public class CommentCreateRequest { @NotBlank(message = "내용을 입력해주세요") private String content; + @NotNull(message = "게시글 ID는 필수입니다") + @Min(value = 1, message = "게시글 ID는 1 이상이어야 합니다") private Long articleId; @Builder diff --git a/src/main/java/com/board/dto/request/CommentUpdateRequest.java b/src/main/java/com/board/dto/request/CommentUpdateRequest.java index 6e67637..91195f9 100644 --- a/src/main/java/com/board/dto/request/CommentUpdateRequest.java +++ b/src/main/java/com/board/dto/request/CommentUpdateRequest.java @@ -1,6 +1,8 @@ package com.board.dto.request; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -13,6 +15,8 @@ public class CommentUpdateRequest { @NotBlank(message = "내용을 입력해주세요") private String content; + @NotNull(message = "게시글 ID는 필수입니다") + @Min(value = 1, message = "코멘트 ID는 1 이상이어야 합니다") private Long commentId; @Builder From ce53e200f9a6bf7cb440a6bdeabb059991165d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Thu, 8 May 2025 20:38:00 +0900 Subject: [PATCH 27/45] =?UTF-8?q?refactor=20:=20=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=EB=90=98=EB=8A=94=20entity=20=EC=86=8D=EC=84=B1=20extends?= =?UTF-8?q?=EC=99=80=20@MappedSuperclass=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EA=B3=B5=ED=86=B5=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/board/entity/ArticleEntity.java | 20 ++---------------- .../java/com/board/entity/CommentEntity.java | 20 ++---------------- .../java/com/common/entity/BaseEntity.java | 21 +++++++++++++++++++ .../com/common/entity/SoftDeletedEntity.java | 17 +++++++++++++++ .../java/com/member/entity/MemberEntity.java | 20 ++---------------- 5 files changed, 44 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/common/entity/BaseEntity.java create mode 100644 src/main/java/com/common/entity/SoftDeletedEntity.java diff --git a/src/main/java/com/board/entity/ArticleEntity.java b/src/main/java/com/board/entity/ArticleEntity.java index e1a9615..ee1650e 100644 --- a/src/main/java/com/board/entity/ArticleEntity.java +++ b/src/main/java/com/board/entity/ArticleEntity.java @@ -1,5 +1,6 @@ package com.board.entity; +import com.common.entity.SoftDeletedEntity; import com.exception.custom.DifferentOwnerException; import com.member.entity.MemberEntity; import jakarta.persistence.*; @@ -8,12 +9,10 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; - @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter -public class ArticleEntity { +public class ArticleEntity extends SoftDeletedEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -30,15 +29,9 @@ public class ArticleEntity { @JoinColumn(name = "member_id") private MemberEntity member; - @Column(nullable = false) - private LocalDateTime createdAt; - @Column(nullable = false) private long viewCount = 0; - @Column(nullable = false) - private boolean isDeleted = false; - public ArticleEntity(String title, String content, MemberEntity member) { this.title = title; this.content = content; @@ -68,13 +61,4 @@ public void validateOwner(MemberEntity member) { throw DifferentOwnerException.from(this.member.getEmail()); } } - - public void softDelete() { - this.isDeleted = true; - } - - @PrePersist - public void setCreatedAtNow() { - this.createdAt = LocalDateTime.now(); - } } diff --git a/src/main/java/com/board/entity/CommentEntity.java b/src/main/java/com/board/entity/CommentEntity.java index c34fd87..7521675 100644 --- a/src/main/java/com/board/entity/CommentEntity.java +++ b/src/main/java/com/board/entity/CommentEntity.java @@ -1,5 +1,6 @@ package com.board.entity; +import com.common.entity.SoftDeletedEntity; import com.exception.custom.DifferentOwnerException; import com.member.entity.MemberEntity; import jakarta.persistence.*; @@ -7,12 +8,10 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; - @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter -public class CommentEntity { +public class CommentEntity extends SoftDeletedEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -30,12 +29,6 @@ public class CommentEntity { @JoinColumn(name = "member_id", nullable = false) private MemberEntity member; - @Column(nullable = false) - private LocalDateTime createdAt; - - @Column - private boolean isDeleted = false; - public CommentEntity(String content, ArticleEntity article, MemberEntity member) { this.content = content; this.article = article; @@ -51,13 +44,4 @@ public void validateOwner(MemberEntity member) { public void update(String content) { this.content = content; } - - public void softDelete() { - this.isDeleted = true; - } - - @PrePersist - public void setCreatedAtNow() { - this.createdAt = LocalDateTime.now(); - } } diff --git a/src/main/java/com/common/entity/BaseEntity.java b/src/main/java/com/common/entity/BaseEntity.java new file mode 100644 index 0000000..2145abf --- /dev/null +++ b/src/main/java/com/common/entity/BaseEntity.java @@ -0,0 +1,21 @@ +package com.common.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.PrePersist; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +public class BaseEntity { + + @Column(nullable = false) + protected LocalDateTime createdAt; + + @PrePersist + public void setCreatedAtNow() { + this.createdAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/common/entity/SoftDeletedEntity.java b/src/main/java/com/common/entity/SoftDeletedEntity.java new file mode 100644 index 0000000..2e9ab83 --- /dev/null +++ b/src/main/java/com/common/entity/SoftDeletedEntity.java @@ -0,0 +1,17 @@ +package com.common.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; + +@Getter +@MappedSuperclass +public class SoftDeletedEntity extends BaseEntity { + + @Column(nullable = false) + protected boolean isDeleted = false; + + public void softDelete() { + this.isDeleted = true; + } +} diff --git a/src/main/java/com/member/entity/MemberEntity.java b/src/main/java/com/member/entity/MemberEntity.java index 5a9ca24..5ab5109 100644 --- a/src/main/java/com/member/entity/MemberEntity.java +++ b/src/main/java/com/member/entity/MemberEntity.java @@ -1,18 +1,17 @@ package com.member.entity; +import com.common.entity.SoftDeletedEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; - @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Table(name = "member") -public class MemberEntity { +public class MemberEntity extends SoftDeletedEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -28,12 +27,6 @@ public class MemberEntity { @Column(nullable = false, unique = true) private String nickName; - @Column(nullable = false) - private LocalDateTime createdAt; - - @Column(nullable = false) - private boolean isDeleted = false; - @Builder public MemberEntity(String email, String password, String nickName) { this.email = email; @@ -47,13 +40,4 @@ public MemberEntity(Long id, String email, String password, String nickName) { this.password = password; this.nickName = nickName; } - - public void softDelete() { - this.isDeleted = true; - } - - @PrePersist - public void setCreatedAtNow() { - this.createdAt = LocalDateTime.now(); - } } From f85e73405d6007c0479c34d98e892fef2121469a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Thu, 8 May 2025 21:19:59 +0900 Subject: [PATCH 28/45] =?UTF-8?q?refactor=20:=20=EB=8F=99=EB=93=B1?= =?UTF-8?q?=EC=84=B1=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/member/entity/MemberEntity.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/member/entity/MemberEntity.java b/src/main/java/com/member/entity/MemberEntity.java index 5ab5109..1383dfc 100644 --- a/src/main/java/com/member/entity/MemberEntity.java +++ b/src/main/java/com/member/entity/MemberEntity.java @@ -2,15 +2,13 @@ import com.common.entity.SoftDeletedEntity; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Table(name = "member") +@EqualsAndHashCode(of = "id", callSuper = false) public class MemberEntity extends SoftDeletedEntity { @Id From 54139aceeba3ecacc10aba754a827fc6de253315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Fri, 9 May 2025 20:17:30 +0900 Subject: [PATCH 29/45] =?UTF-8?q?refactor=20:=20jwt=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=ED=95=84=EC=9A=94=20=EC=9C=A0=EB=AC=B4=20JwtAuthFilter?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EA=B4=80=EB=A6=AC=20-=20HttpMethod?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=EC=84=9C=20=EA=B5=AC=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/config/filter/FilterConfig.java | 5 +-- .../java/com/config/jwt/JwtAuthFilter.java | 37 +++++++++++++++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/config/filter/FilterConfig.java b/src/main/java/com/config/filter/FilterConfig.java index c89f810..4812fd9 100644 --- a/src/main/java/com/config/filter/FilterConfig.java +++ b/src/main/java/com/config/filter/FilterConfig.java @@ -19,10 +19,7 @@ public JwtAuthFilter jwtAuthFilter(JwtUtil jwtUtil, AuthUtil authUtil) { public FilterRegistrationBean jwtAuthFilterRegistration(JwtAuthFilter jwtAuthFilter) { FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(jwtAuthFilter); - registrationBean.addUrlPatterns( - "/articles", "/articles/*", - "/comments", "/comments/*", - "/members/logout", "/members/withdraw"); // 특정 URL 패턴에만 필터 적용 + registrationBean.addUrlPatterns("/*"); // 전체 요청 필터링, JwtAuthFilter 에서 판단 registrationBean.setOrder(1); // 우선순위 설정 (낮을수록 먼저 실행) return registrationBean; } diff --git a/src/main/java/com/config/jwt/JwtAuthFilter.java b/src/main/java/com/config/jwt/JwtAuthFilter.java index cfe0406..890f209 100644 --- a/src/main/java/com/config/jwt/JwtAuthFilter.java +++ b/src/main/java/com/config/jwt/JwtAuthFilter.java @@ -5,10 +5,13 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; import lombok.RequiredArgsConstructor; import org.springframework.web.filter.OncePerRequestFilter; +import java.io.IOException; +import java.util.Map; +import java.util.Set; + @RequiredArgsConstructor public class JwtAuthFilter extends OncePerRequestFilter { @@ -16,11 +19,21 @@ public class JwtAuthFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final AuthUtil authUtil; + private static final Map> AUTH_REQUIRED_PATH = Map.of( + "POST", Set.of("/articles", "/comments", "/members/logout"), + "PUT", Set.of("/articles/*", "/comments/*"), + "DELETE", Set.of("/articles/*", "/comments/*", "/members/withdraw"), + "PATCH", Set.of("/articles/*", "/comments/*") + ); + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - if (request.getMethod().equalsIgnoreCase("GET")) { + String method = request.getMethod().toUpperCase(); + String path = request.getRequestURI(); + + if (!isRequireAuth(method, path)) { filterChain.doFilter(request, response); return; } @@ -33,6 +46,24 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse return; } - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized!!!###"); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized - jwt 인증 실패!!!"); + } + + private boolean isRequireAuth(String method, String path) { + Set authRequiredPaths = AUTH_REQUIRED_PATH.get(method); + if (authRequiredPaths == null) return false; + + for (String authPath : authRequiredPaths) { + if (authPath.endsWith("/*")) { + String base = authPath.substring(0, authPath.length() - 2); + if (path.startsWith(base + "/")) { + return true; + } + } else if (path.equals(authPath)) { + return true; + } + } + + return false; } } From cb5d4468735d037259977386467dc99188a51fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Fri, 9 May 2025 20:36:03 +0900 Subject: [PATCH 30/45] =?UTF-8?q?test=20:=20MemberEntity=EC=9D=98=20@Equal?= =?UTF-8?q?sAndHashCode=20=EC=B6=94=EA=B0=80=EB=A1=9C=20=EC=9D=B8=ED=95=9C?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/com/board/service/ArticleServiceTest.java | 4 ++-- src/test/java/com/board/service/CommentServiceTest.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/board/service/ArticleServiceTest.java b/src/test/java/com/board/service/ArticleServiceTest.java index 270280b..10d7683 100644 --- a/src/test/java/com/board/service/ArticleServiceTest.java +++ b/src/test/java/com/board/service/ArticleServiceTest.java @@ -145,9 +145,9 @@ void deleteArticle_NotAuthor_ThrowsException() { // Given Long memberId = 3L; // 요청한 사용자 ID long articleId = 1L; // 삭제하려는 게시글 ID - MemberEntity requestingMember = new MemberEntity("user@example.com", "1234", "requestingUser"); + MemberEntity requestingMember = new MemberEntity(22L, "user@example.com", "1234", "requestingUser"); MemberEntity articleOwner = new MemberEntity("owner@example.com", "1234", "articleOwner"); - ArticleEntity article = new ArticleEntity("title", "content", articleOwner); + ArticleEntity article = new ArticleEntity(33L, "title", "content", articleOwner, 0); // Mock 설정 when(memberService.findById(memberId)).thenReturn(requestingMember); diff --git a/src/test/java/com/board/service/CommentServiceTest.java b/src/test/java/com/board/service/CommentServiceTest.java index bc1bba8..e5d11ba 100644 --- a/src/test/java/com/board/service/CommentServiceTest.java +++ b/src/test/java/com/board/service/CommentServiceTest.java @@ -154,8 +154,8 @@ class deleteTest { Long memberId = 1L; Long commentId = 3L; - MemberEntity member = new MemberEntity("test@example.com", "password", "nickname"); - MemberEntity anotherMember = new MemberEntity("another@example.com", "anotherPw", "another"); + MemberEntity member = new MemberEntity(10L, "test@example.com", "password", "nickname"); + MemberEntity anotherMember = new MemberEntity(11L, "another@example.com", "anotherPw", "another"); ArticleEntity article = new ArticleEntity("제목", "내용", member); CommentEntity comment = new CommentEntity("기존 댓글 내용", article, anotherMember); From 07e454cf4c0b735d1871624681830dd1e642ef1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Sun, 11 May 2025 16:30:13 +0900 Subject: [PATCH 31/45] =?UTF-8?q?refactor=20:=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EC=A1=B0=ED=9A=8C=EC=88=98=20=EC=A6=9D=EA=B0=80=20?= =?UTF-8?q?->=20=EB=8F=99=EC=8B=9C=EC=84=B1=20=EB=AC=B8=EC=A0=9C=EB=A1=9C?= =?UTF-8?q?=20=EC=9D=B8=ED=95=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/board/entity/ArticleEntity.java | 1 + src/main/java/com/board/repository/ArticleRepository.java | 7 +++++++ src/main/java/com/board/service/ArticleService.java | 5 ++--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/board/entity/ArticleEntity.java b/src/main/java/com/board/entity/ArticleEntity.java index ee1650e..3c78fbf 100644 --- a/src/main/java/com/board/entity/ArticleEntity.java +++ b/src/main/java/com/board/entity/ArticleEntity.java @@ -12,6 +12,7 @@ @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter +@Table(name = "article") public class ArticleEntity extends SoftDeletedEntity { @Id diff --git a/src/main/java/com/board/repository/ArticleRepository.java b/src/main/java/com/board/repository/ArticleRepository.java index d282152..7c4a2a4 100644 --- a/src/main/java/com/board/repository/ArticleRepository.java +++ b/src/main/java/com/board/repository/ArticleRepository.java @@ -4,7 +4,14 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface ArticleRepository extends JpaRepository { Page findAllByIsDeletedFalse(Pageable pageable); + + @Modifying(clearAutomatically = true) + @Query("UPDATE ArticleEntity a SET a.viewCount = a.viewCount + 1 WHERE a.id = :id") + void increaseViewCount(@Param("id") Long id); } diff --git a/src/main/java/com/board/service/ArticleService.java b/src/main/java/com/board/service/ArticleService.java index 8d1d078..0dc3b97 100644 --- a/src/main/java/com/board/service/ArticleService.java +++ b/src/main/java/com/board/service/ArticleService.java @@ -36,9 +36,8 @@ public Page findAll(Pageable pageable) { @Transactional public ArticleEntity findByIdAndIncreaseViewCount(Long id) { - ArticleEntity article = findById(id); - article.increaseViewCount(); - return article; + articleRepository.increaseViewCount(id); + return findById(id); } @Transactional From f0922e98b0ad3cc3cb62c91554577edf8d7c7e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Sun, 11 May 2025 17:32:18 +0900 Subject: [PATCH 32/45] =?UTF-8?q?refactor,test=20:=20=ED=86=B5=ED=95=A9?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ArticleControllerIntegrationTest.java | 10 ++------- .../CommentControllerIntegrationTest.java | 10 ++------- .../MemberControllerIntegrationTest.java | 22 ++++++------------- .../java/com/support/IntegrationTest.java | 20 +++++++++++++++++ 4 files changed, 31 insertions(+), 31 deletions(-) create mode 100644 src/test/java/com/support/IntegrationTest.java diff --git a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java index 2b6ba79..a8fc306 100644 --- a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java @@ -11,30 +11,24 @@ import com.member.dto.request.SignUpRequest; import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; +import com.support.IntegrationTest; import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; -import org.springframework.transaction.annotation.Transactional; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -@SpringBootTest -@AutoConfigureMockMvc -@Transactional -@ActiveProfiles("test") +@IntegrationTest class ArticleControllerIntegrationTest { @Autowired diff --git a/src/test/java/com/board/controller/CommentControllerIntegrationTest.java b/src/test/java/com/board/controller/CommentControllerIntegrationTest.java index 4c28a47..43fd5fc 100644 --- a/src/test/java/com/board/controller/CommentControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/CommentControllerIntegrationTest.java @@ -10,25 +10,19 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; +import com.support.IntegrationTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.transaction.annotation.Transactional; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@SpringBootTest -@AutoConfigureMockMvc -@Transactional -@ActiveProfiles("test") +@IntegrationTest class CommentControllerIntegrationTest { @Autowired diff --git a/src/test/java/com/member/controller/MemberControllerIntegrationTest.java b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java index 3656c4f..b301680 100644 --- a/src/test/java/com/member/controller/MemberControllerIntegrationTest.java +++ b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java @@ -1,12 +1,5 @@ package com.member.controller; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.board.dto.request.ArticleCreateRequest; import com.config.jwt.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; @@ -14,23 +7,22 @@ import com.member.dto.request.SignUpRequest; import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; +import com.support.IntegrationTest; import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; -import org.springframework.transaction.annotation.Transactional; -@SpringBootTest -@AutoConfigureMockMvc -@Transactional -@ActiveProfiles("test") +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@IntegrationTest class MemberControllerIntegrationTest { @Autowired diff --git a/src/test/java/com/support/IntegrationTest.java b/src/test/java/com/support/IntegrationTest.java new file mode 100644 index 0000000..7ba487d --- /dev/null +++ b/src/test/java/com/support/IntegrationTest.java @@ -0,0 +1,20 @@ +package com.support; + +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@ActiveProfiles("test") +public @interface IntegrationTest { +} From b2e1876afd003a1956d501ddab787a9e8220d0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Tue, 13 May 2025 18:14:03 +0900 Subject: [PATCH 33/45] =?UTF-8?q?refactor=20:=20=EB=B9=84=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EB=8F=84=20=EC=9A=94=EC=B2=AD=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=9C=20controller=20=EC=9A=94=EC=B2=AD=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=20-=20=EA=B8=B0=EC=A1=B4=20member=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=A1=B4=EC=9E=AC=ED=95=98=EB=8D=98=20controller?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC(=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20=EB=93=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/config/jwt/JwtAuthFilter.java | 13 ++- .../member/controller/MemberController.java | 25 ----- .../controller/PublicMemberController.java | 44 ++++++++ .../ArticleControllerIntegrationTest.java | 20 ++-- .../MemberControllerIntegrationTest.java | 71 ++---------- .../controller/MemberControllerTest.java | 40 ------- ...PublicMemberControllerIntegrationTest.java | 104 ++++++++++++++++++ .../PublicMemberControllerTest.java | 83 ++++++++++++++ 8 files changed, 259 insertions(+), 141 deletions(-) create mode 100644 src/main/java/com/member/controller/PublicMemberController.java create mode 100644 src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java create mode 100644 src/test/java/com/member/controller/PublicMemberControllerTest.java diff --git a/src/main/java/com/config/jwt/JwtAuthFilter.java b/src/main/java/com/config/jwt/JwtAuthFilter.java index 890f209..6d1e764 100644 --- a/src/main/java/com/config/jwt/JwtAuthFilter.java +++ b/src/main/java/com/config/jwt/JwtAuthFilter.java @@ -5,12 +5,11 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.filter.OncePerRequestFilter; - import java.io.IOException; import java.util.Map; import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.web.filter.OncePerRequestFilter; @RequiredArgsConstructor @@ -20,9 +19,9 @@ public class JwtAuthFilter extends OncePerRequestFilter { private final AuthUtil authUtil; private static final Map> AUTH_REQUIRED_PATH = Map.of( - "POST", Set.of("/articles", "/comments", "/members/logout"), + "POST", Set.of("/articles", "/comments", "/members/*"), "PUT", Set.of("/articles/*", "/comments/*"), - "DELETE", Set.of("/articles/*", "/comments/*", "/members/withdraw"), + "DELETE", Set.of("/articles/*", "/comments/*", "/members/*"), "PATCH", Set.of("/articles/*", "/comments/*") ); @@ -51,7 +50,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse private boolean isRequireAuth(String method, String path) { Set authRequiredPaths = AUTH_REQUIRED_PATH.get(method); - if (authRequiredPaths == null) return false; + if (authRequiredPaths == null) { + return false; + } for (String authPath : authRequiredPaths) { if (authPath.endsWith("/*")) { diff --git a/src/main/java/com/member/controller/MemberController.java b/src/main/java/com/member/controller/MemberController.java index d1b42ae..7845487 100644 --- a/src/main/java/com/member/controller/MemberController.java +++ b/src/main/java/com/member/controller/MemberController.java @@ -1,20 +1,14 @@ package com.member.controller; import com.config.auth.annotation.AuthenticatedMember; -import com.member.dto.request.LoginRequest; -import com.member.dto.request.SignUpRequest; -import com.member.dto.response.LoginResponse; -import com.member.dto.response.SignUpResponse; import com.member.service.MemberService; import com.util.cookie.CookieUtils; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -25,25 +19,6 @@ public class MemberController { private final MemberService memberService; - @PostMapping("/signup") - public ResponseEntity signUp(@Valid @RequestBody SignUpRequest request) { - SignUpResponse signUpResponse = memberService.signUp(request); - return ResponseEntity.ok(signUpResponse); - } - - @PostMapping("/login") - public ResponseEntity login(@Valid @RequestBody LoginRequest loginRequest, - HttpServletResponse response) { - LoginResponse loginResponse = memberService.login(loginRequest); - - Cookie cookie = CookieUtils.createCookie("token", - loginResponse.getAccessToken(), - (int) loginResponse.getExpirationTime()); - response.addCookie(cookie); - - return ResponseEntity.ok(loginResponse); - } - @PostMapping("/logout") public ResponseEntity logout(@AuthenticatedMember Long memberId, HttpServletResponse response) { diff --git a/src/main/java/com/member/controller/PublicMemberController.java b/src/main/java/com/member/controller/PublicMemberController.java new file mode 100644 index 0000000..2268dcd --- /dev/null +++ b/src/main/java/com/member/controller/PublicMemberController.java @@ -0,0 +1,44 @@ +package com.member.controller; + +import com.member.dto.request.LoginRequest; +import com.member.dto.request.SignUpRequest; +import com.member.dto.response.LoginResponse; +import com.member.dto.response.SignUpResponse; +import com.member.service.MemberService; +import com.util.cookie.CookieUtils; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/public/members") +public class PublicMemberController { + + private final MemberService memberService; + + @PostMapping("/signup") + public ResponseEntity signUp(@Valid @RequestBody SignUpRequest request) { + SignUpResponse signUpResponse = memberService.signUp(request); + return ResponseEntity.ok(signUpResponse); + } + + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest loginRequest, + HttpServletResponse response) { + LoginResponse loginResponse = memberService.login(loginRequest); + + Cookie cookie = CookieUtils.createCookie("token", + loginResponse.getAccessToken(), + (int) loginResponse.getExpirationTime()); + response.addCookie(cookie); + + return ResponseEntity.ok(loginResponse); + } +} diff --git a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java index a8fc306..0e84ae5 100644 --- a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java @@ -1,5 +1,15 @@ package com.board.controller; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.board.dto.request.ArticleCreateRequest; import com.board.dto.request.ArticleUpdateRequest; import com.board.entity.ArticleEntity; @@ -23,11 +33,6 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - @IntegrationTest class ArticleControllerIntegrationTest { @@ -67,12 +72,13 @@ void testJwtSecretKey() { } private void signUpAndLogin() throws Exception { - sendPostRequest("/members/signup", new SignUpRequest(MEMBER_EMAIL, MEMBER_NICKNAME, MEMBER_PASSWORD)) + sendPostRequest("/public/members/signup", new SignUpRequest(MEMBER_EMAIL, MEMBER_NICKNAME, MEMBER_PASSWORD)) .andExpect(status().isOk()) .andExpect(jsonPath("$.email").value(MEMBER_EMAIL)) .andExpect(jsonPath("$.nickName").value(MEMBER_NICKNAME)); - MvcResult loginResult = sendPostRequest("/members/login", new LoginRequest(MEMBER_EMAIL, MEMBER_PASSWORD)) + MvcResult loginResult = sendPostRequest("/public/members/login", + new LoginRequest(MEMBER_EMAIL, MEMBER_PASSWORD)) .andExpect(status().isOk()) .andExpect(cookie().exists("token")) .andExpect(jsonPath("$.accessToken").exists()) diff --git a/src/test/java/com/member/controller/MemberControllerIntegrationTest.java b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java index b301680..ddfdfb4 100644 --- a/src/test/java/com/member/controller/MemberControllerIntegrationTest.java +++ b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java @@ -1,10 +1,15 @@ package com.member.controller; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.board.dto.request.ArticleCreateRequest; import com.config.jwt.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.member.dto.request.LoginRequest; -import com.member.dto.request.SignUpRequest; import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; import com.support.IntegrationTest; @@ -17,11 +22,6 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - @IntegrationTest class MemberControllerIntegrationTest { @@ -48,61 +48,6 @@ void setUp() { memberRepository.save(member); } - @Test - @DisplayName("로그인 성공 테스트") - void loginTest() throws Exception { - LoginRequest loginRequest = new LoginRequest(setUpMemberEmail, setUpMemberPassword); - String loginJson = objectMapper.writeValueAsString(loginRequest); - - mockMvc.perform(post("/members/login") - .contentType(MediaType.APPLICATION_JSON) - .content(loginJson)) - .andExpect(status().isOk()) - .andExpect(cookie().exists("token")) // JWT 쿠키 존재 확인 - .andExpect(jsonPath("$.accessToken").exists()); // accessToken 존재 확인 - } - - @Test - @DisplayName("로그인 후 JWT 발행 및 검증") - void loginAndValidateJwtTest() throws Exception { - - LoginRequest loginRequest = new LoginRequest(setUpMemberEmail, setUpMemberPassword); - String loginJson = objectMapper.writeValueAsString(loginRequest); - - MvcResult loginResult = mockMvc.perform(post("/members/login") - .contentType(MediaType.APPLICATION_JSON) - .content(loginJson)) - .andExpect(status().isOk()) - .andExpect(cookie().exists("token")) - .andExpect(jsonPath("$.accessToken").exists()) - .andExpect(jsonPath("$.accessToken").isNotEmpty()) - .andReturn(); - - // JWT 추출 및 검증 - String responseBody = loginResult.getResponse().getContentAsString(); - String token = objectMapper.readTree(responseBody).get("accessToken").asText(); - assertThat(jwtUtil.extractEmail(token)).isEqualTo(setUpMemberEmail); - assertThat(jwtUtil.isTokenValid(token)).isTrue(); - } - - @Test - @DisplayName("회원가입 테스트") - void signUpTest() throws Exception { - final String testEmail = "test@example.com"; - final String testNickName = "test-nickname"; - final String testPassword = "password123"; - - SignUpRequest signUpRequest = new SignUpRequest(testEmail, testNickName, testPassword); - String signUpJson = objectMapper.writeValueAsString(signUpRequest); - - mockMvc.perform(post("/members/signup") - .contentType(MediaType.APPLICATION_JSON) - .content(signUpJson)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.email").value(testEmail)) - .andExpect(jsonPath("$.nickName").value(testNickName)); - } - @Test @DisplayName("로그아웃_요청시_쿠키가_만료된다") void 로그아웃_요청시_쿠키가_만료된다() throws Exception { @@ -132,7 +77,7 @@ void signUpTest() throws Exception { LoginRequest loginRequest = new LoginRequest(setUpMemberEmail, setUpMemberPassword); String loginJson = objectMapper.writeValueAsString(loginRequest); - mockMvc.perform(post("/members/login") + mockMvc.perform(post("/public/members/login") .contentType(MediaType.APPLICATION_JSON) .content(loginJson)) .andExpect(jsonPath("$.statusCode").value(404)); @@ -179,7 +124,7 @@ private String getAccessToken() throws Exception { LoginRequest loginRequest = new LoginRequest(setUpMemberEmail, setUpMemberPassword); String loginJson = objectMapper.writeValueAsString(loginRequest); - MvcResult result = mockMvc.perform(post("/members/login") + MvcResult result = mockMvc.perform(post("/public/members/login") .contentType(MediaType.APPLICATION_JSON) .content(loginJson)) .andExpect(status().isOk()) diff --git a/src/test/java/com/member/controller/MemberControllerTest.java b/src/test/java/com/member/controller/MemberControllerTest.java index 865d179..680d4bb 100644 --- a/src/test/java/com/member/controller/MemberControllerTest.java +++ b/src/test/java/com/member/controller/MemberControllerTest.java @@ -5,22 +5,16 @@ import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.config.auth.AuthUtil; import com.config.auth.AuthenticatedMemberArgumentResolver; import com.fasterxml.jackson.databind.ObjectMapper; -import com.member.dto.request.LoginRequest; -import com.member.dto.request.SignUpRequest; -import com.member.dto.response.LoginResponse; -import com.member.dto.response.SignUpResponse; import com.member.service.MemberService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.http.MediaType; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -49,40 +43,6 @@ void setup() throws Exception { .thenReturn(1L); // Long memberId 주입 } - @Test - void signUp_Success() throws Exception { - // Given - SignUpRequest request = new SignUpRequest("test@example.com", "nickname", "password123"); - SignUpResponse response = new SignUpResponse(1L, "test@example.com", "nickname"); - - when(memberService.signUp(any(SignUpRequest.class))).thenReturn(response); - - // When & Then - mockMvc.perform(post("/members/signup") - .contentType(MediaType.APPLICATION_JSON) - .content(new ObjectMapper().writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value(1L)) - .andExpect(jsonPath("$.email").value("test@example.com")) - .andExpect(jsonPath("$.nickName").value("nickname")); - } - - @Test - void login_Success() throws Exception { - // Given - LoginRequest loginRequest = new LoginRequest("test@example.com", "password123"); - LoginResponse loginResponse = new LoginResponse("mockAccessToken", 3600L); - - when(memberService.login(any(LoginRequest.class))).thenReturn(loginResponse); - - // When & Then - mockMvc.perform(post("/members/login") - .contentType(MediaType.APPLICATION_JSON) - .content(new ObjectMapper().writeValueAsString(loginRequest))) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.accessToken").value("mockAccessToken")); - } - @Test public void logout_ShouldCallMemberServiceLogout() throws Exception { // Given diff --git a/src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java b/src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java new file mode 100644 index 0000000..39146a2 --- /dev/null +++ b/src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java @@ -0,0 +1,104 @@ +package com.member.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.config.jwt.JwtUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.member.dto.request.LoginRequest; +import com.member.dto.request.SignUpRequest; +import com.member.entity.MemberEntity; +import com.member.repository.MemberRepository; +import com.support.IntegrationTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +@IntegrationTest +class PublicMemberControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; // JSON 변환을 위해 사용 + + @Autowired + private MemberRepository memberRepository; + + @Autowired + JwtUtil jwtUtil; + + String setUpMemberEmail = "setupMember@example.com"; + String setUpMemberPassword = "12345"; + String setUpMemberNickname = "setupMemberNickname"; + + @BeforeEach + void setUp() { + // 테스트용 회원 데이터 미리 저장 + MemberEntity member = new MemberEntity(setUpMemberEmail, setUpMemberPassword, setUpMemberNickname); + memberRepository.save(member); + } + + @Test + @DisplayName("로그인 성공 테스트") + void loginTest() throws Exception { + LoginRequest loginRequest = new LoginRequest(setUpMemberEmail, setUpMemberPassword); + String loginJson = objectMapper.writeValueAsString(loginRequest); + + mockMvc.perform(post("/public/members/login") + .contentType(MediaType.APPLICATION_JSON) + .content(loginJson)) + .andExpect(status().isOk()) + .andExpect(cookie().exists("token")) // JWT 쿠키 존재 확인 + .andExpect(jsonPath("$.accessToken").exists()); // accessToken 존재 확인 + } + + @Test + @DisplayName("로그인 후 JWT 발행 및 검증") + void loginAndValidateJwtTest() throws Exception { + + LoginRequest loginRequest = new LoginRequest(setUpMemberEmail, setUpMemberPassword); + String loginJson = objectMapper.writeValueAsString(loginRequest); + + MvcResult loginResult = mockMvc.perform(post("/public/members/login") + .contentType(MediaType.APPLICATION_JSON) + .content(loginJson)) + .andExpect(status().isOk()) + .andExpect(cookie().exists("token")) + .andExpect(jsonPath("$.accessToken").exists()) + .andExpect(jsonPath("$.accessToken").isNotEmpty()) + .andReturn(); + + // JWT 추출 및 검증 + String responseBody = loginResult.getResponse().getContentAsString(); + String token = objectMapper.readTree(responseBody).get("accessToken").asText(); + assertThat(jwtUtil.extractEmail(token)).isEqualTo(setUpMemberEmail); + assertThat(jwtUtil.isTokenValid(token)).isTrue(); + } + + @Test + @DisplayName("회원가입 테스트") + void signUpTest() throws Exception { + final String testEmail = "test@example.com"; + final String testNickName = "test-nickname"; + final String testPassword = "password123"; + + SignUpRequest signUpRequest = new SignUpRequest(testEmail, testNickName, testPassword); + String signUpJson = objectMapper.writeValueAsString(signUpRequest); + + mockMvc.perform(post("/public/members/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(signUpJson)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.email").value(testEmail)) + .andExpect(jsonPath("$.nickName").value(testNickName)); + } +} diff --git a/src/test/java/com/member/controller/PublicMemberControllerTest.java b/src/test/java/com/member/controller/PublicMemberControllerTest.java new file mode 100644 index 0000000..1dba983 --- /dev/null +++ b/src/test/java/com/member/controller/PublicMemberControllerTest.java @@ -0,0 +1,83 @@ +package com.member.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.config.auth.AuthUtil; +import com.config.auth.AuthenticatedMemberArgumentResolver; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.member.dto.request.LoginRequest; +import com.member.dto.request.SignUpRequest; +import com.member.dto.response.LoginResponse; +import com.member.dto.response.SignUpResponse; +import com.member.service.MemberService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(PublicMemberController.class) +class PublicMemberControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private AuthUtil authUtil; + + @MockitoBean + private MemberService memberService; + + @MockitoBean + private AuthenticatedMemberArgumentResolver authenticatedMemberArgumentResolver; + + @Autowired + private ObjectMapper objectMapper; + + @BeforeEach + void setup() throws Exception { + when(authenticatedMemberArgumentResolver.supportsParameter(any())).thenReturn(true); + when(authenticatedMemberArgumentResolver.resolveArgument(any(), any(), any(), any())) + .thenReturn(1L); // Long memberId 주입 + } + + @Test + void signUp_Success() throws Exception { + // Given + SignUpRequest request = new SignUpRequest("test@example.com", "nickname", "password123"); + SignUpResponse response = new SignUpResponse(1L, "test@example.com", "nickname"); + + when(memberService.signUp(any(SignUpRequest.class))).thenReturn(response); + + // When & Then + mockMvc.perform(post("/public/members/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(1L)) + .andExpect(jsonPath("$.email").value("test@example.com")) + .andExpect(jsonPath("$.nickName").value("nickname")); + } + + @Test + void login_Success() throws Exception { + // Given + LoginRequest loginRequest = new LoginRequest("test@example.com", "password123"); + LoginResponse loginResponse = new LoginResponse("mockAccessToken", 3600L); + + when(memberService.login(any(LoginRequest.class))).thenReturn(loginResponse); + + // When & Then + mockMvc.perform(post("/public/members/login") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(loginRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.accessToken").value("mockAccessToken")); + } +} From fd990dd003adfd0b897e332b00ec02df1ae1906f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Tue, 13 May 2025 21:23:07 +0900 Subject: [PATCH 34/45] =?UTF-8?q?refactor=20:=20jwt=EA=B0=80=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EA=B2=BD=EB=A1=9C=20=EB=B0=8F=20httpMetho?= =?UTF-8?q?d=20enum=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=20-=20=EC=A3=BC=EC=86=8C=EC=A0=95=EB=B3=B4=20+=20?= =?UTF-8?q?=ED=95=B4=EB=8B=B9=ED=95=98=EB=8A=94=20httpMethod=20=EC=A2=85?= =?UTF-8?q?=EB=A5=98=EB=93=A4=EB=A1=9C=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/config/jwt/AuthRequiredPath.java | 42 +++++++++++++++++++ .../java/com/config/jwt/JwtAuthFilter.java | 34 ++------------- 2 files changed, 45 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/config/jwt/AuthRequiredPath.java diff --git a/src/main/java/com/config/jwt/AuthRequiredPath.java b/src/main/java/com/config/jwt/AuthRequiredPath.java new file mode 100644 index 0000000..5efdc8f --- /dev/null +++ b/src/main/java/com/config/jwt/AuthRequiredPath.java @@ -0,0 +1,42 @@ +package com.config.jwt; + +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpMethod; + +@RequiredArgsConstructor +@Getter +public enum AuthRequiredPath { + CREATE_ARTICLE("/articles", List.of(HttpMethod.POST)), + UPDATE_ARTICLE("/articles/*", List.of(HttpMethod.PUT, HttpMethod.PATCH)), + DELETE_ARTICLE("/articles/*", List.of(HttpMethod.DELETE)), + + CREATE_COMMENT("/comments", List.of(HttpMethod.POST)), + UPDATE_COMMENT("/comments/*", List.of(HttpMethod.PUT, HttpMethod.PATCH)), + DELETE_COMMENT("/comments/*", List.of(HttpMethod.DELETE)), + + CREATE_MEMBER("/members/*", List.of(HttpMethod.POST)), + DELETE_MEMBER("/members/*", List.of(HttpMethod.DELETE)); + + private final String pathPattern; + private final List methods; + + public static boolean isAuthRequired(HttpMethod method, String path) { + for (AuthRequiredPath arp : AuthRequiredPath.values()) { + if (pathMatchesPattern(path, arp.getPathPattern()) && arp.getMethods().contains(method)) { + return true; + } + } + return false; + } + + private static boolean pathMatchesPattern(String path, String pattern) { + if (pattern.endsWith("/*")) { + String base = pattern.substring(0, pattern.length() - 2); + return path.startsWith(base + "/"); + } else { + return path.equals(pattern); + } + } +} diff --git a/src/main/java/com/config/jwt/JwtAuthFilter.java b/src/main/java/com/config/jwt/JwtAuthFilter.java index 6d1e764..0ef410d 100644 --- a/src/main/java/com/config/jwt/JwtAuthFilter.java +++ b/src/main/java/com/config/jwt/JwtAuthFilter.java @@ -6,9 +6,8 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.Map; -import java.util.Set; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpMethod; import org.springframework.web.filter.OncePerRequestFilter; @@ -18,21 +17,14 @@ public class JwtAuthFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final AuthUtil authUtil; - private static final Map> AUTH_REQUIRED_PATH = Map.of( - "POST", Set.of("/articles", "/comments", "/members/*"), - "PUT", Set.of("/articles/*", "/comments/*"), - "DELETE", Set.of("/articles/*", "/comments/*", "/members/*"), - "PATCH", Set.of("/articles/*", "/comments/*") - ); - @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String method = request.getMethod().toUpperCase(); + HttpMethod method = HttpMethod.valueOf(request.getMethod().toUpperCase()); String path = request.getRequestURI(); - if (!isRequireAuth(method, path)) { + if (!AuthRequiredPath.isAuthRequired(method, path)) { filterChain.doFilter(request, response); return; } @@ -47,24 +39,4 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized - jwt 인증 실패!!!"); } - - private boolean isRequireAuth(String method, String path) { - Set authRequiredPaths = AUTH_REQUIRED_PATH.get(method); - if (authRequiredPaths == null) { - return false; - } - - for (String authPath : authRequiredPaths) { - if (authPath.endsWith("/*")) { - String base = authPath.substring(0, authPath.length() - 2); - if (path.startsWith(base + "/")) { - return true; - } - } else if (path.equals(authPath)) { - return true; - } - } - - return false; - } } From 22430a2309d7c6bf4a154185862f53955f4a3771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Thu, 15 May 2025 16:22:56 +0900 Subject: [PATCH 35/45] =?UTF-8?q?refactor=20:=20=EB=8C=93=EA=B8=80?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20controller=20restApi=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=80=EA=B2=BD=20=20-=20"/comments"=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20"/articles/{articleId}/comments"=20=20-=20=EA=B7=B8?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20articleId=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=A0=84=EB=B6=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/CommentController.java | 20 +- .../dto/request/CommentCreateRequest.java | 8 +- .../java/com/board/entity/CommentEntity.java | 33 +- .../board/repository/CommentRepository.java | 7 +- .../com/board/service/CommentService.java | 27 +- .../java/com/config/jwt/AuthRequiredPath.java | 22 +- .../java/com/exception/ErrorCodeType.java | 3 +- .../custom/NotIncludeBoardException.java | 24 ++ .../CommentControllerIntegrationTest.java | 21 +- .../controller/CommentControllerTest.java | 49 +-- .../com/board/service/CommentServiceTest.java | 287 ++++++++++++------ 11 files changed, 321 insertions(+), 180 deletions(-) create mode 100644 src/main/java/com/exception/custom/NotIncludeBoardException.java diff --git a/src/main/java/com/board/controller/CommentController.java b/src/main/java/com/board/controller/CommentController.java index f3f9dcc..1d3d943 100644 --- a/src/main/java/com/board/controller/CommentController.java +++ b/src/main/java/com/board/controller/CommentController.java @@ -27,13 +27,13 @@ @RequiredArgsConstructor @RestController -@RequestMapping("/comments") +@RequestMapping("/articles/{articleId}/comments") public class CommentController { private final CommentService commentService; @GetMapping - public ResponseEntity> findAllComments(@RequestParam Long articleId, + public ResponseEntity> findAllComments(@PathVariable Long articleId, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "latest") String sort) { @@ -45,28 +45,30 @@ public ResponseEntity> findAllComments(@RequestPar } @PostMapping - public ResponseEntity addComment(@Valid @RequestBody CommentCreateRequest request, + public ResponseEntity addComment(@PathVariable Long articleId, + @Valid @RequestBody CommentCreateRequest request, @AuthenticatedMember Long memberId) { - CommentEntity savedComment = commentService.createComment(request, memberId); + CommentEntity savedComment = commentService.createComment(articleId, request, memberId); return ResponseEntity.status(HttpStatus.CREATED) .body(new CommentResponse(savedComment)); } @PatchMapping("/{commentId}") - public ResponseEntity updateComment(@PathVariable long commentId, + public ResponseEntity updateComment(@PathVariable Long articleId, + @PathVariable Long commentId, @Valid @RequestBody CommentUpdateRequest request, @AuthenticatedMember Long memberId) { - request.setCommentId(commentId); - CommentEntity updatedComment = commentService.updateComment(request, memberId); + CommentEntity updatedComment = commentService.updateComment(articleId, commentId, request, memberId); return ResponseEntity.ok() .body(new CommentResponse(updatedComment)); } @DeleteMapping("/{commentId}") - public ResponseEntity deleteComment(@PathVariable long commentId, + public ResponseEntity deleteComment(@PathVariable Long articleId, + @PathVariable Long commentId, @AuthenticatedMember Long memberId) { - commentService.deleteComment(commentId, memberId); + commentService.deleteComment(articleId, commentId, memberId); return ResponseEntity.noContent() .build(); } diff --git a/src/main/java/com/board/dto/request/CommentCreateRequest.java b/src/main/java/com/board/dto/request/CommentCreateRequest.java index efc06ea..8628721 100644 --- a/src/main/java/com/board/dto/request/CommentCreateRequest.java +++ b/src/main/java/com/board/dto/request/CommentCreateRequest.java @@ -1,8 +1,6 @@ package com.board.dto.request; -import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -13,13 +11,9 @@ public class CommentCreateRequest { @NotBlank(message = "내용을 입력해주세요") private String content; - @NotNull(message = "게시글 ID는 필수입니다") - @Min(value = 1, message = "게시글 ID는 1 이상이어야 합니다") - private Long articleId; @Builder - public CommentCreateRequest(String content, Long articleId) { + public CommentCreateRequest(String content) { this.content = content; - this.articleId = articleId; } } diff --git a/src/main/java/com/board/entity/CommentEntity.java b/src/main/java/com/board/entity/CommentEntity.java index 7521675..d99935a 100644 --- a/src/main/java/com/board/entity/CommentEntity.java +++ b/src/main/java/com/board/entity/CommentEntity.java @@ -2,9 +2,18 @@ import com.common.entity.SoftDeletedEntity; import com.exception.custom.DifferentOwnerException; +import com.exception.custom.NotIncludeBoardException; import com.member.entity.MemberEntity; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -35,13 +44,33 @@ public CommentEntity(String content, ArticleEntity article, MemberEntity member) this.member = member; } + @Builder + public CommentEntity(Long id, String content, ArticleEntity article, MemberEntity member) { + this.id = id; + this.content = content; + this.article = article; + this.member = member; + } + public void validateOwner(MemberEntity member) { if (!this.member.equals(member)) { throw DifferentOwnerException.from(this.member.getEmail()); } } - public void update(String content) { + public void update(String content, MemberEntity modifier) { + validateOwner(modifier); this.content = content; } + + public void softDelete(MemberEntity deleter) { + validateOwner(deleter); + super.softDelete(); + } + + public void validateArticle(Long articleId) { + if (!this.article.getId().equals(articleId)) { + throw NotIncludeBoardException.from(articleId); + } + } } diff --git a/src/main/java/com/board/repository/CommentRepository.java b/src/main/java/com/board/repository/CommentRepository.java index 5c4f7fb..0b4d28b 100644 --- a/src/main/java/com/board/repository/CommentRepository.java +++ b/src/main/java/com/board/repository/CommentRepository.java @@ -1,16 +1,15 @@ package com.board.repository; +import com.board.entity.ArticleEntity; import com.board.entity.CommentEntity; +import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - public interface CommentRepository extends JpaRepository { Optional findByIdAndIsDeletedFalse(Long id); - Page findByArticleIdAndIsDeletedFalse(Long articleId, Pageable pageable); - + Page findByArticleAndIsDeletedFalse(ArticleEntity article, Pageable pageable); } diff --git a/src/main/java/com/board/service/CommentService.java b/src/main/java/com/board/service/CommentService.java index a8851b7..90918b4 100644 --- a/src/main/java/com/board/service/CommentService.java +++ b/src/main/java/com/board/service/CommentService.java @@ -24,34 +24,33 @@ public class CommentService { private final MemberService memberService; public Page findAllComments(Long articleId, Pageable pageable) { - return commentRepository.findByArticleIdAndIsDeletedFalse(articleId, pageable); + ArticleEntity article = articleService.findById(articleId); + return commentRepository.findByArticleAndIsDeletedFalse(article, pageable); } @Transactional - public CommentEntity createComment(CommentCreateRequest request, Long memberId) { - ArticleEntity article = articleService.findById(request.getArticleId()); + public CommentEntity createComment(Long articleId, CommentCreateRequest request, Long memberId) { + ArticleEntity article = articleService.findById(articleId); MemberEntity member = findMemberById(memberId); CommentEntity comment = new CommentEntity(request.getContent(), article, member); return commentRepository.save(comment); } @Transactional - public CommentEntity updateComment(CommentUpdateRequest request, Long memberId) { - CommentEntity comment = findCommentAndValidateOwner(request.getCommentId(), findMemberById(memberId)); - comment.update(request.getContent()); + public CommentEntity updateComment(Long articleId, Long commentId, CommentUpdateRequest request, Long memberId) { + CommentEntity comment = findComment(commentId); + comment.validateArticle(articleId); + MemberEntity member = findMemberById(memberId); + comment.update(request.getContent(), member); return comment; } @Transactional - public void deleteComment(Long commentId, Long memberId) { - CommentEntity comment = findCommentAndValidateOwner(commentId, findMemberById(memberId)); - comment.softDelete(); - } - - private CommentEntity findCommentAndValidateOwner(Long commentId, MemberEntity member) { + public void deleteComment(Long articleId, Long commentId, Long memberId) { CommentEntity comment = findComment(commentId); - comment.validateOwner(member); - return comment; + comment.validateArticle(articleId); + MemberEntity member = findMemberById(memberId); + comment.softDelete(member); } private CommentEntity findComment(Long id) { diff --git a/src/main/java/com/config/jwt/AuthRequiredPath.java b/src/main/java/com/config/jwt/AuthRequiredPath.java index 5efdc8f..789a47b 100644 --- a/src/main/java/com/config/jwt/AuthRequiredPath.java +++ b/src/main/java/com/config/jwt/AuthRequiredPath.java @@ -12,12 +12,13 @@ public enum AuthRequiredPath { UPDATE_ARTICLE("/articles/*", List.of(HttpMethod.PUT, HttpMethod.PATCH)), DELETE_ARTICLE("/articles/*", List.of(HttpMethod.DELETE)), - CREATE_COMMENT("/comments", List.of(HttpMethod.POST)), - UPDATE_COMMENT("/comments/*", List.of(HttpMethod.PUT, HttpMethod.PATCH)), - DELETE_COMMENT("/comments/*", List.of(HttpMethod.DELETE)), + CREATE_COMMENT("/articles/*/comments", List.of(HttpMethod.POST)), + UPDATE_COMMENT("/articles/*/comments/*", List.of(HttpMethod.PUT, HttpMethod.PATCH)), + DELETE_COMMENT("/articles/*/comments/*", List.of(HttpMethod.DELETE)), - CREATE_MEMBER("/members/*", List.of(HttpMethod.POST)), - DELETE_MEMBER("/members/*", List.of(HttpMethod.DELETE)); + CREATE_MEMBER("/members", List.of(HttpMethod.POST)), + DELETE_MEMBER("/members/*", List.of(HttpMethod.DELETE)), + LOGOUT_MEMBER("/members/*", List.of(HttpMethod.POST)); private final String pathPattern; private final List methods; @@ -32,11 +33,10 @@ public static boolean isAuthRequired(HttpMethod method, String path) { } private static boolean pathMatchesPattern(String path, String pattern) { - if (pattern.endsWith("/*")) { - String base = pattern.substring(0, pattern.length() - 2); - return path.startsWith(base + "/"); - } else { - return path.equals(pattern); - } + String regexPattern = pattern + .replace(".", "\\.") + .replace("/*", "/[^/]+") + .replace("/**", "(/[^/]+)*"); + return path.matches("^" + regexPattern + "$"); } } diff --git a/src/main/java/com/exception/ErrorCodeType.java b/src/main/java/com/exception/ErrorCodeType.java index 8925408..87b6d70 100644 --- a/src/main/java/com/exception/ErrorCodeType.java +++ b/src/main/java/com/exception/ErrorCodeType.java @@ -10,7 +10,8 @@ public enum ErrorCodeType implements ErrorType { AUTHENTICATION_ERROR("AUTHENTICATION_ERROR", "인증 실패", HttpStatus.UNAUTHORIZED), AUTHORIZATION_ERROR("AUTHORIZATION_ERROR", "권한 없음", HttpStatus.FORBIDDEN), SYSTEM_ERROR("SYSTEM_ERROR", "시스템 오류", HttpStatus.INTERNAL_SERVER_ERROR), - DUPLICATE("SIGNUP_DUPLICATE", "중복발생", HttpStatus.CONFLICT); + DUPLICATE("SIGNUP_DUPLICATE", "중복발생", HttpStatus.CONFLICT), + MISMATCHED_DATA("MISMATCHED_DATA", "일치하지 않는 데이터", HttpStatus.BAD_REQUEST); private final String code; private final String message; diff --git a/src/main/java/com/exception/custom/NotIncludeBoardException.java b/src/main/java/com/exception/custom/NotIncludeBoardException.java new file mode 100644 index 0000000..f1f264a --- /dev/null +++ b/src/main/java/com/exception/custom/NotIncludeBoardException.java @@ -0,0 +1,24 @@ +package com.exception.custom; + +import com.exception.CustomException; +import com.exception.ErrorCodeType; +import java.util.Map; + +public class NotIncludeBoardException extends CustomException { + + private final Long articleId; + + private NotIncludeBoardException(Long articleId) { + super(ErrorCodeType.MISMATCHED_DATA); + this.articleId = articleId; + } + + @Override + public Map getAdditionalDetails() { + return Map.of("errorMessage", "해당 댓글은 게시글 " + articleId + "에 속하지 않습니다."); + } + + public static NotIncludeBoardException from(Long articleId) { + return new NotIncludeBoardException(articleId); + } +} diff --git a/src/test/java/com/board/controller/CommentControllerIntegrationTest.java b/src/test/java/com/board/controller/CommentControllerIntegrationTest.java index 43fd5fc..4772dbf 100644 --- a/src/test/java/com/board/controller/CommentControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/CommentControllerIntegrationTest.java @@ -1,5 +1,12 @@ package com.board.controller; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.board.dto.request.CommentCreateRequest; import com.board.dto.request.CommentUpdateRequest; import com.board.entity.ArticleEntity; @@ -18,10 +25,6 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @IntegrationTest class CommentControllerIntegrationTest { @@ -59,10 +62,10 @@ void setup() { @DisplayName("댓글 생성 성공") void 댓글_생성_성공() throws Exception { // Given - CommentCreateRequest request = new CommentCreateRequest("댓글 내용", article.getId()); + CommentCreateRequest request = new CommentCreateRequest("댓글 내용"); // When & Then - mockMvc.perform(post("/comments") + mockMvc.perform(post("/articles/" + article.getId() + "/comments") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) .header(AUTHORIZATION_HEADER, jwtToken)) // 가짜 인증 헤더 @@ -79,7 +82,7 @@ void setup() { CommentEntity comment2 = commentRepository.save(new CommentEntity("댓글 내용2", article, member)); // When & Then - mockMvc.perform(get("/comments") + mockMvc.perform(get("/articles/" + article.getId() + "/comments") .param("articleId", String.valueOf(article.getId())) .param("page", "0") .param("size", "10") @@ -98,7 +101,7 @@ void setup() { CommentUpdateRequest request = new CommentUpdateRequest("수정된 댓글 내용", comment.getId()); // When & Then - mockMvc.perform(patch("/comments/" + comment.getId()) + mockMvc.perform(patch("/articles/" + article.getId() + "/comments/" + comment.getId()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) .header(AUTHORIZATION_HEADER, jwtToken)) // JWT 인증 헤더 추가 @@ -114,7 +117,7 @@ void setup() { CommentEntity comment = commentRepository.save(new CommentEntity("댓글 내용", article, member)); // When & Then - mockMvc.perform(delete("/comments/" + comment.getId()) + mockMvc.perform(delete("/articles/" + article.getId() + "/comments/" + comment.getId()) .header(AUTHORIZATION_HEADER, jwtToken)) // JWT 인증 헤더 추가 .andExpect(status().isNoContent()); } diff --git a/src/test/java/com/board/controller/CommentControllerTest.java b/src/test/java/com/board/controller/CommentControllerTest.java index d92bee7..837412f 100644 --- a/src/test/java/com/board/controller/CommentControllerTest.java +++ b/src/test/java/com/board/controller/CommentControllerTest.java @@ -1,7 +1,7 @@ package com.board.controller; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -45,22 +45,30 @@ class CommentControllerTest { @Autowired private ObjectMapper objectMapper; + private final Long articleId = 1L; + private final Long commentId = 10L; + private final Long memberId = 100L; + @BeforeEach void setup() throws Exception { when(authenticatedMemberArgumentResolver.supportsParameter(any())).thenReturn(true); when(authenticatedMemberArgumentResolver.resolveArgument(any(), any(), any(), any())) - .thenReturn(1L); // Long memberId 주입 + .thenReturn(memberId); // @AuthenticatedMember 역할 } @Test @DisplayName("댓글 생성 성공") void 댓글_생성() throws Exception { - CommentCreateRequest request = new CommentCreateRequest("댓글 내용", 1L); + // given + CommentCreateRequest request = new CommentCreateRequest("댓글 내용"); MemberEntity member = new MemberEntity("이메일", "패스워드", "닉네임"); CommentEntity comment = new CommentEntity("댓글 내용", null, member); - when(commentService.createComment(any(CommentCreateRequest.class), anyLong())).thenReturn(comment); - mockMvc.perform(post("/comments") + when(commentService.createComment(eq(articleId), any(CommentCreateRequest.class), eq(memberId))) + .thenReturn(comment); + + // when & then + mockMvc.perform(post("/articles/{articleId}/comments", articleId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) @@ -68,24 +76,21 @@ void setup() throws Exception { .andExpect(jsonPath("$.authorName").value("닉네임")); } - @Test - @DisplayName("댓글_전체_조회_성공") + @DisplayName("댓글 전체 조회 성공") void 댓글_전체_조회_성공() throws Exception { - // Given + // given MemberEntity member = new MemberEntity("email", "password", "nickName"); ArticleEntity article = new ArticleEntity("title", "content", member); - - List responses = List.of( + List comments = List.of( new CommentEntity("내용1", article, member), new CommentEntity("내용2", article, member)); - Page pageResult = new PageImpl<>(responses); + Page pageResult = new PageImpl<>(comments); - when(commentService.findAllComments(anyLong(), any())).thenReturn(pageResult); + when(commentService.findAllComments(eq(articleId), any())).thenReturn(pageResult); - // When & Then - mockMvc.perform(get("/comments") - .param("articleId", "1") + // when & then + mockMvc.perform(get("/articles/{articleId}/comments", articleId) .param("page", "0") .param("size", "10")) .andExpect(status().isOk()) @@ -97,15 +102,16 @@ void setup() throws Exception { @Test @DisplayName("댓글 수정 성공") void 댓글_수정_성공() throws Exception { - // Given - CommentUpdateRequest request = new CommentUpdateRequest("수정된 댓글 내용", 1L); + // given + CommentUpdateRequest request = new CommentUpdateRequest("수정된 댓글 내용", commentId); MemberEntity member = new MemberEntity("email", "password", "nickName"); CommentEntity updatedComment = new CommentEntity("수정된 댓글 내용", null, member); - when(commentService.updateComment(any(CommentUpdateRequest.class), anyLong())).thenReturn(updatedComment); + when(commentService.updateComment(eq(articleId), eq(commentId), any(CommentUpdateRequest.class), eq(memberId))) + .thenReturn(updatedComment); - // When & Then - mockMvc.perform(patch("/comments/1") + // when & then + mockMvc.perform(patch("/articles/{articleId}/comments/{commentId}", articleId, commentId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) @@ -116,8 +122,7 @@ void setup() throws Exception { @Test @DisplayName("댓글 삭제 성공") void 댓글_삭제_성공() throws Exception { - // When & Then - mockMvc.perform(delete("/comments/1")) + mockMvc.perform(delete("/articles/{articleId}/comments/{commentId}", articleId, commentId)) .andExpect(status().isNoContent()); } } diff --git a/src/test/java/com/board/service/CommentServiceTest.java b/src/test/java/com/board/service/CommentServiceTest.java index e5d11ba..f0aabd4 100644 --- a/src/test/java/com/board/service/CommentServiceTest.java +++ b/src/test/java/com/board/service/CommentServiceTest.java @@ -1,15 +1,25 @@ package com.board.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + import com.board.dto.request.CommentCreateRequest; import com.board.dto.request.CommentUpdateRequest; import com.board.entity.ArticleEntity; import com.board.entity.CommentEntity; import com.board.repository.CommentRepository; import com.exception.custom.DifferentOwnerException; +import com.exception.custom.MyEntityNotFoundException; +import com.exception.custom.NotIncludeBoardException; import com.member.entity.MemberEntity; import com.member.service.MemberService; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -20,14 +30,6 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) class CommentServiceTest { @@ -40,130 +42,213 @@ class CommentServiceTest { @Mock private MemberService memberService; + private MemberEntity testMember; + private ArticleEntity testArticle; + private CommentEntity testComment; + private Long articleId = 1L; + private Long memberId = 1L; + private Long commentId = 3L; + + @BeforeEach + void setUp() { + testMember = new MemberEntity(memberId, "test@example.com", "password", "nickname"); + testArticle = new ArticleEntity(articleId, "제목", "내용", testMember, 0); + testComment = new CommentEntity(commentId, "기존 댓글 내용", testArticle, testMember); + } + @Test - @DisplayName("게시글의 댓글들 조회") - void 댓글_조회_성공() { + @DisplayName("특정 게시글의 댓글 목록을 페이지 단위로 조회한다.") + void 페이지_조회() { // Given - MemberEntity member = new MemberEntity("test@example.com", "password", "nickname"); - MemberEntity member2 = new MemberEntity("member2@example.com", "password", "member2"); - MemberEntity member3 = new MemberEntity("member3@example.com", "password", "member3"); - ArticleEntity article = new ArticleEntity("제목", "내용", member); + MemberEntity member2 = new MemberEntity(2L, "member2@example.com", "password", "member2"); + CommentEntity comment2 = new CommentEntity(2L, "댓글내용2", testArticle, member2); + Pageable pageable = PageRequest.of(0, 10); + List comments = List.of(testComment, comment2); + Page commentPage = new PageImpl<>(comments, pageable, comments.size()); + + when(articleService.findById(articleId)).thenReturn(testArticle); + when(commentRepository.findByArticleAndIsDeletedFalse(testArticle, pageable)).thenReturn(commentPage); - CommentEntity comment = new CommentEntity("댓글내용1", article, member); - CommentEntity comment2 = new CommentEntity("댓글내용2", article, member2); - CommentEntity comment3 = new CommentEntity("댓글내용3", article, member3); - CommentEntity comment4 = new CommentEntity("댓글내용4", article, member3); + // When + Page result = commentService.findAllComments(articleId, pageable); + // Then + assertThat(result.getContent()).hasSize(2); + assertThat(result.getContent().get(0).getContent()).isEqualTo(testComment.getContent()); + assertThat(result.getContent().get(0).getMember().getId()).isEqualTo(testMember.getId()); + } + + @Test + @DisplayName("존재하지 않는 게시글 ID로 댓글 목록을 조회하면 예외가 발생한다.") + void 존재하지_않는_게시글_ID로_댓글_목록을_조회하면_예외가_발생() { + // Given Pageable pageable = PageRequest.of(0, 10); + when(articleService.findById(articleId)).thenThrow(MyEntityNotFoundException.class); - List commentEntityList = List.of(comment, comment2, comment3, comment4); - Page commentPage = new PageImpl<>(commentEntityList, pageable, commentEntityList.size()); + // When & Then + assertThatThrownBy(() -> commentService.findAllComments(articleId, pageable)) + .isInstanceOf(MyEntityNotFoundException.class); + } + + @Test + @DisplayName("새로운 댓글을 생성하고 저장한다.") + void 새로운_댓글을_생성하고_저장() { + // Given + String content = "새로운 댓글 내용"; + CommentCreateRequest request = new CommentCreateRequest(content); + CommentEntity savedComment = new CommentEntity(2L, content, testArticle, testMember); - when(commentRepository.findByArticleIdAndIsDeletedFalse(anyLong(), any(Pageable.class))) - .thenReturn(commentPage); + when(articleService.findById(articleId)).thenReturn(testArticle); + when(memberService.findById(memberId)).thenReturn(testMember); + when(commentRepository.save(any(CommentEntity.class))).thenReturn(savedComment); // When - Page returnCommentsList = commentService.findAllComments(1L, pageable); + CommentEntity result = commentService.createComment(articleId, request, memberId); - // then - assertEquals(4, returnCommentsList.getContent().size()); - assertEquals("댓글내용1", returnCommentsList.getContent().get(0).getContent()); - assertEquals(member3, returnCommentsList.getContent().get(3).getMember()); + // Then + assertThat(result.getContent()).isEqualTo(content); + assertThat(result.getArticle().getId()).isEqualTo(articleId); + assertThat(result.getMember().getId()).isEqualTo(memberId); + } + + @Test + @DisplayName("존재하지 않는 게시글에 댓글을 생성하려고 하면 예외가 발생한다.") + void 존재하지_않는_게시글에_댓글을_생성하려고_하면_예외가_발생() { + // Given + CommentCreateRequest request = new CommentCreateRequest("새로운 댓글 내용"); + when(articleService.findById(articleId)).thenThrow(MyEntityNotFoundException.class); + + // When & Then + assertThatThrownBy(() -> commentService.createComment(articleId, request, memberId)) + .isInstanceOf(MyEntityNotFoundException.class); } @Test - @DisplayName("댓글 생성 성공") - void 댓글_생성_성공() { + @DisplayName("존재하지 않는 회원으로 댓글을 생성하려고 하면 예외가 발생한다.") + void 존재하지_않는_회원으로_댓글을_생성하려고_하면_예외가_발생() { // Given - Long memberId = 1L; - Long articleId = 2L; - String content = "댓글 내용"; - CommentCreateRequest request = new CommentCreateRequest(content, articleId); + CommentCreateRequest request = new CommentCreateRequest("새로운 댓글 내용"); + when(articleService.findById(articleId)).thenReturn(testArticle); + when(memberService.findById(memberId)).thenThrow(MyEntityNotFoundException.class); + + // When & Then + assertThatThrownBy(() -> commentService.createComment(articleId, request, memberId)) + .isInstanceOf(MyEntityNotFoundException.class); + } - MemberEntity member = new MemberEntity("test@example.com", "password", "nickname"); - ArticleEntity article = new ArticleEntity("제목", "내용", member); - CommentEntity comment = new CommentEntity(content, article, member); + @Test + @DisplayName("댓글 내용을 수정한다.") + void 댓글_내용을_수정() { + // Given + String updatedContent = "수정된 댓글 내용"; + CommentUpdateRequest request = new CommentUpdateRequest(updatedContent, commentId); - when(articleService.findById(articleId)).thenReturn(article); - when(memberService.findById(memberId)).thenReturn(member); - when(commentRepository.save(any(CommentEntity.class))).thenReturn(comment); + when(commentRepository.findByIdAndIsDeletedFalse(commentId)).thenReturn(Optional.of(testComment)); + when(memberService.findById(memberId)).thenReturn(testMember); // When - CommentEntity createdComment = commentService.createComment(request, memberId); + CommentEntity result = commentService.updateComment(articleId, commentId, request, memberId); // Then - assertNotNull(createdComment); - assertEquals(content, createdComment.getContent()); - assertEquals(article, createdComment.getArticle()); - assertEquals(member, createdComment.getMember()); + assertThat(result.getContent()).isEqualTo(updatedContent); } @Test - @DisplayName("댓글_수정_성공") - void 댓글_수정_성공() { + @DisplayName("존재하지 않는 댓글을 수정하려고 하면 예외가 발생한다.") + void 존재하지_않는_댓글을_수정하려고_하면_예외가_발생() { // Given - Long memberId = 1L; - Long commentId = 3L; - String updatedContent = "수정된 댓글 내용"; - CommentUpdateRequest request = new CommentUpdateRequest(updatedContent, commentId); + CommentUpdateRequest request = new CommentUpdateRequest("수정된 댓글 내용", commentId); + when(commentRepository.findByIdAndIsDeletedFalse(commentId)).thenReturn(Optional.empty()); - MemberEntity member = new MemberEntity("test@example.com", "password", "nickname"); - ArticleEntity article = new ArticleEntity("제목", "내용", member); - CommentEntity comment = new CommentEntity("기존 댓글 내용", article, member); + // When & Then + assertThatThrownBy(() -> commentService.updateComment(articleId, commentId, request, memberId)) + .isInstanceOf(MyEntityNotFoundException.class); + } - when(commentRepository.findByIdAndIsDeletedFalse(commentId)).thenReturn(Optional.of(comment)); - when(memberService.findById(memberId)).thenReturn(member); + @Test + @DisplayName("수정하려는 댓글이 해당 게시글에 속하지 않으면 예외가 발생한다.") + void 수정하려는_댓글이_해당_게시글에_속하지_않으면_예외가_발생() { + + ArticleEntity anotherArticle = new ArticleEntity(999L, "제목", "내용", testMember, 0); + CommentEntity anotherComment = new CommentEntity(commentId, "기존 댓글 내용", anotherArticle, testMember); + + // Given + CommentUpdateRequest request = new CommentUpdateRequest("수정된 댓글 내용", commentId); + + when(commentRepository.findByIdAndIsDeletedFalse(commentId)).thenReturn(Optional.of(anotherComment)); + + // When & Then + assertThatThrownBy(() -> commentService.updateComment(articleId, commentId, request, memberId)) + .isInstanceOf(NotIncludeBoardException.class); + } + + @Test + @DisplayName("댓글을 soft delete 한다.") + void 댓글을_soft_delete_한다() { + // Given + when(commentRepository.findByIdAndIsDeletedFalse(commentId)).thenReturn(Optional.of(testComment)); + when(memberService.findById(memberId)).thenReturn(testMember); // When - CommentEntity updatedComment = commentService.updateComment(request, memberId); + commentService.deleteComment(articleId, commentId, memberId); // Then - assertNotNull(updatedComment); - assertEquals(updatedContent, updatedComment.getContent()); + assertThatCode(() -> commentService.deleteComment(articleId, commentId, memberId)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("존재하지 않는 댓글을 삭제하려고 하면 예외가 발생한다.") + void 존재하지_않는_댓글을_삭제하려고_하면_예외가_발생() { + // given + when(commentRepository.findByIdAndIsDeletedFalse(commentId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> commentService.deleteComment(articleId, commentId, memberId)) + .isInstanceOf(MyEntityNotFoundException.class); + } + + @Test + @DisplayName("삭제하려는 댓글이 해당 게시글에 속하지 않으면 예외가 발생한다.") + void 삭제하려는_댓글이_해당_게시글에_속하지_않으면_예외가_발생() { + // Given + ArticleEntity anotherArticle = new ArticleEntity(999L, "제목", "내용", testMember, 0); + CommentEntity anotherComment = new CommentEntity(commentId, "기존 댓글 내용", anotherArticle, testMember); + + // when + when(commentRepository.findByIdAndIsDeletedFalse(commentId)).thenReturn(Optional.of(anotherComment)); + + // then + assertThatThrownBy(() -> commentService.deleteComment(articleId, commentId, memberId)) + .isInstanceOf(NotIncludeBoardException.class); } + @Test + @DisplayName("삭제 권한이 없는 사용자가 댓글을 삭제하려고 하면 예외가 발생한다.") + void 삭제_권한이_없는_사용자가_댓글을_삭제하려고_하면_예외가_발생() { + // given + MemberEntity anotherMember = new MemberEntity(999L, "not-owner@example.com", "pw", "nick"); + + when(commentRepository.findByIdAndIsDeletedFalse(commentId)).thenReturn(Optional.of(testComment)); + when(memberService.findById(anotherMember.getId())).thenReturn(anotherMember); + + // when & then + assertThatThrownBy(() -> commentService.deleteComment(articleId, commentId, anotherMember.getId())) + .isInstanceOf(DifferentOwnerException.class); + } - @Nested - @DisplayName("댓글 삭제 테스트") - class deleteTest { - @Test - @DisplayName("댓글_삭제_성공") - void 댓글_삭제_성공() { - // Given - Long memberId = 1L; - Long commentId = 3L; - - MemberEntity member = new MemberEntity("test@example.com", "password", "nickname"); - ArticleEntity article = new ArticleEntity("제목", "내용", member); - CommentEntity comment = new CommentEntity("기존 댓글 내용", article, member); - - when(commentRepository.findByIdAndIsDeletedFalse(commentId)).thenReturn(Optional.of(comment)); - when(memberService.findById(memberId)).thenReturn(member); - - // When - commentService.deleteComment(commentId, memberId); - - // Then - assertEquals(true, comment.isDeleted()); - } - - @Test - @DisplayName("작성자 다를 경우 삭제 실패") - void 작성자_다르면_댓글_삭제_실패() { - // Given - Long memberId = 1L; - Long commentId = 3L; - - MemberEntity member = new MemberEntity(10L, "test@example.com", "password", "nickname"); - MemberEntity anotherMember = new MemberEntity(11L, "another@example.com", "anotherPw", "another"); - ArticleEntity article = new ArticleEntity("제목", "내용", member); - CommentEntity comment = new CommentEntity("기존 댓글 내용", article, anotherMember); - - when(commentRepository.findByIdAndIsDeletedFalse(commentId)).thenReturn(Optional.of(comment)); - when(memberService.findById(memberId)).thenReturn(member); - - // When & Then - assertThrows(DifferentOwnerException.class, () -> commentService.deleteComment(commentId, memberId)); - } + @Test + @DisplayName("수정 권한이 없는 사용자가 댓글을 수정하려고 하면 예외가 발생한다.") + void 수정_권한이_없는_사용자가_댓글을_수정하려고_하면_예외가_발생() { + // given + MemberEntity anotherMember = new MemberEntity(999L, "not-owner@example.com", "pw", "nick"); + + CommentUpdateRequest request = new CommentUpdateRequest("수정된 댓글 내용", commentId); + when(commentRepository.findByIdAndIsDeletedFalse(commentId)).thenReturn(Optional.of(testComment)); + when(memberService.findById(anotherMember.getId())).thenReturn(anotherMember); + + // when & then + assertThatThrownBy(() -> commentService.updateComment(articleId, commentId, request, anotherMember.getId())) + .isInstanceOf(DifferentOwnerException.class); } } From 0b9d34f310a0dea58027f8dcaef864c4bcdadff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Thu, 15 May 2025 20:45:43 +0900 Subject: [PATCH 36/45] =?UTF-8?q?refactor=20:=20cookie=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=82=B4=EC=9A=A9=20=EC=A0=84=EB=B6=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=20-=20=EC=9D=B8=EC=A6=9D=20=EC=8B=9C=20=EC=A0=84?= =?UTF-8?q?=EB=B6=80=20jwt=20+=20header=20=EC=9D=B8=EC=A6=9D=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/config/jwt/JwtAuthFilter.java | 5 ++- src/main/java/com/config/jwt/JwtUtil.java | 22 +++------- .../member/controller/MemberController.java | 15 +------ .../controller/PublicMemberController.java | 8 ---- .../java/com/util/cookie/CookieUtils.java | 22 ---------- .../ArticleControllerIntegrationTest.java | 42 +++++++------------ .../MemberControllerIntegrationTest.java | 31 ++++++-------- ...PublicMemberControllerIntegrationTest.java | 15 +++---- 8 files changed, 45 insertions(+), 115 deletions(-) delete mode 100644 src/main/java/com/util/cookie/CookieUtils.java diff --git a/src/main/java/com/config/jwt/JwtAuthFilter.java b/src/main/java/com/config/jwt/JwtAuthFilter.java index 0ef410d..111438f 100644 --- a/src/main/java/com/config/jwt/JwtAuthFilter.java +++ b/src/main/java/com/config/jwt/JwtAuthFilter.java @@ -5,11 +5,12 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpMethod; import org.springframework.web.filter.OncePerRequestFilter; +import java.io.IOException; + @RequiredArgsConstructor public class JwtAuthFilter extends OncePerRequestFilter { @@ -29,7 +30,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse return; } - String token = jwtUtil.extractToken(request); + String token = jwtUtil.extractFromHeader(request); if (token != null && jwtUtil.isTokenValid(token)) { String email = jwtUtil.extractEmail(token); authUtil.saveAuthenticatedMember(email); diff --git a/src/main/java/com/config/jwt/JwtUtil.java b/src/main/java/com/config/jwt/JwtUtil.java index a8e5319..b117caf 100644 --- a/src/main/java/com/config/jwt/JwtUtil.java +++ b/src/main/java/com/config/jwt/JwtUtil.java @@ -4,14 +4,13 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Date; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.nio.charset.StandardCharsets; +import java.util.Date; + @RequiredArgsConstructor @Service public class JwtUtil { @@ -22,19 +21,8 @@ public class JwtUtil { private static final long ACCESS_TOKEN_EXPIRATION = 1000 * 60 * 60; // 1시간 private final JwtProperties jwtProperties; - - public String extractToken(HttpServletRequest request) { - if (request.getCookies() != null) { - return Arrays.stream(request.getCookies()) - .filter(cookie -> COOKIE_NAME.equals(cookie.getName())) - .map(Cookie::getValue) - .findFirst() - .orElseGet(() -> extractFromHeader(request)); // 없으면 헤더에서 추출 - } - return extractFromHeader(request); // 쿠키 자체가 없으면 바로 헤더에서 추출 - } - - private String extractFromHeader(HttpServletRequest request) { + + public String extractFromHeader(HttpServletRequest request) { String header = request.getHeader(AUTHORIZATION_HEADER); if (header != null && header.startsWith(BEARER_PREFIX)) { return header.substring(BEARER_PREFIX.length()); diff --git a/src/main/java/com/member/controller/MemberController.java b/src/main/java/com/member/controller/MemberController.java index 7845487..35d72d4 100644 --- a/src/main/java/com/member/controller/MemberController.java +++ b/src/main/java/com/member/controller/MemberController.java @@ -2,9 +2,6 @@ import com.config.auth.annotation.AuthenticatedMember; import com.member.service.MemberService; -import com.util.cookie.CookieUtils; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -20,22 +17,14 @@ public class MemberController { private final MemberService memberService; @PostMapping("/logout") - public ResponseEntity logout(@AuthenticatedMember Long memberId, - HttpServletResponse response) { + public ResponseEntity logout(@AuthenticatedMember Long memberId) { memberService.logout(memberId); - - Cookie cookie = CookieUtils.invalidateCookie("token"); - response.addCookie(cookie); return ResponseEntity.noContent().build(); } @DeleteMapping("/withdraw") - public ResponseEntity withdraw(@AuthenticatedMember Long memberId, - HttpServletResponse response) { + public ResponseEntity withdraw(@AuthenticatedMember Long memberId) { memberService.withdraw(memberId); - - Cookie cookie = CookieUtils.invalidateCookie("token"); - response.addCookie(cookie); return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/com/member/controller/PublicMemberController.java b/src/main/java/com/member/controller/PublicMemberController.java index 2268dcd..8ae9cda 100644 --- a/src/main/java/com/member/controller/PublicMemberController.java +++ b/src/main/java/com/member/controller/PublicMemberController.java @@ -5,8 +5,6 @@ import com.member.dto.response.LoginResponse; import com.member.dto.response.SignUpResponse; import com.member.service.MemberService; -import com.util.cookie.CookieUtils; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -33,12 +31,6 @@ public ResponseEntity signUp(@Valid @RequestBody SignUpRequest r public ResponseEntity login(@Valid @RequestBody LoginRequest loginRequest, HttpServletResponse response) { LoginResponse loginResponse = memberService.login(loginRequest); - - Cookie cookie = CookieUtils.createCookie("token", - loginResponse.getAccessToken(), - (int) loginResponse.getExpirationTime()); - response.addCookie(cookie); - return ResponseEntity.ok(loginResponse); } } diff --git a/src/main/java/com/util/cookie/CookieUtils.java b/src/main/java/com/util/cookie/CookieUtils.java deleted file mode 100644 index 4acdee1..0000000 --- a/src/main/java/com/util/cookie/CookieUtils.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.util.cookie; - -import jakarta.servlet.http.Cookie; - -public class CookieUtils { - - public static Cookie createCookie(String name, String value, int maxAge) { - Cookie cookie = new Cookie(name, value); - cookie.setHttpOnly(true); - cookie.setPath("/"); - cookie.setMaxAge(maxAge); - return cookie; - } - - public static Cookie invalidateCookie(String name) { - Cookie cookie = new Cookie(name, null); - cookie.setHttpOnly(true); - cookie.setPath("/"); - cookie.setMaxAge(0); // 쿠키 삭제 - return cookie; - } -} diff --git a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java index 0e84ae5..b45f7c9 100644 --- a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java @@ -1,15 +1,5 @@ package com.board.controller; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.board.dto.request.ArticleCreateRequest; import com.board.dto.request.ArticleUpdateRequest; import com.board.entity.ArticleEntity; @@ -22,7 +12,6 @@ import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; import com.support.IntegrationTest; -import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -33,6 +22,12 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @IntegrationTest class ArticleControllerIntegrationTest { @@ -58,8 +53,7 @@ class ArticleControllerIntegrationTest { private static final String MEMBER_PASSWORD = "12345"; private static final String MEMBER_NICKNAME = "setupMemberNickname"; private static final int JWT_EXPIRATION_TIME = 3600000; - - private String tokenCookie; + private String accessToken; @BeforeEach void setUp() throws Exception { @@ -77,23 +71,18 @@ private void signUpAndLogin() throws Exception { .andExpect(jsonPath("$.email").value(MEMBER_EMAIL)) .andExpect(jsonPath("$.nickName").value(MEMBER_NICKNAME)); - MvcResult loginResult = sendPostRequest("/public/members/login", + sendPostRequest("/public/members/login", new LoginRequest(MEMBER_EMAIL, MEMBER_PASSWORD)) .andExpect(status().isOk()) - .andExpect(cookie().exists("token")) .andExpect(jsonPath("$.accessToken").exists()) .andExpect(jsonPath("$.accessToken").isNotEmpty()) .andExpect(jsonPath("$.expirationTime").value(JWT_EXPIRATION_TIME)) .andDo(result -> { String responseBody = result.getResponse().getContentAsString(); - String token = objectMapper.readTree(responseBody).get("accessToken").asText(); - assertThat(jwtUtil.extractEmail(token)).isEqualTo(MEMBER_EMAIL); - assertThat(jwtUtil.isTokenValid(token)).isTrue(); - }).andReturn(); - - tokenCookie = loginResult.getResponse().getCookie("token").getValue(); - assertThat(jwtUtil.isTokenValid(tokenCookie)).isTrue(); - assertThat(jwtUtil.extractEmail(tokenCookie)).isEqualTo(MEMBER_EMAIL); + this.accessToken = objectMapper.readTree(responseBody).get("accessToken").asText(); + assertThat(jwtUtil.extractEmail(accessToken)).isEqualTo(MEMBER_EMAIL); + assertThat(jwtUtil.isTokenValid(accessToken)).isTrue(); + }); } @Test @@ -109,7 +98,7 @@ private ResultActions sendPostRequest(String url, Object request) throws Excepti return mockMvc.perform(post(url) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) - .cookie(new Cookie("token", tokenCookie))); + .header("Authorization", "Bearer " + accessToken)); } @@ -193,7 +182,8 @@ void notLoginDeleteArticleTest() throws Exception { void deleteArticleTest() throws Exception { Long articleId = createArticleAndGetId("테스트 제목", "테스트 내용"); - mockMvc.perform(delete("/articles/" + articleId).cookie(new Cookie("token", tokenCookie))) + mockMvc.perform(delete("/articles/" + articleId) + .header("Authorization", "Bearer " + accessToken)) .andExpect(status().isNoContent()); ArticleEntity deletedArticle = articleRepository.findById(articleId) @@ -216,7 +206,7 @@ private ResultActions sendPutRequest(String url, Object request) throws Exceptio return mockMvc.perform(put(url) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) - .cookie(new Cookie("token", tokenCookie))); + .header("Authorization", "Bearer " + accessToken)); } private MemberEntity createAndSaveMember(String email, String nickname) { diff --git a/src/test/java/com/member/controller/MemberControllerIntegrationTest.java b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java index ddfdfb4..ccebdb1 100644 --- a/src/test/java/com/member/controller/MemberControllerIntegrationTest.java +++ b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java @@ -1,11 +1,5 @@ package com.member.controller; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.board.dto.request.ArticleCreateRequest; import com.config.jwt.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; @@ -13,7 +7,6 @@ import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; import com.support.IntegrationTest; -import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -22,6 +15,11 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @IntegrationTest class MemberControllerIntegrationTest { @@ -57,8 +55,7 @@ void setUp() { // When: 로그아웃 요청 mockMvc.perform(post("/members/logout") .header("Authorization", "Bearer " + token)) - .andExpect(status().isNoContent()) - .andExpect(cookie().maxAge("token", 0)); // Then: 쿠키 만료 확인 + .andExpect(status().isNoContent()); } @Test @@ -69,9 +66,8 @@ void setUp() { // When: 회원 탈퇴 요청 mockMvc.perform(delete("/members/withdraw") - .cookie(new Cookie("token", token))) - .andExpect(status().isNoContent()) - .andExpect(cookie().maxAge("token", 0)); + .header("Authorization", "Bearer " + token)) + .andExpect(status().isNoContent()); // Then: 로그인 실패 LoginRequest loginRequest = new LoginRequest(setUpMemberEmail, setUpMemberPassword); @@ -91,9 +87,8 @@ void setUp() { // 로그아웃 mockMvc.perform(post("/members/logout") - .cookie(new Cookie("token", token))) - .andExpect(status().isNoContent()) - .andExpect(cookie().maxAge("token", 0)); // 쿠키가 만료되었는지 확인 + .header("Authorization", "Bearer " + token)) + .andExpect(status().isNoContent()); // 이후 요청 시 → 쿠키 없음 (즉, 인증 실패 유도) mockMvc.perform(post("/articles") // 인증 필요한 API @@ -110,7 +105,7 @@ void setUp() { // When: 회원 탈퇴 요청 mockMvc.perform(delete("/members/withdraw") - .cookie(new Cookie("token", token))) + .header("Authorization", "Bearer " + token)) .andExpect(status().isNoContent()); // Then: 인증이 필요한 요청 시 실패 @@ -130,7 +125,7 @@ private String getAccessToken() throws Exception { .andExpect(status().isOk()) .andReturn(); - return result.getResponse().getCookie("token").getValue(); // JWT 토큰 쿠키 값 반환 + String responseBody = result.getResponse().getContentAsString(); + return objectMapper.readTree(responseBody).get("accessToken").asText(); // JWT 토큰 쿠키 값 반환 } - } diff --git a/src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java b/src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java index 39146a2..0a19167 100644 --- a/src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java +++ b/src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java @@ -1,11 +1,5 @@ package com.member.controller; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.config.jwt.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.member.dto.request.LoginRequest; @@ -21,9 +15,14 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @IntegrationTest class PublicMemberControllerIntegrationTest { - + @Autowired private MockMvc mockMvc; @@ -57,7 +56,6 @@ void loginTest() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(loginJson)) .andExpect(status().isOk()) - .andExpect(cookie().exists("token")) // JWT 쿠키 존재 확인 .andExpect(jsonPath("$.accessToken").exists()); // accessToken 존재 확인 } @@ -72,7 +70,6 @@ void loginAndValidateJwtTest() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(loginJson)) .andExpect(status().isOk()) - .andExpect(cookie().exists("token")) .andExpect(jsonPath("$.accessToken").exists()) .andExpect(jsonPath("$.accessToken").isNotEmpty()) .andReturn(); From df2932a6b6566b629870ede4bd6a3339d3e3cdcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Sun, 18 May 2025 14:43:25 +0900 Subject: [PATCH 37/45] =?UTF-8?q?feat=20:=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=20-=20"/reissue"=20=EC=9A=94=EC=B2=AD=EC=8B=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=ED=95=98=EC=97=AC=20=EC=97=91=EC=84=B8?= =?UTF-8?q?=EC=8A=A4=20=ED=86=A0=ED=81=B0=20=EB=B0=9C=ED=96=89=20=20-=20?= =?UTF-8?q?=EC=9D=BC=EB=B0=98=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20?= =?UTF-8?q?=EC=95=A1=EC=84=B8=EC=8A=A4/=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EB=AA=A8=EB=91=90=20=EB=B0=9C?= =?UTF-8?q?=EA=B8=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/config/jwt/token/RefreshToken.java | 30 ++++ .../jwt/token/RefreshTokenRepository.java | 6 + .../config/jwt/token/RefreshTokenService.java | 41 ++++++ .../java/com/exception/ErrorCodeType.java | 3 +- .../com/exception/custom/InvalidToken.java | 15 ++ .../member/controller/MemberController.java | 7 + .../com/member/service/MemberService.java | 2 + .../com/member/service/MemberServiceImpl.java | 35 ++++- .../ArticleControllerIntegrationTest.java | 11 +- .../jwt/token/RefreshTokenServiceTest.java | 135 ++++++++++++++++++ .../MemberControllerIntegrationTest.java | 61 +++++++- .../controller/MemberControllerTest.java | 45 +++++- .../member/service/MemberServiceImplTest.java | 72 +++++++++- 13 files changed, 438 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/config/jwt/token/RefreshToken.java create mode 100644 src/main/java/com/config/jwt/token/RefreshTokenRepository.java create mode 100644 src/main/java/com/config/jwt/token/RefreshTokenService.java create mode 100644 src/main/java/com/exception/custom/InvalidToken.java create mode 100644 src/test/java/com/config/jwt/token/RefreshTokenServiceTest.java diff --git a/src/main/java/com/config/jwt/token/RefreshToken.java b/src/main/java/com/config/jwt/token/RefreshToken.java new file mode 100644 index 0000000..6777a35 --- /dev/null +++ b/src/main/java/com/config/jwt/token/RefreshToken.java @@ -0,0 +1,30 @@ +package com.config.jwt.token; + +import com.common.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Table(name = "refreshToken") +@AllArgsConstructor +public class RefreshToken extends BaseEntity { + + @Id + @Column(name = "member_id", updatable = false) + private Long memberId; + + @Column(nullable = false) + private String token; + + public void update(String newToken) { + this.token = newToken; + } +} diff --git a/src/main/java/com/config/jwt/token/RefreshTokenRepository.java b/src/main/java/com/config/jwt/token/RefreshTokenRepository.java new file mode 100644 index 0000000..1410402 --- /dev/null +++ b/src/main/java/com/config/jwt/token/RefreshTokenRepository.java @@ -0,0 +1,6 @@ +package com.config.jwt.token; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RefreshTokenRepository extends JpaRepository { +} diff --git a/src/main/java/com/config/jwt/token/RefreshTokenService.java b/src/main/java/com/config/jwt/token/RefreshTokenService.java new file mode 100644 index 0000000..523ee25 --- /dev/null +++ b/src/main/java/com/config/jwt/token/RefreshTokenService.java @@ -0,0 +1,41 @@ +package com.config.jwt.token; + +import com.exception.custom.MyEntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class RefreshTokenService { + + private final RefreshTokenRepository refreshTokenRepository; + + @Transactional + public void mergeToken(Long memberId, String token) { + RefreshToken refreshToken = refreshTokenRepository.findById(memberId) + .orElse(null); + if (refreshToken != null) { + refreshToken.update(token); + return; + } + refreshTokenRepository.save(new RefreshToken(memberId, token)); + } + + public boolean isTokenValid(Long memberId, String token) { + return refreshTokenRepository.findById(memberId) + .map(saved -> saved.getToken().equals(token)) + .orElse(false); + } + + @Transactional + public void deleteToken(Long memberId) { + refreshTokenRepository.deleteById(memberId); + } + + public RefreshToken findByMemberId(Long memberId) { + return refreshTokenRepository.findById(memberId) + .orElseThrow(() -> MyEntityNotFoundException.from(memberId)); + } +} diff --git a/src/main/java/com/exception/ErrorCodeType.java b/src/main/java/com/exception/ErrorCodeType.java index 87b6d70..10a8881 100644 --- a/src/main/java/com/exception/ErrorCodeType.java +++ b/src/main/java/com/exception/ErrorCodeType.java @@ -11,7 +11,8 @@ public enum ErrorCodeType implements ErrorType { AUTHORIZATION_ERROR("AUTHORIZATION_ERROR", "권한 없음", HttpStatus.FORBIDDEN), SYSTEM_ERROR("SYSTEM_ERROR", "시스템 오류", HttpStatus.INTERNAL_SERVER_ERROR), DUPLICATE("SIGNUP_DUPLICATE", "중복발생", HttpStatus.CONFLICT), - MISMATCHED_DATA("MISMATCHED_DATA", "일치하지 않는 데이터", HttpStatus.BAD_REQUEST); + MISMATCHED_DATA("MISMATCHED_DATA", "일치하지 않는 데이터", HttpStatus.BAD_REQUEST), + INVALID_REFRESH_TOKEN("INVALID_REFRESH_TOKEN", "유효하지 않은 리프레시 토큰", HttpStatus.UNAUTHORIZED); private final String code; private final String message; diff --git a/src/main/java/com/exception/custom/InvalidToken.java b/src/main/java/com/exception/custom/InvalidToken.java new file mode 100644 index 0000000..497dd68 --- /dev/null +++ b/src/main/java/com/exception/custom/InvalidToken.java @@ -0,0 +1,15 @@ +package com.exception.custom; + +import com.exception.CustomException; +import com.exception.ErrorCodeType; +import com.exception.ErrorType; + +public class InvalidToken extends CustomException { + private InvalidToken(ErrorType exceptionType) { + super(exceptionType); + } + + public static InvalidToken getInstance() { + return new InvalidToken(ErrorCodeType.INVALID_REFRESH_TOKEN); + } +} diff --git a/src/main/java/com/member/controller/MemberController.java b/src/main/java/com/member/controller/MemberController.java index 35d72d4..c086f29 100644 --- a/src/main/java/com/member/controller/MemberController.java +++ b/src/main/java/com/member/controller/MemberController.java @@ -1,6 +1,7 @@ package com.member.controller; import com.config.auth.annotation.AuthenticatedMember; +import com.member.dto.response.LoginResponse; import com.member.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -27,4 +28,10 @@ public ResponseEntity withdraw(@AuthenticatedMember Long memberId) { memberService.withdraw(memberId); return ResponseEntity.noContent().build(); } + + @PostMapping("/reissue") + public ResponseEntity reissueToken(@AuthenticatedMember Long memberId) { + LoginResponse newAccessToken = memberService.reissueAccessToken(memberId); + return ResponseEntity.ok(newAccessToken); + } } diff --git a/src/main/java/com/member/service/MemberService.java b/src/main/java/com/member/service/MemberService.java index 0395a9a..07599d3 100644 --- a/src/main/java/com/member/service/MemberService.java +++ b/src/main/java/com/member/service/MemberService.java @@ -20,4 +20,6 @@ public interface MemberService { void withdraw(Long memberId); + LoginResponse reissueAccessToken(Long refreshToken); + } diff --git a/src/main/java/com/member/service/MemberServiceImpl.java b/src/main/java/com/member/service/MemberServiceImpl.java index cff28ee..85fedf4 100644 --- a/src/main/java/com/member/service/MemberServiceImpl.java +++ b/src/main/java/com/member/service/MemberServiceImpl.java @@ -2,10 +2,9 @@ import com.config.jwt.JwtUtil; import com.config.jwt.TokenWithExpiration; -import com.exception.custom.EmailNotFoundException; -import com.exception.custom.LoginException; -import com.exception.custom.MyEntityNotFoundException; -import com.exception.custom.SignUpException; +import com.config.jwt.token.RefreshToken; +import com.config.jwt.token.RefreshTokenService; +import com.exception.custom.*; import com.member.domain.Member; import com.member.dto.request.LoginRequest; import com.member.dto.request.SignUpRequest; @@ -26,6 +25,7 @@ public class MemberServiceImpl implements MemberService { private final MemberRepository memberRepository; + private final RefreshTokenService refreshTokenService; private final JwtUtil jwtUtil; @Override @@ -61,9 +61,14 @@ public LoginResponse login(LoginRequest request) { Member member = new Member(memberEntity); member.checkPassword(request.getPassword()); - TokenWithExpiration tokenWithExpiration = - jwtUtil.generateTokenWithExpiration(member.getEmail()); - return new LoginResponse(tokenWithExpiration.getToken(), tokenWithExpiration.getExpiration()); + TokenWithExpiration accessToken = + jwtUtil.generateAccessToken(member.getId()); + TokenWithExpiration refreshToken = + jwtUtil.generateRefreshToken(member.getId()); + + refreshTokenService.mergeToken(member.getId(), refreshToken.getToken()); + + return new LoginResponse(accessToken, refreshToken); } @Override @@ -78,7 +83,9 @@ public MemberEntity findById(Long id) { .orElseThrow(() -> MyEntityNotFoundException.from(id)); } + @Override public void logout(Long memberId) { + refreshTokenService.deleteToken(memberId); log.info("회원 {} 로그아웃함", memberId); } @@ -87,4 +94,18 @@ public void withdraw(Long memberId) { MemberEntity member = findById(memberId); member.softDelete(); } + + @Override + public LoginResponse reissueAccessToken(Long memberId) { + RefreshToken savedToken = refreshTokenService.findByMemberId(memberId); + + if (!jwtUtil.isTokenValid(savedToken.getToken())) { + throw InvalidToken.getInstance(); + } + + TokenWithExpiration newAccessToken = jwtUtil.generateAccessToken(memberId); + return LoginResponse.builder() + .accessToken(newAccessToken) + .build(); // 리프래시 토큰은 줄 필요 없음 + } } diff --git a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java index b45f7c9..6198100 100644 --- a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java @@ -74,13 +74,14 @@ private void signUpAndLogin() throws Exception { sendPostRequest("/public/members/login", new LoginRequest(MEMBER_EMAIL, MEMBER_PASSWORD)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.accessToken").exists()) - .andExpect(jsonPath("$.accessToken").isNotEmpty()) - .andExpect(jsonPath("$.expirationTime").value(JWT_EXPIRATION_TIME)) + .andExpect(jsonPath("$.accessToken.token").exists()) + .andExpect(jsonPath("$.accessToken.token").isNotEmpty()) + .andExpect(jsonPath("$.accessToken.expiration").value(JWT_EXPIRATION_TIME)) .andDo(result -> { String responseBody = result.getResponse().getContentAsString(); - this.accessToken = objectMapper.readTree(responseBody).get("accessToken").asText(); - assertThat(jwtUtil.extractEmail(accessToken)).isEqualTo(MEMBER_EMAIL); + this.accessToken = objectMapper.readTree(responseBody).get("accessToken").get("token").asText(); + Long memberId = jwtUtil.extractMemberIdFromToken(accessToken); + assertThat(jwtUtil.extractMemberIdFromToken(accessToken)).isEqualTo(memberId); assertThat(jwtUtil.isTokenValid(accessToken)).isTrue(); }); } diff --git a/src/test/java/com/config/jwt/token/RefreshTokenServiceTest.java b/src/test/java/com/config/jwt/token/RefreshTokenServiceTest.java new file mode 100644 index 0000000..4fc852f --- /dev/null +++ b/src/test/java/com/config/jwt/token/RefreshTokenServiceTest.java @@ -0,0 +1,135 @@ +package com.config.jwt.token; + +import com.exception.custom.MyEntityNotFoundException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class RefreshTokenServiceTest { + + @InjectMocks + private RefreshTokenService refreshTokenService; + @Mock + private RefreshTokenRepository refreshTokenRepository; + + private final Long TEST_MEMBER_ID = 1L; + private final String TEST_TOKEN = "testRefreshToken"; + private final RefreshToken TEST_REFRESH_TOKEN = new RefreshToken(TEST_MEMBER_ID, TEST_TOKEN); + + @Test + @DisplayName("mergeToken - 기존 토큰이 없을 때 저장") + void mergeToken_새로운토큰저장() { + // given + given(refreshTokenRepository.findById(TEST_MEMBER_ID)).willReturn(Optional.empty()); + given(refreshTokenRepository.save(any(RefreshToken.class))).willReturn(TEST_REFRESH_TOKEN); + + // when + refreshTokenService.mergeToken(TEST_MEMBER_ID, TEST_TOKEN); + + // then + verify(refreshTokenRepository, times(1)).save(any(RefreshToken.class)); + } + + @Test() + @DisplayName("mergeToken - 기존 토큰이 있을 때 업데이트") + void mergeToken_기존토큰업데이트() { + // given + String newRefreshToken = "newRefreshToken"; + RefreshToken existingRefreshToken = new RefreshToken(TEST_MEMBER_ID, "oldRefreshToken"); + given(refreshTokenRepository.findById(TEST_MEMBER_ID)).willReturn(Optional.of(existingRefreshToken)); + + // when + refreshTokenService.mergeToken(TEST_MEMBER_ID, newRefreshToken); + + // then + assertThat(existingRefreshToken.getToken()).isEqualTo(newRefreshToken); + verify(refreshTokenRepository, times(1)).findById(TEST_MEMBER_ID); + verify(refreshTokenRepository, never()).save(any(RefreshToken.class)); + } + + @Test + @DisplayName("isTokenValid - 토큰이 유효할 때 true 반환") + void isTokenValid_유효한토큰() { + // given + given(refreshTokenRepository.findById(TEST_MEMBER_ID)).willReturn(Optional.of(TEST_REFRESH_TOKEN)); + + // when + boolean isValid = refreshTokenService.isTokenValid(TEST_MEMBER_ID, TEST_TOKEN); + + // then + assertThat(isValid).isTrue(); + } + + @Test + @DisplayName("isTokenValid - 토큰이 유효하지 않을 때 false 반환") + void isTokenValid_유효하지않은토큰() { + // given + given(refreshTokenRepository.findById(TEST_MEMBER_ID)).willReturn(Optional.of(TEST_REFRESH_TOKEN)); + String invalidToken = "invalidRefreshToken"; + + // when + boolean isValid = refreshTokenService.isTokenValid(TEST_MEMBER_ID, invalidToken); + + // then + assertThat(isValid).isFalse(); + } + + @Test + @DisplayName("isTokenValid - 해당 멤버 ID로 저장된 토큰이 없을 때 false 반환") + void isTokenValid_토큰없음() { + // given + given(refreshTokenRepository.findById(TEST_MEMBER_ID)).willReturn(Optional.empty()); + + // when + boolean isValid = refreshTokenService.isTokenValid(TEST_MEMBER_ID, TEST_TOKEN); + + // then + assertThat(isValid).isFalse(); + } + + @Test + @DisplayName("deleteToken - 멤버 ID로 토큰 삭제") + void deleteToken_토큰삭제() { + // when + refreshTokenService.deleteToken(TEST_MEMBER_ID); + + // then + verify(refreshTokenRepository, times(1)).deleteById(TEST_MEMBER_ID); + } + + @Test + @DisplayName("findByMemberId - 멤버 ID로 토큰 찾기 - 존재할 때") + void findByMemberId_토큰존재() { + // given + given(refreshTokenRepository.findById(TEST_MEMBER_ID)).willReturn(Optional.of(TEST_REFRESH_TOKEN)); + + // when + RefreshToken foundToken = refreshTokenService.findByMemberId(TEST_MEMBER_ID); + + // then + assertThat(foundToken).isEqualTo(TEST_REFRESH_TOKEN); + } + + @Test + @DisplayName("findByMemberId - 멤버 ID로 토큰 찾기 - 존재하지 않을 때 MyEntityNotFoundException 발생") + void findByMemberId_토큰없음() { + // given + given(refreshTokenRepository.findById(TEST_MEMBER_ID)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> refreshTokenService.findByMemberId(TEST_MEMBER_ID)) + .isInstanceOf(MyEntityNotFoundException.class); + } +} diff --git a/src/test/java/com/member/controller/MemberControllerIntegrationTest.java b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java index ccebdb1..e0b5ed6 100644 --- a/src/test/java/com/member/controller/MemberControllerIntegrationTest.java +++ b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java @@ -4,6 +4,7 @@ import com.config.jwt.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.member.dto.request.LoginRequest; +import com.member.dto.response.LoginResponse; import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; import com.support.IntegrationTest; @@ -15,6 +16,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -38,12 +40,14 @@ class MemberControllerIntegrationTest { String setUpMemberEmail = "setupMember@example.com"; String setUpMemberPassword = "12345"; String setUpMemberNickname = "setupMemberNickname"; + Long setUpMemberId; @BeforeEach void setUp() { // 테스트용 회원 데이터 미리 저장 MemberEntity member = new MemberEntity(setUpMemberEmail, setUpMemberPassword, setUpMemberNickname); - memberRepository.save(member); + MemberEntity save = memberRepository.save(member); + setUpMemberId = save.getId(); } @Test @@ -126,6 +130,59 @@ private String getAccessToken() throws Exception { .andReturn(); String responseBody = result.getResponse().getContentAsString(); - return objectMapper.readTree(responseBody).get("accessToken").asText(); // JWT 토큰 쿠키 값 반환 + return objectMapper.readTree(responseBody) + .get("accessToken") + .get("token") + .asText(); // JWT 토큰 쿠키 값 반환 + } + + @Test + @DisplayName("만료된_엑세스_토큰으로_요청시_401_응답") + void 만료된_엑세스_토큰으로_요청시_401_응답() throws Exception { + // Given: 3초짜리 만료 토큰 생성 + String expiredSoonToken = jwtUtil.generateToken(setUpMemberId, 1); // 3초 + + // 3초 대기 + Thread.sleep(5); + + // When: 인증 필요한 요청 시도 + mockMvc.perform(post("/articles") + .header("Authorization", "Bearer " + expiredSoonToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new ArticleCreateRequest("제목", "내용")))) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("만료된_엑세스_토큰으로_재발급_요청시_401_응답") + void 만료된_엑세스_토큰으로_재발급_요청시_401_응답() throws Exception { + // Given + String expiredSoonToken = jwtUtil.generateToken(setUpMemberId, 1); // 3초 뒤 만료되는 토큰 생성 + Thread.sleep(5); + + // When + mockMvc.perform(post("/members/reissue") + .header("Authorization", "Bearer " + expiredSoonToken)) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("유효한_엑세스_토큰으로_재발급_요청시_새로운_엑세스_토큰_반환") + void 유효한_엑세스_토큰으로_재발급_요청시_성공() throws Exception { + // Given + String validToken = getAccessToken(); + + // When + MvcResult result = mockMvc.perform(post("/members/reissue") + .header("Authorization", "Bearer " + validToken)) + .andExpect(status().isOk()) + .andReturn(); + + // Then + String responseBody = result.getResponse().getContentAsString(); + LoginResponse loginResponse = objectMapper.readValue(responseBody, LoginResponse.class); + assertThat(loginResponse.getAccessToken()).isNotNull(); + assertThat(loginResponse.getAccessToken().getToken()).isNotBlank(); + assertThat(loginResponse.getAccessToken().getExpiration()).isGreaterThan(0L); } } diff --git a/src/test/java/com/member/controller/MemberControllerTest.java b/src/test/java/com/member/controller/MemberControllerTest.java index 680d4bb..9063707 100644 --- a/src/test/java/com/member/controller/MemberControllerTest.java +++ b/src/test/java/com/member/controller/MemberControllerTest.java @@ -1,22 +1,27 @@ package com.member.controller; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.config.auth.AuthUtil; import com.config.auth.AuthenticatedMemberArgumentResolver; +import com.config.jwt.TokenWithExpiration; import com.fasterxml.jackson.databind.ObjectMapper; +import com.member.dto.response.LoginResponse; import com.member.service.MemberService; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(MemberController.class) class MemberControllerTest { @@ -44,6 +49,7 @@ void setup() throws Exception { } @Test + @DisplayName("로그아웃 테스트") public void logout_ShouldCallMemberServiceLogout() throws Exception { // Given Long memberId = 1L; @@ -57,6 +63,7 @@ public void logout_ShouldCallMemberServiceLogout() throws Exception { } @Test + @DisplayName("회원탈퇴 테스트") public void withdraw_ShouldCallMemberServiceWithdraw() throws Exception { // Given Long memberId = 1L; @@ -68,4 +75,28 @@ public void withdraw_ShouldCallMemberServiceWithdraw() throws Exception { // Then: MemberService의 withdraw 호출 확인 verify(memberService).withdraw(memberId); } + + @Test + @DisplayName("리프레시 토큰 재발급 잘 되는지 테스트") + public void reissueToken_ShouldCallMemberServiceReissueAccessToken() throws Exception { + // Given + Long memberId = 1L; + TokenWithExpiration newAccessToken = new TokenWithExpiration("newAccessToken", 3600L); + LoginResponse expectedResponse = LoginResponse.builder() + .accessToken(newAccessToken) + .build(); + + when(memberService.reissueAccessToken(memberId)).thenReturn(expectedResponse); + + // When + MvcResult result = mockMvc.perform(post("/members/reissue")) + .andExpect(status().isOk()) + .andReturn(); + + // Then + verify(memberService).reissueAccessToken(memberId); + String responseBody = result.getResponse().getContentAsString(); + LoginResponse actualResponse = objectMapper.readValue(responseBody, LoginResponse.class); + assertThat(actualResponse.getAccessToken().getToken()).isEqualTo("newAccessToken"); + } } diff --git a/src/test/java/com/member/service/MemberServiceImplTest.java b/src/test/java/com/member/service/MemberServiceImplTest.java index ebc50bd..8db5368 100644 --- a/src/test/java/com/member/service/MemberServiceImplTest.java +++ b/src/test/java/com/member/service/MemberServiceImplTest.java @@ -2,7 +2,11 @@ import com.config.jwt.JwtUtil; import com.config.jwt.TokenWithExpiration; +import com.config.jwt.token.RefreshToken; +import com.config.jwt.token.RefreshTokenService; +import com.exception.custom.InvalidToken; import com.exception.custom.LoginException; +import com.exception.custom.MyEntityNotFoundException; import com.exception.custom.SignUpException; import com.member.dto.request.LoginRequest; import com.member.dto.request.SignUpRequest; @@ -21,6 +25,8 @@ import java.util.Optional; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -34,6 +40,9 @@ class MemberServiceImplTest { @Mock private MemberRepository memberRepository; + @Mock + private RefreshTokenService refreshTokenService; + @Mock private JwtUtil jwtUtil; @@ -89,15 +98,22 @@ void login_Success() { // Given LoginRequest request = new LoginRequest("test@example.com", "1234"); when(memberRepository.findByEmailAndIsDeletedFalse(request.getEmail())).thenReturn(Optional.of(member)); - when(jwtUtil.generateTokenWithExpiration(any(String.class))) - .thenReturn(new TokenWithExpiration("token", 3600000L)); + + TokenWithExpiration accessToken = new TokenWithExpiration("token", 3600000L); + TokenWithExpiration refreshToken = new TokenWithExpiration("refreshToken", 604800000L); + + when(jwtUtil.generateAccessToken(any())).thenReturn(accessToken); + when(jwtUtil.generateRefreshToken(any())).thenReturn(refreshToken); // When LoginResponse response = memberService.login(request); // Then assertNotNull(response); - assertEquals("token", response.getAccessToken()); + assertEquals("token", response.getAccessToken().getToken()); + assertEquals(3600000L, response.getAccessToken().getExpiration()); + assertEquals("refreshToken", response.getRefreshToken().getToken()); + assertEquals(604800000L, response.getRefreshToken().getExpiration()); } @Test @@ -151,4 +167,54 @@ void findById_Success() { assertNotNull(foundMember); assertEquals("test@example.com", foundMember.getEmail()); } + + @Test + @DisplayName("유효한 리프래시 토큰으로 새 액세스 토큰 발급") + void reissueAccessToken_유효한리프래시토큰() { + // Given + Long memberId = 1L; + String refreshTokenValue = "validRefreshToken"; + RefreshToken refreshToken = new RefreshToken(memberId, refreshTokenValue); + TokenWithExpiration newAccessToken = new TokenWithExpiration("newAccessToken", 3600L); + + when(refreshTokenService.findByMemberId(memberId)).thenReturn(refreshToken); + when(jwtUtil.isTokenValid(refreshTokenValue)).thenReturn(true); + when(jwtUtil.generateAccessToken(memberId)).thenReturn(newAccessToken); + + // When + LoginResponse response = memberService.reissueAccessToken(memberId); + + // Then + assertThat(response.getAccessToken().getToken()).isEqualTo("newAccessToken"); + } + + @Test + @DisplayName("유효하지 않은 리프래시 토큰으로 예외 발생") + void reissueAccessToken_유효하지않은리프래시토큰() { + // Given + Long memberId = 1L; + String refreshTokenValue = "invalidRefreshToken"; + RefreshToken refreshToken = new RefreshToken(memberId, refreshTokenValue); + + when(refreshTokenService.findByMemberId(memberId)).thenReturn(refreshToken); + when(jwtUtil.isTokenValid(refreshTokenValue)).thenReturn(false); + + // When & Then + assertThatThrownBy(() -> memberService.reissueAccessToken(memberId)) + .isInstanceOf(InvalidToken.class); + } + + @Test + @DisplayName("reissueAccessToken - 멤버 ID로 리프래시 토큰을 찾을 수 없을 때 MyEntityNotFoundException 발생") + void reissueAccessToken_리프래시토큰없음() { + // Given + Long memberId = 1L; + when(refreshTokenService.findByMemberId(memberId)).thenThrow(MyEntityNotFoundException.from(memberId)); + + // When & Then + assertThatThrownBy(() -> memberService.reissueAccessToken(memberId)) + .isInstanceOf(MyEntityNotFoundException.class); + } + + } From fbfff37159df6ad7e41e451a3113c0690fd48f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Sun, 18 May 2025 14:45:06 +0900 Subject: [PATCH 38/45] =?UTF-8?q?refactor=20:=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=EC=9D=98=20id=EB=A5=BC=20emial=20->=20memberId=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=20-=20=EB=91=98=EB=8B=A4=20=EA=B3=A0?= =?UTF-8?q?=EC=9C=A0=EA=B0=92=EC=9D=B4=EC=A7=80=EB=A7=8C=20id=EB=A1=9C=20?= =?UTF-8?q?=ED=95=98=EB=8A=94=EA=B2=83=EC=9D=B4=20=EB=8D=94=20=EB=B9=A0?= =?UTF-8?q?=EB=A5=B4=EA=B3=A0=20=EC=9C=84=ED=97=98=20=EC=A0=81=EC=9D=8C(?= =?UTF-8?q?=EB=8D=94=20=EC=A7=A7=EA=B3=A0,=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=EC=84=B1=20=EB=82=AE=EC=9D=8C)=20=20-=20?= =?UTF-8?q?=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85=ED=95=98=EB=A9=B4=EC=84=9C=20=EC=95=A1?= =?UTF-8?q?=EC=84=B8=EC=8A=A4/=EB=A6=AC=ED=94=84=EB=A0=88=EC=8A=A4=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=A0=84=EB=B6=80=20id=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=B4=EC=84=9C=20=EB=B0=9C=EA=B8=89=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/config/auth/AuthUtil.java | 16 ++++------- .../AuthenticatedMemberArgumentResolver.java | 3 +- .../java/com/config/jwt/JwtAuthFilter.java | 4 +-- src/main/java/com/config/jwt/JwtUtil.java | 28 ++++++++++++------- src/main/java/com/member/domain/Member.java | 2 ++ .../member/dto/response/LoginResponse.java | 13 ++++++--- .../CommentControllerIntegrationTest.java | 13 ++++----- ...PublicMemberControllerIntegrationTest.java | 11 ++++---- .../PublicMemberControllerTest.java | 23 +++++++++------ 9 files changed, 64 insertions(+), 49 deletions(-) diff --git a/src/main/java/com/config/auth/AuthUtil.java b/src/main/java/com/config/auth/AuthUtil.java index d283211..d2a562f 100644 --- a/src/main/java/com/config/auth/AuthUtil.java +++ b/src/main/java/com/config/auth/AuthUtil.java @@ -1,24 +1,24 @@ package com.config.auth; -import static com.config.auth.AuthConstants.AUTHENTICATED_USER; - import com.exception.custom.ServerException; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; +import static com.config.auth.AuthConstants.AUTHENTICATED_USER; + @Component public class AuthUtil { - public void saveAuthenticatedMember(String email) { + public void saveAuthenticatedMember(Long memberId) { RequestAttributes requestAttributes = getRequestAttributes(); - requestAttributes.setAttribute(AUTHENTICATED_USER, email, RequestAttributes.SCOPE_REQUEST); + requestAttributes.setAttribute(AUTHENTICATED_USER, memberId, RequestAttributes.SCOPE_REQUEST); } - public String getMemberEmail() { + public Long getMemberId() { RequestAttributes requestAttributes = getRequestAttributes(); - return (String) requestAttributes.getAttribute(AUTHENTICATED_USER, RequestAttributes.SCOPE_REQUEST); + return (Long) requestAttributes.getAttribute(AUTHENTICATED_USER, RequestAttributes.SCOPE_REQUEST); } private RequestAttributes getRequestAttributes() { @@ -28,8 +28,4 @@ private RequestAttributes getRequestAttributes() { } return requestAttributes; } - - public boolean isAuthenticated() { - return getMemberEmail() != null; - } } diff --git a/src/main/java/com/config/auth/AuthenticatedMemberArgumentResolver.java b/src/main/java/com/config/auth/AuthenticatedMemberArgumentResolver.java index e82a2c3..83120c2 100644 --- a/src/main/java/com/config/auth/AuthenticatedMemberArgumentResolver.java +++ b/src/main/java/com/config/auth/AuthenticatedMemberArgumentResolver.java @@ -29,7 +29,6 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - String email = authUtil.getMemberEmail(); - return memberService.findByEmail(email).getId(); // MemberEntity를 반환하여 파라미터에 주입 + return authUtil.getMemberId(); // MemberEntity를 반환하여 파라미터에 주입 } } diff --git a/src/main/java/com/config/jwt/JwtAuthFilter.java b/src/main/java/com/config/jwt/JwtAuthFilter.java index 111438f..09e8767 100644 --- a/src/main/java/com/config/jwt/JwtAuthFilter.java +++ b/src/main/java/com/config/jwt/JwtAuthFilter.java @@ -32,8 +32,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse String token = jwtUtil.extractFromHeader(request); if (token != null && jwtUtil.isTokenValid(token)) { - String email = jwtUtil.extractEmail(token); - authUtil.saveAuthenticatedMember(email); + Long memberId = jwtUtil.extractMemberIdFromToken(token); + authUtil.saveAuthenticatedMember(memberId); filterChain.doFilter(request, response); return; } diff --git a/src/main/java/com/config/jwt/JwtUtil.java b/src/main/java/com/config/jwt/JwtUtil.java index b117caf..77228bd 100644 --- a/src/main/java/com/config/jwt/JwtUtil.java +++ b/src/main/java/com/config/jwt/JwtUtil.java @@ -17,11 +17,11 @@ public class JwtUtil { private static final String AUTHORIZATION_HEADER = "Authorization"; private static final String BEARER_PREFIX = "Bearer "; - private static final String COOKIE_NAME = "token"; private static final long ACCESS_TOKEN_EXPIRATION = 1000 * 60 * 60; // 1시간 + private static final long REFRESH_TOKEN_EXPIRATION = 1000L * 60 * 60 * 24 * 7; // 7일 private final JwtProperties jwtProperties; - + public String extractFromHeader(HttpServletRequest request) { String header = request.getHeader(AUTHORIZATION_HEADER); if (header != null && header.startsWith(BEARER_PREFIX)) { @@ -30,27 +30,34 @@ public String extractFromHeader(HttpServletRequest request) { return null; } - public TokenWithExpiration generateTokenWithExpiration(String subject) { - return new TokenWithExpiration(generateToken(subject), ACCESS_TOKEN_EXPIRATION); + public TokenWithExpiration generateAccessToken(Long memberId) { + String token = generateToken(memberId, ACCESS_TOKEN_EXPIRATION); + return new TokenWithExpiration(token, ACCESS_TOKEN_EXPIRATION); + } + + public TokenWithExpiration generateRefreshToken(Long memberId) { + String token = generateToken(memberId, REFRESH_TOKEN_EXPIRATION); + return new TokenWithExpiration(token, REFRESH_TOKEN_EXPIRATION); } - public String generateToken(String email) { + public String generateToken(Long memberId, long expirationTime) { return Jwts.builder() - .setSubject(email) // sub : 이메일(jwt 주인) + .setSubject(String.valueOf(memberId)) // sub : 이메일(jwt 주인) .setIssuer(jwtProperties.getIssuer()) // Issuer 설정 (필수아님) .setIssuedAt(new Date()) // 발급시간 (필수아님) - .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION)) // 유효시간 필수! + .setExpiration(new Date(System.currentTimeMillis() + expirationTime)) // 유효시간 필수! .signWith(Keys.hmacShaKeyFor(getSigningKey()), SignatureAlgorithm.HS256) .compact(); } - public String extractEmail(String token) { - return Jwts.parserBuilder() + public Long extractMemberIdFromToken(String token) { + String subject = Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody() .getSubject(); + return Long.valueOf(subject); } public boolean isTokenValid(String token) { @@ -66,6 +73,7 @@ public boolean isTokenValid(String token) { } private byte[] getSigningKey() { - return jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8); + return jwtProperties.getSecretKey() + .getBytes(StandardCharsets.UTF_8); } } diff --git a/src/main/java/com/member/domain/Member.java b/src/main/java/com/member/domain/Member.java index f60124c..79ebae7 100644 --- a/src/main/java/com/member/domain/Member.java +++ b/src/main/java/com/member/domain/Member.java @@ -7,10 +7,12 @@ @Getter public class Member { + private final Long id; private final String email; private final String password; public Member(MemberEntity memberEntity) { + this.id = memberEntity.getId(); this.email = memberEntity.getEmail(); this.password = memberEntity.getPassword(); } diff --git a/src/main/java/com/member/dto/response/LoginResponse.java b/src/main/java/com/member/dto/response/LoginResponse.java index 982180f..65fa5bb 100644 --- a/src/main/java/com/member/dto/response/LoginResponse.java +++ b/src/main/java/com/member/dto/response/LoginResponse.java @@ -1,12 +1,17 @@ package com.member.dto.response; +import com.config.jwt.TokenWithExpiration; +import lombok.Builder; import lombok.Getter; -import lombok.RequiredArgsConstructor; @Getter -@RequiredArgsConstructor public class LoginResponse { + private final TokenWithExpiration accessToken; + private final TokenWithExpiration refreshToken; - private final String accessToken; - private final long expirationTime; + @Builder + public LoginResponse(TokenWithExpiration accessToken, TokenWithExpiration refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } } diff --git a/src/test/java/com/board/controller/CommentControllerIntegrationTest.java b/src/test/java/com/board/controller/CommentControllerIntegrationTest.java index 4772dbf..5b5b986 100644 --- a/src/test/java/com/board/controller/CommentControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/CommentControllerIntegrationTest.java @@ -1,12 +1,5 @@ package com.board.controller; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.board.dto.request.CommentCreateRequest; import com.board.dto.request.CommentUpdateRequest; import com.board.entity.ArticleEntity; @@ -25,6 +18,10 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @IntegrationTest class CommentControllerIntegrationTest { @@ -55,7 +52,7 @@ class CommentControllerIntegrationTest { void setup() { member = memberRepository.save(new MemberEntity("test@example.com", "password", "nickname")); article = articleRepository.save(new ArticleEntity("title", "content", member)); - jwtToken = "Bearer " + jwtUtil.generateToken(member.getEmail()); + jwtToken = "Bearer " + jwtUtil.generateToken(member.getId(), 1000 * 60 * 60); } @Test diff --git a/src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java b/src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java index 0a19167..bff495a 100644 --- a/src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java +++ b/src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java @@ -38,12 +38,13 @@ class PublicMemberControllerIntegrationTest { String setUpMemberEmail = "setupMember@example.com"; String setUpMemberPassword = "12345"; String setUpMemberNickname = "setupMemberNickname"; + Long setUpMemberId; @BeforeEach void setUp() { // 테스트용 회원 데이터 미리 저장 MemberEntity member = new MemberEntity(setUpMemberEmail, setUpMemberPassword, setUpMemberNickname); - memberRepository.save(member); + setUpMemberId = memberRepository.save(member).getId(); } @Test @@ -70,14 +71,14 @@ void loginAndValidateJwtTest() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(loginJson)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.accessToken").exists()) - .andExpect(jsonPath("$.accessToken").isNotEmpty()) + .andExpect(jsonPath("$.accessToken.token").exists()) + .andExpect(jsonPath("$.accessToken.token").isNotEmpty()) .andReturn(); // JWT 추출 및 검증 String responseBody = loginResult.getResponse().getContentAsString(); - String token = objectMapper.readTree(responseBody).get("accessToken").asText(); - assertThat(jwtUtil.extractEmail(token)).isEqualTo(setUpMemberEmail); + String token = objectMapper.readTree(responseBody).get("accessToken").get("token").asText(); + assertThat(jwtUtil.extractMemberIdFromToken(token)).isEqualTo(setUpMemberId); assertThat(jwtUtil.isTokenValid(token)).isTrue(); } diff --git a/src/test/java/com/member/controller/PublicMemberControllerTest.java b/src/test/java/com/member/controller/PublicMemberControllerTest.java index 1dba983..9b64880 100644 --- a/src/test/java/com/member/controller/PublicMemberControllerTest.java +++ b/src/test/java/com/member/controller/PublicMemberControllerTest.java @@ -1,13 +1,8 @@ package com.member.controller; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.config.auth.AuthUtil; import com.config.auth.AuthenticatedMemberArgumentResolver; +import com.config.jwt.TokenWithExpiration; import com.fasterxml.jackson.databind.ObjectMapper; import com.member.dto.request.LoginRequest; import com.member.dto.request.SignUpRequest; @@ -22,6 +17,12 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @WebMvcTest(PublicMemberController.class) class PublicMemberControllerTest { @@ -69,7 +70,10 @@ void signUp_Success() throws Exception { void login_Success() throws Exception { // Given LoginRequest loginRequest = new LoginRequest("test@example.com", "password123"); - LoginResponse loginResponse = new LoginResponse("mockAccessToken", 3600L); + TokenWithExpiration accessToken = new TokenWithExpiration("mockAccessToken", 3600L); + TokenWithExpiration refreshToken = new TokenWithExpiration("mockRefreshToken", 604800L); + LoginResponse loginResponse = new LoginResponse(accessToken, refreshToken); + when(memberService.login(any(LoginRequest.class))).thenReturn(loginResponse); @@ -78,6 +82,9 @@ void login_Success() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(new ObjectMapper().writeValueAsString(loginRequest))) .andExpect(status().isOk()) - .andExpect(jsonPath("$.accessToken").value("mockAccessToken")); + .andExpect(jsonPath("$.accessToken.token").value("mockAccessToken")) + .andExpect(jsonPath("$.accessToken.expiration").value(3600)) + .andExpect(jsonPath("$.refreshToken.token").value("mockRefreshToken")) + .andExpect(jsonPath("$.refreshToken.expiration").value(604800)); } } From 168e5effbb90f4b5146b5f4bafb0ac62876a5917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Mon, 19 May 2025 18:49:47 +0900 Subject: [PATCH 39/45] =?UTF-8?q?test,=20feat=20:=20=EC=8A=A4=ED=94=84?= =?UTF-8?q?=EB=A7=81=20=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=EC=9A=A9=20=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80=20=20-=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EC=8B=9C=20=EC=86=8D=EB=8F=84=EC=A0=80=ED=95=98?= =?UTF-8?q?=EA=B0=80=20=ED=81=AC=EA=B8=B0=20=EB=95=8C=EB=AC=B8=EC=97=90=20?= =?UTF-8?q?=ED=8A=B9=EC=A0=95=20=EC=BC=80=EC=9D=B4=EC=8A=A4=EC=97=90?= =?UTF-8?q?=EC=84=9C=EB=A7=8C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/com/support/IsolatedTest.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/test/java/com/support/IsolatedTest.java diff --git a/src/test/java/com/support/IsolatedTest.java b/src/test/java/com/support/IsolatedTest.java new file mode 100644 index 0000000..68c7814 --- /dev/null +++ b/src/test/java/com/support/IsolatedTest.java @@ -0,0 +1,19 @@ +package com.support; + +import org.springframework.test.annotation.DirtiesContext; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public @interface IsolatedTest { // 스프링 컨텍스트 재로딩 + // 특정 케이스에만 사용 + // - DB 트랜잭션으로 롤백되지 않는 외부 시스템과 연동되는 경우 + // - 스프링 컨텍스트 초기화 필요시 + // - 정적(static) 변수값이 공유되는데 초기화 필요시 + // - 비동기 처리(@Async, @Scheduled 등) 테스트 시 +} From 16e145ee767e317c8376f4c7e1f1ed51b11af6bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Wed, 21 May 2025 20:38:25 +0900 Subject: [PATCH 40/45] =?UTF-8?q?feat,=20build,=20test=20:=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=97=90=20@Transactional?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0=20=ED=9B=84=20CleanDatabase=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=20-=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=97=90=20CleanDatabaseBeforeEachTest=20=EC=83=81?= =?UTF-8?q?=EC=86=8D=ED=95=98=EB=A9=B4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EC=8B=9C=20=EB=AA=A8=EB=93=A0=20db=20tabl?= =?UTF-8?q?e=20truncate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 ++ .../com/member/service/MemberServiceImpl.java | 8 ++++- .../ArticleControllerIntegrationTest.java | 21 +++++++----- .../CommentControllerIntegrationTest.java | 16 +++++---- .../MemberControllerIntegrationTest.java | 17 +++++----- ...PublicMemberControllerIntegrationTest.java | 15 ++++----- .../support/CleanDatabaseBeforeEachTest.java | 16 +++++++++ .../java/com/support/DatabaseCleaner.java | 33 +++++++++++++++++++ src/test/java/com/support/IsolatedTest.java | 16 ++++----- 9 files changed, 101 insertions(+), 43 deletions(-) create mode 100644 src/test/java/com/support/CleanDatabaseBeforeEachTest.java create mode 100644 src/test/java/com/support/DatabaseCleaner.java diff --git a/build.gradle b/build.gradle index 9f38a7e..e6d0fb4 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,8 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testCompileOnly 'org.projectlombok:lombok' // 테스트 의존성 추가 + testAnnotationProcessor 'org.projectlombok:lombok' // 테스트 의존성 추가 // JWT implementation 'io.jsonwebtoken:jjwt-api:0.11.5' diff --git a/src/main/java/com/member/service/MemberServiceImpl.java b/src/main/java/com/member/service/MemberServiceImpl.java index 85fedf4..26c9ba4 100644 --- a/src/main/java/com/member/service/MemberServiceImpl.java +++ b/src/main/java/com/member/service/MemberServiceImpl.java @@ -4,7 +4,11 @@ import com.config.jwt.TokenWithExpiration; import com.config.jwt.token.RefreshToken; import com.config.jwt.token.RefreshTokenService; -import com.exception.custom.*; +import com.exception.custom.EmailNotFoundException; +import com.exception.custom.InvalidToken; +import com.exception.custom.LoginException; +import com.exception.custom.MyEntityNotFoundException; +import com.exception.custom.SignUpException; import com.member.domain.Member; import com.member.dto.request.LoginRequest; import com.member.dto.request.SignUpRequest; @@ -54,6 +58,7 @@ private void validateDuplicate(SignUpRequest request) { } @Override + @Transactional public LoginResponse login(LoginRequest request) { MemberEntity memberEntity = memberRepository.findByEmailAndIsDeletedFalse(request.getEmail()) .orElseThrow(() -> LoginException.from(ErrorMessage.NOT_CORRECT_LOGIN)); @@ -84,6 +89,7 @@ public MemberEntity findById(Long id) { } @Override + @Transactional public void logout(Long memberId) { refreshTokenService.deleteToken(memberId); log.info("회원 {} 로그아웃함", memberId); diff --git a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java index 6198100..f31d569 100644 --- a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java @@ -1,5 +1,14 @@ package com.board.controller; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.board.dto.request.ArticleCreateRequest; import com.board.dto.request.ArticleUpdateRequest; import com.board.entity.ArticleEntity; @@ -11,7 +20,7 @@ import com.member.dto.request.SignUpRequest; import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; -import com.support.IntegrationTest; +import com.support.CleanDatabaseBeforeEachTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -22,14 +31,7 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@IntegrationTest -class ArticleControllerIntegrationTest { +class ArticleControllerIntegrationTest extends CleanDatabaseBeforeEachTest { @Autowired private MockMvc mockMvc; @@ -130,6 +132,7 @@ void findAllExcludingDeletedArticlesTest() throws Exception { // 게시물 1개 삭제 article3.softDelete(); + articleRepository.save(article3); // When: 전체 조회 요청 mockMvc.perform(get("/articles").param("page", "0").param("size", "10")) diff --git a/src/test/java/com/board/controller/CommentControllerIntegrationTest.java b/src/test/java/com/board/controller/CommentControllerIntegrationTest.java index 5b5b986..6331a9a 100644 --- a/src/test/java/com/board/controller/CommentControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/CommentControllerIntegrationTest.java @@ -1,5 +1,12 @@ package com.board.controller; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.board.dto.request.CommentCreateRequest; import com.board.dto.request.CommentUpdateRequest; import com.board.entity.ArticleEntity; @@ -10,7 +17,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; -import com.support.IntegrationTest; +import com.support.CleanDatabaseBeforeEachTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -18,12 +25,7 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@IntegrationTest -class CommentControllerIntegrationTest { +class CommentControllerIntegrationTest extends CleanDatabaseBeforeEachTest { @Autowired private MockMvc mockMvc; diff --git a/src/test/java/com/member/controller/MemberControllerIntegrationTest.java b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java index e0b5ed6..873c3cb 100644 --- a/src/test/java/com/member/controller/MemberControllerIntegrationTest.java +++ b/src/test/java/com/member/controller/MemberControllerIntegrationTest.java @@ -1,5 +1,11 @@ package com.member.controller; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.board.dto.request.ArticleCreateRequest; import com.config.jwt.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; @@ -7,7 +13,7 @@ import com.member.dto.response.LoginResponse; import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; -import com.support.IntegrationTest; +import com.support.CleanDatabaseBeforeEachTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -16,14 +22,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@IntegrationTest -class MemberControllerIntegrationTest { +class MemberControllerIntegrationTest extends CleanDatabaseBeforeEachTest { @Autowired private MockMvc mockMvc; diff --git a/src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java b/src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java index bff495a..b2c9e66 100644 --- a/src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java +++ b/src/test/java/com/member/controller/PublicMemberControllerIntegrationTest.java @@ -1,12 +1,17 @@ package com.member.controller; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.config.jwt.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.member.dto.request.LoginRequest; import com.member.dto.request.SignUpRequest; import com.member.entity.MemberEntity; import com.member.repository.MemberRepository; -import com.support.IntegrationTest; +import com.support.CleanDatabaseBeforeEachTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -15,13 +20,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@IntegrationTest -class PublicMemberControllerIntegrationTest { +class PublicMemberControllerIntegrationTest extends CleanDatabaseBeforeEachTest { @Autowired private MockMvc mockMvc; diff --git a/src/test/java/com/support/CleanDatabaseBeforeEachTest.java b/src/test/java/com/support/CleanDatabaseBeforeEachTest.java new file mode 100644 index 0000000..8e42cc5 --- /dev/null +++ b/src/test/java/com/support/CleanDatabaseBeforeEachTest.java @@ -0,0 +1,16 @@ +package com.support; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +@IsolatedTest +public abstract class CleanDatabaseBeforeEachTest { + + @Autowired + private DatabaseCleaner databaseCleaner; + + @BeforeEach + void cleanDatabase() { + databaseCleaner.truncateAll(); + } +} diff --git a/src/test/java/com/support/DatabaseCleaner.java b/src/test/java/com/support/DatabaseCleaner.java new file mode 100644 index 0000000..a0bb427 --- /dev/null +++ b/src/test/java/com/support/DatabaseCleaner.java @@ -0,0 +1,33 @@ +package com.support; + +import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class DatabaseCleaner { + + private final EntityManager em; + private List tableNames; + + @PostConstruct + public void init() { + tableNames = em.createNativeQuery( + "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'PUBLIC'") + .getResultList(); + } + + @Transactional + public void truncateAll() { + em.flush(); + em.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); + for (String table : tableNames) { + em.createNativeQuery("TRUNCATE TABLE " + table).executeUpdate(); + } + em.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); + } +} diff --git a/src/test/java/com/support/IsolatedTest.java b/src/test/java/com/support/IsolatedTest.java index 68c7814..aacb4cf 100644 --- a/src/test/java/com/support/IsolatedTest.java +++ b/src/test/java/com/support/IsolatedTest.java @@ -1,19 +1,17 @@ package com.support; -import org.springframework.test.annotation.DirtiesContext; - import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -public @interface IsolatedTest { // 스프링 컨텍스트 재로딩 - // 특정 케이스에만 사용 - // - DB 트랜잭션으로 롤백되지 않는 외부 시스템과 연동되는 경우 - // - 스프링 컨텍스트 초기화 필요시 - // - 정적(static) 변수값이 공유되는데 초기화 필요시 - // - 비동기 처리(@Async, @Scheduled 등) 테스트 시 +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@interface IsolatedTest { } From edbb190355d7eaefefe7583167b94b72f109a38a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Wed, 21 May 2025 20:57:50 +0900 Subject: [PATCH 41/45] =?UTF-8?q?test=20:=20ddl-auto=20=EC=86=8D=EC=84=B1?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=20-=20truncate=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EB=A1=9C=20ddl-auto:=20create=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/resources/application-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 0e8ecc8..48ff0f9 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -7,7 +7,7 @@ spring: jpa: hibernate: - ddl-auto: create-drop + ddl-auto: create show-sql: true properties: hibernate: From 91ea56c2e2d2ddbe53704329df4571fd6d86abd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Sat, 31 May 2025 15:41:12 +0900 Subject: [PATCH 42/45] =?UTF-8?q?feat=20:=20=EB=8C=80=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=20-=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=EC=97=90=20=EB=8C=80=EB=8C=93=EA=B8=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EA=B0=80=EB=8A=A5=20=20-=20=EB=8C=80=EB=8C=93?= =?UTF-8?q?=EA=B8=80=EC=97=90=EB=8A=94=20=EB=8C=80=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EB=B6=88=EA=B0=80(=EC=A6=89=20level=200,?= =?UTF-8?q?=201=EB=A7=8C=20=EA=B0=80=EB=8A=A5)=20=20-=20=EB=8C=93=EA=B8=80?= =?UTF-8?q?=EB=B3=84=20=EB=8C=80=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=20=20-=20=EB=8C=80=EB=8C=93=EA=B8=80?= =?UTF-8?q?=EC=9D=80=20=EB=A8=BC=EC=A0=80=20=EC=9E=91=EC=84=B1=ED=95=9C?= =?UTF-8?q?=EA=B2=8C=20=EB=A8=BC=EC=A0=80=20=EB=B3=B4=EC=9E=84(=EC=98=A4?= =?UTF-8?q?=EB=9E=98=EB=90=9C=EC=88=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/CommentController.java | 32 ++++++++------- .../dto/request/CommentCreateRequest.java | 9 ++++- .../board/dto/response/CommentResponse.java | 19 ++++++++- .../java/com/board/entity/CommentEntity.java | 33 +++++++++++----- .../board/repository/CommentRepository.java | 9 ++++- .../com/board/service/CommentService.java | 39 ++++++++++++++++++- 6 files changed, 111 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/board/controller/CommentController.java b/src/main/java/com/board/controller/CommentController.java index 1d3d943..1f0cf7d 100644 --- a/src/main/java/com/board/controller/CommentController.java +++ b/src/main/java/com/board/controller/CommentController.java @@ -15,15 +15,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @RestController @@ -38,10 +30,7 @@ public ResponseEntity> findAllComments(@PathVariab @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "latest") String sort) { Pageable pageable = PageRequest.of(page, size, SortUtils.getCommentSort(sort)); - Page commentPage = commentService.findAllComments(articleId, pageable) - .map(CommentResponse::new); - - return ResponseEntity.ok(PageResponse.from(commentPage)); + return ResponseEntity.ok(PageResponse.from(commentService.findAllTopLevelComments(articleId, pageable))); } @PostMapping @@ -51,7 +40,7 @@ public ResponseEntity addComment(@PathVariable Long articleId, CommentEntity savedComment = commentService.createComment(articleId, request, memberId); return ResponseEntity.status(HttpStatus.CREATED) - .body(new CommentResponse(savedComment)); + .body(CommentResponse.of(savedComment, 0)); } @PatchMapping("/{commentId}") @@ -60,8 +49,10 @@ public ResponseEntity updateComment(@PathVariable Long articleI @Valid @RequestBody CommentUpdateRequest request, @AuthenticatedMember Long memberId) { CommentEntity updatedComment = commentService.updateComment(articleId, commentId, request, memberId); + int replyCount = commentService.getReplyCount(updatedComment); + return ResponseEntity.ok() - .body(new CommentResponse(updatedComment)); + .body(CommentResponse.of(updatedComment, replyCount)); } @DeleteMapping("/{commentId}") @@ -72,4 +63,15 @@ public ResponseEntity deleteComment(@PathVariable Long articleId, return ResponseEntity.noContent() .build(); } + + @GetMapping("/{commentId}/replies") + public ResponseEntity> findReplies(@PathVariable Long articleId, + @PathVariable Long commentId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "oldest") String sort) { + Pageable pageable = PageRequest.of(page, size, SortUtils.getCommentSort(sort)); + Page replies = commentService.findReplies(commentId, pageable); + return ResponseEntity.ok(PageResponse.from(replies.map(CommentResponse::fromReply))); + } } diff --git a/src/main/java/com/board/dto/request/CommentCreateRequest.java b/src/main/java/com/board/dto/request/CommentCreateRequest.java index 8628721..62f6310 100644 --- a/src/main/java/com/board/dto/request/CommentCreateRequest.java +++ b/src/main/java/com/board/dto/request/CommentCreateRequest.java @@ -12,8 +12,15 @@ public class CommentCreateRequest { @NotBlank(message = "내용을 입력해주세요") private String content; - @Builder + private Long parentId; + public CommentCreateRequest(String content) { this.content = content; } + + @Builder + public CommentCreateRequest(String content, Long parentId) { + this.content = content; + this.parentId = parentId; + } } diff --git a/src/main/java/com/board/dto/response/CommentResponse.java b/src/main/java/com/board/dto/response/CommentResponse.java index 56606a0..538223c 100644 --- a/src/main/java/com/board/dto/response/CommentResponse.java +++ b/src/main/java/com/board/dto/response/CommentResponse.java @@ -16,13 +16,30 @@ public class CommentResponse { private final String authorName; private final LocalDateTime createdAt; private final boolean isDeleted; + private final Integer replyCount; - public CommentResponse(CommentEntity comment) { + public CommentResponse(CommentEntity comment, Integer replyCount) { this.id = comment.getId(); this.content = comment.getContent(); this.authorId = comment.getMember().getId(); this.authorName = comment.getMember().getNickName(); this.createdAt = comment.getCreatedAt(); this.isDeleted = comment.isDeleted(); + this.replyCount = replyCount; + } + + public static CommentResponse fromReply(CommentEntity comment) { + return new CommentResponse(comment, null); + } + + public static CommentResponse fromComment(CommentEntity comment, int replyCount) { + return new CommentResponse(comment, replyCount); + } + + public static CommentResponse of(CommentEntity comment, int replyCount) { + if (comment.isReply()) { + return fromReply(comment); + } + return fromComment(comment, replyCount); } } diff --git a/src/main/java/com/board/entity/CommentEntity.java b/src/main/java/com/board/entity/CommentEntity.java index d99935a..24b3d7a 100644 --- a/src/main/java/com/board/entity/CommentEntity.java +++ b/src/main/java/com/board/entity/CommentEntity.java @@ -4,19 +4,15 @@ import com.exception.custom.DifferentOwnerException; import com.exception.custom.NotIncludeBoardException; import com.member.entity.MemberEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.ArrayList; +import java.util.List; + @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @@ -38,13 +34,19 @@ public class CommentEntity extends SoftDeletedEntity { @JoinColumn(name = "member_id", nullable = false) private MemberEntity member; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private CommentEntity parent; + + @OneToMany(mappedBy = "parent") + private List children = new ArrayList<>(); + public CommentEntity(String content, ArticleEntity article, MemberEntity member) { this.content = content; this.article = article; this.member = member; } - @Builder public CommentEntity(Long id, String content, ArticleEntity article, MemberEntity member) { this.id = id; this.content = content; @@ -52,6 +54,19 @@ public CommentEntity(Long id, String content, ArticleEntity article, MemberEntit this.member = member; } + @Builder + public CommentEntity(Long id, String content, ArticleEntity article, MemberEntity member, CommentEntity parent) { + this.id = id; + this.content = content; + this.article = article; + this.member = member; + this.parent = parent; + } + + public boolean isReply() { + return parent != null; + } + public void validateOwner(MemberEntity member) { if (!this.member.equals(member)) { throw DifferentOwnerException.from(this.member.getEmail()); diff --git a/src/main/java/com/board/repository/CommentRepository.java b/src/main/java/com/board/repository/CommentRepository.java index 0b4d28b..8dc06ce 100644 --- a/src/main/java/com/board/repository/CommentRepository.java +++ b/src/main/java/com/board/repository/CommentRepository.java @@ -2,14 +2,19 @@ import com.board.entity.ArticleEntity; import com.board.entity.CommentEntity; -import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface CommentRepository extends JpaRepository { Optional findByIdAndIsDeletedFalse(Long id); - Page findByArticleAndIsDeletedFalse(ArticleEntity article, Pageable pageable); + Page findByArticleAndParentIsNullAndIsDeletedFalse(ArticleEntity article, Pageable pageable); + + Page findByParentAndIsDeletedFalse(CommentEntity parent, Pageable pageable); + + int countByParentAndIsDeletedFalse(CommentEntity parent); } diff --git a/src/main/java/com/board/service/CommentService.java b/src/main/java/com/board/service/CommentService.java index 90918b4..6bd60e4 100644 --- a/src/main/java/com/board/service/CommentService.java +++ b/src/main/java/com/board/service/CommentService.java @@ -2,6 +2,7 @@ import com.board.dto.request.CommentCreateRequest; import com.board.dto.request.CommentUpdateRequest; +import com.board.dto.response.CommentResponse; import com.board.entity.ArticleEntity; import com.board.entity.CommentEntity; import com.board.repository.CommentRepository; @@ -23,15 +24,41 @@ public class CommentService { private final ArticleService articleService; private final MemberService memberService; - public Page findAllComments(Long articleId, Pageable pageable) { + public Page findAllTopLevelComments(Long articleId, Pageable pageable) { ArticleEntity article = articleService.findById(articleId); - return commentRepository.findByArticleAndIsDeletedFalse(article, pageable); + Page comments = commentRepository.findByArticleAndParentIsNullAndIsDeletedFalse(article, pageable); + return comments.map(comment -> CommentResponse.of(comment, getReplyCount(comment))); + } + + public int getReplyCount(CommentEntity comment) { + return commentRepository.countByParentAndIsDeletedFalse(comment); } @Transactional public CommentEntity createComment(Long articleId, CommentCreateRequest request, Long memberId) { ArticleEntity article = articleService.findById(articleId); MemberEntity member = findMemberById(memberId); + + if (request.getParentId() != null) { + CommentEntity parent = findComment(request.getParentId()); + + if (parent.isReply()) { + throw new IllegalArgumentException("대댓글에는 대댓글을 달 수 없습니다"); + } + + if (!parent.getArticle().getId().equals(articleId)) { + throw new IllegalArgumentException("부모 댓글이 해당 게시글에 속하지 않습니다"); + } + + CommentEntity comment = CommentEntity.builder() + .content(request.getContent()) + .article(article) + .member(member) + .parent(parent) + .build(); + return commentRepository.save(comment); + } + CommentEntity comment = new CommentEntity(request.getContent(), article, member); return commentRepository.save(comment); } @@ -61,4 +88,12 @@ private CommentEntity findComment(Long id) { private MemberEntity findMemberById(Long memberId) { return memberService.findById(memberId); } + + public Page findReplies(Long parentId, Pageable pageable) { + CommentEntity parent = findComment(parentId); + if (parent.isReply()) { + throw new IllegalArgumentException("대댓글에는 대댓글이 없습니다."); + } + return commentRepository.findByParentAndIsDeletedFalse(parent, pageable); + } } From d8cfa3295623dc373f82f74149bd12b9bf18d123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Sat, 31 May 2025 15:41:48 +0900 Subject: [PATCH 43/45] =?UTF-8?q?test=20:=20=EB=8C=80=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommentControllerIntegrationTest.java | 37 ++++- .../controller/CommentControllerTest.java | 50 +++++-- .../com/board/service/CommentServiceTest.java | 130 ++++++++++++++++-- 3 files changed, 186 insertions(+), 31 deletions(-) diff --git a/src/test/java/com/board/controller/CommentControllerIntegrationTest.java b/src/test/java/com/board/controller/CommentControllerIntegrationTest.java index 6331a9a..683912e 100644 --- a/src/test/java/com/board/controller/CommentControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/CommentControllerIntegrationTest.java @@ -1,12 +1,5 @@ package com.board.controller; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.board.dto.request.CommentCreateRequest; import com.board.dto.request.CommentUpdateRequest; import com.board.entity.ArticleEntity; @@ -25,6 +18,10 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + class CommentControllerIntegrationTest extends CleanDatabaseBeforeEachTest { @Autowired @@ -120,4 +117,30 @@ void setup() { .header(AUTHORIZATION_HEADER, jwtToken)) // JWT 인증 헤더 추가 .andExpect(status().isNoContent()); } + + @Test + @DisplayName("대댓글_목록_조회_성공") + void 대댓글_목록_조회_성공() throws Exception { + // Given + CommentEntity comment = commentRepository.save(new CommentEntity("부모 댓글", article, member)); + CommentEntity childComment1 = commentRepository.save(CommentEntity.builder() + .content("대댓글 1") + .article(article) + .member(member) + .parent(comment) + .build()); + CommentEntity childComment2 = commentRepository.save(CommentEntity.builder() + .content("대댓글 2") + .article(article) + .member(member) + .parent(comment) + .build()); + + // When & Then + mockMvc.perform(get("/articles/{articleId}/comments/{commentId}/replies", article.getId(), comment.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content[0].content").value("대댓글 1")) + .andExpect(jsonPath("$.content[1].content").value("대댓글 2")); + } } diff --git a/src/test/java/com/board/controller/CommentControllerTest.java b/src/test/java/com/board/controller/CommentControllerTest.java index 837412f..2c760b1 100644 --- a/src/test/java/com/board/controller/CommentControllerTest.java +++ b/src/test/java/com/board/controller/CommentControllerTest.java @@ -1,24 +1,14 @@ package com.board.controller; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.board.dto.request.CommentCreateRequest; import com.board.dto.request.CommentUpdateRequest; +import com.board.dto.response.CommentResponse; import com.board.entity.ArticleEntity; import com.board.entity.CommentEntity; import com.board.service.CommentService; import com.config.auth.AuthenticatedMemberArgumentResolver; import com.fasterxml.jackson.databind.ObjectMapper; import com.member.entity.MemberEntity; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -26,10 +16,20 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @WebMvcTest(CommentController.class) class CommentControllerTest { @@ -86,8 +86,9 @@ void setup() throws Exception { new CommentEntity("내용1", article, member), new CommentEntity("내용2", article, member)); Page pageResult = new PageImpl<>(comments); + Page response = pageResult.map(comment -> CommentResponse.of(comment, 0)); - when(commentService.findAllComments(eq(articleId), any())).thenReturn(pageResult); + when(commentService.findAllTopLevelComments(eq(articleId), any())).thenReturn(response); // when & then mockMvc.perform(get("/articles/{articleId}/comments", articleId) @@ -125,4 +126,29 @@ void setup() throws Exception { mockMvc.perform(delete("/articles/{articleId}/comments/{commentId}", articleId, commentId)) .andExpect(status().isNoContent()); } + + @Test + @DisplayName("대댓글 목록 조회 성공") + void 대댓글_목록_조회_성공() throws Exception { + // given + MemberEntity member = new MemberEntity("email", "password", "nickname"); + ArticleEntity article = new ArticleEntity("title", "content", member); + + CommentEntity reply1 = new CommentEntity("대댓글1", article, member); + CommentEntity reply2 = new CommentEntity("대댓글2", article, member); + Page replies = new PageImpl<>(List.of(reply1, reply2)); + + when(commentService.findReplies(eq(commentId), any(Pageable.class))) + .thenReturn(replies); + + // when & then + mockMvc.perform(get("/articles/{articleId}/comments/{commentId}/replies", articleId, commentId) + .param("page", "0") + .param("size", "10") + .param("sort", "oldest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").isArray()) + .andExpect(jsonPath("$.content[0].content").value("대댓글1")) + .andExpect(jsonPath("$.content[1].authorName").value("nickname")); + } } diff --git a/src/test/java/com/board/service/CommentServiceTest.java b/src/test/java/com/board/service/CommentServiceTest.java index f0aabd4..1b889ec 100644 --- a/src/test/java/com/board/service/CommentServiceTest.java +++ b/src/test/java/com/board/service/CommentServiceTest.java @@ -1,13 +1,8 @@ package com.board.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - import com.board.dto.request.CommentCreateRequest; import com.board.dto.request.CommentUpdateRequest; +import com.board.dto.response.CommentResponse; import com.board.entity.ArticleEntity; import com.board.entity.CommentEntity; import com.board.repository.CommentRepository; @@ -16,8 +11,6 @@ import com.exception.custom.NotIncludeBoardException; import com.member.entity.MemberEntity; import com.member.service.MemberService; -import java.util.List; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -30,6 +23,13 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class CommentServiceTest { @@ -67,15 +67,15 @@ void setUp() { Page commentPage = new PageImpl<>(comments, pageable, comments.size()); when(articleService.findById(articleId)).thenReturn(testArticle); - when(commentRepository.findByArticleAndIsDeletedFalse(testArticle, pageable)).thenReturn(commentPage); + when(commentRepository.findByArticleAndParentIsNullAndIsDeletedFalse(testArticle, pageable)).thenReturn(commentPage); // When - Page result = commentService.findAllComments(articleId, pageable); + Page result = commentService.findAllTopLevelComments(articleId, pageable); // Then assertThat(result.getContent()).hasSize(2); assertThat(result.getContent().get(0).getContent()).isEqualTo(testComment.getContent()); - assertThat(result.getContent().get(0).getMember().getId()).isEqualTo(testMember.getId()); + assertThat(result.getContent().get(0).getAuthorId()).isEqualTo(testMember.getId()); } @Test @@ -86,7 +86,7 @@ void setUp() { when(articleService.findById(articleId)).thenThrow(MyEntityNotFoundException.class); // When & Then - assertThatThrownBy(() -> commentService.findAllComments(articleId, pageable)) + assertThatThrownBy(() -> commentService.findAllTopLevelComments(articleId, pageable)) .isInstanceOf(MyEntityNotFoundException.class); } @@ -251,4 +251,110 @@ void setUp() { assertThatThrownBy(() -> commentService.updateComment(articleId, commentId, request, anotherMember.getId())) .isInstanceOf(DifferentOwnerException.class); } + + @Test + @DisplayName("전체 댓글 조회 시 각 댓글의 대댓글 수를 정확히 조회한다.") + void 전체댓글조회시_대댓글수_정확조회() { + // Given + CommentEntity reply1 = new CommentEntity(10L, "대댓글1", testArticle, testMember, testComment); + Pageable pageable = PageRequest.of(0, 10); + List comments = List.of(testComment); + Page commentPage = new PageImpl<>(comments, pageable, comments.size()); + + when(articleService.findById(articleId)).thenReturn(testArticle); + when(commentRepository.findByArticleAndParentIsNullAndIsDeletedFalse(testArticle, pageable)).thenReturn(commentPage); + when(commentRepository.countByParentAndIsDeletedFalse(testComment)).thenReturn(5); + + // When + Page result = commentService.findAllTopLevelComments(articleId, pageable); + + // Then + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getReplyCount()).isEqualTo(5); + } + + @Test + @DisplayName("댓글의 대댓글 수를 정확히 조회한다.") + void 대댓글수_정확조회() { + // Given + when(commentRepository.countByParentAndIsDeletedFalse(testComment)).thenReturn(3); + + // When + int replyCount = commentService.getReplyCount(testComment); + + // Then + assertThat(replyCount).isEqualTo(3); + } + + @Test + @DisplayName("대댓글에 대댓글을 작성하려고 하면 예외가 발생한다.") + void 대댓글에_대댓글작성_예외() { + // Given + CommentEntity parentReply = new CommentEntity(99L, "부모 대댓글", testArticle, testMember, testComment); + + CommentCreateRequest request = new CommentCreateRequest("대댓글의 대댓글", parentReply.getId()); + when(articleService.findById(articleId)).thenReturn(testArticle); + when(memberService.findById(memberId)).thenReturn(testMember); + when(commentRepository.findByIdAndIsDeletedFalse(parentReply.getId())).thenReturn(Optional.of(parentReply)); + + // When & Then + assertThatThrownBy(() -> commentService.createComment(articleId, request, memberId)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("댓글에 대댓글을 작성하고, 정상적으로 저장된다.") + void 대댓글_작성_성공() { + // Given + CommentCreateRequest request = new CommentCreateRequest("대댓글 내용", testComment.getId()); + CommentEntity reply = new CommentEntity(20L, "대댓글 내용", testArticle, testMember, testComment); + + when(articleService.findById(articleId)).thenReturn(testArticle); + when(memberService.findById(memberId)).thenReturn(testMember); + when(commentRepository.findByIdAndIsDeletedFalse(testComment.getId())).thenReturn(Optional.of(testComment)); + when(commentRepository.save(any(CommentEntity.class))).thenReturn(reply); + + // When + CommentEntity result = commentService.createComment(articleId, request, memberId); + + // Then + assertThat(result.getContent()).isEqualTo("대댓글 내용"); + assertThat(result.getParent()).isEqualTo(testComment); + } + + @Test + @DisplayName("부모 댓글이 다른 게시글에 속하면 예외가 발생한다.") + void 부모댓글이_다른게시글이면_예외() { + // Given + ArticleEntity otherArticle = new ArticleEntity(2L, "다른 글", "내용", testMember, 0); + CommentEntity otherComment = new CommentEntity(200L, "다른 글의 댓글", otherArticle, testMember); + CommentCreateRequest request = new CommentCreateRequest("잘못된 대댓글", otherComment.getId()); + + when(articleService.findById(articleId)).thenReturn(testArticle); + when(memberService.findById(memberId)).thenReturn(testMember); + when(commentRepository.findByIdAndIsDeletedFalse(otherComment.getId())).thenReturn(Optional.of(otherComment)); + + // When & Then + assertThatThrownBy(() -> commentService.createComment(articleId, request, memberId)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("대댓글 목록을 페이지 단위로 조회한다.") + void 대댓글_리스트_조회() { + // Given + CommentEntity reply1 = new CommentEntity(301L, "대댓글1", testArticle, testMember, testComment); + Pageable pageable = PageRequest.of(0, 10); + Page replyPage = new PageImpl<>(List.of(reply1), pageable, 1); + + when(commentRepository.findByIdAndIsDeletedFalse(testComment.getId())).thenReturn(Optional.of(testComment)); + when(commentRepository.findByParentAndIsDeletedFalse(testComment, pageable)).thenReturn(replyPage); + + // When + Page result = commentService.findReplies(testComment.getId(), pageable); + + // Then + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getContent()).isEqualTo("대댓글1"); + } } From 90e07f8c424a8d85b704cee4650d1fbaf6c13a4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Sun, 1 Jun 2025 15:54:58 +0900 Subject: [PATCH 44/45] =?UTF-8?q?feat=20:=20Redis=20=ED=99=9C=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EC=A1=B0=ED=9A=8C=EC=88=98=20=EC=A6=9D?= =?UTF-8?q?=EA=B0=80=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=20-=205=EB=B6=84=EC=97=90=201=EB=B2=88=EC=94=A9=20=EC=8A=A4?= =?UTF-8?q?=EC=BC=80=EC=A4=84=EB=9F=AC=EB=A1=9C=20Redis=EC=97=90=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EB=90=9C=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=88=98=20DB=EC=97=90=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=20=20-=20=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20Redis=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=B4=EC=84=9C=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=88=98=20=EC=A6=9D=EA=B0=80/=EC=A1=B0=ED=9A=8C=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 ++ src/main/java/com/BoardApplication.java | 2 + .../board/controller/ArticleController.java | 17 ++-- .../board/dto/response/ArticleResponse.java | 10 +++ .../java/com/board/entity/ArticleEntity.java | 4 +- .../board/repository/ArticleRepository.java | 6 +- .../ArticleViewCountSyncScheduler.java | 36 ++++++++ .../com/board/service/ArticleService.java | 12 +-- .../cache/ArticleViewCountCacheService.java | 57 ++++++++++++ .../com/config/redis/EmbeddedRedisConfig.java | 30 +++++++ .../java/com/config/redis/RedisConfig.java | 23 +++++ src/main/resources/application.yml | 4 + .../ArticleControllerIntegrationTest.java | 40 +++++++-- .../controller/ArticleControllerTest.java | 24 ++--- .../ArticleViewCountCacheServiceTest.java | 90 +++++++++++++++++++ .../com/config/redis/RedisConnectionTest.java | 21 +++++ src/test/resources/application-test.yml | 5 ++ 17 files changed, 345 insertions(+), 42 deletions(-) create mode 100644 src/main/java/com/board/scheduler/ArticleViewCountSyncScheduler.java create mode 100644 src/main/java/com/board/service/cache/ArticleViewCountCacheService.java create mode 100644 src/main/java/com/config/redis/EmbeddedRedisConfig.java create mode 100644 src/main/java/com/config/redis/RedisConfig.java create mode 100644 src/test/java/com/board/service/cache/ArticleViewCountCacheServiceTest.java create mode 100644 src/test/java/com/config/redis/RedisConnectionTest.java diff --git a/build.gradle b/build.gradle index e6d0fb4..359d131 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,12 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-cache' + implementation('it.ozimov:embedded-redis:0.7.3') { + exclude group: "org.slf4j", module: "slf4j-simple" + } } tasks.named('test') { diff --git a/src/main/java/com/BoardApplication.java b/src/main/java/com/BoardApplication.java index f6dc92e..bea8972 100644 --- a/src/main/java/com/BoardApplication.java +++ b/src/main/java/com/BoardApplication.java @@ -3,9 +3,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.cache.annotation.EnableCaching; @SpringBootApplication @ConfigurationPropertiesScan +@EnableCaching public class BoardApplication { public static void main(String[] args) { diff --git a/src/main/java/com/board/controller/ArticleController.java b/src/main/java/com/board/controller/ArticleController.java index 3458b63..1ec1904 100644 --- a/src/main/java/com/board/controller/ArticleController.java +++ b/src/main/java/com/board/controller/ArticleController.java @@ -5,6 +5,7 @@ import com.board.dto.response.ArticleResponse; import com.board.entity.ArticleEntity; import com.board.service.ArticleService; +import com.board.service.cache.ArticleViewCountCacheService; import com.config.auth.annotation.AuthenticatedMember; import com.util.page.PageResponse; import com.util.sort.SortUtils; @@ -15,15 +16,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor @RestController @@ -31,6 +24,7 @@ public class ArticleController { private final ArticleService articleService; + private final ArticleViewCountCacheService viewCountCacheService; @PostMapping("") public ResponseEntity addArticle(@Valid @RequestBody ArticleCreateRequest request, @@ -56,10 +50,11 @@ public ResponseEntity> findAllArticles(@RequestPar @GetMapping("/{id}") public ResponseEntity findArticle(@PathVariable long id) { - ArticleEntity article = articleService.findByIdAndIncreaseViewCount(id); + ArticleEntity article = articleService.findById(id); + viewCountCacheService.increase(id); return ResponseEntity.ok() - .body(new ArticleResponse(article)); + .body(ArticleResponse.from(article, viewCountCacheService.getViewCount(id))); } @DeleteMapping("/{id}") diff --git a/src/main/java/com/board/dto/response/ArticleResponse.java b/src/main/java/com/board/dto/response/ArticleResponse.java index ca42d8d..3dca41b 100644 --- a/src/main/java/com/board/dto/response/ArticleResponse.java +++ b/src/main/java/com/board/dto/response/ArticleResponse.java @@ -38,4 +38,14 @@ public static ArticleResponse withoutContent(ArticleEntity article) { article.getMember().getId(), article.getViewCount()); } + + public static ArticleResponse from(ArticleEntity article, long viewCount) { + return new ArticleResponse( + article.getId(), + article.getTitle(), + article.getContent(), + article.getMember().getId(), + viewCount + ); + } } diff --git a/src/main/java/com/board/entity/ArticleEntity.java b/src/main/java/com/board/entity/ArticleEntity.java index 3c78fbf..6b7db75 100644 --- a/src/main/java/com/board/entity/ArticleEntity.java +++ b/src/main/java/com/board/entity/ArticleEntity.java @@ -48,8 +48,8 @@ public ArticleEntity(Long id, String title, String content, MemberEntity member, this.viewCount = viewCount; } - public void increaseViewCount() { - this.viewCount++; + public void increaseViewCount(long viewCount) { + this.viewCount += viewCount; } public void update(String title, String content) { diff --git a/src/main/java/com/board/repository/ArticleRepository.java b/src/main/java/com/board/repository/ArticleRepository.java index 7c4a2a4..f6af7f5 100644 --- a/src/main/java/com/board/repository/ArticleRepository.java +++ b/src/main/java/com/board/repository/ArticleRepository.java @@ -11,7 +11,7 @@ public interface ArticleRepository extends JpaRepository { Page findAllByIsDeletedFalse(Pageable pageable); - @Modifying(clearAutomatically = true) - @Query("UPDATE ArticleEntity a SET a.viewCount = a.viewCount + 1 WHERE a.id = :id") - void increaseViewCount(@Param("id") Long id); + @Modifying(clearAutomatically = true) // 현재 안씀 + @Query("UPDATE ArticleEntity a SET a.viewCount = a.viewCount + :count WHERE a.id = :id") + void increaseViewCount(@Param("id") Long id, @Param("count") long count); } diff --git a/src/main/java/com/board/scheduler/ArticleViewCountSyncScheduler.java b/src/main/java/com/board/scheduler/ArticleViewCountSyncScheduler.java new file mode 100644 index 0000000..e13952e --- /dev/null +++ b/src/main/java/com/board/scheduler/ArticleViewCountSyncScheduler.java @@ -0,0 +1,36 @@ +package com.board.scheduler; + +import com.board.service.ArticleService; +import com.board.service.cache.ArticleViewCountCacheService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Slf4j +@RequiredArgsConstructor +@Component +public class ArticleViewCountSyncScheduler { + + private final ArticleViewCountCacheService cacheService; + private final ArticleService articleService; + + private static final String PREFIX = "article:viewCount:"; + + @Scheduled(fixedRate = 300000) + public void syncViewCountsToDB() { + Map cachedViewCounts = cacheService.getAllViewCounts(); + + for (Map.Entry entry : cachedViewCounts.entrySet()) { + Long articleId = entry.getKey(); + Long viewCount = entry.getValue(); + + articleService.incrementViewCount(articleId, viewCount); + cacheService.reset(articleId); // 반영 후 캐시 초기화 + } + + log.info("동기화 완료: {}개의 게시글 조회수를 DB에 반영했습니다.", cachedViewCounts.size()); + } +} diff --git a/src/main/java/com/board/service/ArticleService.java b/src/main/java/com/board/service/ArticleService.java index 0dc3b97..0504d87 100644 --- a/src/main/java/com/board/service/ArticleService.java +++ b/src/main/java/com/board/service/ArticleService.java @@ -34,12 +34,6 @@ public Page findAll(Pageable pageable) { return articleRepository.findAllByIsDeletedFalse(pageable); } - @Transactional - public ArticleEntity findByIdAndIncreaseViewCount(Long id) { - articleRepository.increaseViewCount(id); - return findById(id); - } - @Transactional public void delete(Long id, Long memberId) { getOwnedArticle(id, memberId).softDelete(); @@ -59,6 +53,12 @@ private ArticleEntity getOwnedArticle(Long articleId, Long memberId) { return article; } + @Transactional + public void incrementViewCount(Long articleId, long viewCount) { + ArticleEntity article = findById(articleId); + article.increaseViewCount(viewCount); + } + public ArticleEntity findById(Long id) { return articleRepository.findById(id) .orElseThrow(() -> MyEntityNotFoundException.from(id)); diff --git a/src/main/java/com/board/service/cache/ArticleViewCountCacheService.java b/src/main/java/com/board/service/cache/ArticleViewCountCacheService.java new file mode 100644 index 0000000..0ea4fd0 --- /dev/null +++ b/src/main/java/com/board/service/cache/ArticleViewCountCacheService.java @@ -0,0 +1,57 @@ +package com.board.service.cache; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +@RequiredArgsConstructor +@Service +public class ArticleViewCountCacheService { + + private static final String PREFIX = "article:viewCount:"; + + private final RedisTemplate redisTemplate; + + public void increase(Long articleId) { + String key = getKey(articleId); + redisTemplate.opsForValue().increment(key); + } + + public long getViewCount(Long articleId) { + String key = getKey(articleId); + String value = redisTemplate.opsForValue().get(key); + if (value == null) { + return 0; + } + return Long.parseLong(value); + } + + public void reset(Long articleId) { + String key = getKey(articleId); + redisTemplate.delete(key); + } + + public Map getAllViewCounts() { + Set keys = redisTemplate.keys(PREFIX + "*"); + Map result = new HashMap<>(); + if (keys.isEmpty()) { + return result; + } + for (String key : keys) { + String value = redisTemplate.opsForValue().get(key); + if (value != null) { + Long articleId = Long.parseLong(key.replace(PREFIX, "")); + result.put(articleId, Long.parseLong(value)); + } + } + return result; + } + + private String getKey(Long articleId) { + return PREFIX + articleId; + } +} diff --git a/src/main/java/com/config/redis/EmbeddedRedisConfig.java b/src/main/java/com/config/redis/EmbeddedRedisConfig.java new file mode 100644 index 0000000..9aeb316 --- /dev/null +++ b/src/main/java/com/config/redis/EmbeddedRedisConfig.java @@ -0,0 +1,30 @@ +package com.config.redis; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import redis.embedded.RedisServer; + +import java.io.IOException; + +@Configuration +@Profile("test") +public class EmbeddedRedisConfig { + + private RedisServer redisServer; + + @PostConstruct + public void startRedis() throws IOException { + redisServer = new RedisServer(6380); + redisServer.start(); + } + + @PreDestroy + public void stopRedis() { + if (redisServer != null) { + redisServer.stop(); + } + } + +} diff --git a/src/main/java/com/config/redis/RedisConfig.java b/src/main/java/com/config/redis/RedisConfig.java new file mode 100644 index 0000000..cf6759e --- /dev/null +++ b/src/main/java/com/config/redis/RedisConfig.java @@ -0,0 +1,23 @@ +package com.config.redis; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(connectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + redisTemplate.afterPropertiesSet(); + return redisTemplate; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7a43f09..6282d3d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,6 +12,10 @@ spring: hibernate: format_sql: true + redis: + host: localhost + port: 6379 + datasource: url: jdbc:h2:mem:testdb diff --git a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java index f31d569..66a72e1 100644 --- a/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java +++ b/src/test/java/com/board/controller/ArticleControllerIntegrationTest.java @@ -1,18 +1,10 @@ package com.board.controller; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.board.dto.request.ArticleCreateRequest; import com.board.dto.request.ArticleUpdateRequest; import com.board.entity.ArticleEntity; import com.board.repository.ArticleRepository; +import com.board.service.cache.ArticleViewCountCacheService; import com.config.jwt.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; @@ -31,6 +23,12 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + class ArticleControllerIntegrationTest extends CleanDatabaseBeforeEachTest { @Autowired @@ -48,6 +46,9 @@ class ArticleControllerIntegrationTest extends CleanDatabaseBeforeEachTest { @Autowired private JwtUtil jwtUtil; + @Autowired + private ArticleViewCountCacheService viewCountCacheService; + @Value("${jwt.secret_key}") private String secretKey; @@ -230,4 +231,25 @@ private Long createArticleAndGetId(String title, String content) throws Exceptio return JsonPath.parse(mvcResult.getResponse().getContentAsString()).read("$.id", Long.class); } + + @Test + @DisplayName("게시글 조회 시 Redis 캐시의 조회수가 증가한다") + void 게시글_조회시_조회수_캐시_증가() throws Exception { + // given + MemberEntity member = createAndSaveMember("viewer@test.com", "viewer"); + ArticleEntity article = articleRepository.save(new ArticleEntity("조회수 테스트 제목", "조회수 테스트 내용", member)); + Long articleId = article.getId(); + + // when + for (int i = 0; i < 3; i++) { + mockMvc.perform(get("/articles/" + articleId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.title").value("조회수 테스트 제목")); + } + + // then + long viewCount = viewCountCacheService.getViewCount(articleId); + assertThat(viewCount).isEqualTo(3L); + } + } diff --git a/src/test/java/com/board/controller/ArticleControllerTest.java b/src/test/java/com/board/controller/ArticleControllerTest.java index 7a6399e..59cf3bf 100644 --- a/src/test/java/com/board/controller/ArticleControllerTest.java +++ b/src/test/java/com/board/controller/ArticleControllerTest.java @@ -1,24 +1,15 @@ package com.board.controller; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.board.dto.request.ArticleCreateRequest; import com.board.dto.request.ArticleUpdateRequest; import com.board.dto.response.ArticleResponse; import com.board.entity.ArticleEntity; import com.board.service.ArticleService; +import com.board.service.cache.ArticleViewCountCacheService; import com.config.auth.AuthenticatedMemberArgumentResolver; import com.fasterxml.jackson.databind.ObjectMapper; import com.member.entity.MemberEntity; import com.member.service.MemberService; -import java.util.List; import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -30,6 +21,14 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @WebMvcTest(ArticleController.class) class ArticleControllerTest { @@ -42,6 +41,9 @@ class ArticleControllerTest { @MockitoBean private MemberService memberService; + @MockitoBean + private ArticleViewCountCacheService viewCountCacheService; + @MockitoBean private AuthenticatedMemberArgumentResolver authenticatedMemberArgumentResolver; @@ -134,7 +136,7 @@ void addArticle_Success() throws Exception { .viewCount(0L) // 초기 조회수 설정 .build(); - when(articleService.findByIdAndIncreaseViewCount(1L)).thenReturn(article); + when(articleService.findById(1L)).thenReturn(article); // When & Then mockMvc.perform(get("/articles/1")) diff --git a/src/test/java/com/board/service/cache/ArticleViewCountCacheServiceTest.java b/src/test/java/com/board/service/cache/ArticleViewCountCacheServiceTest.java new file mode 100644 index 0000000..324bb9e --- /dev/null +++ b/src/test/java/com/board/service/cache/ArticleViewCountCacheServiceTest.java @@ -0,0 +1,90 @@ +package com.board.service.cache; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class ArticleViewCountCacheServiceTest { + + @Autowired + private ArticleViewCountCacheService articleViewCountCacheService; + + @Autowired + private RedisTemplate redisTemplate; + + private static final String KEY_PREFIX = "article:viewCount:"; + + @BeforeEach + void setup() { + redisTemplate.getConnectionFactory().getConnection().flushAll(); + } + + @AfterEach + void tearDown() { + redisTemplate.getConnectionFactory().getConnection().flushAll(); + } + + @DisplayName("increase() 호출 시 게시글 조회수가 1 증가한다.") + @Test + void 게시글_조회수가_1_증가() { + // given (준비) + Long articleId = 100L; + String redisKey = KEY_PREFIX + articleId; + + // when (실행) + articleViewCountCacheService.increase(articleId); // 1번째 증가 + articleViewCountCacheService.increase(articleId); // 2번째 증가 + articleViewCountCacheService.increase(articleId); // 3번째 증가 + + // then (검증) + // 1. 서비스 메서드를 통해 반환되는 조회수 검증 + long viewCount = articleViewCountCacheService.getViewCount(articleId); + assertThat(viewCount).isEqualTo(3); + + // 2. Redis에 실제로 저장된 값도 검증 + String storedValueInRedis = redisTemplate.opsForValue().get(redisKey); + assertThat(storedValueInRedis).isEqualTo("3"); + } + + @DisplayName("getViewCount() 호출 시 캐시에 값이 없으면 0을 반환한다.") + @Test + void 캐시에_값이_없으면_0을_반환() { + // given (준비) + Long articleId = 200L; // 아직 Redis에 없는 ID + + // when (실행) + long viewCount = articleViewCountCacheService.getViewCount(articleId); + + // then (검증) + assertThat(viewCount).isEqualTo(0); + } + + @DisplayName("reset() 호출 시 해당 게시글의 조회수 캐시가 삭제된다.") + @Test + void 게시글의_조회수_캐시가_삭제() { + // given (준비) + Long articleId = 300L; + String redisKey = KEY_PREFIX + articleId; + articleViewCountCacheService.increase(articleId); // 조회수 1로 만듦 + assertThat(articleViewCountCacheService.getViewCount(articleId)).isEqualTo(1); + + // when (실행) + articleViewCountCacheService.reset(articleId); // 조회수 초기화 (삭제) + + // then (검증) + // 1. 서비스 메서드를 통해 조회수가 0으로 반환되는지 확인 + long viewCountAfterReset = articleViewCountCacheService.getViewCount(articleId); + assertThat(viewCountAfterReset).isEqualTo(0); + + // 2. Redis에 실제로 해당 키가 없는지 확인 + Boolean keyExists = redisTemplate.hasKey(redisKey); + assertThat(keyExists).isFalse(); + } +} diff --git a/src/test/java/com/config/redis/RedisConnectionTest.java b/src/test/java/com/config/redis/RedisConnectionTest.java new file mode 100644 index 0000000..db3cb83 --- /dev/null +++ b/src/test/java/com/config/redis/RedisConnectionTest.java @@ -0,0 +1,21 @@ +package com.config.redis; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +class RedisConnectionTest { + @Autowired + private RedisTemplate redisTemplate; + + @Test + void redis_set_and_get() { + redisTemplate.opsForValue().set("testkey", "hello redis"); + String value = (String) redisTemplate.opsForValue().get("testkey"); + assertEquals("hello redis", value); + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 48ff0f9..b307dab 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -12,6 +12,11 @@ spring: properties: hibernate: format_sql: true + + data: + redis: + host: localhost + port: 6380 #테스트용 다른 포트 jwt: issuer: test-issuer From e6a552b0c7b2ec939752e7eb346d2c334c27a777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=EC=A7=84?= Date: Sun, 1 Jun 2025 16:47:54 +0900 Subject: [PATCH 45/45] =?UTF-8?q?refactor=20:=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=88=98=20=EC=A6=9D=EA=B0=80=20Controller=20>=20Service?= =?UTF-8?q?=EB=A1=9C=20=EB=A1=9C=EC=A7=81=20=EC=9D=B4=EB=8F=99=20,=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=82=AD=EC=A0=9C=EC=8B=9C=20Red?= =?UTF-8?q?is=EC=97=90=EB=8F=84=20=EC=82=AD=EC=A0=9C=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/board/controller/ArticleController.java | 9 ++------- src/main/java/com/board/service/ArticleService.java | 11 +++++++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/board/controller/ArticleController.java b/src/main/java/com/board/controller/ArticleController.java index 1ec1904..64cdb3c 100644 --- a/src/main/java/com/board/controller/ArticleController.java +++ b/src/main/java/com/board/controller/ArticleController.java @@ -5,7 +5,6 @@ import com.board.dto.response.ArticleResponse; import com.board.entity.ArticleEntity; import com.board.service.ArticleService; -import com.board.service.cache.ArticleViewCountCacheService; import com.config.auth.annotation.AuthenticatedMember; import com.util.page.PageResponse; import com.util.sort.SortUtils; @@ -24,7 +23,6 @@ public class ArticleController { private final ArticleService articleService; - private final ArticleViewCountCacheService viewCountCacheService; @PostMapping("") public ResponseEntity addArticle(@Valid @RequestBody ArticleCreateRequest request, @@ -50,18 +48,15 @@ public ResponseEntity> findAllArticles(@RequestPar @GetMapping("/{id}") public ResponseEntity findArticle(@PathVariable long id) { - ArticleEntity article = articleService.findById(id); - viewCountCacheService.increase(id); - + ArticleResponse articleWithViewCount = articleService.getArticleWithViewCount(id); return ResponseEntity.ok() - .body(ArticleResponse.from(article, viewCountCacheService.getViewCount(id))); + .body(articleWithViewCount); } @DeleteMapping("/{id}") public ResponseEntity deleteArticle(@PathVariable long id, @AuthenticatedMember Long memberId) { articleService.delete(id, memberId); - return ResponseEntity.noContent() .build(); } diff --git a/src/main/java/com/board/service/ArticleService.java b/src/main/java/com/board/service/ArticleService.java index 0504d87..1ec8078 100644 --- a/src/main/java/com/board/service/ArticleService.java +++ b/src/main/java/com/board/service/ArticleService.java @@ -2,8 +2,10 @@ import com.board.dto.request.ArticleCreateRequest; import com.board.dto.request.ArticleUpdateRequest; +import com.board.dto.response.ArticleResponse; import com.board.entity.ArticleEntity; import com.board.repository.ArticleRepository; +import com.board.service.cache.ArticleViewCountCacheService; import com.exception.custom.MyEntityNotFoundException; import com.member.entity.MemberEntity; import com.member.service.MemberService; @@ -20,6 +22,7 @@ public class ArticleService { private final ArticleRepository articleRepository; private final MemberService memberService; + private final ArticleViewCountCacheService viewCountCacheService; @Transactional public ArticleEntity save(ArticleCreateRequest request, Long memberId) { @@ -37,6 +40,7 @@ public Page findAll(Pageable pageable) { @Transactional public void delete(Long id, Long memberId) { getOwnedArticle(id, memberId).softDelete(); + viewCountCacheService.reset(id); } @Transactional @@ -53,6 +57,13 @@ private ArticleEntity getOwnedArticle(Long articleId, Long memberId) { return article; } + public ArticleResponse getArticleWithViewCount(Long articleId) { + ArticleEntity article = findById(articleId); + viewCountCacheService.increase(articleId); + long totalViewCount = article.getViewCount() + viewCountCacheService.getViewCount(articleId); + return ArticleResponse.from(article, totalViewCount); + } + @Transactional public void incrementViewCount(Long articleId, long viewCount) { ArticleEntity article = findById(articleId);