diff --git a/src/main/java/in/koreatech/koin/KoinApplication.java b/src/main/java/in/koreatech/koin/KoinApplication.java index 6908a96b4..20aa0a74a 100644 --- a/src/main/java/in/koreatech/koin/KoinApplication.java +++ b/src/main/java/in/koreatech/koin/KoinApplication.java @@ -3,11 +3,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.scheduling.annotation.EnableScheduling; @EnableScheduling @SpringBootApplication @ConfigurationPropertiesScan +@EnableCaching public class KoinApplication { public static void main(String[] args) { diff --git a/src/main/java/in/koreatech/koin/admin/benefit/repository/AdminBenefitCategoryMapRepository.java b/src/main/java/in/koreatech/koin/admin/benefit/repository/AdminBenefitCategoryMapRepository.java index ef2ba5f82..8b33c83d3 100644 --- a/src/main/java/in/koreatech/koin/admin/benefit/repository/AdminBenefitCategoryMapRepository.java +++ b/src/main/java/in/koreatech/koin/admin/benefit/repository/AdminBenefitCategoryMapRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.repository.Repository; import in.koreatech.koin.domain.benefit.model.BenefitCategoryMap; +import org.springframework.data.repository.query.Param; public interface AdminBenefitCategoryMapRepository extends Repository { @@ -19,7 +20,7 @@ public interface AdminBenefitCategoryMapRepository extends Repository findAllByBenefitCategoryIdOrderByShopName(Integer benefitId); + List findAllByBenefitCategoryIdOrderByShopName(@Param("benefitId") Integer benefitId); @Modifying @Query(""" @@ -27,13 +28,15 @@ public interface AdminBenefitCategoryMapRepository extends Repository shopIds); + void deleteByBenefitCategoryIdAndShopIds( + @Param("benefitId") Integer benefitId, + @Param("shopIds") List shopIds); @Modifying @Query(""" DELETE FROM BenefitCategoryMap bcm WHERE bcm.benefitCategory.id = :benefitId """) - void deleteByBenefitCategoryId(Integer benefitId); + void deleteByBenefitCategoryId(@Param("benefitId") Integer benefitId); } diff --git a/src/main/java/in/koreatech/koin/admin/member/repository/AdminMemberRepository.java b/src/main/java/in/koreatech/koin/admin/member/repository/AdminMemberRepository.java index 2a23b719c..4bd51b56f 100644 --- a/src/main/java/in/koreatech/koin/admin/member/repository/AdminMemberRepository.java +++ b/src/main/java/in/koreatech/koin/admin/member/repository/AdminMemberRepository.java @@ -11,16 +11,22 @@ import in.koreatech.koin.domain.member.exception.MemberNotFoundException; import in.koreatech.koin.domain.member.model.Member; +import org.springframework.data.repository.query.Param; public interface AdminMemberRepository extends Repository { @EntityGraph(attributePaths = {"track"}) @Query("select m from Member m where m.track.name = :trackName and m.isDeleted = :isDeleted") - Page findAllByTrackAndIsDeleted(String trackName, Boolean isDeleted, Pageable pageable); + Page findAllByTrackAndIsDeleted( + @Param("trackName") String trackName, + @Param("isDeleted") Boolean isDeleted, + Pageable pageable); @EntityGraph(attributePaths = {"track"}) @Query("select count(m) from Member m where m.track.name = :trackName and m.isDeleted = :isDeleted") - Integer countAllByTrackAndIsDeleted(String trackName, Boolean isDeleted); + Integer countAllByTrackAndIsDeleted( + @Param("trackName") String trackName, + @Param("isDeleted") Boolean isDeleted); Member save(Member member); diff --git a/src/main/java/in/koreatech/koin/admin/notice/repository/AdminKoinArticleRepository.java b/src/main/java/in/koreatech/koin/admin/notice/repository/AdminKoinArticleRepository.java index 69c6f21fe..53a927615 100644 --- a/src/main/java/in/koreatech/koin/admin/notice/repository/AdminKoinArticleRepository.java +++ b/src/main/java/in/koreatech/koin/admin/notice/repository/AdminKoinArticleRepository.java @@ -6,9 +6,10 @@ import org.springframework.data.repository.Repository; import in.koreatech.koin.domain.community.article.model.KoinArticle; +import org.springframework.data.repository.query.Param; public interface AdminKoinArticleRepository extends Repository { @Query(value = "SELECT * FROM new_koin_articles WHERE article_id = :noticeId", nativeQuery = true) - Optional findByArticleId(Integer noticeId); + Optional findByArticleId(@Param("noticeId") Integer noticeId); } diff --git a/src/main/java/in/koreatech/koin/admin/notice/repository/AdminKoreatechArticleRepository.java b/src/main/java/in/koreatech/koin/admin/notice/repository/AdminKoreatechArticleRepository.java index 27ca8ff66..b619caff8 100644 --- a/src/main/java/in/koreatech/koin/admin/notice/repository/AdminKoreatechArticleRepository.java +++ b/src/main/java/in/koreatech/koin/admin/notice/repository/AdminKoreatechArticleRepository.java @@ -6,9 +6,10 @@ import org.springframework.data.repository.Repository; import in.koreatech.koin.domain.community.article.model.KoreatechArticle; +import org.springframework.data.repository.query.Param; public interface AdminKoreatechArticleRepository extends Repository { @Query(value = "SELECT * FROM new_koreatech_articles WHERE article_id = :noticeId", nativeQuery = true) - Optional findByArticleId(Integer noticeId); + Optional findByArticleId(@Param("noticeId") Integer noticeId); } diff --git a/src/main/java/in/koreatech/koin/admin/shop/dto/AdminCreateMenuRequest.java b/src/main/java/in/koreatech/koin/admin/shop/dto/AdminCreateMenuRequest.java index 525526686..65baa32be 100644 --- a/src/main/java/in/koreatech/koin/admin/shop/dto/AdminCreateMenuRequest.java +++ b/src/main/java/in/koreatech/koin/admin/shop/dto/AdminCreateMenuRequest.java @@ -4,11 +4,9 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; -import java.util.List; - import com.fasterxml.jackson.databind.annotation.JsonNaming; - import in.koreatech.koin.domain.shop.model.menu.Menu; +import in.koreatech.koin.domain.shop.model.shop.Shop; import in.koreatech.koin.global.validation.NotBlankElement; import in.koreatech.koin.global.validation.SingleMenuPrice; import in.koreatech.koin.global.validation.UniqueId; @@ -18,6 +16,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PositiveOrZero; import jakarta.validation.constraints.Size; +import java.util.List; @JsonNaming(value = SnakeCaseStrategy.class) @SingleMenuPrice @@ -58,10 +57,10 @@ public record AdminCreateMenuRequest( Integer singlePrice ) { - public Menu toEntity(Integer shopId) { + public Menu toEntity(Shop shop) { return Menu.builder() .name(name) - .shopId(shopId) + .shop(shop) .description(description) .build(); } diff --git a/src/main/java/in/koreatech/koin/admin/shop/dto/AdminMenuDetailResponse.java b/src/main/java/in/koreatech/koin/admin/shop/dto/AdminMenuDetailResponse.java index b003b9597..f15854e08 100644 --- a/src/main/java/in/koreatech/koin/admin/shop/dto/AdminMenuDetailResponse.java +++ b/src/main/java/in/koreatech/koin/admin/shop/dto/AdminMenuDetailResponse.java @@ -58,7 +58,7 @@ public static AdminMenuDetailResponse createForSingleOption(Menu menu, List searchByName(String searchKeyword); + List searchByName(@Param("searchKeyword") String searchKeyword); } diff --git a/src/main/java/in/koreatech/koin/admin/shop/service/AdminShopService.java b/src/main/java/in/koreatech/koin/admin/shop/service/AdminShopService.java index 359d3eb50..fa324dd89 100644 --- a/src/main/java/in/koreatech/koin/admin/shop/service/AdminShopService.java +++ b/src/main/java/in/koreatech/koin/admin/shop/service/AdminShopService.java @@ -165,8 +165,8 @@ public void createShopCategory(AdminCreateShopCategoryRequest adminCreateShopCat @Transactional public void createMenu(Integer shopId, AdminCreateMenuRequest adminCreateMenuRequest) { - adminShopRepository.getById(shopId); - Menu menu = adminCreateMenuRequest.toEntity(shopId); + Shop shop = adminShopRepository.getById(shopId); + Menu menu = adminCreateMenuRequest.toEntity(shop); Menu savedMenu = adminMenuRepository.save(menu); for (Integer categoryId : adminCreateMenuRequest.categoryIds()) { MenuCategory menuCategory = adminMenuCategoryRepository.getById(categoryId); @@ -311,7 +311,7 @@ public void deleteMenuCategory(Integer shopId, Integer categoryId) { @Transactional public void deleteMenu(Integer shopId, Integer menuId) { Menu menu = adminMenuRepository.getById(menuId); - if (!Objects.equals(menu.getShopId(), shopId)) { + if (!Objects.equals(menu.getShop().getId(), shopId)) { throw new KoinIllegalArgumentException("해당 상점의 카테고리가 아닙니다."); } adminMenuRepository.deleteById(menuId); diff --git a/src/main/java/in/koreatech/koin/admin/user/repository/AdminOwnerRepository.java b/src/main/java/in/koreatech/koin/admin/user/repository/AdminOwnerRepository.java index 1ebc65c9c..7b4ae2209 100644 --- a/src/main/java/in/koreatech/koin/admin/user/repository/AdminOwnerRepository.java +++ b/src/main/java/in/koreatech/koin/admin/user/repository/AdminOwnerRepository.java @@ -10,7 +10,7 @@ import in.koreatech.koin.domain.owner.exception.OwnerNotFoundException; import in.koreatech.koin.domain.owner.model.Owner; import in.koreatech.koin.domain.user.model.UserType; -import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.repository.query.Param; public interface AdminOwnerRepository extends Repository { diff --git a/src/main/java/in/koreatech/koin/admin/user/repository/AdminUserRepository.java b/src/main/java/in/koreatech/koin/admin/user/repository/AdminUserRepository.java index cb2941451..b11470156 100644 --- a/src/main/java/in/koreatech/koin/admin/user/repository/AdminUserRepository.java +++ b/src/main/java/in/koreatech/koin/admin/user/repository/AdminUserRepository.java @@ -8,12 +8,13 @@ import in.koreatech.koin.domain.user.exception.UserNotFoundException; import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.domain.user.model.UserType; +import org.springframework.data.repository.query.Param; public interface AdminUserRepository extends Repository { User save(User user); - Optional findByEmail(String Email); + Optional findByEmail(String email); Optional findById(Integer id); @@ -22,7 +23,9 @@ SELECT COUNT(u) FROM User u WHERE u.userType = :userType AND u.isAuthed = :isAuthed """) - Integer findUsersCountByUserTypeAndIsAuthed(UserType userType, Boolean isAuthed); + Integer findUsersCountByUserTypeAndIsAuthed( + @Param("userType") UserType userType, + @Param("isAuthed") Boolean isAuthed); default User getByEmail(String email) { return findByEmail(email) diff --git a/src/main/java/in/koreatech/koin/domain/ownershop/service/OwnerShopService.java b/src/main/java/in/koreatech/koin/domain/ownershop/service/OwnerShopService.java index d4715f18a..d96c59240 100644 --- a/src/main/java/in/koreatech/koin/domain/ownershop/service/OwnerShopService.java +++ b/src/main/java/in/koreatech/koin/domain/ownershop/service/OwnerShopService.java @@ -142,7 +142,7 @@ private Shop getOwnerShopById(Integer shopId, Integer ownerId) { public MenuDetailResponse getMenuByMenuId(Integer ownerId, Integer menuId) { Menu menu = menuRepository.getById(menuId); - getOwnerShopById(menu.getShopId(), ownerId); + getOwnerShopById(menu.getShop().getId(), ownerId); List menuCategories = menu.getMenuCategoryMaps() .stream() .map(MenuCategoryMap::getMenuCategory) @@ -166,7 +166,7 @@ public MenuCategoriesResponse getCategories(Integer shopId, Integer ownerId) { @Transactional public void deleteMenuByMenuId(Integer ownerId, Integer menuId) { Menu menu = menuRepository.getById(menuId); - getOwnerShopById(menu.getShopId(), ownerId); + getOwnerShopById(menu.getShop().getId(), ownerId); menuRepository.deleteById(menuId); } @@ -179,8 +179,8 @@ public void deleteCategory(Integer ownerId, Integer categoryId) { @Transactional public void createMenu(Integer shopId, Integer ownerId, CreateMenuRequest createMenuRequest) { - getOwnerShopById(shopId, ownerId); - Menu menu = createMenuRequest.toEntity(shopId); + Shop shop = getOwnerShopById(shopId, ownerId); + Menu menu = createMenuRequest.toEntity(shop); Menu savedMenu = menuRepository.save(menu); for (Integer categoryId : createMenuRequest.categoryIds()) { MenuCategory menuCategory = menuCategoryRepository.getById(categoryId); @@ -229,7 +229,7 @@ public void createMenuCategory(Integer shopId, Integer ownerId, CreateCategoryRe @Transactional public void modifyMenu(Integer ownerId, Integer menuId, ModifyMenuRequest modifyMenuRequest) { Menu menu = menuRepository.getById(menuId); - getOwnerShopById(menu.getShopId(), ownerId); + getOwnerShopById(menu.getShop().getId(), ownerId); menu.modifyMenu( modifyMenuRequest.name(), modifyMenuRequest.description() diff --git a/src/main/java/in/koreatech/koin/domain/shop/cache/ShopRedisRepository.java b/src/main/java/in/koreatech/koin/domain/shop/cache/ShopRedisRepository.java new file mode 100644 index 000000000..61a2a846a --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/shop/cache/ShopRedisRepository.java @@ -0,0 +1,32 @@ +package in.koreatech.koin.domain.shop.cache; + +import com.fasterxml.jackson.databind.ObjectMapper; +import in.koreatech.koin.domain.shop.cache.dto.ShopsCache; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Getter +@RequiredArgsConstructor +@Repository +public class ShopRedisRepository { + + private static final String KEY = "Shops"; + + private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + public void save(ShopsCache shopsCache) { + redisTemplate.opsForValue().set(KEY, shopsCache); + } + + public ShopsCache getShopsResponseByRedis() { + Object data = redisTemplate.opsForValue().get(KEY); + return objectMapper.convertValue(data, ShopsCache.class); + } + + public Boolean isCacheAvailable() { + return redisTemplate.hasKey(KEY); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/shop/cache/ShopsCacheService.java b/src/main/java/in/koreatech/koin/domain/shop/cache/ShopsCacheService.java new file mode 100644 index 000000000..9a65d05c2 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/shop/cache/ShopsCacheService.java @@ -0,0 +1,29 @@ +package in.koreatech.koin.domain.shop.cache; + +import in.koreatech.koin.domain.shop.cache.dto.ShopsCache; +import in.koreatech.koin.domain.shop.repository.shop.ShopRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class ShopsCacheService { + + private final ShopRepository shopRepository; + private final ShopRedisRepository shopRedisRepository; + + public ShopsCache findAllShopCache() { + if (shopRedisRepository.isCacheAvailable()) { + return shopRedisRepository.getShopsResponseByRedis(); + } + return refreshShopsCache(); + } + + public ShopsCache refreshShopsCache() { + ShopsCache shopsCache = ShopsCache.from(shopRepository.findAll()); + shopRedisRepository.save(shopsCache); + return shopsCache; + } +} diff --git a/src/main/java/in/koreatech/koin/domain/shop/cache/dto/EventArticleCache.java b/src/main/java/in/koreatech/koin/domain/shop/cache/dto/EventArticleCache.java new file mode 100644 index 000000000..e68bb61af --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/shop/cache/dto/EventArticleCache.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.domain.shop.cache.dto; + +import in.koreatech.koin.domain.shop.model.article.EventArticle; +import in.koreatech.koin.domain.shop.model.article.EventArticleImage; +import java.util.List; + +public record EventArticleCache( + String title, + List thumbnailImages +) { + + public static EventArticleCache from(EventArticle eventArticle) { + return new EventArticleCache( + eventArticle.getTitle(), + eventArticle.getThumbnailImages().stream() + .map(EventArticleImage::getThumbnailImage) + .toList() + ); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopCache.java b/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopCache.java new file mode 100644 index 000000000..be5d057b6 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopCache.java @@ -0,0 +1,97 @@ +package in.koreatech.koin.domain.shop.cache.dto; + +import in.koreatech.koin.domain.shop.model.menu.Menu; +import in.koreatech.koin.domain.shop.model.shop.Shop; +import in.koreatech.koin.domain.shop.model.shop.ShopImage; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.TextStyle; +import java.util.List; +import java.util.Locale; + +public record ShopCache( + Integer id, + String name, + String internalName, + String chosung, + String phone, + String address, + String description, + Boolean delivery, + Integer deliveryPrice, + boolean payCard, + boolean payBank, + boolean isDeleted, + boolean isEvent, + String remarks, + Integer hit, + List shopCategories, + List shopOpens, + List shopImages, + List eventArticles, + List reviews, + List menuNames, + String bank, + String accountNumber +) { + + public static ShopCache from( + Shop shop + ) { + return new ShopCache( + shop.getId(), + shop.getName(), + shop.getInternalName(), + shop.getChosung(), + shop.getPhone(), + shop.getAddress(), + shop.getDescription(), + shop.getDelivery(), + shop.getDeliveryPrice(), + shop.isPayCard(), + shop.isPayBank(), + shop.isDeleted(), + shop.isEvent(), + shop.getRemarks(), + shop.getHit(), + shop.getShopCategories().stream().map(ShopCategoryCache::from).toList(), + shop.getShopOpens().stream().map(ShopOpenCache::from).toList(), + shop.getShopImages().stream().map(ShopImage::getImageUrl).toList(), + shop.getEventArticles().stream().map(EventArticleCache::from).toList(), + shop.getReviews().stream().map(ShopReviewCache::from).toList(), + shop.getMenus().stream().map(Menu::getName).toList(), + shop.getBank(), + shop.getAccountNumber() + ); + } + + public boolean isOpen(LocalDateTime now) { + String currDayOfWeek = now.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.US).toUpperCase(); + String prevDayOfWeek = now.minusDays(1).getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.US).toUpperCase(); + for (ShopOpenCache shopOpen : shopOpens) { + if (shopOpen.closed()) { + continue; + } + if (shopOpen.dayOfWeek().equals(currDayOfWeek) && + isBetweenDate(now, shopOpen, now.toLocalDate()) + ) { + return true; + } + if (shopOpen.dayOfWeek().equals(prevDayOfWeek) && + isBetweenDate(now, shopOpen, now.minusDays(1).toLocalDate()) + ) { + return true; + } + } + return false; + } + + private boolean isBetweenDate(LocalDateTime now, ShopOpenCache shopOpen, LocalDate criteriaDate) { + LocalDateTime start = LocalDateTime.of(criteriaDate, shopOpen.openTime()); + LocalDateTime end = LocalDateTime.of(criteriaDate, shopOpen.closeTime()); + if (!shopOpen.closeTime().isAfter(shopOpen.openTime())) { + end = end.plusDays(1); + } + return !start.isAfter(now) && !end.isBefore(now); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopCategoryCache.java b/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopCategoryCache.java new file mode 100644 index 000000000..bf06b9d5d --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopCategoryCache.java @@ -0,0 +1,18 @@ +package in.koreatech.koin.domain.shop.cache.dto; + +import in.koreatech.koin.domain.shop.model.shop.ShopCategoryMap; + +public record ShopCategoryCache( + Integer id, + String name, + String imageUrl +) { + + public static ShopCategoryCache from(ShopCategoryMap shopCategoryMap) { + return new ShopCategoryCache( + shopCategoryMap.getId(), + shopCategoryMap.getShopCategory().getName(), + shopCategoryMap.getShopCategory().getImageUrl() + ); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopOpenCache.java b/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopOpenCache.java new file mode 100644 index 000000000..e613889b8 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopOpenCache.java @@ -0,0 +1,21 @@ +package in.koreatech.koin.domain.shop.cache.dto; + +import in.koreatech.koin.domain.shop.model.shop.ShopOpen; +import java.time.LocalTime; + +public record ShopOpenCache( + String dayOfWeek, + boolean closed, + LocalTime openTime, + LocalTime closeTime +) { + + public static ShopOpenCache from(ShopOpen shopOpen) { + return new ShopOpenCache( + shopOpen.getDayOfWeek(), + shopOpen.isClosed(), + shopOpen.getOpenTime(), + shopOpen.getCloseTime() + ); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopReviewCache.java b/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopReviewCache.java new file mode 100644 index 000000000..e55489b7f --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopReviewCache.java @@ -0,0 +1,24 @@ +package in.koreatech.koin.domain.shop.cache.dto; + +import in.koreatech.koin.domain.shop.model.review.ShopReview; +import in.koreatech.koin.domain.shop.model.review.ShopReviewImage; +import java.util.List; + +public record ShopReviewCache( + String content, + Integer rating, + Integer reviewerId, + List images, + List reports +) { + + public static ShopReviewCache from(ShopReview shopReview) { + return new ShopReviewCache( + shopReview.getContent(), + shopReview.getRating(), + shopReview.getReviewer().getId(), + shopReview.getImages().stream().map(ShopReviewImage::getImageUrls).toList(), + shopReview.getReports().stream().map(ShopReviewReportCache::from).toList() + ); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopReviewReportCache.java b/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopReviewReportCache.java new file mode 100644 index 000000000..af13df42a --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopReviewReportCache.java @@ -0,0 +1,21 @@ +package in.koreatech.koin.domain.shop.cache.dto; + +import in.koreatech.koin.domain.shop.model.review.ReportStatus; +import in.koreatech.koin.domain.shop.model.review.ShopReviewReport; + +public record ShopReviewReportCache( + String title, + String content, + ReportStatus reportStatus, + Integer userId +) { + + public static ShopReviewReportCache from(ShopReviewReport shopReviewReport) { + return new ShopReviewReportCache( + shopReviewReport.getTitle(), + shopReviewReport.getContent(), + shopReviewReport.getReportStatus(), + shopReviewReport.getUserId().getId() + ); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopsCache.java b/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopsCache.java new file mode 100644 index 000000000..bf4712ed7 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/shop/cache/dto/ShopsCache.java @@ -0,0 +1,13 @@ +package in.koreatech.koin.domain.shop.cache.dto; + +import in.koreatech.koin.domain.shop.model.shop.Shop; +import java.util.List; + +public record ShopsCache( + List shopCaches +) { + + public static ShopsCache from(List shops) { + return new ShopsCache(shops.stream().map(ShopCache::from).toList()); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopApi.java b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopApi.java index b6e4f336e..a2a39b54a 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopApi.java +++ b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopApi.java @@ -3,6 +3,7 @@ import static in.koreatech.koin.domain.user.model.UserType.STUDENT; import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH; +import in.koreatech.koin.domain.shop.dto.search.RelatedKeyword; import java.util.List; import org.springframework.http.ResponseEntity; @@ -283,7 +284,36 @@ ResponseEntity reportReview( @GetMapping("/v2/shops") ResponseEntity getShopsV2( @RequestParam(name = "sorter", defaultValue = "NONE") ShopsSortCriteria sortBy, - @RequestParam(name = "filter") List shopsFilterCriterias + @RequestParam(name = "filter") List shopsFilterCriterias, + @RequestParam(name = "query", required = false) String query + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation( + summary = "주변상점 검색어에 따른 연관검색어 조회", + description = """ + ### 검색어와 유사한 이름의 음식을 조회 + - 검색어와 관련된 음식의 이름을 조회합니다. ex) 김 → 김치찌개, 김치짜글이 + - 이때 shopIds 필드는 해당 음식과 관련된 상점의 ID를 반환합니다.(관련된 = 해당 음식 이름의 메뉴를 판매중 or 상점에 해당 음식 이름이 포함) + - 이 경우 음식과 관련된 것으로 조회하는 것이기 때문에 shopId는 null입니다. + ### 검색어와 유사한 이름의 상점명을 가진 상점 조회 + - 검색어와 유사한 상점 이름이 조회됩니다. ex) 즐 → 즐겨먹기 + - 이 경우 shopIds는 빈 배열, shopId는 해당 상점의 ID를 값으로 가집니다. + + **shopId가 null인 경우: 연관검색어가 음식 이름인 경우** + **shopId가 이 아닌 경우: 연관검색어가 상점 이름인 경우** + """ + ) + @GetMapping("/shops/search/related/{query}") + ResponseEntity getRelatedKeyword( + @PathVariable(name = "query") String query ); @ApiResponses( diff --git a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopController.java b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopController.java index 363a07b01..940df7def 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/controller/ShopController.java +++ b/src/main/java/in/koreatech/koin/domain/shop/controller/ShopController.java @@ -3,45 +3,45 @@ import static in.koreatech.koin.domain.user.model.UserType.STUDENT; import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH; -import java.util.Collections; -import java.util.List; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import in.koreatech.koin.domain.shop.dto.review.CreateReviewRequest; import in.koreatech.koin.domain.shop.dto.menu.MenuCategoriesResponse; import in.koreatech.koin.domain.shop.dto.menu.MenuDetailResponse; +import in.koreatech.koin.domain.shop.dto.menu.ShopMenuResponse; +import in.koreatech.koin.domain.shop.dto.review.CreateReviewRequest; import in.koreatech.koin.domain.shop.dto.review.ModifyReviewRequest; import in.koreatech.koin.domain.shop.dto.review.ReviewsSortCriteria; -import in.koreatech.koin.domain.shop.dto.shop.ShopCategoriesResponse; -import in.koreatech.koin.domain.shop.dto.shop.ShopEventsResponse; -import in.koreatech.koin.domain.shop.dto.menu.ShopMenuResponse; import in.koreatech.koin.domain.shop.dto.review.ShopMyReviewsResponse; -import in.koreatech.koin.domain.shop.dto.shop.ShopResponse; import in.koreatech.koin.domain.shop.dto.review.ShopReviewReportCategoryResponse; import in.koreatech.koin.domain.shop.dto.review.ShopReviewReportRequest; import in.koreatech.koin.domain.shop.dto.review.ShopReviewResponse; import in.koreatech.koin.domain.shop.dto.review.ShopReviewsResponse; +import in.koreatech.koin.domain.shop.dto.search.RelatedKeyword; +import in.koreatech.koin.domain.shop.dto.shop.ShopCategoriesResponse; +import in.koreatech.koin.domain.shop.dto.shop.ShopEventsResponse; +import in.koreatech.koin.domain.shop.dto.shop.ShopResponse; import in.koreatech.koin.domain.shop.dto.shop.ShopsFilterCriteria; import in.koreatech.koin.domain.shop.dto.shop.ShopsResponse; import in.koreatech.koin.domain.shop.dto.shop.ShopsResponseV2; import in.koreatech.koin.domain.shop.dto.shop.ShopsSortCriteria; +import in.koreatech.koin.domain.shop.service.SearchService; import in.koreatech.koin.domain.shop.service.ShopReviewService; import in.koreatech.koin.domain.shop.service.ShopService; import in.koreatech.koin.global.auth.Auth; import in.koreatech.koin.global.auth.UserId; import io.swagger.v3.oas.annotations.Parameter; import jakarta.validation.Valid; +import java.util.Collections; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor @@ -49,6 +49,7 @@ public class ShopController implements ShopApi { private final ShopService shopService; private final ShopReviewService shopReviewService; + private final SearchService searchService; @GetMapping("/shops/{shopId}/menus/{menuId}") public ResponseEntity findMenu( @@ -183,12 +184,13 @@ public ResponseEntity reportReview( @GetMapping("/v2/shops") public ResponseEntity getShopsV2( @RequestParam(name = "sorter", defaultValue = "NONE") ShopsSortCriteria sortBy, - @RequestParam(name = "filter", required = false) List shopsFilterCriterias + @RequestParam(name = "filter", required = false) List shopsFilterCriterias, + @RequestParam(name = "query", required = false, defaultValue = "") String query ) { if (shopsFilterCriterias == null) { shopsFilterCriterias = Collections.emptyList(); } - var shops = shopService.getShopsV2(sortBy, shopsFilterCriterias); + var shops = shopService.getShopsV2(sortBy, shopsFilterCriterias, query); return ResponseEntity.ok(shops); } @@ -201,6 +203,13 @@ public ResponseEntity getReview( return ResponseEntity.ok(shopReviewResponse); } + @GetMapping("/shops/search/related/{query}") + public ResponseEntity getRelatedKeyword( + @PathVariable(name = "query") String query + ) { + return ResponseEntity.ok(searchService.getRelatedKeywordByQuery(query)); + } + @PostMapping("/shops/{shopId}/call-notification") public ResponseEntity createCallNotification( @Parameter(in = PATH) @PathVariable("shopId") Integer shopId, diff --git a/src/main/java/in/koreatech/koin/domain/shop/dto/menu/CreateMenuRequest.java b/src/main/java/in/koreatech/koin/domain/shop/dto/menu/CreateMenuRequest.java index 16a5ba40f..169062e1e 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/dto/menu/CreateMenuRequest.java +++ b/src/main/java/in/koreatech/koin/domain/shop/dto/menu/CreateMenuRequest.java @@ -4,11 +4,9 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; -import java.util.List; - import com.fasterxml.jackson.databind.annotation.JsonNaming; - import in.koreatech.koin.domain.shop.model.menu.Menu; +import in.koreatech.koin.domain.shop.model.shop.Shop; import in.koreatech.koin.global.validation.NotBlankElement; import in.koreatech.koin.global.validation.SingleMenuPrice; import in.koreatech.koin.global.validation.UniqueId; @@ -18,6 +16,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PositiveOrZero; import jakarta.validation.constraints.Size; +import java.util.List; @JsonNaming(value = SnakeCaseStrategy.class) @SingleMenuPrice @@ -59,10 +58,10 @@ public record CreateMenuRequest( Integer singlePrice ) { - public Menu toEntity(Integer shopId) { + public Menu toEntity(Shop shop) { return Menu.builder() .name(name) - .shopId(shopId) + .shop(shop) .description(description) .build(); } diff --git a/src/main/java/in/koreatech/koin/domain/shop/dto/menu/MenuDetailResponse.java b/src/main/java/in/koreatech/koin/domain/shop/dto/menu/MenuDetailResponse.java index a5ddf85a2..8a8353abf 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/dto/menu/MenuDetailResponse.java +++ b/src/main/java/in/koreatech/koin/domain/shop/dto/menu/MenuDetailResponse.java @@ -58,7 +58,7 @@ public static MenuDetailResponse createForSingleOption(Menu menu, List keywords +) { + public static RelatedKeyword of(List menuKeywords, List shopKeywords) { + Set keywords = new TreeSet<>(shopKeywords); + keywords.addAll(menuKeywords); + return new RelatedKeyword( + keywords.stream() + .filter(innerKeyword -> !innerKeyword.shopIds.isEmpty() || innerKeyword.shopId != null) + .limit(5) + .collect(Collectors.toCollection(TreeSet::new)) + ); + } + + @JsonNaming(value = SnakeCaseStrategy.class) + public record InnerKeyword( + String keyword, + List shopIds, + Integer shopId + ) implements Comparable { + + @Override + public int compareTo(InnerKeyword other) { + if (this.shopId != null && other.shopId == null) { + return -1; + } + if (this.shopId == null && other.shopId != null) { + return 1; + } + return this.keyword.compareTo(other.keyword); + } + } +} diff --git a/src/main/java/in/koreatech/koin/domain/shop/dto/shop/ShopsResponseV2.java b/src/main/java/in/koreatech/koin/domain/shop/dto/shop/ShopsResponseV2.java index 9b13162ea..d1b89e205 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/dto/shop/ShopsResponseV2.java +++ b/src/main/java/in/koreatech/koin/domain/shop/dto/shop/ShopsResponseV2.java @@ -4,117 +4,124 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import in.koreatech.koin.domain.shop.cache.dto.ShopCache; +import in.koreatech.koin.domain.shop.cache.dto.ShopCategoryCache; +import in.koreatech.koin.domain.shop.repository.shop.dto.ShopInfoV2; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; import java.util.Comparator; import java.util.List; import java.util.Map; - -import com.fasterxml.jackson.databind.annotation.JsonNaming; - -import in.koreatech.koin.domain.shop.model.shop.Shop; -import in.koreatech.koin.domain.shop.repository.shop.dto.ShopInfoV2; -import io.swagger.v3.oas.annotations.media.Schema; +import java.util.function.Predicate; @JsonNaming(value = SnakeCaseStrategy.class) public record ShopsResponseV2( - @Schema(example = "100", description = "상점 개수", requiredMode = REQUIRED) - Integer count, + @Schema(example = "100", description = "상점 개수", requiredMode = REQUIRED) + Integer count, - @Schema(description = "상점 정보") - List shops + @Schema(description = "상점 정보") + List shops ) { + private static Predicate queryPredicate(String query) { + String trimmedQuery = query.replaceAll(" ", ""); + return (shop -> + shop.name().contains(trimmedQuery) || shop.menuNames().stream().anyMatch(s -> s.contains(trimmedQuery)) + ); + } + public static ShopsResponseV2 from( - List shops, - Map shopInfoMap, - ShopsSortCriteria sortBy, - List shopsFilterCriterias, - LocalDateTime now + List shops, + Map shopInfoMap, + ShopsSortCriteria sortBy, + List shopsFilterCriterias, + LocalDateTime now, + String query ) { List innerShopResponses = shops.stream() - .map(it -> { - ShopInfoV2 shopInfo = shopInfoMap.get(it.getId()); - return InnerShopResponse.from( - it, - shopInfo.durationEvent(), - it.isOpen(now), - shopInfo.averageRate(), - shopInfo.reviewCount() - ); - }) - .filter(ShopsFilterCriteria.createCombinedFilter(shopsFilterCriterias)) - .sorted(InnerShopResponse.getComparator(sortBy)) - .toList(); + .filter(queryPredicate(query)) + .map(it -> { + ShopInfoV2 shopInfo = shopInfoMap.get(it.id()); + return InnerShopResponse.from( + it, + shopInfo.durationEvent(), + it.isOpen(now), + shopInfo.averageRate(), + shopInfo.reviewCount() + ); + }) + .filter(ShopsFilterCriteria.createCombinedFilter(shopsFilterCriterias)) + .sorted(InnerShopResponse.getComparator(sortBy)) + .toList(); return new ShopsResponseV2( - innerShopResponses.size(), - innerShopResponses + innerShopResponses.size(), + innerShopResponses ); } @JsonNaming(value = SnakeCaseStrategy.class) public record InnerShopResponse( - @Schema(example = "[1, 2, 3]", description = "속해있는 상점 카테고리들의 고유 id 리스트", requiredMode = NOT_REQUIRED) - List categoryIds, + @Schema(example = "[1, 2, 3]", description = "속해있는 상점 카테고리들의 고유 id 리스트", requiredMode = NOT_REQUIRED) + List categoryIds, - @Schema(example = "true", description = "배달 가능 여부", requiredMode = REQUIRED) - boolean delivery, + @Schema(example = "true", description = "배달 가능 여부", requiredMode = REQUIRED) + boolean delivery, - @Schema(example = "1", description = "고유 id", requiredMode = REQUIRED) - Integer id, + @Schema(example = "1", description = "고유 id", requiredMode = REQUIRED) + Integer id, - @Schema(example = "수신반점", description = "이름", requiredMode = REQUIRED) - String name, + @Schema(example = "수신반점", description = "이름", requiredMode = REQUIRED) + String name, - @Schema(example = "true", description = "계좌 이체 가능 여부", requiredMode = REQUIRED) - boolean payBank, + @Schema(example = "true", description = "계좌 이체 가능 여부", requiredMode = REQUIRED) + boolean payBank, - @Schema(example = "true", description = "카드 계산 가능 여부", requiredMode = REQUIRED) - boolean payCard, + @Schema(example = "true", description = "카드 계산 가능 여부", requiredMode = REQUIRED) + boolean payCard, - @Schema(example = "041-000-0000", description = "전화번호", requiredMode = NOT_REQUIRED) - String phone, + @Schema(example = "041-000-0000", description = "전화번호", requiredMode = NOT_REQUIRED) + String phone, - @Schema(example = "true", description = "삭제 여부", requiredMode = REQUIRED) - boolean isEvent, + @Schema(example = "true", description = "삭제 여부", requiredMode = REQUIRED) + boolean isEvent, - @Schema(example = "true", description = "운영중 여부", requiredMode = REQUIRED) - boolean isOpen, + @Schema(example = "true", description = "운영중 여부", requiredMode = REQUIRED) + boolean isOpen, - @Schema(example = "4.9", description = "평균 별점", requiredMode = REQUIRED) - double averageRate, + @Schema(example = "4.9", description = "평균 별점", requiredMode = REQUIRED) + double averageRate, - @Schema(example = "10", description = "리뷰 개수", requiredMode = REQUIRED) - long reviewCount + @Schema(example = "10", description = "리뷰 개수", requiredMode = REQUIRED) + long reviewCount ) { public static InnerShopResponse from( - Shop shop, - Boolean isEvent, - Boolean isOpen, - Double averageRate, - Long reviewCount + ShopCache shop, + Boolean isEvent, + Boolean isOpen, + Double averageRate, + Long reviewCount ) { return new InnerShopResponse( - shop.getShopCategories().stream().map(shopCategoryMap -> - shopCategoryMap.getShopCategory().getId() - ).toList(), - shop.getDelivery(), - shop.getId(), - shop.getName(), - shop.isPayBank(), - shop.isPayCard(), - shop.getPhone(), - isEvent, - isOpen, - averageRate, - reviewCount + shop.shopCategories().stream().map(ShopCategoryCache::id).toList(), + shop.delivery(), + shop.id(), + shop.name(), + shop.payBank(), + shop.payCard(), + shop.phone(), + isEvent, + isOpen, + averageRate, + reviewCount ); } public static Comparator getComparator(ShopsSortCriteria shopsSortCriteria) { return Comparator - .comparing(InnerShopResponse::isOpen, Comparator.reverseOrder()) - .thenComparing(shopsSortCriteria.getComparator()); + .comparing(InnerShopResponse::isOpen, Comparator.reverseOrder()) + .thenComparing(shopsSortCriteria.getComparator()); } } } diff --git a/src/main/java/in/koreatech/koin/domain/shop/model/menu/Menu.java b/src/main/java/in/koreatech/koin/domain/shop/model/menu/Menu.java index 8c1d180a6..7e02af3d3 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/model/menu/Menu.java +++ b/src/main/java/in/koreatech/koin/domain/shop/model/menu/Menu.java @@ -4,22 +4,25 @@ import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; -import java.util.ArrayList; -import java.util.List; - import in.koreatech.koin.admin.shop.dto.AdminModifyMenuRequest; import in.koreatech.koin.domain.shop.dto.menu.ModifyMenuRequest; import in.koreatech.koin.domain.shop.dto.menu.ModifyMenuRequest.InnerOptionPrice; +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.EntityManager; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import java.util.ArrayList; +import java.util.List; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -36,9 +39,9 @@ public class Menu extends BaseEntity { @GeneratedValue(strategy = IDENTITY) private Integer id; - @NotNull - @Column(name = "shop_id", nullable = false) - private Integer shopId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "shop_id", referencedColumnName = "id", nullable = false) + private Shop shop; @Size(max = 255) @NotNull @@ -64,11 +67,11 @@ public class Menu extends BaseEntity { @Builder private Menu( - Integer shopId, + Shop shop, String name, String description ) { - this.shopId = shopId; + this.shop = shop; this.name = name; this.description = description; } @@ -77,15 +80,6 @@ public boolean hasMultipleOption() { return menuOptions.size() > SINGLE_OPTION_COUNT; } - @Override - public String toString() { - return "Menu{" + - "id=" + id + - ", shopId=" + shopId + - ", name='" + name + '\'' + - '}'; - } - public void modifyMenu( String name, String description diff --git a/src/main/java/in/koreatech/koin/domain/shop/model/menu/MenuSearchKeyWord.java b/src/main/java/in/koreatech/koin/domain/shop/model/menu/MenuSearchKeyWord.java new file mode 100644 index 000000000..a783431f7 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/shop/model/menu/MenuSearchKeyWord.java @@ -0,0 +1,39 @@ +package in.koreatech.koin.domain.shop.model.menu; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import in.koreatech.koin.global.domain.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "shop_menu_search_keywords") +@NoArgsConstructor(access = PROTECTED) +public class MenuSearchKeyWord extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Integer id; + + @Size(max = 255) + @NotNull + @Column(name = "keyword", nullable = false) + private String keyword; + + @Builder + private MenuSearchKeyWord( + String keyword + ) { + this.keyword = keyword; + } +} diff --git a/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java b/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java index fea68847e..bb6531545 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java +++ b/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java @@ -7,6 +7,7 @@ import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; +import in.koreatech.koin.domain.shop.model.menu.Menu; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.TextStyle; @@ -122,6 +123,9 @@ public class Shop extends BaseEntity { @OneToMany(mappedBy = "shop", orphanRemoval = true, cascade = {PERSIST, REFRESH, MERGE, REMOVE}) private List shopImages = new ArrayList<>(); + @OneToMany(mappedBy = "shop", orphanRemoval = true, cascade = {PERSIST, REFRESH, MERGE, REMOVE}) + private List menus = new ArrayList<>(); + @OneToMany(mappedBy = "shop", orphanRemoval = true, cascade = {PERSIST, REFRESH, MERGE, REMOVE}) private List menuCategories = new ArrayList<>(); diff --git a/src/main/java/in/koreatech/koin/domain/shop/repository/menu/MenuRepository.java b/src/main/java/in/koreatech/koin/domain/shop/repository/menu/MenuRepository.java index 451f12724..7fd23645f 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/repository/menu/MenuRepository.java +++ b/src/main/java/in/koreatech/koin/domain/shop/repository/menu/MenuRepository.java @@ -1,13 +1,11 @@ package in.koreatech.koin.domain.shop.repository.menu; +import in.koreatech.koin.domain.shop.exception.MenuNotFoundException; +import in.koreatech.koin.domain.shop.model.menu.Menu; import java.util.List; import java.util.Optional; - import org.springframework.data.repository.Repository; -import in.koreatech.koin.domain.shop.exception.MenuNotFoundException; -import in.koreatech.koin.domain.shop.model.menu.Menu; - public interface MenuRepository extends Repository { Optional findById(Integer menuId); diff --git a/src/main/java/in/koreatech/koin/domain/shop/repository/menu/MenuSearchKeywordRepository.java b/src/main/java/in/koreatech/koin/domain/shop/repository/menu/MenuSearchKeywordRepository.java new file mode 100644 index 000000000..03c90b0a9 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/shop/repository/menu/MenuSearchKeywordRepository.java @@ -0,0 +1,15 @@ +package in.koreatech.koin.domain.shop.repository.menu; + +import in.koreatech.koin.domain.shop.model.menu.MenuSearchKeyWord; +import java.util.List; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; + +public interface MenuSearchKeywordRepository extends Repository { + + @Query("SELECT DISTINCT m.keyword FROM MenuSearchKeyWord m WHERE m.keyword LIKE %:query%") + List findDistinctNameContains(@Param("query") String query); + + MenuSearchKeyWord save(MenuSearchKeyWord menuSearchKeyWord); +} diff --git a/src/main/java/in/koreatech/koin/domain/shop/repository/shop/ShopRepository.java b/src/main/java/in/koreatech/koin/domain/shop/repository/shop/ShopRepository.java index 339a3fd1d..f581eaeff 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/repository/shop/ShopRepository.java +++ b/src/main/java/in/koreatech/koin/domain/shop/repository/shop/ShopRepository.java @@ -1,12 +1,12 @@ package in.koreatech.koin.domain.shop.repository.shop; +import in.koreatech.koin.domain.shop.exception.ShopNotFoundException; +import in.koreatech.koin.domain.shop.model.shop.Shop; import java.util.List; import java.util.Optional; - +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; - -import in.koreatech.koin.domain.shop.exception.ShopNotFoundException; -import in.koreatech.koin.domain.shop.model.shop.Shop; +import org.springframework.data.repository.query.Param; public interface ShopRepository extends Repository { @@ -29,4 +29,7 @@ default Shop getByOwnerId(Integer ownerId) { } List findAll(); + + @Query("SELECT s FROM Shop s WHERE s.name LIKE %:query%") + List findDistinctNameContains(@Param("query") String query); } diff --git a/src/main/java/in/koreatech/koin/domain/shop/service/SearchService.java b/src/main/java/in/koreatech/koin/domain/shop/service/SearchService.java new file mode 100644 index 000000000..2a81509ee --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/shop/service/SearchService.java @@ -0,0 +1,65 @@ +package in.koreatech.koin.domain.shop.service; + +import in.koreatech.koin.domain.shop.cache.ShopsCacheService; +import in.koreatech.koin.domain.shop.cache.dto.ShopCache; +import in.koreatech.koin.domain.shop.cache.dto.ShopsCache; +import in.koreatech.koin.domain.shop.dto.search.RelatedKeyword; +import in.koreatech.koin.domain.shop.dto.search.RelatedKeyword.InnerKeyword; +import in.koreatech.koin.domain.shop.repository.menu.MenuSearchKeywordRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class SearchService { + + private final MenuSearchKeywordRepository menuSearchKeywordRepository; + private final ShopsCacheService shopsCacheService; + + private static final String BLANK = " "; + private static final String EMPTY = ""; + + public RelatedKeyword getRelatedKeywordByQuery(String query) { + String trimmedQuery = removeBlank(query); + ShopsCache shops = shopsCacheService.findAllShopCache(); + List relatedMenuKeywords = findAllRelatedMenuKeyword(trimmedQuery, shops); + List relatedShopNames = findAllRelatedShopsKeyword(trimmedQuery, shops); + return RelatedKeyword.of(relatedMenuKeywords, relatedShopNames); + } + + private List findAllRelatedMenuKeyword(String query, ShopsCache shops) { + List menuKeywords = menuSearchKeywordRepository.findDistinctNameContains(query); + return menuKeywords.stream() + .map(keyword -> new InnerKeyword( + keyword, + relatedShopIds(keyword, shops), + null + )).toList(); + } + + private List relatedShopIds(String keyword, ShopsCache shops) { + return shops.shopCaches().stream() + .filter(shop -> + shop.menuNames().stream().anyMatch(menuName -> menuName.contains(keyword)) || + shop.name().contains(keyword)) + .map(ShopCache::id) + .toList(); + } + + private List findAllRelatedShopsKeyword(String query, ShopsCache shops) { + return shops.shopCaches().stream() + .filter(shop -> shop.name().contains(query)) + .map(shop -> new InnerKeyword( + shop.name(), + List.of(), + shop.id() + )).toList(); + } + + private String removeBlank(String query) { + return query.replaceAll(BLANK, EMPTY); + } +} 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 24feaaad6..458a4b6ac 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,16 +2,8 @@ import static in.koreatech.koin.global.domain.notification.model.NotificationSubscribeType.REVIEW_PROMPT; -import java.time.Clock; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - +import in.koreatech.koin.domain.shop.cache.ShopsCacheService; +import in.koreatech.koin.domain.shop.cache.dto.ShopsCache; import in.koreatech.koin.domain.shop.dto.menu.MenuCategoriesResponse; import in.koreatech.koin.domain.shop.dto.menu.MenuDetailResponse; import in.koreatech.koin.domain.shop.dto.menu.ShopMenuResponse; @@ -41,7 +33,15 @@ import in.koreatech.koin.domain.user.repository.UserRepository; import in.koreatech.koin.global.domain.notification.repository.NotificationSubscribeRepository; import in.koreatech.koin.global.exception.KoinIllegalArgumentException; +import java.time.Clock; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @Transactional(readOnly = true) @@ -54,6 +54,7 @@ public class ShopService { private final ShopRepository shopRepository; private final ShopCategoryRepository shopCategoryRepository; private final EventArticleRepository eventArticleRepository; + private final ShopsCacheService shopsCache; private final ShopCustomRepository shopCustomRepository; private final NotificationSubscribeRepository notificationSubscribeRepository; private final ShopNotificationBufferRepository shopNotificationBufferRepository; @@ -112,14 +113,25 @@ public ShopEventsResponse getAllEvents() { return ShopEventsResponse.of(shops, clock); } - public ShopsResponseV2 getShopsV2(ShopsSortCriteria sortBy, List shopsFilterCriterias) { + public ShopsResponseV2 getShopsV2( + ShopsSortCriteria sortBy, + List shopsFilterCriterias, + String query + ) { if (shopsFilterCriterias.contains(null)) { throw KoinIllegalArgumentException.withDetail("유효하지 않은 필터입니다."); } - List shops = shopRepository.findAll(); + ShopsCache shopCaches = shopsCache.findAllShopCache(); LocalDateTime now = LocalDateTime.now(clock); Map shopInfoMap = shopCustomRepository.findAllShopInfo(now); - return ShopsResponseV2.from(shops, shopInfoMap, sortBy, shopsFilterCriterias, now); + return ShopsResponseV2.from( + shopCaches.shopCaches(), + shopInfoMap, + sortBy, + shopsFilterCriterias, + now, + query + ); } @Transactional diff --git a/src/main/java/in/koreatech/koin/global/config/RedisConfig.java b/src/main/java/in/koreatech/koin/global/config/RedisConfig.java index 9c701cb02..8f58e0565 100644 --- a/src/main/java/in/koreatech/koin/global/config/RedisConfig.java +++ b/src/main/java/in/koreatech/koin/global/config/RedisConfig.java @@ -2,8 +2,9 @@ import static java.nio.charset.StandardCharsets.UTF_8; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import java.time.Duration; - import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -19,9 +20,6 @@ import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.web.client.RestTemplate; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - @Configuration @EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP) @Profile("!test") diff --git a/src/main/java/in/koreatech/koin/global/scheduler/CacheShopsScheduler.java b/src/main/java/in/koreatech/koin/global/scheduler/CacheShopsScheduler.java new file mode 100644 index 000000000..f3cc64e53 --- /dev/null +++ b/src/main/java/in/koreatech/koin/global/scheduler/CacheShopsScheduler.java @@ -0,0 +1,17 @@ +package in.koreatech.koin.global.scheduler; + +import in.koreatech.koin.domain.shop.cache.ShopsCacheService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class CacheShopsScheduler { + private final ShopsCacheService shopsCacheService; + + @Scheduled(fixedRate = 30000) + public void refreshCache() { + shopsCacheService.refreshShopsCache(); + } +} diff --git a/src/main/resources/db/migration/V93__add_menu_search_keyword_table.sql b/src/main/resources/db/migration/V93__add_menu_search_keyword_table.sql new file mode 100644 index 000000000..058eb5607 --- /dev/null +++ b/src/main/resources/db/migration/V93__add_menu_search_keyword_table.sql @@ -0,0 +1,8 @@ +CREATE TABLE if not exists `shop_menu_search_keywords` +( + id INT UNSIGNED AUTO_INCREMENT NOT NULL, + keyword VARCHAR(255) NOT NULL, + created_at timestamp default CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp default CURRENT_TIMESTAMP NOT NULL on update CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) + ); diff --git a/src/main/resources/db/migration/V94__add_menu_search_keyword_table.sql b/src/main/resources/db/migration/V94__add_menu_search_keyword_table.sql new file mode 100644 index 000000000..058eb5607 --- /dev/null +++ b/src/main/resources/db/migration/V94__add_menu_search_keyword_table.sql @@ -0,0 +1,8 @@ +CREATE TABLE if not exists `shop_menu_search_keywords` +( + id INT UNSIGNED AUTO_INCREMENT NOT NULL, + keyword VARCHAR(255) NOT NULL, + created_at timestamp default CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp default CURRENT_TIMESTAMP NOT NULL on update CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) + ); diff --git a/src/main/resources/db/migration/V95__insert_initial_menu_keywords.sql b/src/main/resources/db/migration/V95__insert_initial_menu_keywords.sql new file mode 100644 index 000000000..46b568b2b --- /dev/null +++ b/src/main/resources/db/migration/V95__insert_initial_menu_keywords.sql @@ -0,0 +1,1174 @@ +INSERT INTO shop_menu_search_keywords (keyword) +VALUES ('흑마늘족발'), + ('허니마늘족발'), + ('바베큐마늘족발'), + ('직화불족발'), + ('보쌈'), + ('가족의족보'), + ('반반족발'), + ('쟁반국수'), + ('냉채족발'), + ('얼큰술국'), + ('보불셋트'), + ('오리엔탈마늘샐러드'), + ('가마보꼬모듬오뎅탕'), + ('떡볶이'), + ('모둠튀김'), + ('순대'), + ('어묵'), + ('볶음밥'), + ('돈까스'), + ('감성떡볶이'), + ('치즈떡볶이'), + ('매운크림떡볶이'), + ('감성김밥'), + ('참치김밥'), + ('새우튀김김밥'), + ('김치돈까스김밥'), + ('참치마요컵밥'), + ('스팸김치컵밥'), + ('감성돈까스'), + ('치즈돈까스'), + ('새우튀김'), + ('오징어튀김'), + ('고구마튀김'), + ('김말이'), + ('치즈스틱'), + ('치즈볼'), + ('어묵튀김'), + ('목살볶음밥'), + ('김치목살볶음밥'), + ('새우볶음밥'), + ('불닭볶음밥'), + ('콜라/사이다'), + ('쿨피스'), + ('뼈해장국'), + ('닭볶음탕'), + ('고추장불고기'), + ('닭갈비'), + ('김치찌개'), + ('간장불고기'), + ('알탕'), + ('알밥'), + ('순두부찌개'), + ('낙지볶음덮밥'), + ('원조김밥'), + ('계란말이김밥'), + ('김치김밥'), + ('치즈김밥'), + ('소고기김밥'), + ('매운참치김밥'), + ('오뎅'), + ('김치만두'), + ('고기만두'), + ('군만두'), + ('물만두'), + ('갈비만두'), + ('매콤만두'), + ('새우만두'), + ('김말이튀김'), + ('우동'), + ('라면'), + ('계란라면'), + ('떡라면'), + ('김치라면'), + ('치즈라면'), + ('만두라면'), + ('짬뽕라면'), + ('짬뽕우동'), + ('라볶이'), + ('치즈라볶이'), + ('모듬떡볶이'), + ('모듬라볶이'), + ('잔치국수'), + ('수제비'), + ('얼큰수제비'), + ('항아리수제비'), + ('항아리칼국수'), + ('항아리칼제비'), + ('칼국수'), + ('얼큰칼국수'), + ('쫄면'), + ('떡만두국'), + ('만두국'), + ('떡국'), + ('비빔밥'), + ('참치비빔밥'), + ('양푼비빔밥'), + ('돌솥비빔밥'), + ('치즈돌솥비빔밥'), + ('김치알밥'), + ('치즈김치알밥'), + ('육개장'), + ('내장탕'), + ('갈비탕'), + ('올갱이해장국'), + ('스페셜정식'), + ('철판햄버그스테이크'), + ('고구마돈까스'), + ('매운돈까스'), + ('생선까스'), + ('김치덮밥'), + ('오징어덮밥'), + ('제육덮밥'), + ('참치덮밥'), + ('뚝배기불고기'), + ('철판낙지볶음'), + ('철판오삼불고기'), + ('꽁치김치조림'), + ('참치김치찌개'), + ('된장찌개'), + ('부대찌개'), + ('치즈부대찌개'), + ('고등어김치조림'), + ('오므라이스'), + ('치즈오므라이스'), + ('카레라이스'), + ('물냉면'), + ('비빔냉면'), + ('막국수'), + ('비빔막국수'), + ('열무국수'), + ('열무냉면'), + ('순살돈까스'), + ('토마토스파게티'), + ('크림스파게티'), + ('맞춤도시락'), + ('즉석떡볶이'), + ('병맥주'), + ('소주'), + ('음료수'), + ('후라이드'), + ('새우버거'), + ('치킨버거'), + ('불고기버거'), + ('양념치킨'), + ('간장치킨'), + ('스노윙치즈'), + ('핫블링'), + ('크리미언'), + ('소이갈릭'), + ('쇼킹핫'), + ('매콤치즈스노윙'), + ('포테이토짜용치킨'), + ('허니갈릭'), + ('오리엔탈파닭'), + ('파닭발'), + ('순살치킨'), + ('닭다리'), + ('닭날개/윙봉'), + ('스노윙치킨'), + ('마늘치킨'), + ('후닭'), + ('핫후닭'), + ('네네볼'), + ('네네스위틱'), + ('감자튀김'), + ('짜용소스'), + ('뚝배기뼈해장국'), + ('김치짜글이'), + ('노걸대왕갈비탕'), + ('콩나물해장국'), + ('황태해장국'), + ('얼큰이내장탕'), + ('막걸리'), + ('맥주'), + ('복분자'), + ('청하'), + ('삼겹살'), + ('막창'), + ('순살간장조림닭볶음'), + ('순살매콤닭볶음'), + ('순살된장닭볶음'), + ('순살닭도리탕'), + ('닭똥집볶음'), + ('닭치찌개'), + ('음료수 큰거'), + ('공기밥'), + ('해물칼국수'), + ('버섯육개장'), + ('찐만두'), + ('콩국수'), + ('냉면'), + ('손짜장'), + ('간짜장'), + ('손우동'), + ('손짬뽕'), + ('불짬뽕'), + ('굴짬뽕'), + ('대왕성특면'), + ('낙지짬뽕'), + ('해물삼선짬뽕'), + ('해물쟁반짜장'), + ('해물쟁반짬뽕'), + ('해물삼선간짜장'), + ('짜장밥'), + ('짬뽕밥'), + ('잡채밥'), + ('삼선볶음밥'), + ('계살볶음밥'), + ('대왕성특밥'), + ('송이덮밥'), + ('잡탕밥'), + ('유산슬밥'), + ('탕수육'), + ('사천탕수육'), + ('고기잡채'), + ('양장피'), + ('난자완스'), + ('깐풍육'), + ('깐풍기'), + ('깐쇼새우'), + ('유산슬'), + ('짬짜면'), + ('볶짜면'), + ('볶짬면'), + ('탕짜면'), + ('탕짬면'), + ('탕볶밥'), + ('물막국수'), + ('이탤리노치즈피자'), + ('콤비네이션피자'), + ('양송이피자'), + ('불고기피자'), + ('웨지감자피자'), + ('웨지고구마피자'), + ('베이컨피자'), + ('베이컨포테이토피자'), + ('오로너비아니피자'), + ('핫소스'), + ('갈릭소스'), + ('피클'), + ('치즈크러스트 엣지'), + ('콜라'), + ('사이다'), + ('치즈치킨'), + ('칠리양념'), + ('마늘간장'), + ('카레양념'), + ('치킨무'), + ('파닭'), + ('속초명가닭강정'), + ('탕수육치킨'), + ('악마치킨'), + ('디디한마리'), + ('빅디디'), + ('후라이드윙'), + ('닭다리후라이드'), + ('핫후라이드'), + ('디디콤보'), + ('리치디디'), + ('치만이순살세트'), + ('치만이세트'), + ('고추장찌개'), + ('콩구수'), + ('김치볶음밥'), + ('피자치탕'), + ('감자피자치탕'), + ('고구마피자치탕'), + ('김치피자치탕'), + ('만두피자치탕'), + ('데리야키치탕'), + ('딥치즈싸이버거'), + ('싸이버거'), + ('언블리버블버거'), + ('쉬림프싸이플렉스버거'), + ('불싸이버거'), + ('화이트갈릭버거'), + ('싸이플렉스버거'), + ('간장마늘싸이버거'), + ('딥치즈버거'), + ('양념치킨싸이버거'), + ('휠렛버거'), + ('인크레더블버거'), + ('등심탕수육'), + ('치킨탕수육'), + ('오뎅탕'), + ('누룽지'), + ('계란찜'), + ('김가루밥'), + ('몽룡이네 닭볶음탕'), + ('몽룡이네 닭찜'), + ('짜장'), + ('짬뽕'), + ('쟁반짜장'), + ('짜장면'), + ('삼선간짜장'), + ('삼선우동'), + ('삼선짬뽕'), + ('차돌짬뽕'), + ('쟁반짬뽕'), + ('삼선짬뽕밥'), + ('아구찜'), + ('돌문어찜'), + ('돌문어볶음'), + ('오리주물럭'), + ('찜닭'), + ('곱창전골'), + ('매운돼지갈비찜'), + ('닭도리탕'), + ('오삼불고기'), + ('낙지볶음'), + ('골뱅이무침'), + ('동태탕'), + ('돼지짜글이'), + ('제육볶음'), + ('오징어볶음'), + ('쭈꾸미볶음'), + ('똥집볶음'), + ('닭발'), + ('뼈없는닭발'), + ('고갈비'), + ('두부김치'), + ('해물김치전'), + ('계란말이'), + ('설렁탕'), + ('사골우거지탕'), + ('사골만두국'), + ('수제돈까스'), + ('청국장찌개'), + ('낙지순두부찌개'), + ('오징어찌개'), + ('참치찌개'), + ('양념주먹밥'), + ('황금올리브치킨'), + ('반반치킨'), + ('간장닭날개'), + ('빠리치킨'), + ('양파닭'), + ('마라핫치킨'), + ('소이갈릭치킨'), + ('허니갈릭스치킨'), + ('치즐링'), + ('자메이카통자리구이'), + ('스모크치킨'), + ('이스탄불'), + ('순살크래커'), + ('순살소이갈릭스'), + ('순살빠리치킨'), + ('순살후라이드'), + ('순살마라핫'), + ('순살치즐링'), + ('순살양념'), + ('순살파닭'), + ('골뱅이 무침'), + ('생맥주'), + ('꽃게탕'), + ('해물탕'), + ('매운뼈없는닭발'), + ('똥집야채볶음'), + ('매운똥집야채볶음'), + ('고추장감자찌개'), + ('동태찌개'), + ('병천순대'), + ('농어'), + ('도미'), + ('우럭'), + ('광어'), + ('놀래미'), + ('홍탁'), + ('능성어'), + ('감성돔'), + ('돗돔'), + ('돌돔'), + ('도다리'), + ('통우럭매운탕'), + ('생선초밥'), + ('방어'), + ('숭어'), + ('전복'), + ('멍게'), + ('개불'), + ('봉구스밥버거'), + ('햄밥버거'), + ('치즈밥버거'), + ('햄치즈밥버거'), + ('김치떡갈비밥버거'), + ('마요떡갈비밥버거'), + ('치즈떡갈비밥버거'), + ('제육밥버거'), + ('김치제육밥버거'), + ('치즈제육밥버거'), + ('카레밥버거'), + ('짜장밥버거'), + ('치즈불닭'), + ('불닭'), + ('불날개'), + ('불닭발'), + ('뼈없는불닭발'), + ('불족발'), + ('불똥집'), + ('불오돌뼈'), + ('야채똥집'), + ('훈제치킨'), + ('주먹밥'), + ('치즈추가'), + ('생왕소금구이'), + ('수입삼겹살'), + ('소갈비살'), + ('생삼겹살'), + ('항정살'), + ('불돈모듬'), + ('생돈모듬'), + ('돼지막창'), + ('돼지껍데기'), + ('오리지날순살'), + ('오리지날 후라이드'), + ('양념'), + ('간장'), + ('매운양념'), + ('크리스피 순살'), + ('모듬튀김'), + ('떡튀김'), + ('도시락세트'), + ('조개칼국수'), + ('얼큰이칼국수'), + ('오리육개장'), + ('미린다'), + ('자장면'), + ('울면'), + ('사천짜장'), + ('유니짜장'), + ('삼선울면'), + ('기스면'), + ('마파두부밥'), + ('고추덮밥'), + ('해삼탕밥'), + ('잡채'), + ('마파두부'), + ('덴뿌라'), + ('팔보채'), + ('잡탕'), + ('고추잡채'), + ('라조기'), + ('라조육'), + ('깐풍새우'), + ('해삼탕'), + ('파고추장삼겹'), + ('소갈비살비빔밥'), + ('소불고기덮밥'), + ('라면사리'), + ('속초원조닭강정'), + ('매운강정'), + ('마늘간장강정'), + ('스위트칠리강정'), + ('까르보나라'), + ('통닭발'), + ('무뼈닭발'), + ('국물떡볶이'), + ('쏘야밥버거'), + ('바삭멸치밥버거'), + ('진미채밥버거'), + ('소불고기밥버거'), + ('김치불고기밥버거'), + ('청양불고기밥버거'), + ('통살돈까스밥버거'), + ('통살돈까스마요밥버거'), + ('칠리치킨밥버거'), + ('치킨마요밥버거'), + ('버터장조림밥버거'), + ('오징어밥버거'), + ('전주비빔밥버거'), + ('추억의도시락밥버거'), + ('고추장삼겹살'), + ('들깨수제비'), + ('해물파전'), + ('감자탕전골'), + ('붉닭발'), + ('뼈없는 불닭발'), + ('국물닭발'), + ('치즈피자'), + ('고구마피자'), + ('통새우피자'), + ('핫치킨새우피자'), + ('불갈비피자'), + ('페파로니피자'), + ('토핑족발'), + ('슈퍼콤비네이션'), + ('토핑보쌈'), + ('핫치킨피자'), + ('포테이토베이컨피자'), + ('불고기새우피자'), + ('감자새우피자'), + ('왕족발'), + ('치즈바이트피자'), + ('야채피자'), + ('포테이토피자'), + ('파인애플피자'), + ('고구마감자피자'), + ('게맛살골드피자'), + ('왕보쌈'), + ('왕족막국수'), + ('해파리냉채'), + ('사골떡만두국'), + ('우거지뼈다귀탕'), + ('사골곰탕'), + ('뼈다귀전골'), + ('육개장전골'), + ('육계닭도리탕'), + ('토종닭도리탕'), + ('묵은지수육찜'), + ('육계장'), + ('소고기덮밥'), + ('왕돈까스'), + ('모듬김밥'), + ('기본김밥'), + ('제육도시락'), + ('숯불고기도시락'), + ('우삼겹도시락'), + ('마스도시락'), + ('왕천파닭'), + ('삼겹살도시락'), + ('순살간장'), + ('특삼겹살도시락'), + ('순살반반'), + ('순살매운양념'), + ('쥬피터도시락'), + ('똥집튀김'), + ('돼지김치찌개'), + ('똥집양념'), + ('똥집간장'), + ('미니김치찜'), + ('똥집반반'), + ('우삼겹된장찌개'), + ('닭떡볶이'), + ('우삼겹떡볶이'), + ('규동'), + ('우삼겹숙주덮밥'), + ('우삼겹볶음밥'), + ('족발'), + ('참기름계란밥'), + ('스팸컵밥'), + ('미니족'), + ('불족'), + ('제육컵밥'), + ('샐러드비빔밥'), + ('붉닭샐비'), + ('안심탕수육'), + ('아나고'), + ('탄산음료'), + ('특전복죽'), + ('해삼'), + ('전복죽'), + ('오징어'), + ('낙지'), + ('게불'), + ('장조림'), + ('자연송이죽'), + ('자연송이전복죽'), + ('전복인삼닭죽'), + ('매생이굴죽'), + ('바다치즈죽'), + ('순한불낙죽'), + ('매콤불낙죽'), + ('브로콜리새우죽'), + ('커리치킨죽'), + ('버섯굴죽'), + ('인삼닭죽'), + ('모듬해물죽'), + ('한우야채죽'), + ('버섯들깨죽'), + ('낙지김치죽'), + ('참치야채죽'), + ('버섯야채죽'), + ('얼큰김치죽'), + ('황태콩나물죽'), + ('홍합미역죽'), + ('야채죽'), + ('치킨데리야끼볶음밥'), + ('불낙지볶음밥'), + ('해물볶음밥'), + ('햄야채볶음밥'), + ('불고기볶음밥'), + ('흑임자죽'), + ('녹두죽'), + ('호박죽'), + ('마죽'), + ('팥죽'), + ('흰죽'), + ('주니어불고기버거'), + ('화이트짬봉'), + ('애니버거'), + ('삼선고추짱뽕'), + ('애니불고기버거'), + ('미친짬뽕'), + ('속풀이짬뽕'), + ('에그불고기버거'), + ('얼큰이짬뽕'), + ('누룽지탕'), + ('애니치즈버거'), + ('핫치킨버거'), + ('황제짬뽕'), + ('왕만두'), + ('떡갈비버거'), + ('수제돈가스버거'), + ('수제 치즈돈가스버거'), + ('수제 피자돈가스버거'), + ('수제핫버거'), + ('핫도그소세지 버거'), + ('칠리소세지 버거'), + ('치즈소세지 버거'), + ('순살강정'), + ('치킨너겟'), + ('아메리카노'), + ('카푸치노'), + ('카페라떼'), + ('우유'), + ('복숭아 아이스티'), + ('블루베리 아이스티'), + ('레몬 에이드'), + ('딸기쥬스'), + ('망고쥬스'), + ('오렌지 쥬스'), + ('키위쥬스'), + ('블루베리 쥬스'), + ('후라이드치킨'), + ('올리고당양념'), + ('순살치폴레양념'), + ('순살양념치킨'), + ('매운불양념치킨'), + ('치폴레양념치킨'), + ('순살매운불양념'), + ('치즈슈프림양념'), + ('순살치즈슈프림양념'), + ('새치 고기고기'), + ('돈치 고기고기'), + ('동백'), + ('돈까스도련님'), + ('치킨제육'), + ('간장한마리치킨'), + ('간장다리치킨'), + ('간장날개치킨'), + ('강정치킨'), + ('순살 양념'), + ('순살/다리/날개'), + ('레몬파닭'), + ('순살레몬파닭'), + ('닭강정'), + ('청국장'), + ('우렁쌈밥정식'), + ('제육정식'), + ('산채나물밥'), + ('민물새우탕'), + ('황태찜'), + ('오골계백숙'), + ('한마리반치킨'), + ('크리스피'), + ('다리치킨'), + ('날개치킨'), + ('카레치킨'), + ('케이준감자튀김'), + ('크림어니언치킨'), + ('청고추드레싱치킨'), + ('까르보나라치킨'), + ('낙곱새'), + ('낙삼새'), + ('떡사리'), + ('오뎅사리'), + ('우동사리'), + ('계란'), + ('참치주먹밥'), + ('비엔나'), + ('치즈떡'), + ('바닐라라떼'), + ('헤이즐넛라떼'), + ('캐러멜마끼아또'), + ('카페모카'), + ('더치커피'), + ('샷추가'), + ('아이스티'), + ('자몽쥬스'), + ('유자깔라만시'), + ('핫초코'), + ('녹차'), + ('홍차'), + ('민트초코라떼'), + ('고구마라떼'), + ('블루베리라떼'), + ('딸기스무디'), + ('키위스무디'), + ('망고피치스무디'), + ('Freddo 조리퐁퐁'), + ('Freddo 블루베리'), + ('쿠키앤크림'), + ('쿠키'), + ('블루베리스콘'), + ('베이글'), + ('크림치즈프렛즐'), + ('케이크'), + ('샌드위치'), + ('옛날통치킨'), + ('소스'), + ('알싸한마늘간장치킨'), + ('고추마늘간장'), + ('치즈스노우퀸'), + ('꿀간장치킨'), + ('마늘빵치킨'), + ('따뜻한족발'), + ('매운족발'), + ('달콤한 탕수육'), + ('족발덮밥'), + ('불닭곱창'), + ('맛초킹'), + ('해바라기후라이드'), + ('뿌링클'), + ('커리퀸'), + ('매운양념치킨'), + ('맵삭치킨'), + ('치즈뿌리오'), + ('페퍼로니피자'), + ('이탈리안치즈피자'), + ('투어고구마피자'), + ('투어콤비네이션피자'), + ('슈퍼콤비네이션피자'), + ('프리미엄수제불고기피자'), + ('통고구마피자'), + ('스페셜고구마샐러드피자'), + ('숯불갈비피자'), + ('칠리핫치킨피자'), + ('스페셜단호박샐러드피자'), + ('스페셜피자'), + ('수블라키피자'), + ('폭립베이컨피자'), + ('스페셜쉬림프골드피자'), + ('숯불갈비바이트피자'), + ('스페셜바이트피자'), + ('베이컨포테이토바이트피자'), + ('폭립베이컨바이트피자'), + ('구운치킨'), + ('치즈스파게티'), + ('감자스낵'), + ('돈까스카레'), + ('새우돈까스덮밥'), + ('돈까스덮밥'), + ('소불고기철판볶음밥'), + ('스팸철판볶음밥'), + ('김치 부대찌개'), + ('묵은지김치찌개'), + ('빅치킨마요'), + ('돈치마요'), + ('치킨마요'), + ('참치마요'), + ('튼튼도시락'), + ('케이준후라이'), + ('오리지널 닭강정'), + ('참치야채 감초고추장'), + ('소불고기 감초고추장 비빔밥'), + ('시골제육 두부강된장 비빔밥'), + ('두부강된장소스'), + ('왕카레돈까스덮밥'), + ('왕치킨마요'), + ('튀김류'), + ('중국당면'), + ('대창'), + ('새우'), + ('탕수육도련님'), + ('칠리 찹쌀탕수육'), + ('미니 찹쌀탕수육'), + ('뉴 감자고로케'), + ('토네이도 소세지'), + ('치킨'), + ('메가 치킨마요'), + ('메가 치킨제육'), + ('김말이피치탕'), + ('닭백숙'), + ('에스프레소'), + ('카라멜마끼야또'), + ('민트카페모카'), + ('아인슈페너'), + ('그린티라떼'), + ('초코라떼'), + ('자색고구마라떼'), + ('밀크티라떼'), + ('흑당라떼'), + ('흑당밀크티'), + ('얼그레이'), + ('잉글리쉬 블랙퍼스트'), + ('허브티'), + ('프룻티'), + ('바나나'), + ('딸바'), + ('딸기'), + ('망고스무디'), + ('블루베리스무디'), + ('플레인요거트'), + ('딸기요거트'), + ('유자요거트'), + ('블루베리요거트'), + ('망고요거트'), + ('새우데리야끼'), + ('해물매콤토마토'), + ('프렌치토스트'), + ('가든샐러드'), + ('치킨샐러드'), + ('허니브레드'), + ('레몬에이드'), + ('자몽에이드'), + ('블루베리에이드'), + ('청포도에이드'), + ('블루레몬에이드'), + ('체리에이드'), + ('유자에이드'), + ('패션후르츠에이드'), + ('나폴리피자'), + ('스테이크피자'), + ('까르보네피자'), + ('아이리쉬포테이토피자'), + ('고르곤졸라피자'), + ('더블갈릭바베큐피자'), + ('깐쇼새우피자'), + ('직화파인애플피자'), + ('닭안심살피자'), + ('퀘사디아피자'), + ('직화홀릭바이트피자'), + ('도이치바이트피자'), + ('멕시칸바이트피자'), + ('미트러버피자'), + ('킹소시지피자'), + ('치킨스틱'), + ('치킨텐더'), + ('새우링'), + ('웨지감자'), + ('치즈오븐스파게티'), + ('갈릭포테이토'), + ('야채곱창'), + ('오돌뼈'), + ('떡 추가'), + ('햇반'), + ('순대국밥'), + ('고기국밥'), + ('곱창볶음'), + ('크림순대볶음'), + ('허니고르곤피자'), + ('육해공골드피자'), + ('불새피자'), + ('바이트골드피자'), + ('농촌피자'), + ('어촌피자'), + ('왕창포테이토피자'), + ('고기농장피자'), + ('스위트골드피자'), + ('단호박피자'), + ('치즈그라인스파게티'), + ('윙봉'), + ('불고기스파게티'), + ('오굿박스'), + ('고구마무스'), + ('치즈크러스트'), + ('고구마크러스트'), + ('체다엣지'), + ('골드엣지'), + ('바이트골드'), + ('흑미도우'), + ('간장바베큐'), + ('고추장바베큐'), + ('매콤후라이드'), + ('깐풍치킨'), + ('웰빙파닭'), + ('앙념치킨'), + ('순살3종세트'), + ('똥집후라이드'), + ('깐풍똥집'), + ('모듬감자튀김'), + ('눈꽃치즈떡볶이'), + ('파절이'), + ('배달소주'), + ('옛날통닭'), + ('쟁반막국수'), + ('무김치'), + ('야채'), + ('닭똥집'), + ('마늘간장치킨'), + ('매운간장치킨'), + ('뼈있는파닭'), + ('순살간장치킨'), + ('순살마늘간장치킨'), + ('순살매운간장'), + ('돼지갈비'), + ('가브리살'), + ('돼지모듬'), + ('목살'), + ('한우버섯죽'), + ('한우미역죽'), + ('삼선짬뽕죽'), + ('모듬해물된장죽'), + ('새우미역죽'), + ('게살날치알죽'), + ('삼계탕'), + ('잣죽'), + ('음료'), + ('철판김치볶음밥'), + ('치즈철판김치볶음밥'), + ('갈치무우조림'), + ('교촌 소이 살살'), + ('교촌 후라이드'), + ('교촌 샐러드'), + ('교촌 웨지 감자'), + ('고량주'), + ('이과두주'), + ('연태고량주'), + ('오리지널 감자'), + ('양념 감자'), + ('통오징어 튀김'), + ('크림 생맥주'), + ('레드락'), + ('진저하이볼'), + ('참이슬'), + ('사과맥주'), + ('포도맥주'), + ('레몬맥주'), + ('딸기맥주'), + ('자몽맥주'), + ('청사과맥주'), + ('더치맥주'), + ('꿀맥주'), + ('망고맥주'), + ('바닐라맥주'), + ('수박맥주'), + ('청포도맥주'), + ('호가든'), + ('코젤'), + ('갈릭칠리 소스'), + ('갈릭치즈 소스'), + ('어니언 소스'), + ('갈릭 소스'), + ('스위트칠리 소스'), + ('사워크림 소스'), + ('허니머스타드 소스'), + ('케찹'), + ('핫칠리 소스'), + ('핫비비큐 소스'), + ('나쵸치즈 소스'), + ('슈퍼슈프림피자'), + ('군고구마피자'), + ('버팔로윙'), + ('해물뼈전골'), + ('해물뼈찜'), + ('뼈다귀해장국'), + ('얼큰이 갈비탕'), + ('우거지탕'), + ('뚝불고기'), + ('해물알탕'), + ('소곱창전골'), + ('뚝불'), + ('햄볶음밥'), + ('비빔만두'), + ('어묵탕'), + ('치킨까스'), + ('냠냠김밥'), + ('냠냠라면'), + ('골뱅이소면'), + ('비빔국수'), + ('물국수'), + ('돈가스'), + ('치즈돈가스'), + ('매운치즈돈가스'), + ('단호박치즈돈가스'), + ('콩나물불고기'), + ('가쓰오탕수육'), + ('허니골드탕수육'), + ('불피자탕수육'), + ('피자탕수육'), + ('눈꽃탕수육'), + ('김치피자탕수육'), + ('쩐더탕수육'), + ('양념탕수육'), + ('간장탕수육'), + ('눈꽃치킨'), + ('후라이드파닭'), + ('쏘핫간장치킨'), + ('간장파닭'), + ('치즈파닭'), + ('꿀맵닭'), + ('멸치국수'), + ('돔베고기'), + ('돼지국밥'), + ('선지해장국'), + ('왕갈비탕'), + ('벌집생삼겹살'), + ('매운간짜장'), + ('백짬뽕'), + ('고추짬뽕'), + ('고추잡채밥'), + ('착한특밥'), + ('삼선누룽지탕'), + ('연태주'), + ('딸기라떼'), + ('딸기에이드'), + ('딸기티'), + ('로제떡볶이'), + ('허니갈릭프라이'), + ('불고기치즈프라이'), + ('왕새우튀김'), + ('홉스순살치킨'), + ('홉스양념치킨'), + ('로제파스타치킨'), + ('크림파스타치킨'), + ('참치폭탄주먹밥'), + ('감자튀김 추가'), + ('음료수 사이즈업'), + ('리얼티라미수찰떡'), + ('리얼꿀 미니호떡'), + ('숯불고기 샐비'), + ('우삼겹 샐비'), + ('달걀 후라이'), + ('치즈'), + ('컵라면'), + ('에그 샌드위치'), + ('햄치즈 샌드위치'), + ('크랩 샌드위치'), + ('크로와상 샌드위치'), + ('밑반찬'), + ('알곱창'), + ('고구마치즈스틱'), + ('양념감자튀김'), + ('데리야끼막창'), + ('쏘방 라면'), + ('김치말이국수'), + ('순살블랙찜닭'), + ('순살레드찜닭'), + ('순살매콤로제찜닭'), + ('중독닭볶음탕'), + ('목삼겹살'), + ('땡초치킨'), + ('땡초 불 파닭'), + ('우삼겹짬뽕순두부탕'), + ('산더미마라전골'), + ('크리미어니언'), + ('햄왕창부대찌개'), + ('물총조개탕'), + ('조선 매운 우육탕'), + ('매운바지락술찜'), + ('땡초어니언치킨'), + ('청양고추마요치킨'), + ('고기듬뿍김치찌개'), + ('조선 우육탕'), + ('얼큰꼬치어묵탕'), + ('돼지김치두루치기'), + ('매콤제육볶음'), + ('조선 매운 우육면'), + ('조선 우육면'), + ('순살 치즈만땅 찜닭'), + ('똥집고금구이'), + ('킬바사소시지구이'), + ('소면'), + ('쫄뱅이'), + ('꿔바로우'), + ('삼치구이'), + ('묵은지 순살 닭볶음탕'), + ('백세주'), + ('치즈계란말이'), + ('우삼겹달걀폭탄떡볶이'), + ('우육면 + 꿔바로우'), + ('먹태구이'), + ('오지치즈후라이'), + ('짜게치'), + ('조개라면'), + ('간장계란밥'), + ('참치마요밥'), + ('마무리볶음밥'), + ('중화면사리'), + ('치즈사리'), + ('관자쇼마이 홍유초수'), + ('왕어혈교 홍유초수'), + ('세모 멘보샤'), + ('야채춘권'), + ('페페로니피자'), + ('청경채'), + ('숙주'), + ('크리스피 새우롤'), + ('마초 떡볶이'), + ('나 홀로 떡볶이'), + ('매콤로제 떡볶이'), + ('뚜껑김치 삼겹살'), + ('김치전골'), + ('팔도비빔면'), + ('냉동순대'), + ('모듬순대'), + ('얼큰순대국밥'), + ('얼큰우동국밥'), + ('순대전골'), + ('토핑추가'), + ('육회비빔밥'), + ('웰빙비빔밥'), + ('삼겹비빔밥'), + ('우삼겹비빔밥'), + ('할라피뇨통살버거'), + ('안동찜닭'), + ('편육'), + ('가자미회국수'), + ('낙지소면'), + ('간장석쇠불고기'), + ('매콤고추장불고기'), + ('춘천닭갈비'), + ('삼겹파전'), + ('순두부짬뽕'), + ('만두짬뽕'), + ('해물오뎅탕'), + ('알짬뽕'), + ('나가사끼짬뽕'), + ('돈코츠라멘'), + ('탄탄멘'), + ('간재미회국수'), + ('메밀소바'), + ('김치국수'), + ('유부초밥'), + ('스프 & 브레드'), + ('장봉뵈르'), + ('바질오일파스타'), + ('베이컨토마토파스타'), + ('햄치즈파니니'), + ('치킨파니니'), + ('머쉬룸파니니'), + ('카야잼버터토스트'), + ('티라미수'), + ('치즈케이크'), + ('크로플'), + ('리얼 티라미수 찰떡'), + ('에그샌드위치'), + ('햄치즈샌드위치'), + ('크랩샌드위치'), + ('크로와상샌드위치'), + ('쏘방라면'), + ('손두부찌개'), + ('묵은지닭볶음탕'), + ('햄 김치 볶음밥'), + ('국수 및 면류'), + ('해장라면'), + ('치킨 및 닭요리'), + ('허니벌꿀치킨'), + ('땡초치즈불닭'), + ('숯불무뼈닭발'), + ('가마솥 치킨 후라이드'), + ('가마솥 치킨 양념치킨'), + ('가마솥 치킨 갈릭소스'), + ('가마솥 치킨 순살 후라이드'), + ('가마솥 치킨 순살 양념치킨'), + ('가마솥 치킨 순살 갈릭소스'), + ('고기류'), + ('돼지양념갈비'), + ('목살소금구이'), + ('소불고기'), + ('오돌뼈볶음'), + ('사이드 및 기타'), + ('타코야끼'), + ('콘소메치킨'), + ('닭똥집튀김'), + ('닭껍질튀김'), + ('염통꼬치'), + ('닭껍질 꼬치구이'), + ('숯불 치즈새우'), + ('달콤채식단피자'), + ('킹새우통치킨피자'), + ('통치킨오믈렛피자'), + ('불고기파스타치킨'), + ('디저트 및 음료'), + ('생과일 파인애플샤베트'), + ('생과일 코코넛샤베트'), + ('요구르트 샤베트'), + ('주류 및 음료수'), + ('후루츠 하이볼'), + ('레몬 하이볼'), + ('깔라만시 하이볼'), + ('자몽 하이볼'), + ('망고하이볼'); diff --git a/src/test/java/in/koreatech/koin/acceptance/OwnerShopApiTest.java b/src/test/java/in/koreatech/koin/acceptance/OwnerShopApiTest.java index bbe5cb785..f4df2177b 100644 --- a/src/test/java/in/koreatech/koin/acceptance/OwnerShopApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/OwnerShopApiTest.java @@ -367,7 +367,7 @@ void setUp() { ) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(menu.getId())) - .andExpect(jsonPath("$.shop_id").value(menu.getShopId())) + .andExpect(jsonPath("$.shop_id").value(menu.getShop().getId())) .andExpect(jsonPath("$.name").value(menu.getName())) .andExpect(jsonPath("$.is_hidden").value(menu.isHidden())) .andExpect(jsonPath("$.is_single").value(false)) diff --git a/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java b/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java index ff291d402..e9ab0bae6 100644 --- a/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java @@ -78,7 +78,7 @@ void setUp() { void 옵션이_하나_있는_상점의_메뉴를_조회한다() throws Exception { Menu menu = menuFixture.짜장면_단일메뉴(마슬랜, menuCategoryFixture.메인메뉴(마슬랜)); mockMvc.perform( - get("/shops/{shopId}/menus/{menuId}", menu.getShopId(), menu.getId()) + get("/shops/{shopId}/menus/{menuId}", menu.getShop().getId(), menu.getId()) ) .andExpect(status().isOk()) .andExpect(content().json(""" @@ -108,7 +108,7 @@ void setUp() { Menu menu = menuFixture.짜장면_옵션메뉴(마슬랜, menuCategoryFixture.메인메뉴(마슬랜)); mockMvc.perform( - get("/shops/{shopId}/menus/{menuId}", menu.getShopId(), menu.getId()) + get("/shops/{shopId}/menus/{menuId}", menu.getShop().getId(), menu.getId()) ) .andExpect(status().isOk()) .andExpect(content().json(""" @@ -148,7 +148,7 @@ void setUp() { Menu menu = menuFixture.짜장면_단일메뉴(마슬랜, menuCategoryFixture.추천메뉴(마슬랜)); mockMvc.perform( - get("/shops/{shopId}/menus/categories", menu.getShopId()) + get("/shops/{shopId}/menus/categories", menu.getShop().getId()) ) .andExpect(status().isOk()) .andExpect(content().json(""" @@ -1021,6 +1021,45 @@ void setUp() { """, 티바_영업여부, 마슬랜_영업여부))); } + @Test + void 검색어를_입력해서_상점을_조회한다() throws Exception { + Shop 배달_안되는_신전_떡볶이 = shopFixture.배달_안되는_신전_떡볶이(owner); + ShopReview 리뷰_4점 = shopReviewFixture.리뷰_4점(익명_학생, 배달_안되는_신전_떡볶이); + shopReviewReportFixture.리뷰_신고(익명_학생, 리뷰_4점, DISMISSED); + + shopReviewFixture.리뷰_4점(익명_학생, 마슬랜); + // 2024-01-15 12:00 월요일 기준 + boolean 신전_떡볶이_영업여부 = true; + boolean 마슬랜_영업여부 = true; + mockMvc.perform( + get("/v2/shops") + .queryParam("query", "떡") + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" + { + "count": 1, + "shops": [ + { + "category_ids": [ + \s + ], + "delivery": false, + "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 + } + ] + } + """, 마슬랜_영업여부, 신전_떡볶이_영업여부))); + } + @Test void 전화하기_발생시_정보가_알림큐에_저장된다() throws Exception { mockMvc.perform( diff --git a/src/test/java/in/koreatech/koin/acceptance/ShopSearchApiTest.java b/src/test/java/in/koreatech/koin/acceptance/ShopSearchApiTest.java new file mode 100644 index 000000000..7a7e56df6 --- /dev/null +++ b/src/test/java/in/koreatech/koin/acceptance/ShopSearchApiTest.java @@ -0,0 +1,124 @@ +package in.koreatech.koin.acceptance; + +import static in.koreatech.koin.domain.shop.model.review.ReportStatus.DISMISSED; +import static in.koreatech.koin.domain.shop.model.review.ReportStatus.UNHANDLED; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import in.koreatech.koin.AcceptanceTest; +import in.koreatech.koin.domain.owner.model.Owner; +import in.koreatech.koin.domain.shop.model.menu.Menu; +import in.koreatech.koin.domain.shop.model.menu.MenuCategory; +import in.koreatech.koin.domain.shop.model.menu.MenuSearchKeyWord; +import in.koreatech.koin.domain.shop.model.review.ShopReview; +import in.koreatech.koin.domain.shop.model.shop.Shop; +import in.koreatech.koin.domain.shop.repository.menu.MenuSearchKeywordRepository; +import in.koreatech.koin.domain.student.model.Student; +import in.koreatech.koin.fixture.EventArticleFixture; +import in.koreatech.koin.fixture.MenuCategoryFixture; +import in.koreatech.koin.fixture.MenuFixture; +import in.koreatech.koin.fixture.ShopCategoryFixture; +import in.koreatech.koin.fixture.ShopFixture; +import in.koreatech.koin.fixture.ShopReviewFixture; +import in.koreatech.koin.fixture.ShopReviewReportFixture; +import in.koreatech.koin.fixture.UserFixture; +import java.time.LocalDate; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@SuppressWarnings("NonAsciiCharacters") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ShopSearchApiTest extends AcceptanceTest { + + @Autowired + private UserFixture userFixture; + + @Autowired + private ShopFixture shopFixture; + + @Autowired + private MenuFixture menuFixture; + + @Autowired + private MenuCategoryFixture menuCategoryFixture; + + @Autowired + private MenuSearchKeywordRepository menuSearchKeywordRepository; + + private Shop 마슬랜; + private Owner owner; + + @BeforeAll + void setUp() { + clear(); + owner = userFixture.준영_사장님(); + 마슬랜 = shopFixture.마슬랜(owner); + menuSearchKeywordRepository.save(MenuSearchKeyWord.builder() + .keyword("짜장면") + .build()); + menuSearchKeywordRepository.save(MenuSearchKeyWord.builder() + .keyword("마늘치킨") + .build()); + menuSearchKeywordRepository.save(MenuSearchKeyWord.builder() + .keyword("짜장밥") + .build()); + menuSearchKeywordRepository.save(MenuSearchKeyWord.builder() + .keyword("마늘통구이") + .build()); + menuSearchKeywordRepository.save(MenuSearchKeyWord.builder() + .keyword("짜장") + .build()); + menuFixture.짜장면_단일메뉴(마슬랜, menuCategoryFixture.메인메뉴(마슬랜)); + } + + @Test + void 검색_문자와_관련된_키워드를_조회한다() throws Exception { + mockMvc.perform( + get("/shops/search/related/짜") + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" + { + "keywords": [ + { + "keyword": "짜장", + "shop_ids": [1], + "shop_id": null + }, + { + "keyword": "짜장면", + "shop_ids": [1], + "shop_id": null + } + ] + } + """))) + .andDo(print()); + } + + @Test + void 검색_문자와_관련된_키워드를_조회한다_상점인_경우에는_상점id도_조회된다() throws Exception { + mockMvc.perform( + get("/shops/search/related/마") + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" + { + "keywords": [ + { + "keyword": "마슬랜 치킨", + "shop_ids": [], + "shop_id": 1 + } + ] + } + """))) + .andDo(print()); + } +} diff --git a/src/test/java/in/koreatech/koin/fixture/MenuFixture.java b/src/test/java/in/koreatech/koin/fixture/MenuFixture.java index eb38e756c..45216433e 100644 --- a/src/test/java/in/koreatech/koin/fixture/MenuFixture.java +++ b/src/test/java/in/koreatech/koin/fixture/MenuFixture.java @@ -30,7 +30,7 @@ public MenuFixture( public Menu 짜장면_옵션메뉴(Shop shop, MenuCategory menuCategory) { Menu menu = Menu.builder() - .shopId(shop.getId()) + .shop(shop) .name("짜장면") .description("맛있는 짜장면") .build(); @@ -72,7 +72,7 @@ public MenuFixture( public Menu 짜장면_단일메뉴(Shop shop, MenuCategory menuCategory) { Menu menu = Menu.builder() - .shopId(shop.getId()) + .shop(shop) .name("짜장면") .description("맛있는 짜장면") .build(); @@ -104,4 +104,39 @@ public MenuFixture( menuCategory.getMenuCategoryMaps().add(menuCategoryMap); return menuRepository.save(menu); } + + public Menu 짜파게티_단일메뉴(Shop shop, MenuCategory menuCategory) { + Menu menu = Menu.builder() + .shop(shop) + .name("짜파게티") + .description("맛있는 짜장면") + .build(); + + menu.getMenuImages().addAll( + List.of( + MenuImage.builder() + .menu(menu) + .imageUrl("https://test.com/짜장면.jpg") + .build(), + MenuImage.builder() + .menu(menu) + .imageUrl("https://test.com/짜장면22.jpg") + .build() + ) + ); + menu.getMenuOptions().add( + MenuOption.builder() + .menu(menu) + .option("짜파게티") + .price(7000) + .build() + ); + MenuCategoryMap menuCategoryMap = MenuCategoryMap.builder() + .menu(menu) + .menuCategory(menuCategory) + .build(); + menu.getMenuCategoryMaps().add(menuCategoryMap); + menuCategory.getMenuCategoryMaps().add(menuCategoryMap); + return menuRepository.save(menu); + } }