Skip to content

Commit

Permalink
Merge pull request #98 from cvs-go/feature#97
Browse files Browse the repository at this point in the history
상품 좋아요 태그 조회 추가
  • Loading branch information
feel-coding authored Sep 6, 2023
2 parents 446cbbf + 1b0a702 commit 86b4bab
Show file tree
Hide file tree
Showing 11 changed files with 310 additions and 41 deletions.
24 changes: 20 additions & 4 deletions src/docs/asciidoc/api-doc.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,23 @@ include::{snippets}/product-controller-test/respond_200_when_read_product_succes
| `404 NOT FOUND` | `NOT_FOUND_PRODUCT` | 해당하는 상품이 없는 경우
|===

=== 3-4. 상품 좋아요 생성
=== 3-4. 특정 상품 좋아요 태그 조회
==== Path Parameters
include::{snippets}/product-controller-test/respond_200_when_read_product_like_tags_successfully/path-parameters.adoc[]
==== Sample Request
include::{snippets}/product-controller-test/respond_200_when_read_product_like_tags_successfully/http-request.adoc[]
==== Response Fields
include::{snippets}/product-controller-test/respond_200_when_read_product_like_tags_successfully/response-fields.adoc[]
==== Sample Response
include::{snippets}/product-controller-test/respond_200_when_read_product_like_tags_successfully/http-response.adoc[]
==== Error Response
|===
| HTTP Status | Error Code | Detail

| `404 NOT FOUND` | `NOT_FOUND_PRODUCT` | 해당하는 상품이 없는 경우
|===

=== 3-5. 상품 좋아요 생성
==== Path Parameters
include::{snippets}/product-controller-test/respond_201_when_create_product_like_succeed/path-parameters.adoc[]
==== Sample Request
Expand All @@ -225,7 +241,7 @@ include::{snippets}/product-controller-test/respond_201_when_create_product_like
| `409 CONFLICT` | `DUPLICATE_PRODUCT_LIKE` | 해당하는 상품 좋아요가 이미 있는 경우
|===

=== 3-5. 상품 좋아요 삭제
=== 3-6. 상품 좋아요 삭제
==== Path Parameters
include::{snippets}/product-controller-test/respond_200_when_delete_product_like_succeed/path-parameters.adoc[]
==== Sample Request
Expand All @@ -240,7 +256,7 @@ include::{snippets}/product-controller-test/respond_200_when_delete_product_like
| `404 NOT FOUND` | `NOT_FOUND_PRODUCT_LIKE` | 해당하는 상품 좋아요가 없는 경우
|===

=== 3-6. 상품 북마크 생성
=== 3-7. 상품 북마크 생성
==== Path Parameters
include::{snippets}/product-controller-test/respond_201_when_create_product_bookmark_succeed/path-parameters.adoc[]
==== Sample Request
Expand All @@ -255,7 +271,7 @@ include::{snippets}/product-controller-test/respond_201_when_create_product_book
| `409 CONFLICT` | `DUPLICATE_PRODUCT_BOOKMARK` | 해당하는 상품 북마크가 이미 있는 경우
|===

=== 3-7. 상품 북마크 삭제
=== 3-8. 상품 북마크 삭제
==== Path Parameters
include::{snippets}/product-controller-test/respond_200_when_delete_product_bookmark_succeed/path-parameters.adoc[]
==== Sample Request
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/cvsgo/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ public void addInterceptors(InterceptorRegistry registry) {
.addPathPatterns("/**")
.excludePathPatterns("/", "/docs/**", "/*.ico", "/api/auth/login", "/api/users",
"/api/tags", "/api/users/emails/*/exists", "/api/users/nicknames/*/exists",
"/api/products", "/api/products/*");
"/api/products", "/api/products/*", "/api/products/*/tags");
}
}
8 changes: 8 additions & 0 deletions src/main/java/com/cvsgo/controller/ProductController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import com.cvsgo.argumentresolver.LoginUser;
import com.cvsgo.dto.SuccessResponse;
import com.cvsgo.dto.product.ReadProductLikeTagResponseDto;
import com.cvsgo.dto.product.ReadProductDetailResponseDto;
import com.cvsgo.dto.product.ReadProductFilterResponseDto;
import com.cvsgo.dto.product.ReadProductRequestDto;
import com.cvsgo.dto.product.ReadProductResponseDto;
import com.cvsgo.entity.User;
import com.cvsgo.service.ProductService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
Expand Down Expand Up @@ -42,6 +44,12 @@ public SuccessResponse<ReadProductDetailResponseDto> readProduct(@LoginUser User
return SuccessResponse.from(productService.readProduct(user, productId));
}

