From b4eaa0464a3c29c58d44d8ad84e14ebc9fb00716 Mon Sep 17 00:00:00 2001 From: YongHwan Kim Date: Sun, 1 Oct 2023 21:54:35 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20=EA=B4=80=EC=8B=AC=EC=83=81=ED=92=88?= =?UTF-8?q?=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84=20(#126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #125 feat: 관심상품의 카테고리 목록 조회 서비스 구현 - /api/wish/categories * #125 test: 관심상품의 카테고리 조회 목록 테스트 코드 추가 * #125 test: 관심상품의 카테고리 조회 목록 테스트 코드 추가 --- .../app/api/wishitem/WishItemController.java | 6 ++ .../app/api/wishitem/WishItemService.java | 22 +++++- .../response/WishCategoryItemResponse.java | 21 +++++ .../response/WishCategoryListResponse.java | 25 ++++++ .../java/codesquard/app/domain/wish/Wish.java | 4 +- .../app/domain/wish/WishRepository.java | 4 + backend/src/main/resources/db/mysql/data.sql | 38 +++++++++- .../java/codesquard/app/ItemTestSupport.java | 28 +++++++ .../app/api/item/ItemServiceTest.java | 2 +- .../api/wishitem/WishItemControllerTest.java | 76 +++++++++++++++++++ .../app/api/wishitem/WishItemServiceTest.java | 64 ++++++++++++++++ 11 files changed, 283 insertions(+), 7 deletions(-) create mode 100644 backend/src/main/java/codesquard/app/api/wishitem/response/WishCategoryItemResponse.java create mode 100644 backend/src/main/java/codesquard/app/api/wishitem/response/WishCategoryListResponse.java create mode 100644 backend/src/test/java/codesquard/app/ItemTestSupport.java create mode 100644 backend/src/test/java/codesquard/app/api/wishitem/WishItemControllerTest.java diff --git a/backend/src/main/java/codesquard/app/api/wishitem/WishItemController.java b/backend/src/main/java/codesquard/app/api/wishitem/WishItemController.java index 24a4aeca5..6606df9fe 100644 --- a/backend/src/main/java/codesquard/app/api/wishitem/WishItemController.java +++ b/backend/src/main/java/codesquard/app/api/wishitem/WishItemController.java @@ -9,6 +9,7 @@ import codesquard.app.api.item.response.ItemResponses; import codesquard.app.api.response.ApiResponse; +import codesquard.app.api.wishitem.response.WishCategoryListResponse; import codesquard.app.domain.oauth.support.AuthPrincipal; import codesquard.app.domain.oauth.support.Principal; import codesquard.app.domain.wish.WishStatus; @@ -33,4 +34,9 @@ public ApiResponse findAll(@RequestParam(required = false) Long c @RequestParam(required = false, defaultValue = "10") int size, @RequestParam(required = false) Long cursor) { return ApiResponse.ok("관심상품 조회에 성공하였습니다.", wishItemService.findAll(categoryId, size, cursor)); } + + @GetMapping("/categories") + public ApiResponse readWishCategories(@AuthPrincipal Principal principal) { + return ApiResponse.ok("관심상품의 카테고리 목록 조회를 완료하였습니다.", wishItemService.readWishCategories(principal)); + } } diff --git a/backend/src/main/java/codesquard/app/api/wishitem/WishItemService.java b/backend/src/main/java/codesquard/app/api/wishitem/WishItemService.java index 674e8ef1b..5f2ec66fb 100644 --- a/backend/src/main/java/codesquard/app/api/wishitem/WishItemService.java +++ b/backend/src/main/java/codesquard/app/api/wishitem/WishItemService.java @@ -1,6 +1,8 @@ package codesquard.app.api.wishitem; -import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; @@ -12,10 +14,13 @@ import codesquard.app.api.errors.exception.RestApiException; import codesquard.app.api.item.response.ItemResponse; import codesquard.app.api.item.response.ItemResponses; +import codesquard.app.api.wishitem.response.WishCategoryListResponse; +import codesquard.app.domain.category.Category; import codesquard.app.domain.item.Item; import codesquard.app.domain.item.ItemRepository; import codesquard.app.domain.member.Member; import codesquard.app.domain.member.MemberRepository; +import codesquard.app.domain.oauth.support.Principal; import codesquard.app.domain.pagination.PaginationUtils; import codesquard.app.domain.wish.Wish; import codesquard.app.domain.wish.WishPaginationRepository; @@ -24,6 +29,7 @@ import lombok.RequiredArgsConstructor; @Service +@Transactional(readOnly = true) @RequiredArgsConstructor public class WishItemService { @@ -50,7 +56,7 @@ private void register(Long itemId, Long memberId) { item.wishRegister(); Member member = memberRepository.findById(memberId) .orElseThrow(() -> new RestApiException(MemberErrorCode.NOT_FOUND_MEMBER)); - wishRepository.save(new Wish(member, item, LocalDateTime.now())); + wishRepository.save(new Wish(member, item)); } private void cancel(Long itemId) { @@ -60,9 +66,19 @@ private void cancel(Long itemId) { wishRepository.deleteByItemId(itemId); } - @Transactional(readOnly = true) public ItemResponses findAll(Long categoryId, int size, Long cursor) { Slice itemResponses = wishPaginationRepository.findAll(categoryId, size, cursor); return PaginationUtils.getItemResponses(itemResponses); } + + public WishCategoryListResponse readWishCategories(Principal principal) { + List wishes = wishRepository.findAllByMemberId(principal.getMemberId()); + List categories = wishes.stream() + .map(Wish::getItem) + .map(Item::getCategory) + .sorted(Comparator.comparing(Category::getId)) + .distinct() + .collect(Collectors.toUnmodifiableList()); + return WishCategoryListResponse.of(categories); + } } diff --git a/backend/src/main/java/codesquard/app/api/wishitem/response/WishCategoryItemResponse.java b/backend/src/main/java/codesquard/app/api/wishitem/response/WishCategoryItemResponse.java new file mode 100644 index 000000000..eea63a6e1 --- /dev/null +++ b/backend/src/main/java/codesquard/app/api/wishitem/response/WishCategoryItemResponse.java @@ -0,0 +1,21 @@ +package codesquard.app.api.wishitem.response; + +import codesquard.app.domain.category.Category; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class WishCategoryItemResponse { + private Long categoryId; + private String categoryName; + + public static WishCategoryItemResponse from(Category category) { + return new WishCategoryItemResponse(category.getId(), category.getName()); + } + + @Override + public String toString() { + return String.format("%s, %s(categoryId=%d, categoryName=%s)", "관심상품 카테고리 항목", this.getClass().getSimpleName(), + categoryId, categoryName); + } +} diff --git a/backend/src/main/java/codesquard/app/api/wishitem/response/WishCategoryListResponse.java b/backend/src/main/java/codesquard/app/api/wishitem/response/WishCategoryListResponse.java new file mode 100644 index 000000000..dbeb6232e --- /dev/null +++ b/backend/src/main/java/codesquard/app/api/wishitem/response/WishCategoryListResponse.java @@ -0,0 +1,25 @@ +package codesquard.app.api.wishitem.response; + +import java.util.List; +import java.util.stream.Collectors; + +import codesquard.app.domain.category.Category; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class WishCategoryListResponse { + private List categories; + + public static WishCategoryListResponse of(List categories) { + return new WishCategoryListResponse(categories.stream() + .map(WishCategoryItemResponse::from) + .collect(Collectors.toUnmodifiableList()) + ); + } + + @Override + public String toString() { + return String.format("%s, %s(categories=%s)", "관심상품 카테고리 리스트 응답", this.getClass().getSimpleName(), categories); + } +} diff --git a/backend/src/main/java/codesquard/app/domain/wish/Wish.java b/backend/src/main/java/codesquard/app/domain/wish/Wish.java index 06ae03acf..d3214f022 100644 --- a/backend/src/main/java/codesquard/app/domain/wish/Wish.java +++ b/backend/src/main/java/codesquard/app/domain/wish/Wish.java @@ -36,9 +36,9 @@ public class Wish { private Item item; private LocalDateTime createdAt; - public Wish(Member member, Item item, LocalDateTime createdAt) { + public Wish(Member member, Item item) { this.member = member; this.item = item; - this.createdAt = createdAt; + this.createdAt = LocalDateTime.now(); } } diff --git a/backend/src/main/java/codesquard/app/domain/wish/WishRepository.java b/backend/src/main/java/codesquard/app/domain/wish/WishRepository.java index 060863ff3..2528ef374 100644 --- a/backend/src/main/java/codesquard/app/domain/wish/WishRepository.java +++ b/backend/src/main/java/codesquard/app/domain/wish/WishRepository.java @@ -1,5 +1,7 @@ package codesquard.app.domain.wish; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; public interface WishRepository extends JpaRepository { @@ -9,4 +11,6 @@ public interface WishRepository extends JpaRepository { int countWishByItemId(Long itemId); boolean existsByMemberIdAndItemId(Long memberId, Long itemId); + + List findAllByMemberId(Long memberId); } diff --git a/backend/src/main/resources/db/mysql/data.sql b/backend/src/main/resources/db/mysql/data.sql index 358398626..4e51b4df9 100644 --- a/backend/src/main/resources/db/mysql/data.sql +++ b/backend/src/main/resources/db/mysql/data.sql @@ -60,6 +60,13 @@ VALUES (0, '롤러블레이드 팝니다', now(), 169000, '청운동', 'ON_SALE' 'https://second-hand-team03-a.s3.ap-northeast-2.amazonaws.com/public/sample/roller_blade.jpeg', '롤러 블레이드', 0, 0, 1, 1); +INSERT INTO image (image_url, thumbnail, item_id) +VALUES ('https://second-hand-team03-a.s3.ap-northeast-2.amazonaws.com/public/sample/roller_blade.jpeg', + true, + 1); + + + INSERT INTO item(chat_count, content, created_at, @@ -79,7 +86,36 @@ VALUES (0, '롤러블레이드 팝니다', now(), 169000, '역삼1동', 'ON_SALE INSERT INTO image (image_url, thumbnail, item_id) VALUES ('https://second-hand-team03-a.s3.ap-northeast-2.amazonaws.com/public/sample/roller_blade.jpeg', true, - 1); + 2); + + +INSERT INTO item(chat_count, + content, + created_at, + price, + region, + status, + thumbnail_url, + title, + view_count, + wish_count, + category_id, + member_id) +VALUES (0, '의자 팝니다.', now(), 130000, '역삼1동', 'ON_SALE', + 'https://second-hand-team03-a.s3.ap-northeast-2.amazonaws.com/public/sample/char.jpeg', + '옛날 의자', 0, 0, 6, 1); + +INSERT INTO image (image_url, thumbnail, item_id) +VALUES ('https://second-hand-team03-a.s3.ap-northeast-2.amazonaws.com/public/sample/char.jpeg', + true, + 3); + +INSERT INTO wish(created_at, item_id, member_id) +VALUES (now(), 1, 2); +INSERT INTO wish(created_at, item_id, member_id) +VALUES (now(), 2, 2); +INSERT INTO wish(created_at, item_id, member_id) +VALUES (now(), 3, 2); INSERT INTO chat_room(created_at, item_id, member_id) VALUES (now(), 1, 2); diff --git a/backend/src/test/java/codesquard/app/ItemTestSupport.java b/backend/src/test/java/codesquard/app/ItemTestSupport.java new file mode 100644 index 000000000..e66264b20 --- /dev/null +++ b/backend/src/test/java/codesquard/app/ItemTestSupport.java @@ -0,0 +1,28 @@ +package codesquard.app; + +import static java.time.LocalDateTime.*; + +import codesquard.app.domain.category.Category; +import codesquard.app.domain.item.Item; +import codesquard.app.domain.item.ItemStatus; +import codesquard.app.domain.member.Member; + +public class ItemTestSupport { + + public static Item createItem(String title, String content, Long price, ItemStatus status, String region, + Member member, Category category) { + return Item.builder() + .title(title) + .content(content) + .price(price) + .status(status) + .region(region) + .createdAt(now()) + .wishCount(0L) + .viewCount(0L) + .chatCount(0L) + .member(member) + .category(category) + .build(); + } +} diff --git a/backend/src/test/java/codesquard/app/api/item/ItemServiceTest.java b/backend/src/test/java/codesquard/app/api/item/ItemServiceTest.java index b57494e24..8daf02e2c 100644 --- a/backend/src/test/java/codesquard/app/api/item/ItemServiceTest.java +++ b/backend/src/test/java/codesquard/app/api/item/ItemServiceTest.java @@ -445,7 +445,7 @@ public void findDetailItemBySeller() { new Image("imageUrlValue2", saveItem, false)); imageRepository.saveAll(images); - Wish wish = new Wish(member, item, now()); + Wish wish = new Wish(member, item); wishRepository.save(wish); // when diff --git a/backend/src/test/java/codesquard/app/api/wishitem/WishItemControllerTest.java b/backend/src/test/java/codesquard/app/api/wishitem/WishItemControllerTest.java new file mode 100644 index 000000000..054ab40d8 --- /dev/null +++ b/backend/src/test/java/codesquard/app/api/wishitem/WishItemControllerTest.java @@ -0,0 +1,76 @@ +package codesquard.app.api.wishitem; + +import static codesquard.app.CategoryTestSupport.*; +import static org.hamcrest.Matchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import codesquard.app.ControllerTestSupport; +import codesquard.app.api.wishitem.response.WishCategoryListResponse; +import codesquard.app.domain.oauth.support.Principal; + +@ActiveProfiles("test") +@WebMvcTest(controllers = WishItemController.class) +class WishItemControllerTest extends ControllerTestSupport { + private MockMvc mockMvc; + + @Autowired + private MappingJackson2HttpMessageConverter jackson2HttpMessageConverter; + + @Autowired + private WishItemController wishItemController; + + @MockBean + private WishItemService wishItemService; + + @BeforeEach + public void setup() { + mockMvc = MockMvcBuilders.standaloneSetup(wishItemController) + .setControllerAdvice(globalExceptionHandler) + .setCustomArgumentResolvers(authPrincipalArgumentResolver) + .setMessageConverters(jackson2HttpMessageConverter) + .alwaysDo(print()) + .build(); + + given(authPrincipalArgumentResolver.supportsParameter(any())).willReturn(true); + + Principal principal = new Principal(1L, "23Yong@gmail.com", "23Yong", null, null); + given(authPrincipalArgumentResolver.resolveArgument(any(), any(), any(), any())).willReturn(principal); + } + + @DisplayName("관심 상품들의 카테고리 목록을 요청한다") + @Test + public void readWishCategories() throws Exception { + // given + WishCategoryListResponse response = WishCategoryListResponse.of( + List.of(findByName("스포츠/레저"), findByName("가구/인테리어"))); + given(wishItemService.readWishCategories(ArgumentMatchers.any(Principal.class))) + .willReturn(response); + + // when & then + mockMvc.perform(get("/api/wishes/categories")) + .andExpect(status().isOk()) + .andExpect(jsonPath("statusCode").value(equalTo(200))) + .andExpect(jsonPath("message").value(equalTo("관심상품의 카테고리 목록 조회를 완료하였습니다."))) + .andExpect(jsonPath("data.categories[*].categoryName").value( + containsInAnyOrder("스포츠/레저", "가구/인테리어"))); + } + +} diff --git a/backend/src/test/java/codesquard/app/api/wishitem/WishItemServiceTest.java b/backend/src/test/java/codesquard/app/api/wishitem/WishItemServiceTest.java index 2af5c0c39..97ce6a41b 100644 --- a/backend/src/test/java/codesquard/app/api/wishitem/WishItemServiceTest.java +++ b/backend/src/test/java/codesquard/app/api/wishitem/WishItemServiceTest.java @@ -1,5 +1,11 @@ package codesquard.app.api.wishitem; +import static codesquard.app.CategoryTestSupport.*; +import static codesquard.app.ItemTestSupport.*; +import static codesquard.app.MemberTestSupport.*; +import static codesquard.app.MemberTownTestSupport.*; +import static codesquard.app.RegionTestSupport.*; +import static codesquard.app.domain.item.ItemStatus.*; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; @@ -7,6 +13,7 @@ import javax.persistence.EntityManager; +import org.assertj.core.groups.Tuple; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -17,12 +24,20 @@ import codesquard.app.api.item.request.ItemRegisterRequest; import codesquard.app.api.item.response.ItemResponse; import codesquard.app.api.item.response.ItemResponses; +import codesquard.app.api.wishitem.response.WishCategoryListResponse; import codesquard.app.domain.category.Category; +import codesquard.app.domain.category.CategoryRepository; +import codesquard.app.domain.image.ImageRepository; import codesquard.app.domain.item.Item; import codesquard.app.domain.item.ItemRepository; import codesquard.app.domain.item.ItemStatus; import codesquard.app.domain.member.Member; import codesquard.app.domain.member.MemberRepository; +import codesquard.app.domain.membertown.MemberTownRepository; +import codesquard.app.domain.oauth.support.Principal; +import codesquard.app.domain.region.Region; +import codesquard.app.domain.region.RegionRepository; +import codesquard.app.domain.wish.Wish; import codesquard.app.domain.wish.WishRepository; import codesquard.app.domain.wish.WishStatus; import codesquard.support.SupportRepository; @@ -42,13 +57,25 @@ class WishItemServiceTest { @Autowired private ItemRepository itemRepository; @Autowired + private CategoryRepository categoryRepository; + @Autowired + private MemberTownRepository memberTownRepository; + @Autowired + private RegionRepository regionRepository; + @Autowired + private ImageRepository imageRepository; + @Autowired private EntityManager em; @AfterEach void tearDown() { wishRepository.deleteAllInBatch(); + imageRepository.deleteAllInBatch(); itemRepository.deleteAllInBatch(); + categoryRepository.deleteAllInBatch(); + memberTownRepository.deleteAllInBatch(); memberRepository.deleteAllInBatch(); + regionRepository.deleteAllInBatch(); } @Test @@ -155,4 +182,41 @@ void wishListByCategoryTest() { // then assertThat(responses.getContents()).hasSize(2); } + + @DisplayName("한 회원이 등록한 관심 상품들의 중복되지 않은 카테고리를 조회한다") + @Test + public void readWishCategories() { + // given + List categories = categoryRepository.saveAll(List.of(findByName("스포츠/레저"), findByName("가구/인테리어"))); + List members = memberRepository.saveAll(List.of( + createMember("avatarUrlValue", "23Yong@gmail.com", "23Yong"), + createMember("avatarUrlValue", "bruni@gmail.com", "bruni"))); + List regions = regionRepository.saveAll( + List.of(createRegion("서울 송파구 가락동"), createRegion("서울 종로구 청운동"))); + + Member seller = members.get(0); + Member buyer = members.get(1); + memberTownRepository.saveAll(List.of( + createMemberTown(seller, regions.get(0), true), + createMemberTown(seller, regions.get(1), false), + createMemberTown(buyer, regions.get(0), true), + createMemberTown(buyer, regions.get(0), true))); + Item item1 = createItem("빈티지 롤러 블레이드", "어린시절 추억의향수를 불러 일으키는 롤러 스케이트입니다.", 200000L, ON_SALE, + "가락동", seller, categories.get(0)); + Item item2 = createItem("빈티지 의자", "의자 팝니다.", 80000L, ON_SALE, + "가락동", seller, categories.get(1)); + itemRepository.saveAll(List.of(item1, item2)); + wishRepository.saveAll(List.of(new Wish(buyer, item1), new Wish(buyer, item2))); + Principal principal = Principal.from(buyer); + + // when + WishCategoryListResponse response = wishItemService.readWishCategories(principal); + + // then + assertThat(response) + .extracting("categories").asList() + .extracting("categoryId", "categoryName") + .containsExactlyInAnyOrder(Tuple.tuple(1L, "스포츠/레저"), Tuple.tuple(2L, "가구/인테리어")); + + } }