diff --git a/src/main/java/in/koreatech/koin/domain/benefit/model/BenefitCategoryMap.java b/src/main/java/in/koreatech/koin/domain/benefit/model/BenefitCategoryMap.java index 5da5479df..f416584cd 100644 --- a/src/main/java/in/koreatech/koin/domain/benefit/model/BenefitCategoryMap.java +++ b/src/main/java/in/koreatech/koin/domain/benefit/model/BenefitCategoryMap.java @@ -5,6 +5,7 @@ import in.koreatech.koin.domain.shop.model.shop.Shop; import in.koreatech.koin.global.domain.BaseEntity; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; @@ -12,6 +13,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.validation.constraints.Size; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -34,9 +36,14 @@ public class BenefitCategoryMap extends BaseEntity { @JoinColumn(name = "benefit_id", referencedColumnName = "id", nullable = false) private BenefitCategory benefitCategory; + @Size(min = 2, max = 20) + @Column(name = "detail") + private String detail; + @Builder - public BenefitCategoryMap(Shop shop, BenefitCategory benefitCategory) { + public BenefitCategoryMap(Shop shop, BenefitCategory benefitCategory, String detail) { this.shop = shop; this.benefitCategory = benefitCategory; + this.detail = detail; } } diff --git a/src/main/java/in/koreatech/koin/domain/benefit/repository/BenefitCategoryMapRepository.java b/src/main/java/in/koreatech/koin/domain/benefit/repository/BenefitCategoryMapRepository.java index a29f48c36..cb2590edb 100644 --- a/src/main/java/in/koreatech/koin/domain/benefit/repository/BenefitCategoryMapRepository.java +++ b/src/main/java/in/koreatech/koin/domain/benefit/repository/BenefitCategoryMapRepository.java @@ -2,6 +2,7 @@ import java.util.List; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; import in.koreatech.koin.domain.benefit.model.BenefitCategoryMap; @@ -10,5 +11,12 @@ public interface BenefitCategoryMapRepository extends Repository findAllByBenefitCategoryId(Integer benefitCategoryId); + @Query(""" + SELECT bcm FROM BenefitCategoryMap bcm + JOIN FETCH bcm.shop s + JOIN FETCH bcm.benefitCategory bc + """) + List findAllWithFetchJoin(); + BenefitCategoryMap save(BenefitCategoryMap benefitCategoryMap); } diff --git a/src/main/java/in/koreatech/koin/domain/shop/dto/shop/response/ShopsResponseV2.java b/src/main/java/in/koreatech/koin/domain/shop/dto/shop/response/ShopsResponseV2.java index a391a3204..e0700f1ff 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/dto/shop/response/ShopsResponseV2.java +++ b/src/main/java/in/koreatech/koin/domain/shop/dto/shop/response/ShopsResponseV2.java @@ -42,7 +42,8 @@ public static ShopsResponseV2 from( ShopsSortCriteria sortBy, List shopsFilterCriterias, LocalDateTime now, - String query + String query, + Map> benefitDetail ) { List innerShopResponses = shops.stream() .filter(queryPredicate(query)) @@ -53,7 +54,8 @@ public static ShopsResponseV2 from( shopInfo.durationEvent(), it.isOpen(now), shopInfo.averageRate(), - shopInfo.reviewCount() + shopInfo.reviewCount(), + benefitDetail.getOrDefault(it.id(), List.of()) ); }) .filter(ShopsFilterCriteria.createCombinedFilter(shopsFilterCriterias)) @@ -101,7 +103,10 @@ public record InnerShopResponse( double averageRate, @Schema(example = "10", description = "리뷰 개수", requiredMode = REQUIRED) - long reviewCount + long reviewCount, + + @Schema(example = "['배달비 무료', '콜라 서비스']", description = "혜택 설명", requiredMode = NOT_REQUIRED) + List benefitDetails ) { @JsonNaming(value = SnakeCaseStrategy.class) @@ -138,7 +143,8 @@ public static InnerShopResponse from( Boolean isEvent, Boolean isOpen, Double averageRate, - Long reviewCount + Long reviewCount, + List benefitDetails ) { return new InnerShopResponse( shop.shopCategories().stream().map(ShopCategoryCache::id).toList(), @@ -159,7 +165,8 @@ public static InnerShopResponse from( isEvent, isOpen, averageRate, - reviewCount + reviewCount, + benefitDetails ); } diff --git a/src/main/java/in/koreatech/koin/domain/shop/service/ShopService.java b/src/main/java/in/koreatech/koin/domain/shop/service/ShopService.java index edd973b1a..d73512fbb 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/service/ShopService.java +++ b/src/main/java/in/koreatech/koin/domain/shop/service/ShopService.java @@ -2,6 +2,8 @@ import static in.koreatech.koin.global.domain.notification.model.NotificationSubscribeType.REVIEW_PROMPT; +import in.koreatech.koin.domain.benefit.model.BenefitCategoryMap; +import in.koreatech.koin.domain.benefit.repository.BenefitCategoryMapRepository; import in.koreatech.koin.domain.shop.cache.ShopsCacheService; import in.koreatech.koin.domain.shop.cache.dto.ShopsCache; import in.koreatech.koin.domain.shop.dto.shop.ShopsFilterCriteria; @@ -24,6 +26,8 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -43,6 +47,7 @@ public class ShopService { private final ShopCustomRepository shopCustomRepository; private final NotificationSubscribeRepository notificationSubscribeRepository; private final ShopReviewNotificationRedisRepository shopReviewNotificationRedisRepository; + private final BenefitCategoryMapRepository benefitCategoryMapRepository; public ShopResponse getShop(Integer shopId) { Shop shop = shopRepository.getById(shopId); @@ -63,9 +68,9 @@ public ShopCategoriesResponse getShopsCategories() { } public ShopsResponseV2 getShopsV2( - ShopsSortCriteria sortBy, - List filterCriteria, - String query + ShopsSortCriteria sortBy, + List filterCriteria, + String query ) { if (filterCriteria.contains(null)) { throw KoinIllegalArgumentException.withDetail("유효하지 않은 필터입니다."); @@ -73,13 +78,27 @@ public ShopsResponseV2 getShopsV2( ShopsCache shopCaches = shopsCache.findAllShopCache(); LocalDateTime now = LocalDateTime.now(clock); Map shopInfoMap = shopCustomRepository.findAllShopInfo(now); + List benefitCategorys = benefitCategoryMapRepository.findAllWithFetchJoin(); + Map> benefitDetailMap = new HashMap<>(benefitCategorys.size()); + benefitCategorys.forEach(benefitCategory -> { + int shopId = benefitCategory.getShop().getId(); + String benefitDetail = benefitCategory.getDetail(); + if (benefitDetailMap.containsKey(shopId)) { + benefitDetailMap.get(shopId).add(benefitDetail); + } else { + List details = new ArrayList<>(); + details.add(benefitDetail); + benefitDetailMap.put(shopId, details); + } + }); return ShopsResponseV2.from( - shopCaches.shopCaches(), - shopInfoMap, - sortBy, - filterCriteria, - now, - query + shopCaches.shopCaches(), + shopInfoMap, + sortBy, + filterCriteria, + now, + query, + benefitDetailMap ); } @@ -88,9 +107,9 @@ public void publishCallNotification(Integer shopId, Integer studentId) { if (isSubscribeReviewNotification(studentId)) { ShopReviewNotification shopReviewNotification = ShopReviewNotification.builder() - .shopId(shopId) - .studentId(studentId) - .build(); + .shopId(shopId) + .studentId(studentId) + .build(); double score = LocalDateTime.now(clock).plusHours(1).toEpochSecond(ZoneOffset.UTC); shopReviewNotificationRedisRepository.save(shopReviewNotification, score); diff --git a/src/main/resources/db/migration/V104__add_shop_benefit_detail.sql b/src/main/resources/db/migration/V104__add_shop_benefit_detail.sql new file mode 100644 index 000000000..c25f2cb76 --- /dev/null +++ b/src/main/resources/db/migration/V104__add_shop_benefit_detail.sql @@ -0,0 +1,2 @@ +ALTER TABLE `shop_benefit_category_map` + ADD COLUMN `detail` VARCHAR(20); diff --git a/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java b/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java index 60c6a7a0a..90c103288 100644 --- a/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java @@ -7,6 +7,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import in.koreatech.koin.domain.benefit.model.BenefitCategory; +import in.koreatech.koin.fixture.BenefitCategoryFixture; +import in.koreatech.koin.fixture.BenefitCategoryMapFixture; import java.time.LocalDate; import org.junit.jupiter.api.BeforeAll; @@ -40,6 +43,12 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ShopApiTest extends AcceptanceTest { + @Autowired + private BenefitCategoryFixture benefitCategoryFixture; + + @Autowired + private BenefitCategoryMapFixture benefitCategoryMapFixture; + @Autowired private UserFixture userFixture; @@ -1897,4 +1906,64 @@ void setUp() { ) .andExpect(status().isOk()); } + + @Test + void 리뷰를_조회하면_혜택_정보가_조회된다() throws Exception { + Shop 영업중인_티바 = shopFixture.영업중인_티바(owner); + shopReviewFixture.리뷰_4점(익명_학생, 영업중인_티바); + + shopReviewFixture.리뷰_4점(익명_학생, 마슬랜); + shopReviewFixture.리뷰_4점(익명_학생, 마슬랜); + // 2024-01-15 12:00 월요일 기준 + boolean 마슬랜_영업여부 = true; + boolean 티바_영업여부 = true; + + BenefitCategory 최소주문금액_무료 = benefitCategoryFixture.최소주문금액_무료(); + BenefitCategory 서비스_증정 = benefitCategoryFixture.서비스_증정(); + benefitCategoryMapFixture.설명이_포함된_혜택_추가(영업중인_티바, 최소주문금액_무료, "무료"); + benefitCategoryMapFixture.설명이_포함된_혜택_추가(영업중인_티바, 서비스_증정, "콜라"); + mockMvc.perform( + get("/v2/shops") + .queryParam("sorter", "COUNT_DESC") + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" + { + "count": 2, + "shops": [ + { + "category_ids": [ + \s + ], + "delivery": true, + "id": 1, + "name": "마슬랜 치킨", + "pay_bank": true, + "pay_card": true, + "phone": "010-7574-1212", + "is_event": false, + "is_open": %s, + "average_rate": 4.0, + "review_count": 2, + "benefit_details": [] + },{ + "category_ids": [ + \s + ], + "delivery": true, + "id": 2, + "name": "티바", + "pay_bank": true, + "pay_card": true, + "phone": "010-7788-9900", + "is_event": false, + "is_open": %s, + "average_rate": 4.0, + "review_count": 1, + "benefit_details": ["무료", "콜라"] + } + ] + } + """, 티바_영업여부, 마슬랜_영업여부))); + } } diff --git a/src/test/java/in/koreatech/koin/fixture/BenefitCategoryMapFixture.java b/src/test/java/in/koreatech/koin/fixture/BenefitCategoryMapFixture.java index 8eca67285..abf8583c4 100644 --- a/src/test/java/in/koreatech/koin/fixture/BenefitCategoryMapFixture.java +++ b/src/test/java/in/koreatech/koin/fixture/BenefitCategoryMapFixture.java @@ -25,4 +25,12 @@ public BenefitCategoryMapFixture( .benefitCategory(benefitCategory) .build()); } + + public BenefitCategoryMap 설명이_포함된_혜택_추가(Shop shop, BenefitCategory benefitCategory, String detail) { + return benefitCategoryMapRepository.save(BenefitCategoryMap.builder() + .shop(shop) + .benefitCategory(benefitCategory) + .detail(detail) + .build()); + } }