diff --git a/src/main/java/com/fc/shimpyo_be/domain/favorite/service/FavoriteService.java b/src/main/java/com/fc/shimpyo_be/domain/favorite/service/FavoriteService.java index f3e490a1..280f9ae2 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/favorite/service/FavoriteService.java +++ b/src/main/java/com/fc/shimpyo_be/domain/favorite/service/FavoriteService.java @@ -48,7 +48,7 @@ public FavoritesResponseDto getFavorites(long memberId, Pageable pageable) { Member member = memberService.getMemberById(memberId); Page favorites = favoriteRepository.findAllByMemberId(member.getId(), pageable); for (Favorite favorite : favorites) { - productResponses.add(ProductMapper.toProductResponse(favorite.getProduct())); + productResponses.add(ProductMapper.toProductResponse(favorite.getProduct(),true)); } return FavoritesResponseDto.builder() .pageCount(favorites.getTotalPages()) diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/entity/Product.java b/src/main/java/com/fc/shimpyo_be/domain/product/entity/Product.java index f14551ac..f0b332a4 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/product/entity/Product.java +++ b/src/main/java/com/fc/shimpyo_be/domain/product/entity/Product.java @@ -1,5 +1,6 @@ package com.fc.shimpyo_be.domain.product.entity; +import com.fc.shimpyo_be.domain.favorite.entity.Favorite; import com.fc.shimpyo_be.domain.product.util.CategoryConverter; import com.fc.shimpyo_be.domain.room.entity.Room; import jakarta.persistence.CascadeType; @@ -59,12 +60,14 @@ public class Product { private List photoUrls = new ArrayList<>(); @OneToMany(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) private List rooms = new ArrayList<>(); + @OneToMany(mappedBy = "product", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + private List favorites = new ArrayList<>(); @Builder public Product(Long id, String name, Address address, Category category, String description, float starAvg, String thumbnail, ProductOption productOption, Amenity amenity, - List photoUrls, List rooms) { + List photoUrls, List rooms, List favorites) { this.id = id; this.name = name; this.address = address; @@ -76,6 +79,7 @@ public Product(Long id, String name, Address address, Category category, String this.amenity = amenity; this.photoUrls = photoUrls; this.rooms = rooms; + this.favorites = favorites; } public void updateStarAvg(float starAvg) { diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/service/ProductService.java b/src/main/java/com/fc/shimpyo_be/domain/product/service/ProductService.java index 097e4828..5b28b31c 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/product/service/ProductService.java +++ b/src/main/java/com/fc/shimpyo_be/domain/product/service/ProductService.java @@ -1,8 +1,10 @@ package com.fc.shimpyo_be.domain.product.service; +import com.fc.shimpyo_be.domain.favorite.entity.Favorite; import com.fc.shimpyo_be.domain.product.dto.request.SearchKeywordRequest; import com.fc.shimpyo_be.domain.product.dto.response.PaginatedProductResponse; import com.fc.shimpyo_be.domain.product.dto.response.ProductDetailsResponse; +import com.fc.shimpyo_be.domain.product.dto.response.ProductResponse; import com.fc.shimpyo_be.domain.product.entity.Product; import com.fc.shimpyo_be.domain.product.exception.ProductNotFoundException; import com.fc.shimpyo_be.domain.product.repository.ProductCustomRepositoryImpl; @@ -11,7 +13,9 @@ import com.fc.shimpyo_be.domain.room.entity.Room; import com.fc.shimpyo_be.domain.room.repository.RoomRepository; import com.fc.shimpyo_be.global.util.DateTimeUtil; +import com.fc.shimpyo_be.global.util.SecurityUtil; import java.time.LocalDate; +import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; @@ -34,6 +38,8 @@ public class ProductService { private final RedisTemplate restTemplate; + private final SecurityUtil securityUtil; + public PaginatedProductResponse getProducts(final SearchKeywordRequest searchKeywordRequest, final Pageable pageable) { @@ -43,7 +49,7 @@ public PaginatedProductResponse getProducts(final SearchKeywordRequest searchKey return PaginatedProductResponse.builder() .productResponses( - products.getContent().stream().map(ProductMapper::toProductResponse).toList()) + getProductResponseSettingFavorites(products.getContent())) .pageCount(products.getTotalPages()) .build(); } @@ -52,8 +58,10 @@ public ProductDetailsResponse getProductDetails(final Long productId, final Stri final String endDate) { Product product = productRepository.findById(productId) .orElseThrow(ProductNotFoundException::new); + HashSet favoriteProductIds = getFavoriteProductIds(List.of(product)); ProductDetailsResponse productDetailsResponse = ProductMapper.toProductDetailsResponse( - product); + product, favoriteProductIds.contains(product.getId())); + productDetailsResponse.rooms().forEach( roomResponse -> roomResponse.setRemaining( countAvailableForReservationUsingRoomCode(roomResponse.getRoomCode(), startDate, @@ -61,7 +69,8 @@ public ProductDetailsResponse getProductDetails(final Long productId, final Stri return productDetailsResponse; } - public long countAvailableForReservationUsingRoomCode(final Long roomCode, final String startDate, + public long countAvailableForReservationUsingRoomCode(final Long roomCode, + final String startDate, final String endDate) { AtomicLong remaining = new AtomicLong(); List rooms = Optional.of(roomRepository.findByCode(roomCode)).orElseThrow(); @@ -93,5 +102,31 @@ public boolean isAvailableForReservation(final Long roomId, final String startDa return true; } + private List getProductResponseSettingFavorites(List products) { + + HashSet favoriteProductIds = getFavoriteProductIds(products); + + return products.stream().map(product -> ProductMapper.toProductResponse(product, + favoriteProductIds.contains(product.getId()))).toList(); + + } + + private HashSet getFavoriteProductIds(List products) { + Long userId = securityUtil.getNullableCurrentMemberId(); + HashSet favoriteProductId = new HashSet<>(); + if (userId != null) { + for (Product product : products) { + for (Favorite favorite : product.getFavorites()) { + if (favorite.getMember().getId().equals(userId)) { + favoriteProductId.add(product.getId()); + break; + } + } + } + } + + return favoriteProductId; + } + } diff --git a/src/main/java/com/fc/shimpyo_be/domain/product/util/ProductMapper.java b/src/main/java/com/fc/shimpyo_be/domain/product/util/ProductMapper.java index f7c906e9..b7b5d56e 100644 --- a/src/main/java/com/fc/shimpyo_be/domain/product/util/ProductMapper.java +++ b/src/main/java/com/fc/shimpyo_be/domain/product/util/ProductMapper.java @@ -19,13 +19,7 @@ public class ProductMapper { - public static ProductResponse toProductResponse(Product product) { - - List rooms = product.getRooms(); - long price = rooms.isEmpty() ? 0 : rooms.stream().map(PricePickerByDateUtil::getPrice) - .min((o1, o2) -> Math.toIntExact( - o1 - o2)).orElseThrow(); - price = price == 0 ? 100000 : price; + public static ProductResponse toProductResponse(Product product, boolean isFavorite) { return ProductResponse.builder().productId(product.getId()).productName(product.getName()) .address( @@ -33,23 +27,16 @@ public static ProductResponse toProductResponse(Product product) { .category(product.getCategory().getName()) .image(product.getThumbnail()) .starAvg(product.getStarAvg()) - .price(price) + .price(getPrice(product)) .capacity(product.getRooms().isEmpty() ? 0 : Long.valueOf( product.getRooms().stream().map(Room::getCapacity).min((o1, o2) -> o2 - o1) .orElseThrow())) - .favorites(false) + .favorites(isFavorite) .build(); } - public static ProductDetailsResponse toProductDetailsResponse(Product product) { - - List images = new ArrayList<>(); - images.add(product.getThumbnail()); - - if (product.getPhotoUrls() != null) { - images.addAll(product.getPhotoUrls().stream().map(ProductImage::getPhotoUrl).toList()); - } + public static ProductDetailsResponse toProductDetailsResponse(Product product, boolean isFavorite) { return ProductDetailsResponse.builder() .productId(product.getId()) @@ -60,8 +47,8 @@ public static ProductDetailsResponse toProductDetailsResponse(Product product) { .productAmenityResponse(toProductAmenityResponse(product.getAmenity())) .starAvg(product.getStarAvg()) .productOptionResponse(toProductOptionResponse(product.getProductOption())) - .favorites(false) - .images(images) + .favorites(isFavorite) + .images(getImage(product)) .rooms(product.getRooms().stream().map(RoomMapper::toRoomResponse).distinct().toList()) .build(); } @@ -102,4 +89,24 @@ private static ProductOptionResponse toProductOptionResponse(ProductOption produ .build(); } + private static long getPrice(Product product) { + List rooms = product.getRooms(); + long price = rooms.isEmpty() ? 0 : rooms.stream().map(PricePickerByDateUtil::getPrice) + .min((o1, o2) -> Math.toIntExact( + o1 - o2)).orElseThrow(); + + return price == 0 ? 100000 : price; + } + + private static List getImage(Product product) { + List images = new ArrayList<>(); + images.add(product.getThumbnail()); + + if (product.getPhotoUrls() != null) { + images.addAll(product.getPhotoUrls().stream().map(ProductImage::getPhotoUrl).toList()); + } + + return images; + } + } diff --git a/src/main/java/com/fc/shimpyo_be/global/util/SecurityUtil.java b/src/main/java/com/fc/shimpyo_be/global/util/SecurityUtil.java index 2f92fb1d..0a7b0ec2 100644 --- a/src/main/java/com/fc/shimpyo_be/global/util/SecurityUtil.java +++ b/src/main/java/com/fc/shimpyo_be/global/util/SecurityUtil.java @@ -15,4 +15,13 @@ public Long getCurrentMemberId() { } return Long.parseLong(authentication.getName()); } + + public Long getNullableCurrentMemberId() { + final Authentication authentication = SecurityContextHolder.getContext() + .getAuthentication(); + if (authentication == null || authentication.getName() == null) { + return null; + } + return Long.parseLong(authentication.getName()); + } } diff --git a/src/test/java/com/fc/shimpyo_be/domain/product/docs/ProductRestIntegrationDocsTest.java b/src/test/java/com/fc/shimpyo_be/domain/product/docs/ProductRestIntegrationDocsTest.java index d6511cb4..c033e407 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/product/docs/ProductRestIntegrationDocsTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/product/docs/ProductRestIntegrationDocsTest.java @@ -1,14 +1,22 @@ package com.fc.shimpyo_be.domain.product.docs; +import static org.junit.matchers.JUnitMatchers.everyItem; +import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fc.shimpyo_be.config.RestDocsSupport; +import com.fc.shimpyo_be.domain.favorite.entity.Favorite; +import com.fc.shimpyo_be.domain.favorite.repository.FavoriteRepository; +import com.fc.shimpyo_be.domain.member.entity.Authority; +import com.fc.shimpyo_be.domain.member.entity.Member; +import com.fc.shimpyo_be.domain.member.repository.MemberRepository; import com.fc.shimpyo_be.domain.product.entity.Product; import com.fc.shimpyo_be.domain.product.entity.ProductImage; import com.fc.shimpyo_be.domain.product.factory.ProductFactory; @@ -16,10 +24,12 @@ import com.fc.shimpyo_be.domain.product.repository.ProductRepository; import com.fc.shimpyo_be.domain.room.entity.Room; import com.fc.shimpyo_be.domain.room.repository.RoomRepository; +import com.fc.shimpyo_be.global.util.SecurityUtil; 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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.restdocs.payload.JsonFieldType; @@ -45,6 +55,15 @@ class ProductRestIntegrationDocsTest extends RestDocsSupport { @Autowired private RedisTemplate restTemplate; + @Autowired + private FavoriteRepository favoriteRepository; + + @Autowired + private MemberRepository memberRepository; + + @MockBean + private SecurityUtil securityUtil; + @DisplayName("숙소 저장 후, 검색 조회 및 페이징할 수 있다.") @Test void getProducts() throws Exception { @@ -301,5 +320,34 @@ void isAvailableForReservation() throws Exception { )); } + @Test + @DisplayName("전체 조회에서 즐겨찾기 여부를 볼 수 있다.") + void isAvailableGetFavoriteInGetProducts() throws Exception { + //given + given(securityUtil.getNullableCurrentMemberId()).willReturn(1L); + Product product = productRepository.save(ProductFactory.createTestProduct()); + Room room = roomRepository.save(ProductFactory.createTestRoom(product, 0L)); + Member member = Member.builder() + .email("test@mail.com") + .name("test") + .password("$10$ygrAExVYmFTkZn2d0.Pk3Ot5CNZwIBjZH5f.WW0AnUq4w4PtBi9Nm") + .photoUrl( + "https://fastly.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI") + .authority(Authority.ROLE_USER) + .build(); + memberRepository.save(member); + Favorite favorite = Favorite.builder().product(product).member(member).build(); + favoriteRepository.save(favorite); + + // when + ResultActions getProductAction = mockMvc.perform( + get("/api/products")); + + //then + getProductAction + .andDo(MockMvcResultHandlers.print()).andExpect(status().isOk()).andExpect(jsonPath("$.data.productResponses[0].favorites").value(true)); + + } + } \ No newline at end of file diff --git a/src/test/java/com/fc/shimpyo_be/domain/product/unit/controller/ProductRestControllerTest.java b/src/test/java/com/fc/shimpyo_be/domain/product/unit/controller/ProductRestControllerTest.java index 22242f63..b279d066 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/product/unit/controller/ProductRestControllerTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/product/unit/controller/ProductRestControllerTest.java @@ -41,7 +41,7 @@ class ProductRestControllerTest { void getAllProducts() { //given List productResponses = new ArrayList<>(); - productResponses.add(ProductMapper.toProductResponse(ProductFactory.createTestProduct())); + productResponses.add(ProductMapper.toProductResponse(ProductFactory.createTestProduct(),false)); PaginatedProductResponse paginatedProductResponse = PaginatedProductResponse.builder() .productResponses(productResponses) .pageCount(1) @@ -67,7 +67,7 @@ void getAllProducts() { void getProductDetails() { //given Product product = ProductFactory.createTestProduct(); - ProductDetailsResponse expectedResult = ProductMapper.toProductDetailsResponse(product); + ProductDetailsResponse expectedResult = ProductMapper.toProductDetailsResponse(product,false); doReturn(expectedResult).when(productService) .getProductDetails(1L, "2024-12-27", "2024-12-28"); //when diff --git a/src/test/java/com/fc/shimpyo_be/domain/product/unit/service/ProductServiceTest.java b/src/test/java/com/fc/shimpyo_be/domain/product/unit/service/ProductServiceTest.java index f93adeb9..ce5fd2fa 100644 --- a/src/test/java/com/fc/shimpyo_be/domain/product/unit/service/ProductServiceTest.java +++ b/src/test/java/com/fc/shimpyo_be/domain/product/unit/service/ProductServiceTest.java @@ -19,8 +19,10 @@ import com.fc.shimpyo_be.domain.product.util.ProductMapper; import com.fc.shimpyo_be.domain.room.dto.response.RoomResponse; import com.fc.shimpyo_be.domain.room.entity.Room; +import com.fc.shimpyo_be.global.util.SecurityUtil; import java.util.List; import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -34,6 +36,9 @@ @ExtendWith(MockitoExtension.class) class ProductServiceTest { + @Mock + private SecurityUtil securityUtil; + @Mock private ProductRepository productRepository; @@ -44,6 +49,10 @@ class ProductServiceTest { @InjectMocks private ProductService productService; + @BeforeEach + void init() { + given(securityUtil.getNullableCurrentMemberId()).willReturn(null); + } @Test void getProducts() { @@ -64,14 +73,15 @@ void getProducts() { //then assertThat(result.productResponses()).usingRecursiveAssertion().isEqualTo( - productPage.getContent().stream().map(ProductMapper::toProductResponse).toList()); + productPage.getContent().stream() + .map(product -> ProductMapper.toProductResponse(product, false)).toList()); } @Test void getProductDetails() { //given Product product = ProductFactory.createTestProduct(); - Room room = ProductFactory.createTestRoom(product,0L); + Room room = ProductFactory.createTestRoom(product, 0L); product.getRooms().add(room); given(productRepository.findById(product.getId())).willReturn(Optional.ofNullable(product)); doReturn(1L).when( @@ -83,7 +93,8 @@ void getProductDetails() { "2023-11-27", "2023-11-28"); //then for (int i = 0; i < result.rooms().size(); i++) { - RoomResponse roomResponse = ProductMapper.toProductDetailsResponse(product).rooms() + RoomResponse roomResponse = ProductMapper.toProductDetailsResponse(product, false) + .rooms() .get(i); roomResponse.setRemaining(1L); assertThat(result.rooms().get(i)).usingRecursiveComparison()