Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 헤어스타일에 해당하는 리뷰 조회 api 구현 #253

Merged
Merged
6 changes: 6 additions & 0 deletions src/docs/asciidoc/auth/mail.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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. 성공
Expand Down
18 changes: 17 additions & 1 deletion src/docs/asciidoc/review/review-search.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
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[]

Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public ResponseEntity<SessionIdResponse> 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));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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() {}
Expand All @@ -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<ReviewResponse> castedResult = (PagedResponse<ReviewResponse>) result;
ListResponse<ReviewResponse> castedResult = (ListResponse<ReviewResponse>) result;
castedResult.getResult().forEach((response -> response.addIsWriter(userId)));
return castedResult;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.inq.wishhair.wesharewishhair.global.dto.response;

import java.util.List;

@FunctionalInterface
public interface ListResponse<T> {

List<T> getResult();
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

@Getter
@AllArgsConstructor
public class PagedResponse<T> {
public class PagedResponse<T> implements ListResponse<T> {

private List<T> result;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

@Getter
@AllArgsConstructor
public class ResponseWrapper<T> {
public class ResponseWrapper<T> implements ListResponse<T> {

private List<T> result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class ReviewSearchController {
public ResponseEntity<ReviewDetailResponse> findReview(@PathVariable Long reviewId,
@ExtractPayload Long userId) {

ReviewDetailResponse result = reviewSearchService.findReviewById(reviewId, userId);
ReviewDetailResponse result = reviewSearchService.findReviewById(userId, reviewId);

return ResponseEntity.ok(result);
}
Expand Down Expand Up @@ -60,4 +60,10 @@ public ResponseWrapper<ReviewSimpleResponse> findReviewOfMonth() {

return reviewSearchService.findReviewOfMonth();
}

@GetMapping("/hair_style/{hairStyleId}")
public ResponseWrapper<ReviewResponse> findHairStyleReview(@PathVariable Long hairStyleId,
@ExtractPayload Long userId) {
return reviewSearchService.findReviewByHairStyle(userId, hairStyleId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -25,4 +24,7 @@ public interface ReviewQueryRepository {

//지난달에 작성한 리뷰 조회
List<Review> findReviewByCreatedDate();

//헤어스타일의 리뷰 조회
List<ReviewQueryResponse> findReviewByHairStyle(Long hairStyleId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,23 @@ public List<Review> findReviewByCreatedDate() {
.fetch();
}

@Override
public List<ReviewQueryResponse> 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<ReviewQueryResponse> assembleReviewProjection() {
return new QReviewQueryResponse(review, likes.as("likes"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,11 @@ public ResponseWrapper<ReviewSimpleResponse> findReviewOfMonth() {
List<Review> result = reviewRepository.findReviewByCreatedDate();
return toWrappedSimpleResponse(result);
}

/*헤어스타일의 리뷰 조회*/
@AddisWriter
public ResponseWrapper<ReviewResponse> findReviewByHairStyle(Long userId, Long hairStyleId) {
List<ReviewQueryResponse> result = reviewRepository.findReviewByHairStyle(hairStyleId);
return toWrappedReviewResponse(result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ public static ReviewDetailResponse toReviewDetailResponse(ReviewQueryResponse qu
}

public static ResponseWrapper<ReviewSimpleResponse> toWrappedSimpleResponse(List<Review> reviews) {
return new ResponseWrapper<>(toSimpleResponse(reviews));
List<ReviewSimpleResponse> responses = reviews.stream().map(ReviewSimpleResponse::new).toList();
return new ResponseWrapper<>(responses);
}

private static List<ReviewSimpleResponse> toSimpleResponse(List<Review> reviews) {
return reviews.stream().map(ReviewSimpleResponse::new).toList();
public static ResponseWrapper<ReviewResponse> toWrappedReviewResponse(List<ReviewQueryResponse> responses) {
List<ReviewResponse> reviewResponses = responses.stream().map(ReviewResponseAssembler::toReviewResponse).toList();
return new ResponseWrapper<>(reviewResponses);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,51 @@ void success() throws Exception {

}

@Nested
@DisplayName("헤어스타일 리뷰 조회 API")
class findHairStyleReview {
@Test
@DisplayName("헤어스타일의 리뷰를 조회한다")
void success() throws Exception {
//given
ResponseWrapper<ReviewResponse> 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<ReviewResponse> assemblePagedResponse(int count, Long userId) {
Paging defaultPaging = new Paging(count, 0, false);
return new PagedResponse<>(generateReviewResponses(count, userId), defaultPaging);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,21 @@ void findReviewByCreatedDate() {
() -> assertThat(result.get(0).getPhotos()).hasSize(A.getStoreUrls().size())
);
}

@Test
@DisplayName("헤어스타일의 리뷰를 조회한다")
void findReviewByHairStyle() {
//when
List<ReviewQueryResponse> result = reviewRepository.findReviewByHairStyle(hairStyle.getId());

//then
assertAll(
() -> assertThat(result).isNotEmpty(),
() -> {
ReviewQueryResponse actual = result.get(0);
assertThat(actual.getLikes()).isZero();
assertThat(actual.getReview()).isEqualTo(review);
}
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ public class ReviewSearchServiceTest extends ServiceTest {

private final List<Review> 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));
Expand Down Expand Up @@ -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<ReviewResponse> 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<Integer> indexes, List<LocalDateTime> times) {
for (int i = 0; i < indexes.size(); i++) {
int index = indexes.get(i);
Expand Down