diff --git a/backend/src/main/java/reviewme/config/WebConfig.java b/backend/src/main/java/reviewme/config/WebConfig.java new file mode 100644 index 000000000..423c8f0e5 --- /dev/null +++ b/backend/src/main/java/reviewme/config/WebConfig.java @@ -0,0 +1,16 @@ +package reviewme.config; + +import java.util.List; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import reviewme.global.HeaderPropertyArgumentResolver; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new HeaderPropertyArgumentResolver()); + } +} diff --git a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java index c5260df73..b3423e102 100644 --- a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java +++ b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java @@ -73,8 +73,10 @@ public ProblemDetail handleServletRequestBindingException(Exception ex) { return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "요청을 읽을 수 없습니다."); } - @ExceptionHandler({MethodValidationException.class, BindException.class, - TypeMismatchException.class, HandlerMethodValidationException.class}) + @ExceptionHandler({ + MethodValidationException.class, BindException.class, + TypeMismatchException.class, HandlerMethodValidationException.class + }) public ProblemDetail handleRequestFormatException(Exception ex) { return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "요청의 형식이 잘못되었습니다."); } diff --git a/backend/src/main/java/reviewme/global/HeaderProperty.java b/backend/src/main/java/reviewme/global/HeaderProperty.java new file mode 100644 index 000000000..86462c596 --- /dev/null +++ b/backend/src/main/java/reviewme/global/HeaderProperty.java @@ -0,0 +1,18 @@ +package reviewme.global; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface HeaderProperty { + + @AliasFor("headerName") + String value() default ""; + + @AliasFor("value") + String headerName() default ""; +} diff --git a/backend/src/main/java/reviewme/global/HeaderPropertyArgumentResolver.java b/backend/src/main/java/reviewme/global/HeaderPropertyArgumentResolver.java new file mode 100644 index 000000000..5c825e3de --- /dev/null +++ b/backend/src/main/java/reviewme/global/HeaderPropertyArgumentResolver.java @@ -0,0 +1,32 @@ +package reviewme.global; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import reviewme.global.exception.MissingHeaderPropertyException; + +public class HeaderPropertyArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(HeaderProperty.class); + } + + @Override + public String resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + + HeaderProperty parameterAnnotation = parameter.getParameterAnnotation(HeaderProperty.class); + String headerName = parameterAnnotation.headerName(); + String headerProperty = request.getHeader(headerName); + + if (headerProperty == null) { + throw new MissingHeaderPropertyException(headerName); + } + return headerProperty; + } +} diff --git a/backend/src/main/java/reviewme/global/exception/MissingHeaderPropertyException.java b/backend/src/main/java/reviewme/global/exception/MissingHeaderPropertyException.java new file mode 100644 index 000000000..1da1ae3f0 --- /dev/null +++ b/backend/src/main/java/reviewme/global/exception/MissingHeaderPropertyException.java @@ -0,0 +1,8 @@ +package reviewme.global.exception; + +public class MissingHeaderPropertyException extends BadRequestException { + + public MissingHeaderPropertyException(String headerName) { + super("요청에 %s이(가) 존재하지 않아요.".formatted(headerName)); + } +} diff --git a/backend/src/main/java/reviewme/review/controller/ReviewController.java b/backend/src/main/java/reviewme/review/controller/ReviewController.java index a56198ea6..3e338f34e 100644 --- a/backend/src/main/java/reviewme/review/controller/ReviewController.java +++ b/backend/src/main/java/reviewme/review/controller/ReviewController.java @@ -1,6 +1,5 @@ package reviewme.review.controller; -import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import java.net.URI; import lombok.RequiredArgsConstructor; @@ -11,6 +10,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import reviewme.global.HeaderProperty; import reviewme.review.dto.request.CreateReviewRequest; import reviewme.review.dto.response.ReceivedReviewsResponse; import reviewme.review.dto.response.ReviewDetailResponse; @@ -32,8 +32,9 @@ public ResponseEntity createReview(@Valid @RequestBody CreateReviewRequest } @GetMapping("/reviews") - public ResponseEntity findReceivedReviews(HttpServletRequest request) { - String groupAccessCode = request.getHeader(GROUP_ACCESS_CODE_HEADER); + public ResponseEntity findReceivedReviews( + @HeaderProperty(GROUP_ACCESS_CODE_HEADER) String groupAccessCode + ) { ReceivedReviewsResponse response = reviewService.findReceivedReviews(groupAccessCode); return ResponseEntity.ok(response); } @@ -45,9 +46,9 @@ public ResponseEntity findReviewCreationSetup(@RequestParam } @GetMapping("/reviews/{id}") - public ResponseEntity findReceivedReviewDetail(@PathVariable long id, - HttpServletRequest request) { - String groupAccessCode = request.getHeader(GROUP_ACCESS_CODE_HEADER); + public ResponseEntity findReceivedReviewDetail( + @PathVariable long id, + @HeaderProperty(GROUP_ACCESS_CODE_HEADER) String groupAccessCode) { ReviewDetailResponse response = reviewService.findReceivedReviewDetail(groupAccessCode, id); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/reviewme/review/service/exception/InvalidGroupAccessCodeException.java b/backend/src/main/java/reviewme/review/service/exception/InvalidGroupAccessCodeException.java deleted file mode 100644 index c392eb315..000000000 --- a/backend/src/main/java/reviewme/review/service/exception/InvalidGroupAccessCodeException.java +++ /dev/null @@ -1,10 +0,0 @@ -package reviewme.review.service.exception; - -import reviewme.global.exception.BadRequestException; - -public class InvalidGroupAccessCodeException extends BadRequestException { - - public InvalidGroupAccessCodeException() { - super("올바르지 않은 확인 코드예요."); - } -} diff --git a/backend/src/main/java/reviewme/review/service/exception/InvalidReviewRequestCodeException.java b/backend/src/main/java/reviewme/review/service/exception/InvalidReviewRequestCodeException.java deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/src/test/java/reviewme/global/HeaderPropertyArgumentResolverTest.java b/backend/src/test/java/reviewme/global/HeaderPropertyArgumentResolverTest.java new file mode 100644 index 000000000..fdaae95df --- /dev/null +++ b/backend/src/test/java/reviewme/global/HeaderPropertyArgumentResolverTest.java @@ -0,0 +1,56 @@ +package reviewme.global; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.NativeWebRequest; +import reviewme.global.exception.MissingHeaderPropertyException; + +class HeaderPropertyArgumentResolverTest { + + private final HeaderPropertyArgumentResolver resolver = new HeaderPropertyArgumentResolver(); + private final MethodParameter parameter = mock(MethodParameter.class); + private final HeaderProperty headerProperty = mock(HeaderProperty.class); + + @BeforeEach + void setUp() { + given(parameter.hasParameterAnnotation(HeaderProperty.class)).willReturn(true); + given(parameter.getParameterAnnotation(HeaderProperty.class)).willReturn(headerProperty); + } + + @Test + void 검증값이_헤더에_존재하지_않으면_검증에_실패한다() { + // given + NativeWebRequest request = mock(NativeWebRequest.class); + given(request.getNativeRequest()).willReturn(new MockHttpServletRequest()); + given(headerProperty.headerName()).willReturn("test"); + + // when, then + assertThatThrownBy(() -> resolver.resolveArgument(parameter, null, request, null)) + .isInstanceOf(MissingHeaderPropertyException.class); + } + + @Test + void 검증값이_헤더에_존재하면_값을_반환한다() { + // given + String headerName = "test"; + String headerValue = "1234"; + NativeWebRequest request = mock(NativeWebRequest.class); + MockHttpServletRequest mockRequest = (new MockHttpServletRequest()); + mockRequest.addHeader(headerName, headerValue); + given(request.getNativeRequest()).willReturn(mockRequest); + given(headerProperty.headerName()).willReturn(headerName); + + // when + String actual = resolver.resolveArgument(parameter, null, request, null); + + // then + assertThat(actual).isEqualTo(headerValue); + } +} diff --git a/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java b/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java index e5c5d3663..be62e6f80 100644 --- a/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java +++ b/backend/src/test/java/reviewme/review/service/ReviewServiceTest.java @@ -31,7 +31,6 @@ import reviewme.review.repository.ReviewContentRepository; import reviewme.review.repository.ReviewKeywordRepository; import reviewme.review.repository.ReviewRepository; -import reviewme.review.service.exception.InvalidGroupAccessCodeException; import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.support.ServiceTest;