diff --git a/src/docs/asciidoc/auth/mail.adoc b/src/docs/asciidoc/auth/mail.adoc index 518c3459..f71f2ec5 100644 --- a/src/docs/asciidoc/auth/mail.adoc +++ b/src/docs/asciidoc/auth/mail.adoc @@ -14,9 +14,11 @@ Http Response include::{snippets}/check-duplicate-email/success/http-response.adoc[] === 2. 중복된 이메일로 실패 +Http Response include::{snippets}/check-duplicate-email/fail-by-duplicated-email/http-response.adoc[] === 3. 틀린 이메일 형식으로 실패 +Http Response include::{snippets}/check-duplicate-email/fail-by-wrong-email/http-response.adoc[] == 검증 메일 전송 API @@ -29,6 +31,10 @@ Http Response include::{snippets}/send-authorization-mail/success/http-response.adoc[] include::{snippets}/send-authorization-mail/success/response-fields.adoc[] +=== 2. 틀린 이메일 형식으로 실패 +Http Response +include::{snippets}/send-authorization-mail/fail-by-wrong-email/http-response.adoc[] + == 메일 검증 API === 1. 성공 diff --git a/src/docs/asciidoc/review/review-search.adoc b/src/docs/asciidoc/review/review-search.adoc index 542b33db..f772f0ac 100644 --- a/src/docs/asciidoc/review/review-search.adoc +++ b/src/docs/asciidoc/review/review-search.adoc @@ -63,4 +63,20 @@ include::{snippets}/find-review-of-month/success/response-fields.adoc[] === 2. 헤더에 토큰을 포함하지 않아 실패 Http Response include::{snippets}/find-review-of-month/fail-by-no-access-token/http-response.adoc[] -include::{snippets}/find-review-of-month/fail-by-no-access-token/response-fields.adoc[] \ No newline at end of file +include::{snippets}/find-review-of-month/fail-by-no-access-token/response-fields.adoc[] + +== 헤어스타일 리뷰 검색 API +=== 1. 성공 +Http Request +include::{snippets}/find-hair-style-review/success/http-request.adoc[] +include::{snippets}/find-hair-style-review/success/request-headers.adoc[] +include::{snippets}/find-hair-style-review/success/path-parameters.adoc[] + +Http Response +include::{snippets}/find-hair-style-review/success/http-response.adoc[] +include::{snippets}/find-hair-style-review/success/response-fields.adoc[] + +=== 2. 헤더에 토크을 포함하지 않아 실패 +Http Response +include::{snippets}/find-hair-style-review/fail-by-no-access-token/http-response.adoc[] + diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/auth/controller/MailAuthController.java b/src/main/java/com/inq/wishhair/wesharewishhair/auth/controller/MailAuthController.java index 89b4c121..41c620ba 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/auth/controller/MailAuthController.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/auth/controller/MailAuthController.java @@ -49,7 +49,7 @@ public ResponseEntity sendAuthorizationMail(@RequestBody Mail String authKey = createAuthKey(); String sessionId = registerAuthKey(request, authKey); - eventPublisher.publishEvent(new AuthMailSendEvent(mailRequest.getEmail(), authKey)); + eventPublisher.publishEvent(new AuthMailSendEvent(new Email(mailRequest.getEmail()), authKey)); return ResponseEntity.ok(new SessionIdResponse(sessionId)); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/auth/event/AuthMailSendEvent.java b/src/main/java/com/inq/wishhair/wesharewishhair/auth/event/AuthMailSendEvent.java index 57aebc57..794e9958 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/auth/event/AuthMailSendEvent.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/auth/event/AuthMailSendEvent.java @@ -1,4 +1,6 @@ package com.inq.wishhair.wesharewishhair.auth.event; -public record AuthMailSendEvent(String address, String authKey) { +import com.inq.wishhair.wesharewishhair.user.domain.Email; + +public record AuthMailSendEvent(Email email, String authKey) { } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/aop/annotation/AddisWriter.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/aop/annotation/AddisWriter.java index 3c4fc0cf..256cd1b0 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/global/aop/annotation/AddisWriter.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/aop/annotation/AddisWriter.java @@ -5,7 +5,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -@Target({ElementType.TYPE, ElementType.METHOD}) +@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AddisWriter { } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/aop/aspect/AddIsWriterAspect.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/aop/aspect/AddIsWriterAspect.java index bb2f693e..befa7090 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/global/aop/aspect/AddIsWriterAspect.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/aop/aspect/AddIsWriterAspect.java @@ -1,6 +1,6 @@ package com.inq.wishhair.wesharewishhair.global.aop.aspect; -import com.inq.wishhair.wesharewishhair.global.dto.response.PagedResponse; +import com.inq.wishhair.wesharewishhair.global.dto.response.ListResponse; import com.inq.wishhair.wesharewishhair.global.exception.ErrorCode; import com.inq.wishhair.wesharewishhair.global.exception.WishHairException; import com.inq.wishhair.wesharewishhair.review.service.dto.response.ReviewDetailResponse; @@ -13,8 +13,9 @@ @Aspect public class AddIsWriterAspect { - @Pointcut("execution(com.inq.wishhair.wesharewishhair.global.dto.response.PagedResponse *(..))") - private void pagedResponsePointcut() {} + @Pointcut("execution(com.inq.wishhair.wesharewishhair.global.dto.response.PagedResponse *(..)) ||" + + "execution(com.inq.wishhair.wesharewishhair.global.dto.response.ResponseWrapper *(..))") + private void listResponsePointcut() {} @Pointcut("execution(com.inq.wishhair.wesharewishhair.review.service.dto.response.ReviewDetailResponse *(..))") private void reviewDetailResponsePointcut() {} @@ -23,13 +24,13 @@ private void reviewDetailResponsePointcut() {} private void addWriterAnnotation() {} @SuppressWarnings("unchecked") - @Around("pagedResponsePointcut() && addWriterAnnotation() && args(userId, ..)") + @Around("listResponsePointcut() && addWriterAnnotation() && args(userId, ..)") public Object addIsWriterToPagedResponse(ProceedingJoinPoint joinPoint, Long userId) throws Throwable { - PagedResponse result = (PagedResponse) joinPoint.proceed(); + ListResponse result = (ListResponse) joinPoint.proceed(); if (!result.getResult().isEmpty() && !(result.getResult().get(0) instanceof ReviewResponse)) { throw new WishHairException(ErrorCode.AOP_GENERIC_EXCEPTION); } - PagedResponse castedResult = (PagedResponse) result; + ListResponse castedResult = (ListResponse) result; castedResult.getResult().forEach((response -> response.addIsWriter(userId))); return castedResult; } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/dto/response/ListResponse.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/dto/response/ListResponse.java new file mode 100644 index 00000000..a74763ce --- /dev/null +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/dto/response/ListResponse.java @@ -0,0 +1,9 @@ +package com.inq.wishhair.wesharewishhair.global.dto.response; + +import java.util.List; + +@FunctionalInterface +public interface ListResponse { + + List getResult(); +} diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/dto/response/PagedResponse.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/dto/response/PagedResponse.java index f9b7f146..69c7c123 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/global/dto/response/PagedResponse.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/dto/response/PagedResponse.java @@ -8,7 +8,7 @@ @Getter @AllArgsConstructor -public class PagedResponse { +public class PagedResponse implements ListResponse { private List result; diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/dto/response/ResponseWrapper.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/dto/response/ResponseWrapper.java index 5579694b..9a570171 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/global/dto/response/ResponseWrapper.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/dto/response/ResponseWrapper.java @@ -7,7 +7,7 @@ @Getter @AllArgsConstructor -public class ResponseWrapper { +public class ResponseWrapper implements ListResponse { private List result; } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/global/mail/event/MailSendEventListener.java b/src/main/java/com/inq/wishhair/wesharewishhair/global/mail/event/MailSendEventListener.java index 67602b9d..7f013d41 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/global/mail/event/MailSendEventListener.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/global/mail/event/MailSendEventListener.java @@ -19,7 +19,7 @@ public class MailSendEventListener { @Async("mailAsyncExecutor") @EventListener public void sendAuthMail(AuthMailSendEvent event) throws Exception { - emailSender.sendAuthMail(event.address(), event.authKey()); + emailSender.sendAuthMail(event.email().getValue(), event.authKey()); } @Async("mailAsyncExecutor") diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/controller/ReviewSearchController.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/controller/ReviewSearchController.java index 32bde2ff..a9034015 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/controller/ReviewSearchController.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/controller/ReviewSearchController.java @@ -30,7 +30,7 @@ public class ReviewSearchController { public ResponseEntity findReview(@PathVariable Long reviewId, @ExtractPayload Long userId) { - ReviewDetailResponse result = reviewSearchService.findReviewById(reviewId, userId); + ReviewDetailResponse result = reviewSearchService.findReviewById(userId, reviewId); return ResponseEntity.ok(result); } @@ -60,4 +60,10 @@ public ResponseWrapper findReviewOfMonth() { return reviewSearchService.findReviewOfMonth(); } + + @GetMapping("/hair_style/{hairStyleId}") + public ResponseWrapper findHairStyleReview(@PathVariable Long hairStyleId, + @ExtractPayload Long userId) { + return reviewSearchService.findReviewByHairStyle(userId, hairStyleId); + } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/infra/query/ReviewQueryRepository.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/infra/query/ReviewQueryRepository.java index c13d938d..a2e7e5f5 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/infra/query/ReviewQueryRepository.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/infra/query/ReviewQueryRepository.java @@ -5,7 +5,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -25,4 +24,7 @@ public interface ReviewQueryRepository { //지난달에 작성한 리뷰 조회 List findReviewByCreatedDate(); + + //헤어스타일의 리뷰 조회 + List findReviewByHairStyle(Long hairStyleId); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/infra/query/ReviewQueryRepositoryImpl.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/infra/query/ReviewQueryRepositoryImpl.java index 79048110..9ea8f968 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/infra/query/ReviewQueryRepositoryImpl.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/infra/query/ReviewQueryRepositoryImpl.java @@ -135,6 +135,23 @@ public List findReviewByCreatedDate() { .fetch(); } + @Override + public List findReviewByHairStyle(Long hairStyleId) { + return factory + .select(assembleReviewProjection()) + .from(review) + .leftJoin(like).on(review.id.eq(like.reviewId)) + .leftJoin(review.hairStyle) + .fetchJoin() + .leftJoin(review.writer) + .fetchJoin() + .groupBy(review.id) + .orderBy(likes.desc()) + .offset(0) + .limit(5) + .fetch(); + } + private ConstructorExpression assembleReviewProjection() { return new QReviewQueryResponse(review, likes.as("likes")); } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/service/ReviewSearchService.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/service/ReviewSearchService.java index 83feb784..edaefb48 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/service/ReviewSearchService.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/service/ReviewSearchService.java @@ -68,4 +68,11 @@ public ResponseWrapper findReviewOfMonth() { List result = reviewRepository.findReviewByCreatedDate(); return toWrappedSimpleResponse(result); } + + /*헤어스타일의 리뷰 조회*/ + @AddisWriter + public ResponseWrapper findReviewByHairStyle(Long userId, Long hairStyleId) { + List result = reviewRepository.findReviewByHairStyle(hairStyleId); + return toWrappedReviewResponse(result); + } } diff --git a/src/main/java/com/inq/wishhair/wesharewishhair/review/service/dto/response/ReviewResponseAssembler.java b/src/main/java/com/inq/wishhair/wesharewishhair/review/service/dto/response/ReviewResponseAssembler.java index 4ebe7bd3..ad89da4a 100644 --- a/src/main/java/com/inq/wishhair/wesharewishhair/review/service/dto/response/ReviewResponseAssembler.java +++ b/src/main/java/com/inq/wishhair/wesharewishhair/review/service/dto/response/ReviewResponseAssembler.java @@ -46,10 +46,12 @@ public static ReviewDetailResponse toReviewDetailResponse(ReviewQueryResponse qu } public static ResponseWrapper toWrappedSimpleResponse(List reviews) { - return new ResponseWrapper<>(toSimpleResponse(reviews)); + List responses = reviews.stream().map(ReviewSimpleResponse::new).toList(); + return new ResponseWrapper<>(responses); } - private static List toSimpleResponse(List reviews) { - return reviews.stream().map(ReviewSimpleResponse::new).toList(); + public static ResponseWrapper toWrappedReviewResponse(List responses) { + List reviewResponses = responses.stream().map(ReviewResponseAssembler::toReviewResponse).toList(); + return new ResponseWrapper<>(reviewResponses); } } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/auth/controller/MailAuthControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/auth/controller/MailAuthControllerTest.java index 4e6314e2..9156dada 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/auth/controller/MailAuthControllerTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/auth/controller/MailAuthControllerTest.java @@ -75,6 +75,23 @@ void success() throws Exception { assertThat(count).isEqualTo(1); } + @Test + @DisplayName("올바르지 않은 형식의 이메일로 400 예외를 던진다") + void failByWrongEmail() throws Exception { + //given + MailRequest request = new MailRequest(WRONG_EMAIL); + ErrorCode expectedError = ErrorCode.USER_INVALID_EMAIL; + + //when + MockHttpServletRequestBuilder requestBuilder = generateMailSendRequest(request); + + //then + assertException(expectedError, requestBuilder, status().isBadRequest()); + + int count = (int) events.stream(AuthMailSendEvent.class).count(); + assertThat(count).isZero(); + } + private MockHttpServletRequestBuilder generateMailSendRequest(MailRequest request) throws JsonProcessingException { return MockMvcRequestBuilders .post(SEND_URL) @@ -136,9 +153,6 @@ void failByWrongEmail() throws Exception { //then assertException(expectedError, requestBuilder, status().isBadRequest()); - - int count = (int) events.stream(AuthMailSendEvent.class).count(); - assertThat(count).isZero(); } private MockHttpServletRequestBuilder generateCheckEmailRequest(MailRequest request) throws JsonProcessingException { diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/global/mail/event/MailSendEventListenerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/global/mail/event/MailSendEventListenerTest.java index 44491974..b7a415ef 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/global/mail/event/MailSendEventListenerTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/global/mail/event/MailSendEventListenerTest.java @@ -6,6 +6,7 @@ import com.inq.wishhair.wesharewishhair.global.mail.utils.EmailSender; import com.inq.wishhair.wesharewishhair.user.controller.dto.request.PointUseRequest; import com.inq.wishhair.wesharewishhair.user.controller.utils.PointUseRequestUtils; +import com.inq.wishhair.wesharewishhair.user.domain.Email; import com.inq.wishhair.wesharewishhair.user.event.RefundMailSendEvent; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -34,7 +35,7 @@ void sendAuthMail() throws Exception { doNothing().when(emailSender).sendAuthMail(ADDRESS, authKey); //when, then - assertDoesNotThrow(() -> listener.sendAuthMail(new AuthMailSendEvent(ADDRESS, authKey))); + assertDoesNotThrow(() -> listener.sendAuthMail(new AuthMailSendEvent(new Email(ADDRESS), authKey))); } @Test diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/controller/ReviewSearchControllerTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/controller/ReviewSearchControllerTest.java index 1e8a06df..5393428c 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/review/controller/ReviewSearchControllerTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/controller/ReviewSearchControllerTest.java @@ -233,6 +233,51 @@ void success() throws Exception { } + @Nested + @DisplayName("헤어스타일 리뷰 조회 API") + class findHairStyleReview { + @Test + @DisplayName("헤어스타일의 리뷰를 조회한다") + void success() throws Exception { + //given + ResponseWrapper expectedResponse = new ResponseWrapper<>(generateReviewResponses(2, 1L)); + given(reviewSearchService.findReviewByHairStyle(1L, 1L)) + .willReturn(expectedResponse); + + //when + MockHttpServletRequestBuilder requestBuilder = RestDocumentationRequestBuilders + .get(BASE_URL + "/hair_style/{hairStyleId}", 1L) + .header(AUTHORIZATION, BEARER + ACCESS_TOKEN); + + //then + mockMvc.perform(requestBuilder) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + accessTokenHeaderDocument(), + pathParameters( + parameterWithName("hairStyleId").description("헤어스타일 아이디") + ), + reviewResponseDocument("result[].") + ) + ); + } + + @Test + @DisplayName("헤더에 토큰을 포함하지 않아 실패") + void failByNoAccessToken() throws Exception { + //given + ErrorCode expectedError = ErrorCode.AUTH_REQUIRED_LOGIN; + + //when + MockHttpServletRequestBuilder requestBuilder = RestDocumentationRequestBuilders + .get(BASE_URL + "/hair_style/{hairStyleId}", 1L); + + //then + assertException(expectedError, requestBuilder, status().isUnauthorized()); + } + } + private PagedResponse assemblePagedResponse(int count, Long userId) { Paging defaultPaging = new Paging(count, 0, false); return new PagedResponse<>(generateReviewResponses(count, userId), defaultPaging); diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/infra/query/ReviewQueryRepositoryTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/infra/query/ReviewQueryRepositoryTest.java index 64bf24fa..6ed06f2e 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/review/infra/query/ReviewQueryRepositoryTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/infra/query/ReviewQueryRepositoryTest.java @@ -137,4 +137,21 @@ void findReviewByCreatedDate() { () -> assertThat(result.get(0).getPhotos()).hasSize(A.getStoreUrls().size()) ); } + + @Test + @DisplayName("헤어스타일의 리뷰를 조회한다") + void findReviewByHairStyle() { + //when + List result = reviewRepository.findReviewByHairStyle(hairStyle.getId()); + + //then + assertAll( + () -> assertThat(result).isNotEmpty(), + () -> { + ReviewQueryResponse actual = result.get(0); + assertThat(actual.getLikes()).isZero(); + assertThat(actual.getReview()).isEqualTo(review); + } + ); + } } diff --git a/src/test/java/com/inq/wishhair/wesharewishhair/review/service/ReviewSearchServiceTest.java b/src/test/java/com/inq/wishhair/wesharewishhair/review/service/ReviewSearchServiceTest.java index 11b4f573..d2b3829d 100644 --- a/src/test/java/com/inq/wishhair/wesharewishhair/review/service/ReviewSearchServiceTest.java +++ b/src/test/java/com/inq/wishhair/wesharewishhair/review/service/ReviewSearchServiceTest.java @@ -37,12 +37,13 @@ public class ReviewSearchServiceTest extends ServiceTest { private final List reviews = new ArrayList<>(); private User user; + private HairStyle hairStyle; @BeforeEach void setUp() { //given user = userRepository.save(UserFixture.A.toEntity()); - HairStyle hairStyle = hairStyleRepository.save(HairStyleFixture.A.toEntity()); + hairStyle = hairStyleRepository.save(HairStyleFixture.A.toEntity()); for (ReviewFixture fixture : ReviewFixture.values()) { reviews.add(fixture.toEntity(user, hairStyle)); @@ -210,6 +211,24 @@ void findReviewOfMonth() { assertReviewSimpleResponseMatch(result.getResult(), List.of(5, 4, 1)); } + @Test + @DisplayName("특정 헤어스타일의 리뷰를 좋아요 순으로 조회한다") + void findReviewByHairStyle() { + //given + saveReview(List.of(1, 2, 4, 5), List.of(now(), now(), now(), now())); + + User user1 = userRepository.save(UserFixture.B.toEntity()); + addLikes(user, List.of(1, 4, 5)); + addLikes(user1, List.of(4, 5)); + addLikes(user1, List.of(5)); + + //when + ResponseWrapper result = reviewSearchService.findReviewByHairStyle(user.getId(), hairStyle.getId()); + + //then + assertReviewResponseMatch(result.getResult(), List.of(5, 4, 1, 2), List.of(3L, 2L, 1L, 0L)); + } + private void saveReview(List indexes, List times) { for (int i = 0; i < indexes.size(); i++) { int index = indexes.get(i);