@GetMapping("/{productId}/tags")
public SuccessResponse<List<ReadProductLikeTagResponseDto>> readProductLikeTags(
@PathVariable Long productId) {
return SuccessResponse.from(productService.readProductLikeTags(productId));
}

@PostMapping("/{productId}/likes")
@ResponseStatus(HttpStatus.CREATED)
public SuccessResponse<Void> createProductLike(@LoginUser User user,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.cvsgo.dto.product;

import com.querydsl.core.annotations.QueryProjection;
import lombok.Getter;

@Getter
public class ReadProductLikeTagResponseDto {

private final Long id;
private final String name;
private final Long tagCount;

@QueryProjection
public ReadProductLikeTagResponseDto(Long id, String name, Long tagCount) {
this.id = id;
this.name = name;
this.tagCount = tagCount;
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/cvsgo/repository/TagCustomRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.cvsgo.repository;

import com.cvsgo.dto.product.ReadProductLikeTagResponseDto;
import com.cvsgo.entity.Product;
import java.util.List;

public interface TagCustomRepository {

List<ReadProductLikeTagResponseDto> findTop3ByProduct(Product product);

}
37 changes: 37 additions & 0 deletions src/main/java/com/cvsgo/repository/TagCustomRepositoryImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.cvsgo.repository;

import static com.cvsgo.entity.QProductLike.productLike;
import static com.cvsgo.entity.QTag.tag;
import static com.cvsgo.entity.QUserTag.userTag;

import com.cvsgo.dto.product.QReadProductLikeTagResponseDto;
import com.cvsgo.dto.product.ReadProductLikeTagResponseDto;
import com.cvsgo.entity.Product;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@RequiredArgsConstructor
@Repository
public class TagCustomRepositoryImpl implements TagCustomRepository {

private final JPAQueryFactory queryFactory;

public List<ReadProductLikeTagResponseDto> findTop3ByProduct(Product product) {
return queryFactory.select(new QReadProductLikeTagResponseDto(
userTag.tag.id,
userTag.tag.name,
userTag.tag.count()
))
.from(userTag)
.join(userTag.tag, tag)
.join(productLike).on(productLike.user.eq(userTag.user))
.where(productLike.product.eq(product))
.groupBy(userTag.tag.id, userTag.tag.name)
.orderBy(userTag.tag.count().desc())
.limit(3)
.fetch();
}

}
2 changes: 1 addition & 1 deletion src/main/java/com/cvsgo/repository/TagRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
import com.cvsgo.entity.Tag;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TagRepository extends JpaRepository<Tag, Long> {
public interface TagRepository extends JpaRepository<Tag, Long>, TagCustomRepository {

}
18 changes: 18 additions & 0 deletions src/main/java/com/cvsgo/service/ProductService.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.cvsgo.dto.product.ConvenienceStoreEventDto;
import com.cvsgo.dto.product.ConvenienceStoreEventQueryDto;
import com.cvsgo.dto.product.EventTypeDto;
import com.cvsgo.dto.product.ReadProductLikeTagResponseDto;
import com.cvsgo.dto.product.ReadUserProductRequestDto;
import com.cvsgo.dto.product.ReadProductDetailQueryDto;
import com.cvsgo.dto.product.ReadProductDetailResponseDto;
Expand All @@ -31,6 +32,7 @@
import com.cvsgo.repository.ProductBookmarkRepository;
import com.cvsgo.repository.ProductLikeRepository;
import com.cvsgo.repository.ProductRepository;
import com.cvsgo.repository.TagRepository;
import com.cvsgo.repository.UserRepository;
import java.util.Arrays;
import java.util.List;
Expand All @@ -50,6 +52,7 @@ public class ProductService {
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;
private final ConvenienceStoreRepository convenienceStoreRepository;
private final TagRepository tagRepository;
private final ProductLikeRepository productLikeRepository;
private final ProductBookmarkRepository productBookmarkRepository;
private final UserRepository userRepository;
Expand Down Expand Up @@ -89,6 +92,21 @@ public ReadProductDetailResponseDto readProduct(User user, Long productId) {
getConvenienceStoreEvents(convenienceStoreEvents));
}

/**
* 상품 ID를 통해 특정 상품에 좋아요 한 유저의 상위 3개 태그를 조회한다.
*
* @param productId 상품 ID
* @return 태그 정보
* @throws NotFoundException 해당하는 아이디를 가진 상품이 없는 경우
*/
@Transactional(readOnly = true)
public List<ReadProductLikeTagResponseDto> readProductLikeTags(Long productId) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> NOT_FOUND_PRODUCT);

return tagRepository.findTop3ByProduct(product);
}

/**
* 상품 좋아요를 생성한다.
*
Expand Down
65 changes: 65 additions & 0 deletions src/test/java/com/cvsgo/controller/ProductControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import com.cvsgo.dto.product.ReadProductDetailQueryDto;
import com.cvsgo.dto.product.ReadProductDetailResponseDto;
import com.cvsgo.dto.product.ReadProductFilterResponseDto;
import com.cvsgo.dto.product.ReadProductLikeTagResponseDto;
import com.cvsgo.dto.product.ReadProductQueryDto;
import com.cvsgo.dto.product.ReadProductRequestDto;
import com.cvsgo.dto.product.ReadProductResponseDto;
Expand All @@ -49,6 +50,7 @@
import com.cvsgo.entity.Product;
import com.cvsgo.entity.ProductBookmark;
import com.cvsgo.entity.ProductLike;
import com.cvsgo.entity.Tag;
import com.cvsgo.entity.User;
import com.cvsgo.interceptor.AuthInterceptor;
import com.cvsgo.service.ProductService;
Expand Down Expand Up @@ -188,6 +190,40 @@ void respond_404_when_read_product_but_product_does_not_exist() throws Exception
.andDo(print());
}

@Test
@DisplayName("상품 좋아요 태그를 정상적으로 조회하면 HTTP 200을 응답한다")
void respond_200_when_read_product_like_tags_successfully() throws Exception {
given(productService.readProductLikeTags(any())).willReturn(getProductLikeTagsResponse());

mockMvc.perform(RestDocumentationRequestBuilders.get("/api/products/{productId}/tags", 1L)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(print())
.andDo(document(documentIdentifier,
getDocumentRequest(),
getDocumentResponse(),
pathParameters(
parameterWithName("productId").description("상품 ID")
),
relaxedResponseFields(
fieldWithPath("data.id").type(JsonFieldType.NUMBER).description("태그 ID").optional(),
fieldWithPath("data.name").type(JsonFieldType.STRING).description("태그 이름").optional(),
fieldWithPath("data.tagCount").type(JsonFieldType.NUMBER).description("태그 수").optional()
)
));
}

@Test
@DisplayName("상품 좋아요 태그 조회 시 해당 ID를 가진 상품이 존재하지 않으면 HTTP 404를 응답한다")
void respond_404_when_read_product_like_tags_but_product_does_not_exist() throws Exception {
given(productService.readProductLikeTags(any())).willThrow(NOT_FOUND_PRODUCT);

mockMvc.perform(get("/api/products/{productId}/tags", 1000L)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound())
.andDo(print());
}

@Test
@DisplayName("상품 좋아요 생성에 성공하면 HTTP 201을 응답한다")
void respond_201_when_create_product_like_succeed() throws Exception {
Expand Down Expand Up @@ -403,6 +439,21 @@ void respond_200_when_read_product_filter_successfully() throws Exception {
.name("농심")
.build();

Tag tag1 = Tag.builder()
.id(1L)
.name("맵부심")
.build();

Tag tag2 = Tag.builder()
.id(2L)
.name("다이어터")
.build();

Tag tag3 = Tag.builder()
.id(3L)
.name("초코러버")
.build();

Product product1 = Product.builder()
.id(1L)
.name("아이시스 500ml")
Expand Down Expand Up @@ -501,6 +552,20 @@ private ReadProductDetailResponseDto getProductResponse() {
return ReadProductDetailResponseDto.of(productDetailQueryDto, convenienceStoreEvents);
}

private List<ReadProductLikeTagResponseDto> getProductLikeTagsResponse() {
ReadProductLikeTagResponseDto productLikeTagResponseDto1 = new ReadProductLikeTagResponseDto(
tag1.getId(), tag1.getName(), 215L
);
ReadProductLikeTagResponseDto productLikeTagResponseDto2 = new ReadProductLikeTagResponseDto(
tag2.getId(), tag2.getName(), 163L
);
ReadProductLikeTagResponseDto productLikeTagResponseDto3 = new ReadProductLikeTagResponseDto(
tag3.getId(), tag3.getName(), 71L
);

return List.of(productLikeTagResponseDto1, productLikeTagResponseDto2, productLikeTagResponseDto3);
}

private ReadProductFilterResponseDto getProductFilterResponse() {
List<ConvenienceStoreDto> convenienceStores = List.of(
ConvenienceStoreDto.from(cvs1), ConvenienceStoreDto.from(cvs2),
Expand Down
Loading

0 comments on commit 86b4bab

Please sign in to comment.