From 20af82305dcacba1fe35114f12278fd80e6caba4 Mon Sep 17 00:00:00 2001 From: chaewss Date: Thu, 14 Sep 2023 15:23:43 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=ED=83=9C=EA=B7=B8=20=EB=A7=A4?= =?UTF-8?q?=EC=B9=AD=EB=A5=A0=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#103?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/cvsgo/controller/UserController.java | 6 ++++ .../cvsgo/repository/UserTagRepository.java | 3 ++ .../java/com/cvsgo/service/UserService.java | 31 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/src/main/java/com/cvsgo/controller/UserController.java b/src/main/java/com/cvsgo/controller/UserController.java index 71a98bc4..d1894fbd 100644 --- a/src/main/java/com/cvsgo/controller/UserController.java +++ b/src/main/java/com/cvsgo/controller/UserController.java @@ -77,6 +77,12 @@ public SuccessResponse deleteUserFollow(@LoginUser User user, @PathVariabl return SuccessResponse.create(); } + @GetMapping("/users/{userId}/tag-match-percentage") + public SuccessResponse readUserTagMatchPercentage( + @LoginUser User user, @PathVariable Long userId) { + return SuccessResponse.from(userService.readUserTagMatchPercentage(user, userId)); + } + @GetMapping("/users/{userId}/liked-products") public SuccessResponse> readLikedProductList( @PathVariable Long userId, @ModelAttribute ReadUserProductRequestDto request, diff --git a/src/main/java/com/cvsgo/repository/UserTagRepository.java b/src/main/java/com/cvsgo/repository/UserTagRepository.java index cbd3318f..0ae2da8c 100644 --- a/src/main/java/com/cvsgo/repository/UserTagRepository.java +++ b/src/main/java/com/cvsgo/repository/UserTagRepository.java @@ -1,5 +1,6 @@ package com.cvsgo.repository; +import com.cvsgo.entity.User; import com.cvsgo.entity.UserTag; import java.util.List; import org.springframework.data.jpa.repository.EntityGraph; @@ -10,4 +11,6 @@ public interface UserTagRepository extends JpaRepository { @EntityGraph(attributePaths = {"user", "tag"}) List findByUserIdIn(List userIds); + List findAllByUser(User user); + } diff --git a/src/main/java/com/cvsgo/service/UserService.java b/src/main/java/com/cvsgo/service/UserService.java index d51e6d8b..068b6da5 100644 --- a/src/main/java/com/cvsgo/service/UserService.java +++ b/src/main/java/com/cvsgo/service/UserService.java @@ -15,6 +15,7 @@ import com.cvsgo.entity.Tag; import com.cvsgo.entity.User; import com.cvsgo.entity.UserFollow; +import com.cvsgo.entity.UserTag; import com.cvsgo.exception.BadRequestException; import com.cvsgo.exception.DuplicateException; import com.cvsgo.exception.NotFoundException; @@ -22,6 +23,7 @@ import com.cvsgo.repository.TagRepository; import com.cvsgo.repository.UserFollowRepository; import com.cvsgo.repository.UserRepository; +import com.cvsgo.repository.UserTagRepository; import jakarta.persistence.EntityManager; import java.util.List; import lombok.RequiredArgsConstructor; @@ -36,6 +38,8 @@ @RequiredArgsConstructor public class UserService { + private final UserTagRepository userTagRepository; + private final UserRepository userRepository; private final TagRepository tagRepository; @@ -177,4 +181,31 @@ public void deleteUserFollow(User user, Long userId) { userFollowRepository.delete(userFollow); } + /** + * 태그 매칭률을 조회한다. + * + * @param user 로그인한 사용자 + * @param userId 태그 매칭률을 조회할 사용자 ID + * @return 태그 매칭률 + * @throws NotFoundException 해당하는 아이디를 가진 사용자가 없는 경우 + */ + @Transactional(readOnly = true) + public Integer readUserTagMatchPercentage(User user, Long userId) { + User targetUser = userRepository.findById(userId).orElseThrow(() -> NOT_FOUND_USER); + + List loginUserTag = userTagRepository.findAllByUser(user).stream().map(UserTag::getTag) + .toList(); + List targetUserTag = userTagRepository.findAllByUser(targetUser).stream().map( + UserTag::getTag).toList(); + + int matchingCount = 0; + for (Tag tag : loginUserTag) { + if (targetUserTag.contains(tag)) { + matchingCount++; + } + } + + return (int) (((double) matchingCount / loginUserTag.size()) * 100); + } + } From 34863303045d1307c03050cf432a6570b0dd4e08 Mon Sep 17 00:00:00 2001 From: chaewss Date: Thu, 14 Sep 2023 15:24:14 +0900 Subject: [PATCH 2/3] =?UTF-8?q?test:=20=ED=83=9C=EA=B7=B8=20=EB=A7=A4?= =?UTF-8?q?=EC=B9=AD=EB=A5=A0=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=1C#103?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cvsgo/controller/UserControllerTest.java | 24 ++++++- .../repository/UserTagRepositoryTest.java | 72 +++++++++++++++++++ .../com/cvsgo/service/UserServiceTest.java | 45 +++++++++++- 3 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/cvsgo/repository/UserTagRepositoryTest.java diff --git a/src/test/java/com/cvsgo/controller/UserControllerTest.java b/src/test/java/com/cvsgo/controller/UserControllerTest.java index 193a69b8..327cb6c3 100644 --- a/src/test/java/com/cvsgo/controller/UserControllerTest.java +++ b/src/test/java/com/cvsgo/controller/UserControllerTest.java @@ -34,9 +34,9 @@ import com.cvsgo.config.WebConfig; import com.cvsgo.dto.product.ConvenienceStoreEventDto; import com.cvsgo.dto.product.ProductSortBy; -import com.cvsgo.dto.product.ReadUserProductRequestDto; import com.cvsgo.dto.product.ReadProductQueryDto; import com.cvsgo.dto.product.ReadProductResponseDto; +import com.cvsgo.dto.product.ReadUserProductRequestDto; import com.cvsgo.dto.user.SignUpRequestDto; import com.cvsgo.dto.user.SignUpResponseDto; import com.cvsgo.dto.user.UpdateUserRequestDto; @@ -473,6 +473,28 @@ void respond_404_when_delete_user_follow_but_user_follow_does_not_exist() throws .andDo(print()); } + @Test + @DisplayName("태그 매칭률을 정상적으로 조회하면 HTTP 200을 응답한다") + void respond_200_when_read_user_tag_match_percentage_successfully() throws Exception { + Integer percentage = 66; + given(userService.readUserTagMatchPercentage(any(), anyLong())).willReturn(percentage); + + mockMvc.perform(get("/api/users/{userId}/tag-match-percentage", 1L) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(document(documentIdentifier, + getDocumentRequest(), + getDocumentResponse(), + pathParameters( + parameterWithName("userId").description("태그 매칭률을 조회할 회원 ID") + ), + relaxedResponseFields( + fieldWithPath("data").type(JsonFieldType.NUMBER).description("태그 매칭률") + ) + )); + } + @Test @DisplayName("특정 회원의 좋아요 상품 목록을 정상적으로 조회하면 HTTP 200을 응답한다") void respond_200_when_read_liked_product_list_successfully() throws Exception { diff --git a/src/test/java/com/cvsgo/repository/UserTagRepositoryTest.java b/src/test/java/com/cvsgo/repository/UserTagRepositoryTest.java new file mode 100644 index 00000000..a0f9469a --- /dev/null +++ b/src/test/java/com/cvsgo/repository/UserTagRepositoryTest.java @@ -0,0 +1,72 @@ +package com.cvsgo.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.cvsgo.config.TestConfig; +import com.cvsgo.entity.Tag; +import com.cvsgo.entity.User; +import com.cvsgo.entity.UserTag; +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.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@Import(TestConfig.class) +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class UserTagRepositoryTest { + + @Autowired + private UserTagRepository userTagRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private TagRepository tagRepository; + + User user; + + @BeforeEach + void initData() { + tag1 = Tag.builder().name("맵찔이").group(1).build(); + tag2 = Tag.builder().name("맵부심").group(1).build(); + tag3 = Tag.builder().name("초코러버").group(2).build(); + tag4 = Tag.builder().name("비건").group(3).build(); + tag5 = Tag.builder().name("다이어터").group(4).build(); + tag6 = Tag.builder().name("대식가").group(5).build(); + tag7 = Tag.builder().name("소식가").group(5).build(); + tagRepository.saveAll(List.of(tag1, tag2, tag3, tag4, tag5, tag6, tag7)); + + user1 = User.create("abc@naver.com", "password1!", "닉네임1", List.of(tag1, tag3, tag6)); + user2 = User.create("xyz@gmail.com", "password1!", "닉네임2", List.of(tag2, tag4, tag6)); + userRepository.saveAll(List.of(user1, user2)); + } + + @Test + @DisplayName("유저 태그를 조회한다") + void find_user_tags_by_user() { + // when + List userTags = userTagRepository.findAllByUser(user1); + + // then + List tags = userTags.stream().map(UserTag::getTag).toList(); + assertThat(userTags).hasSize(3); + assertThat(tags).extracting("name").containsExactly("맵찔이", "초코러버", "대식가"); + assertThat(tags).extracting("name").doesNotContain("맵부심", "비건", "다이어터", "소식가"); + } + + private Tag tag1; + private Tag tag2; + private Tag tag3; + private Tag tag4; + private Tag tag5; + private Tag tag6; + private Tag tag7; + private User user1; + private User user2; +} diff --git a/src/test/java/com/cvsgo/service/UserServiceTest.java b/src/test/java/com/cvsgo/service/UserServiceTest.java index 4f6768cb..eb0c557d 100644 --- a/src/test/java/com/cvsgo/service/UserServiceTest.java +++ b/src/test/java/com/cvsgo/service/UserServiceTest.java @@ -10,7 +10,6 @@ import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.times; -import com.cvsgo.dto.review.ReadProductReviewResponseDto; import com.cvsgo.dto.user.SignUpRequestDto; import com.cvsgo.dto.user.UpdateUserRequestDto; import com.cvsgo.entity.Review; @@ -26,6 +25,7 @@ import com.cvsgo.repository.TagRepository; import com.cvsgo.repository.UserFollowRepository; import com.cvsgo.repository.UserRepository; +import com.cvsgo.repository.UserTagRepository; import jakarta.persistence.EntityManager; import java.util.Arrays; import java.util.List; @@ -51,6 +51,9 @@ class UserServiceTest { @Mock private TagRepository tagRepository; + @Mock + private UserTagRepository userTagRepository; + @Mock private UserFollowRepository userFollowRepository; @@ -310,6 +313,30 @@ void should_throw_NotFoundException_when_delete_user_follow_but_user_follow_does then(userFollowRepository).should(times(1)).findByUserAndFollower(any(), any()); } + @Test + @DisplayName("태그 매칭률을 조회한다") + void succeed_to_read_user_tag_match_percentage() { + given(userRepository.findById(anyLong())).willReturn(Optional.of(user2)); + given(userTagRepository.findAllByUser(any())).willReturn(List.of(userTag1, userTag2)); + given(userTagRepository.findAllByUser(any())).willReturn(List.of(userTag3, userTag4)); + + userService.readUserTagMatchPercentage(user, user2.getId()); + + then(userRepository).should(times(1)).findById(user2.getId()); + then(userTagRepository).should(times(2)).findAllByUser(any()); + } + + @Test + @DisplayName("태그 매칭률 조회 시 해당하는 아이디를 가진 사용자가 없으면 NotFoundException이 발생한다") + void should_throw_NotFoundException_when_read_user_tag_match_percentage_but_user_does_not_exist() { + final Long userId = 10000L; + + given(userRepository.findById(anyLong())).willReturn(Optional.empty()); + + assertThrows(NotFoundException.class, () -> userService.readUserTagMatchPercentage(user, userId)); + then(userRepository).should(times(1)).findById(anyLong()); + } + User user = User.builder() .id(1L) .userId("abc@naver.com") @@ -336,6 +363,12 @@ void should_throw_NotFoundException_when_delete_user_follow_but_user_follow_does .group(2) .build(); + Tag tag3 = Tag.builder() + .id(5L) + .name("다이어터") + .group(4) + .build(); + UserTag userTag1 = UserTag.builder() .user(user) .tag(tag1) @@ -343,9 +376,19 @@ void should_throw_NotFoundException_when_delete_user_follow_but_user_follow_does UserTag userTag2 = UserTag.builder() .user(user) + .tag(tag3) + .build(); + + UserTag userTag3 = UserTag.builder() + .user(user2) .tag(tag2) .build(); + UserTag userTag4 = UserTag.builder() + .user(user2) + .tag(tag3) + .build(); + Review review1 = Review.builder() .user(user) .build(); From bdf1d59bdad7278d117ccf967302f398300d3476 Mon Sep 17 00:00:00 2001 From: chaewss Date: Thu, 14 Sep 2023 15:24:25 +0900 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20=ED=83=9C=EA=B7=B8=20=EB=A7=A4?= =?UTF-8?q?=EC=B9=AD=EB=A5=A0=20=EC=A1=B0=ED=9A=8C=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20#103?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/api-doc.adoc | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/docs/asciidoc/api-doc.adoc b/src/docs/asciidoc/api-doc.adoc index 7af03a72..5e87d72d 100644 --- a/src/docs/asciidoc/api-doc.adoc +++ b/src/docs/asciidoc/api-doc.adoc @@ -100,7 +100,23 @@ include::{snippets}/user-controller-test/respond_200_when_delete_user_follow_suc | `404 NOT FOUND` | `NOT_FOUND_USER_FOLLOW` | 해당하는 팔로우가 없는 경우 |=== -=== 1-9. 특정 회원의 좋아요 상품 목록 조회 +=== 1-9. 태그 매칭률 조회 +==== Path Parameters +include::{snippets}/user-controller-test/respond_200_when_read_user_tag_match_percentage_successfully/path-parameters.adoc[] +==== Sample Request +include::{snippets}/user-controller-test/respond_200_when_read_user_tag_match_percentage_successfully/http-request.adoc[] +==== Response Fields +include::{snippets}/user-controller-test/respond_200_when_read_user_tag_match_percentage_successfully/response-fields.adoc[] +==== Sample Response +include::{snippets}/user-controller-test/respond_200_when_read_user_tag_match_percentage_successfully/http-response.adoc[] +==== Error Response +|=== +| HTTP Status | Error Code | Detail + +| `404 NOT FOUND` | `NOT_FOUND_USER` | 해당하는 유저가 없는 경우 +|=== + +=== 1-10. 특정 회원의 좋아요 상품 목록 조회 ==== Path Parameters include::{snippets}/user-controller-test/respond_200_when_read_liked_product_list_successfully/path-parameters.adoc[] ==== Request Fields @@ -120,7 +136,7 @@ include::{snippets}/user-controller-test/respond_200_when_read_liked_product_lis ==== Sample Response include::{snippets}/user-controller-test/respond_200_when_read_liked_product_list_successfully/http-response.adoc[] -=== 1-10. 특정 회원의 북마크 상품 목록 조회 +=== 1-11. 특정 회원의 북마크 상품 목록 조회 ==== Path Parameters include::{snippets}/user-controller-test/respond_200_when_read_bookmarked_product_list_successfully/path-parameters.adoc[] ==== Request Fields