From b044d59fb848ba7303d137fc0209702377b4094a Mon Sep 17 00:00:00 2001 From: Bombo Date: Sat, 16 Sep 2023 17:54:55 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[MDH-189]=20feat=20&=20test=20:=20querydsl?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/infra/UserCustomRepositoryImpl.java | 2 +- .../test/java/com/mdh/user/QueryDslTest.java | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 user/src/test/java/com/mdh/user/QueryDslTest.java diff --git a/user/src/main/java/com/mdh/user/user/infra/UserCustomRepositoryImpl.java b/user/src/main/java/com/mdh/user/user/infra/UserCustomRepositoryImpl.java index 34f68906..5dea374c 100644 --- a/user/src/main/java/com/mdh/user/user/infra/UserCustomRepositoryImpl.java +++ b/user/src/main/java/com/mdh/user/user/infra/UserCustomRepositoryImpl.java @@ -23,4 +23,4 @@ public List findByRole(Role role) { .where(user.role.eq(role)) .fetch(); } -} +} \ No newline at end of file diff --git a/user/src/test/java/com/mdh/user/QueryDslTest.java b/user/src/test/java/com/mdh/user/QueryDslTest.java new file mode 100644 index 00000000..426db9cc --- /dev/null +++ b/user/src/test/java/com/mdh/user/QueryDslTest.java @@ -0,0 +1,28 @@ +package com.mdh.user; + +import com.mdh.common.user.domain.Role; +import com.mdh.common.user.persistence.UserRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class QueryDslTest { + + @Autowired + private UserRepository userRepository; + + @Test + void findUserByRoleTest() { + //given + var guest = DataInitializerFactory.guest(); + userRepository.save(guest); + + //when + var findUsers = userRepository.findByRole(Role.GUEST); + + //then + Assertions.assertThat(findUsers).hasSize(1); + } +} From 2fb301ab4ef4df13ed111671dfcc6bf8314b2cdc Mon Sep 17 00:00:00 2001 From: heenahan Date: Tue, 19 Sep 2023 14:23:21 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[MDH-78]=20feat=20&=20test=20:=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=20=EB=A7=A4=EC=9E=A5=20=ED=95=84=ED=84=B0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EB=B0=8F?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mdh/common/global/config/JpaConfig.java | 8 + .../shop/persistence/ShopQueryRepository.java | 18 ++ .../persistence/ShopQueryRepositoryImpl.java | 140 ++++++++++++++ .../dto/ReservationShopSearchQueryDto.java | 16 ++ .../mdh/common/DataInitializerFactory.java | 16 ++ .../shop/persistence/ShopRepositoryTest.java | 182 ++++++++++++++++++ .../user/shop/application/ShopService.java | 18 ++ .../dto/ReservationShopSearchResponse.java | 20 ++ .../com/mdh/user/DataInitializerFactory.java | 9 + .../shop/application/ShopServiceTest.java | 75 ++++++-- 10 files changed, 491 insertions(+), 11 deletions(-) create mode 100644 common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepository.java create mode 100644 common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepositoryImpl.java create mode 100644 common/src/main/java/com/mdh/common/shop/persistence/dto/ReservationShopSearchQueryDto.java create mode 100644 common/src/test/java/com/mdh/common/shop/persistence/ShopRepositoryTest.java create mode 100644 user/src/main/java/com/mdh/user/shop/application/dto/ReservationShopSearchResponse.java diff --git a/common/src/main/java/com/mdh/common/global/config/JpaConfig.java b/common/src/main/java/com/mdh/common/global/config/JpaConfig.java index fbf36cbb..5ec2bff4 100644 --- a/common/src/main/java/com/mdh/common/global/config/JpaConfig.java +++ b/common/src/main/java/com/mdh/common/global/config/JpaConfig.java @@ -1,6 +1,9 @@ package com.mdh.common.global.config; +import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityManager; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @@ -14,4 +17,9 @@ public class JpaConfig { public void setTimeZone() { TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); } + + @Bean + public JPAQueryFactory jpaQueryFactory(EntityManager em) { + return new JPAQueryFactory(em); + } } \ No newline at end of file diff --git a/common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepository.java b/common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepository.java new file mode 100644 index 00000000..db332cb3 --- /dev/null +++ b/common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepository.java @@ -0,0 +1,18 @@ +package com.mdh.common.shop.persistence; + +import com.mdh.common.shop.persistence.dto.ReservationShopSearchQueryDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDate; +import java.time.LocalTime; + +public interface ShopQueryRepository { + Page searchReservationShopByFilter(Pageable pageable, + LocalDate reservationDate, + LocalTime reservationTime, + Integer personCount, + String regionName, + Integer minPrice, + Integer maxPrice); +} \ No newline at end of file diff --git a/common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepositoryImpl.java b/common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepositoryImpl.java new file mode 100644 index 00000000..dbdad074 --- /dev/null +++ b/common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepositoryImpl.java @@ -0,0 +1,140 @@ +package com.mdh.common.shop.persistence; + +import com.mdh.common.shop.domain.Shop; +import com.mdh.common.shop.persistence.dto.ReservationShopSearchQueryDto; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.*; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; + +import static com.mdh.common.reservation.QShopReservation.shopReservation; +import static com.mdh.common.reservation.QShopReservationDateTime.shopReservationDateTime; +import static com.mdh.common.reservation.QShopReservationDateTimeSeat.shopReservationDateTimeSeat; +import static com.mdh.common.shop.domain.QRegion.region; +import static com.mdh.common.shop.domain.QShop.shop; + +@RequiredArgsConstructor +public class ShopQueryRepositoryImpl implements ShopQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + /** + * 보여줄 것? 매장 정보, 예약 날짜, 날짜의 가능한 좌석 수 + * 매장 + * join (매장 예약 날짜 join 매장 예약 좌석 where 날짜 = 날짜 & 시간 = 시간) + * group by 매장 + * order by 매장 예약 가능한 좌석 수(1) + */ + @Override + public Page searchReservationShopByFilter(Pageable pageable, + LocalDate reservationDate, + LocalTime reservationTime, + Integer personCount, + String regionName, + Integer minPrice, + Integer maxPrice) { + var content = jpaQueryFactory + .select(Projections.constructor(ReservationShopSearchQueryDto.class, + shop.id, + shop.name, + shop.description, + shop.shopType, + region.city, + region.district, + shop.shopPrice, + seatStatusCaseBuilder().sum().as("availableSeatCount") + )) + .from(shopReservationDateTimeSeat) + .join(shopReservationDateTime) + .on(shopReservationDateTimeSeat.shopReservationDateTime.id.eq(shopReservationDateTime.id)) + .where(shopReservationDateTime.reservationDate.eq(reservationDate) + .and(shopReservationDateTime.reservationTime.eq(reservationTime))) + .join(shopReservation) + .on(shopReservation.shopId.eq(shopReservationDateTime.shopReservation.shopId)) + .where(personLoeGoe(personCount)) + .join(shop) + .on(shopReservation.shopId.eq(shop.id)) + .join(region) + .on(shop.region.id.eq(region.id)) + .where(regionContains(regionName)) + .where(priceLoeGoe(minPrice, maxPrice)) + .groupBy(shop.id) // 아이디로 group by + .orderBy(getOrderSpecifiers(pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + var totalCount = jpaQueryFactory + .select(shop.countDistinct()) + .from(shopReservationDateTimeSeat) + .join(shopReservationDateTime) + .on(shopReservationDateTimeSeat.shopReservationDateTime.id.eq(shopReservationDateTime.id)) + .where(shopReservationDateTime.reservationDate.eq(reservationDate) + .and(shopReservationDateTime.reservationTime.eq(reservationTime))) + .join(shopReservation) + .on(shopReservation.shopId.eq(shopReservationDateTime.shopReservation.shopId)) + .where(personLoeGoe(personCount)) + .join(shop) + .on(shopReservation.shopId.eq(shop.id)) + .join(region) + .on(shop.region.id.eq(region.id)) + .where(regionContains(regionName)) + .where(priceLoeGoe(minPrice, maxPrice)); + + return PageableExecutionUtils.getPage(content, pageable, totalCount::fetchOne); + } + + private OrderSpecifier[] getOrderSpecifiers(Pageable pageable) { + var sort = pageable.getSort(); + var orderSpecifiers = new ArrayList<>(); + orderSpecifiers.add(new OrderSpecifier<>(Order.DESC, Expressions.stringPath("availableSeatCount"))); + var shopOrderSpecifiers = sort.get().map(o -> { + Order order = o.isAscending() ? Order.ASC : Order.DESC; + String property = o.getProperty(); + PathBuilder pathBuilder = new PathBuilder<>(Shop.class, "shop"); + return new OrderSpecifier(order, pathBuilder.getString(property)); + }).toList(); + orderSpecifiers.addAll(shopOrderSpecifiers); + return orderSpecifiers.stream().toArray(OrderSpecifier[]::new); + } + + private NumberExpression seatStatusCaseBuilder() { + return new CaseBuilder() + .when(shopReservationDateTimeSeat.seatStatus.eq(SeatStatus.AVAILABLE)) + .then(1) + .otherwise(0); + } + + private BooleanExpression personLoeGoe(Integer personCount) { + return personCount != null ? shopReservation.minimumPerson.loe(personCount) + .and(shopReservation.maximumPerson.goe(personCount)) : null; + } + + private BooleanExpression regionContains(String regionName) { + return regionName != null ? region.city.contains(regionName).or(region.district.contains(regionName)) : null; + } + + private BooleanExpression priceLoeGoe(Integer minPrice, Integer maxPrice) { + if (minPrice == null) return maxPriceGoe(maxPrice); + return minPriceLoe(minPrice).and(maxPriceGoe(maxPrice)); + } + + private BooleanExpression minPriceLoe(Integer minPrice) { + return minPrice != null ? shop.shopPrice.lunchMinPrice.loe(minPrice) + .or(shop.shopPrice.dinnerMinPrice.loe(minPrice)) : null; + } + + private BooleanExpression maxPriceGoe(Integer maxPrice) { + return maxPrice != null ? shop.shopPrice.lunchMaxPrice.goe(maxPrice) + .or(shop.shopPrice.dinnerMaxPrice.goe(maxPrice)) : null; + } +} \ No newline at end of file diff --git a/common/src/main/java/com/mdh/common/shop/persistence/dto/ReservationShopSearchQueryDto.java b/common/src/main/java/com/mdh/common/shop/persistence/dto/ReservationShopSearchQueryDto.java new file mode 100644 index 00000000..6e756288 --- /dev/null +++ b/common/src/main/java/com/mdh/common/shop/persistence/dto/ReservationShopSearchQueryDto.java @@ -0,0 +1,16 @@ +package com.mdh.common.shop.persistence.dto; + +import com.mdh.common.shop.domain.ShopPrice; +import com.mdh.common.shop.domain.ShopType; + +public record ReservationShopSearchQueryDto( + Long id, + String name, + String description, + ShopType shopType, + String city, + String district, + ShopPrice shopPrice, + int availableSeatCount +) { +} diff --git a/common/src/test/java/com/mdh/common/DataInitializerFactory.java b/common/src/test/java/com/mdh/common/DataInitializerFactory.java index 8dc38289..392a48b9 100644 --- a/common/src/test/java/com/mdh/common/DataInitializerFactory.java +++ b/common/src/test/java/com/mdh/common/DataInitializerFactory.java @@ -45,6 +45,13 @@ public static Region region() { .build(); } + public static Region region(String city, String district) { + return Region.builder() + .city(city) + .district(district) + .build(); + } + public static ShopDetails shopDetails() { return ShopDetails.builder() .url("https://www.example.com") @@ -65,6 +72,15 @@ public static ShopAddress shopAddress() { .build(); } + public static ShopPrice shopPrice() { + return ShopPrice.builder() + .lunchMinPrice(10000) + .lunchMaxPrice(50000) + .dinnerMinPrice(30000) + .dinnerMaxPrice(80000) + .build(); + } + public static Shop shop( Long userId, ShopDetails shopDetails, diff --git a/common/src/test/java/com/mdh/common/shop/persistence/ShopRepositoryTest.java b/common/src/test/java/com/mdh/common/shop/persistence/ShopRepositoryTest.java new file mode 100644 index 00000000..cec40310 --- /dev/null +++ b/common/src/test/java/com/mdh/common/shop/persistence/ShopRepositoryTest.java @@ -0,0 +1,182 @@ +package com.mdh.common.shop.persistence; + +import com.mdh.common.DataInitializerFactory; +import com.mdh.common.reservation.persistence.*; +import com.mdh.common.user.persistence.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Transactional +class ShopRepositoryTest { + + @Autowired + private UserRepository userRepository; + + @Autowired + private RegionRepository regionRepository; + + @Autowired + private ShopRepository shopRepository; + + @Autowired + private ShopReservationRepository shopReservationRepository; + + @Autowired + private ShopReservationDateTimeRepository shopReservationDateTimeRepository; + + @Autowired + private SeatRepository seatRepository; + + @Autowired + private ShopReservationDateTimeSeatRepository shopReservationDateTimeSeatRepository; + + @Autowired + private ReservationRepository reservationRepository; + + /** + * 2023.9.17 오후 7시 예약 가능한 식당 찾음 + */ + @Test + @DisplayName("특정 날짜와 시간에 예약 가능한 테이블 수로 매장을 정렬한다.") + void findAvailableReservationShopTest() { + //given + shopsAndShopReservations(); + + var pageRequest = PageRequest.of(0, 2, Sort.by("createdDate").ascending()); + + //when + var availableReservationShop = shopRepository.searchReservationShopByFilter(pageRequest, + LocalDate.of(2023, 9, 17), + LocalTime.of(19, 0, 0), + null, + null, + null, + null); + + //then + assertThat(availableReservationShop.getTotalElements()).isEqualTo(3); + assertThat(availableReservationShop.getTotalPages()).isEqualTo(2); + assertThat(availableReservationShop.getContent()).hasSize(2); + assertThat(availableReservationShop.getContent()) + .extracting("availableSeatCount") + .contains(2, 1); + } + + @Test + @DisplayName("특정 시간과 날짜에 예약 가능한 매장을 인원을 기준으로 필터 조회한다.") + void findAvailableReservationShopPersonFilterTest() { + //given + shopsAndShopReservations(); + + var pageRequest = PageRequest.of(0, 2, Sort.by(Sort.Order.asc("createdDate"))); + + //when + var reservationAvailableShop = shopRepository.searchReservationShopByFilter(pageRequest, + LocalDate.of(2023, 9, 17), + LocalTime.of(19, 0, 0), + 6, + null, + null, + null); + + //then + assertThat(reservationAvailableShop.getTotalElements()).isEqualTo(1); + assertThat(reservationAvailableShop.getTotalPages()).isEqualTo(1); + assertThat(reservationAvailableShop.getContent()).hasSize(1); + } + + @Test + @DisplayName("특정 시간과 날짜에 예약 가능한 매장을 지역을 기준으로 필터 조회한다.") + void findAvailableReservationShopRegionFilterTest() { + //given + shopsAndShopReservations(); + + var pageRequest = PageRequest.of(0, 2, Sort.by(Sort.Order.asc("createdDate"))); + + //when + var reservationAvailableShop = shopRepository.searchReservationShopByFilter(pageRequest, + LocalDate.of(2023, 9, 17), + LocalTime.of(19, 0, 0), + null, + "서울", + null, + null); + + //then + assertThat(reservationAvailableShop.getTotalElements()).isEqualTo(3); + assertThat(reservationAvailableShop.getTotalPages()).isEqualTo(2); + assertThat(reservationAvailableShop.getContent()).hasSize(2); + } + + /** + * shop1 -> 2023.9.17 오후 7시 예약 가능 테이블 1개, (서울, 강남구), 인원 2-5 + * shop2 -> 2023.9.17 오후 7시 예약 불가능, (서울, 청담), 인원 2-5 + * shop3 -> 2023.9.17 오후 8시 예약 가능, (서울, 강남구), 인원 2-7 + * shop4 -> 2023.9.17 오후 7시 예약 가능 테이블 2개, (서울, 청담), 인원 2-7 + */ + private void shopsAndShopReservations() { + var owner = DataInitializerFactory.owner(); + var guest = DataInitializerFactory.guest(); + userRepository.saveAll(List.of(owner, guest)); + + var region1 = DataInitializerFactory.region("서울", "강남구"); + var region2 = DataInitializerFactory.region("서울", "청담"); + regionRepository.saveAll(List.of(region1, region2)); + + var shopDetails = DataInitializerFactory.shopDetails(); + var shopAddress = DataInitializerFactory.shopAddress(); + + var shop1 = DataInitializerFactory.shop(owner.getId(), shopDetails, region1, shopAddress); + var shop2 = DataInitializerFactory.shop(owner.getId(), shopDetails, region2, shopAddress); + var shop3 = DataInitializerFactory.shop(owner.getId(), shopDetails, region1, shopAddress); + var shop4 = DataInitializerFactory.shop(owner.getId(), shopDetails, region2, shopAddress); + shopRepository.saveAll(List.of(shop1, shop2, shop3, shop4)); + + var shopReservation1 = DataInitializerFactory.shopReservation(shop1.getId(), 2, 5); + var shopReservation2 = DataInitializerFactory.shopReservation(shop2.getId(), 2, 5); + var shopReservation3 = DataInitializerFactory.shopReservation(shop3.getId(), 2, 7); + var shopReservation4 = DataInitializerFactory.shopReservation(shop4.getId(), 2, 7); + shopReservationRepository.saveAll(List.of(shopReservation1, shopReservation2, shopReservation3, shopReservation4)); + + var reservationDate = LocalDate.of(2023, 9, 17); + var reservationTime1 = LocalTime.of(19, 0, 0); + var reservationTime2 = LocalTime.of(20, 0, 0); + + var shopReservationDateTime1 = DataInitializerFactory.shopReservationDateTime(shopReservation1, reservationDate, reservationTime1); + var shopReservationDateTime2 = DataInitializerFactory.shopReservationDateTime(shopReservation2, reservationDate, reservationTime1); + var shopReservationDateTime3 = DataInitializerFactory.shopReservationDateTime(shopReservation3, reservationDate, reservationTime2); + var shopReservationDateTime4 = DataInitializerFactory.shopReservationDateTime(shopReservation4, reservationDate, reservationTime1); + shopReservationDateTimeRepository.saveAll(List.of(shopReservationDateTime1, shopReservationDateTime2, shopReservationDateTime3, shopReservationDateTime4)); + + var seat1 = DataInitializerFactory.seat(shopReservation1); + var seat2 = DataInitializerFactory.seat(shopReservation2); + var seat3 = DataInitializerFactory.seat(shopReservation3); + var seat4 = DataInitializerFactory.seat(shopReservation4); + var seat5 = DataInitializerFactory.seat(shopReservation4); + seatRepository.saveAll(List.of(seat1, seat2, seat3, seat4, seat5)); + + var shopReservationDateTimeSeat1 = DataInitializerFactory.shopReservationDateTimeSeat(shopReservationDateTime1, seat1); + var shopReservationDateTimeSeat2 = DataInitializerFactory.shopReservationDateTimeSeat(shopReservationDateTime2, seat2); + var shopReservationDateTimeSeat3 = DataInitializerFactory.shopReservationDateTimeSeat(shopReservationDateTime3, seat3); + var shopReservationDateTimeSeat4 = DataInitializerFactory.shopReservationDateTimeSeat(shopReservationDateTime4, seat4); + var shopReservationDateTimeSeat5 = DataInitializerFactory.shopReservationDateTimeSeat(shopReservationDateTime4, seat5); + shopReservationDateTimeSeatRepository.saveAll(List.of(shopReservationDateTimeSeat1, shopReservationDateTimeSeat2, shopReservationDateTimeSeat3, shopReservationDateTimeSeat4, shopReservationDateTimeSeat5)); + + var reservation = DataInitializerFactory.reservation(guest.getId(), shopReservation2, 4); + reservationRepository.save(reservation); + + shopReservationDateTimeSeat2.registerReservation(reservation); + } +} \ No newline at end of file diff --git a/user/src/main/java/com/mdh/user/shop/application/ShopService.java b/user/src/main/java/com/mdh/user/shop/application/ShopService.java index d0128841..b713bc38 100644 --- a/user/src/main/java/com/mdh/user/shop/application/ShopService.java +++ b/user/src/main/java/com/mdh/user/shop/application/ShopService.java @@ -2,6 +2,7 @@ import com.mdh.common.shop.persistence.ShopRepository; import com.mdh.common.waiting.persistence.WaitingLine; +import com.mdh.user.shop.application.dto.ReservationShopSearchResponse; import com.mdh.user.shop.application.dto.ShopDetailInfoResponse; import com.mdh.user.shop.application.dto.ShopResponse; import com.mdh.user.shop.application.dto.ShopResponses; @@ -42,4 +43,21 @@ public ShopDetailInfoResponse findShopDetailsById(Long shopId) { .orElseThrow(() -> new NoSuchElementException("존재하지 않는 매장입니다: " + shopId)); } + @Transactional(readOnly = true) + public ReservationShopSearchResponse searchReservationShop(Pageable pageable, ReservationShopSearchRequest reservationShopSearchRequest) { + var reservationDate = reservationShopSearchRequest.reservationDate(); + var reservationTime = reservationShopSearchRequest.reservationTime(); + var personCount = reservationShopSearchRequest.personCount(); + var region = reservationShopSearchRequest.region(); + var minPrice = reservationShopSearchRequest.minPrice(); + var maxPrice = reservationShopSearchRequest.maxPrice(); + var shopSearchQueryDtos = shopRepository.searchReservationShopByFilter(pageable, + reservationDate, + reservationTime, + personCount, + region, + minPrice, + maxPrice); + return ReservationShopSearchResponse.of(shopSearchQueryDtos); + } } \ No newline at end of file diff --git a/user/src/main/java/com/mdh/user/shop/application/dto/ReservationShopSearchResponse.java b/user/src/main/java/com/mdh/user/shop/application/dto/ReservationShopSearchResponse.java new file mode 100644 index 00000000..50cc13ce --- /dev/null +++ b/user/src/main/java/com/mdh/user/shop/application/dto/ReservationShopSearchResponse.java @@ -0,0 +1,20 @@ +package com.mdh.user.shop.application.dto; + +import com.mdh.common.shop.persistence.dto.ReservationShopSearchQueryDto; +import org.springframework.data.domain.Page; + +import java.util.List; + +public record ReservationShopSearchResponse( + int totalPages, + boolean hasNext, + List shops +) { + public static ReservationShopSearchResponse of(Page page) { + return new ReservationShopSearchResponse( + page.getTotalPages(), + page.hasNext(), + page.getContent() + ); + } +} \ No newline at end of file diff --git a/user/src/test/java/com/mdh/user/DataInitializerFactory.java b/user/src/test/java/com/mdh/user/DataInitializerFactory.java index 16879bff..9f9b18b2 100644 --- a/user/src/test/java/com/mdh/user/DataInitializerFactory.java +++ b/user/src/test/java/com/mdh/user/DataInitializerFactory.java @@ -65,6 +65,15 @@ public static ShopAddress shopAddress() { .build(); } + public static ShopPrice shopPrice() { + return ShopPrice.builder() + .lunchMinPrice(10000) + .lunchMaxPrice(50000) + .dinnerMinPrice(30000) + .dinnerMaxPrice(80000) + .build(); + } + public static Shop shop( Long userId, ShopDetails shopDetails, diff --git a/user/src/test/java/com/mdh/user/shop/application/ShopServiceTest.java b/user/src/test/java/com/mdh/user/shop/application/ShopServiceTest.java index c79d4221..d4ac259f 100644 --- a/user/src/test/java/com/mdh/user/shop/application/ShopServiceTest.java +++ b/user/src/test/java/com/mdh/user/shop/application/ShopServiceTest.java @@ -1,29 +1,32 @@ package com.mdh.user.shop.application; -import com.mdh.user.DataInitializerFactory; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -import com.mdh.common.shop.domain.*; +import com.mdh.common.shop.domain.ShopType; import com.mdh.common.shop.persistence.ShopRepository; +import com.mdh.common.shop.persistence.dto.ReservationShopSearchQueryDto; +import com.mdh.user.DataInitializerFactory; import com.mdh.user.shop.application.dto.ShopDetailInfoResponse; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; -import java.util.NoSuchElementException; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.given; @ExtendWith(MockitoExtension.class) -public class ShopServiceTest { +class ShopServiceTest { @InjectMocks private ShopService shopService; @@ -73,4 +76,54 @@ public void findShopDetailsById_ShouldReturnShopDetailInfoResponse_WhenShopExist mockShop.getShopDetails().getInfo() ); } + + @Test + @DisplayName("예약할 매장들을 필터 조회한다.") + void searchReservationShop() { + //given + var pageSize = 16; + var total = 50; + var pageRequest = PageRequest.of(0, pageSize, Sort.by("createdDate").ascending()); + var request = new ReservationShopSearchRequest(LocalDate.of(2023, 9, 17), + LocalTime.of(19, 0, 0), + 4, + "서울", + 10000, + 20000); + var shopPrice = DataInitializerFactory.shopPrice(); + var reservationShopSearchQueryDto1 = new ReservationShopSearchQueryDto(1L, + "식당 이름", + "식당 설명", + ShopType.KOREAN, + "서울", + "강남구", + shopPrice, + 3); + var reservationShopSearchQueryDto2 = new ReservationShopSearchQueryDto(2L, + "식당 이름", + "식당 설명", + ShopType.AMERICAN, + "서울", + "강남구", + shopPrice, + 3); + var reservationShopQueryDtos = List.of(reservationShopSearchQueryDto1, reservationShopSearchQueryDto2); + var page = new PageImpl<>(reservationShopQueryDtos, pageRequest, total); + + given(shopRepository.searchReservationShopByFilter(any(Pageable.class), + any(LocalDate.class), + any(LocalTime.class), + anyInt(), + anyString(), + anyInt(), + anyInt())).willReturn(page); + + //when + var reservationShopSearchResponse = shopService.searchReservationShop(pageRequest, request); + + //then + assertThat(reservationShopSearchResponse.totalPages()).isEqualTo(total / pageSize + 1); + assertThat(reservationShopSearchResponse.hasNext()).isTrue(); + assertThat(reservationShopSearchResponse.shops()).hasSize(2); + } } \ No newline at end of file From 33d96d132995ace33103607c888eefd83fbfed23 Mon Sep 17 00:00:00 2001 From: heenahan Date: Tue, 19 Sep 2023 14:24:02 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[MDH-78]=20feat=20:=20=EC=98=88=EC=95=BD=20?= =?UTF-8?q?=EB=A7=A4=EC=9E=A5=20=ED=95=84=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ShopController.java | 33 +++++++++++++++++++ .../dto/ReservationShopSearchRequest.java | 33 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 user/src/main/java/com/mdh/user/shop/presentation/controller/ShopController.java create mode 100644 user/src/main/java/com/mdh/user/shop/presentation/controller/dto/ReservationShopSearchRequest.java diff --git a/user/src/main/java/com/mdh/user/shop/presentation/controller/ShopController.java b/user/src/main/java/com/mdh/user/shop/presentation/controller/ShopController.java new file mode 100644 index 00000000..6993dfc2 --- /dev/null +++ b/user/src/main/java/com/mdh/user/shop/presentation/controller/ShopController.java @@ -0,0 +1,33 @@ +package com.mdh.user.shop.presentation.controller; + +import com.mdh.user.global.ApiResponse; +import com.mdh.user.shop.application.ShopService; +import com.mdh.user.shop.application.dto.ReservationShopSearchResponse; +import com.mdh.user.shop.presentation.controller.dto.ReservationShopSearchRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/customer/v1/shops") +public class ShopController { + + private final ShopService shopService; + + @GetMapping("/reservations") + public ResponseEntity> searchReservationShops( + @PageableDefault(sort = "createdDate", direction = Sort.Direction.ASC) Pageable pageable, + @RequestParam Map params) { + var reservationShopSearchResponse = shopService.searchReservationShop(pageable, ReservationShopSearchRequest.of(params)); + return ResponseEntity.ok(ApiResponse.ok(reservationShopSearchResponse)); + } +} \ No newline at end of file diff --git a/user/src/main/java/com/mdh/user/shop/presentation/controller/dto/ReservationShopSearchRequest.java b/user/src/main/java/com/mdh/user/shop/presentation/controller/dto/ReservationShopSearchRequest.java new file mode 100644 index 00000000..a8cc6696 --- /dev/null +++ b/user/src/main/java/com/mdh/user/shop/presentation/controller/dto/ReservationShopSearchRequest.java @@ -0,0 +1,33 @@ +package com.mdh.user.shop.presentation.controller.dto; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Map; + +public record ReservationShopSearchRequest( + + @JsonSerialize(using = LocalDateSerializer.class) + @JsonDeserialize(using = LocalDateDeserializer.class) + LocalDate reservationDate, + + @JsonSerialize(using = LocalTimeSerializer.class) + @JsonDeserialize(using = LocalTimeDeserializer.class) + LocalTime reservationTime, + Integer personCount, + String region, + Integer minPrice, + Integer maxPrice +) { + public static ReservationShopSearchRequest of(Map param) { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.convertValue(param, ReservationShopSearchRequest.class); + } +} \ No newline at end of file From 54b975d47ae9b602e22e9c45a01ace6504000862 Mon Sep 17 00:00:00 2001 From: heenahan Date: Tue, 19 Sep 2023 15:06:05 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[MDH-78]=20feat=20&=20test=20:=20=EA=B0=80?= =?UTF-8?q?=EA=B2=A9=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EC=BF=BC=EB=A6=AC=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shop/persistence/ShopRepositoryImpl.java | 116 +++++++++++++++++- .../shop/persistence/ShopRepositoryTest.java | 37 +++++- 2 files changed, 148 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryImpl.java b/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryImpl.java index 72c22f13..529e820b 100644 --- a/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryImpl.java +++ b/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryImpl.java @@ -1,23 +1,35 @@ package com.mdh.common.shop.persistence; +import com.mdh.common.reservation.domain.SeatStatus; +import com.mdh.common.shop.domain.Shop; import com.mdh.common.shop.domain.ShopType; +import com.mdh.common.shop.persistence.dto.ReservationShopSearchQueryDto; import com.mdh.common.shop.persistence.dto.ShopQueryDto; import com.mdh.common.shop.persistence.dto.ShopSearchCondParam; import com.mdh.common.waiting.domain.ShopWaitingStatus; import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Order; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Projections; -import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.*; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; import org.springframework.stereotype.Repository; import org.springframework.util.MultiValueMap; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static com.mdh.common.reservation.QShopReservation.shopReservation; +import static com.mdh.common.reservation.QShopReservationDateTime.shopReservationDateTime; +import static com.mdh.common.reservation.QShopReservationDateTimeSeat.shopReservationDateTimeSeat; import static com.mdh.common.shop.domain.QRegion.region; import static com.mdh.common.shop.domain.QShop.shop; import static com.mdh.common.shop.persistence.dto.ShopSearchCondParam.*; @@ -64,6 +76,108 @@ public JPAQuery searchShopConditionCount(MultiValueMap con .where(shopQueryDynamicCond(cond)); } + @Override + public Page searchReservationShopByFilter(Pageable pageable, + LocalDate reservationDate, + LocalTime reservationTime, + Integer personCount, + String regionName, + Integer minPrice, + Integer maxPrice) { + var content = jpaQueryFactory + .select(Projections.constructor(ReservationShopSearchQueryDto.class, + shop.id, + shop.name, + shop.description, + shop.shopType, + region.city, + region.district, + shop.shopPrice, + seatStatusCaseBuilder().sum().as("availableSeatCount") + )) + .from(shopReservationDateTimeSeat) + .join(shopReservationDateTime) + .on(shopReservationDateTimeSeat.shopReservationDateTime.id.eq(shopReservationDateTime.id)) + .where(shopReservationDateTime.reservationDate.eq(reservationDate) + .and(shopReservationDateTime.reservationTime.eq(reservationTime))) + .join(shopReservation) + .on(shopReservation.shopId.eq(shopReservationDateTime.shopReservation.shopId)) + .where(personLoeGoe(personCount)) + .join(shop) + .on(shopReservation.shopId.eq(shop.id)) + .join(region) + .on(shop.region.id.eq(region.id)) + .where(regionContains(regionName)) + .where(priceLoeGoe(minPrice, maxPrice)) + .groupBy(shop.id) // 아이디로 group by + .orderBy(getOrderSpecifiers(pageable)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + var totalCount = jpaQueryFactory + .select(shop.countDistinct()) + .from(shopReservationDateTimeSeat) + .join(shopReservationDateTime) + .on(shopReservationDateTimeSeat.shopReservationDateTime.id.eq(shopReservationDateTime.id)) + .where(shopReservationDateTime.reservationDate.eq(reservationDate) + .and(shopReservationDateTime.reservationTime.eq(reservationTime))) + .join(shopReservation) + .on(shopReservation.shopId.eq(shopReservationDateTime.shopReservation.shopId)) + .where(personLoeGoe(personCount)) + .join(shop) + .on(shopReservation.shopId.eq(shop.id)) + .join(region) + .on(shop.region.id.eq(region.id)) + .where(regionContains(regionName)) + .where(priceLoeGoe(minPrice, maxPrice)); + + return PageableExecutionUtils.getPage(content, pageable, totalCount::fetchOne); + } + + private OrderSpecifier[] getOrderSpecifiers(Pageable pageable) { + var sort = pageable.getSort(); + var orderSpecifiers = new ArrayList<>(); + orderSpecifiers.add(new OrderSpecifier<>(Order.DESC, Expressions.stringPath("availableSeatCount"))); + var shopOrderSpecifiers = sort.get().map(o -> { + Order order = o.isAscending() ? Order.ASC : Order.DESC; + String property = o.getProperty(); + PathBuilder pathBuilder = new PathBuilder<>(Shop.class, "shop"); + return new OrderSpecifier(order, pathBuilder.getString(property)); + }).toList(); + orderSpecifiers.addAll(shopOrderSpecifiers); + return orderSpecifiers.stream().toArray(OrderSpecifier[]::new); + } + + private NumberExpression seatStatusCaseBuilder() { + return new CaseBuilder() + .when(shopReservationDateTimeSeat.seatStatus.eq(SeatStatus.AVAILABLE)) + .then(1) + .otherwise(0); + } + + private BooleanExpression personLoeGoe(Integer personCount) { + return personCount != null ? shopReservation.minimumPerson.loe(personCount) + .and(shopReservation.maximumPerson.goe(personCount)) : null; + } + + private BooleanExpression regionContains(String regionName) { + return regionName != null ? region.city.contains(regionName).or(region.district.contains(regionName)) : null; + } + + private BooleanExpression priceLoeGoe(Integer minPrice, Integer maxPrice) { + if (minPrice == null) return maxPriceGoe(maxPrice); + return minPriceLoe(minPrice).and(maxPriceGoe(maxPrice)); + } + + private BooleanExpression minPriceLoe(Integer minPrice) { + return minPrice != null ? shop.shopPrice.shopMinPrice.loe(minPrice) : null; + } + + private BooleanExpression maxPriceGoe(Integer maxPrice) { + return maxPrice != null ? shop.shopPrice.shopMaxPrice.goe(maxPrice) : null; + } + private BooleanBuilder shopQueryDynamicCond(final MultiValueMap cond) { var booleanBuilder = new BooleanBuilder(); diff --git a/common/src/test/java/com/mdh/common/shop/persistence/ShopRepositoryTest.java b/common/src/test/java/com/mdh/common/shop/persistence/ShopRepositoryTest.java index cec40310..3e2716e7 100644 --- a/common/src/test/java/com/mdh/common/shop/persistence/ShopRepositoryTest.java +++ b/common/src/test/java/com/mdh/common/shop/persistence/ShopRepositoryTest.java @@ -1,6 +1,7 @@ package com.mdh.common.shop.persistence; import com.mdh.common.DataInitializerFactory; +import com.mdh.common.menu.domain.MealType; import com.mdh.common.reservation.persistence.*; import com.mdh.common.user.persistence.UserRepository; import org.junit.jupiter.api.DisplayName; @@ -120,11 +121,34 @@ void findAvailableReservationShopRegionFilterTest() { assertThat(reservationAvailableShop.getContent()).hasSize(2); } + @Test + @DisplayName("특정 시간과 날짜에 예약 가능한 매장을 가격을 기준으로 필터 조회한다.") + void findAvailableReservationShopPriceFilterTest() { + //given + shopsAndShopReservations(); + + var pageRequest = PageRequest.of(0, 2, Sort.by(Sort.Order.asc("createdDate"))); + + //when + var reservationAvailableShop = shopRepository.searchReservationShopByFilter(pageRequest, + LocalDate.of(2023, 9, 17), + LocalTime.of(19, 0, 0), + null, + null, + 30000, + 30000); + + //then + assertThat(reservationAvailableShop.getTotalElements()).isEqualTo(1); + assertThat(reservationAvailableShop.getTotalPages()).isEqualTo(1); + assertThat(reservationAvailableShop.getContent()).hasSize(1); + } + /** - * shop1 -> 2023.9.17 오후 7시 예약 가능 테이블 1개, (서울, 강남구), 인원 2-5 - * shop2 -> 2023.9.17 오후 7시 예약 불가능, (서울, 청담), 인원 2-5 - * shop3 -> 2023.9.17 오후 8시 예약 가능, (서울, 강남구), 인원 2-7 - * shop4 -> 2023.9.17 오후 7시 예약 가능 테이블 2개, (서울, 청담), 인원 2-7 + * shop1 -> 2023.9.17 오후 7시 예약 가능 테이블 1개, (서울, 강남구), 인원 2-5, 1만원 + * shop2 -> 2023.9.17 오후 7시 예약 불가능, (서울, 청담), 인원 2-5, 1만원 + * shop3 -> 2023.9.17 오후 8시 예약 가능, (서울, 강남구), 인원 2-7, 3만원 + * shop4 -> 2023.9.17 오후 7시 예약 가능 테이블 2개, (서울, 청담), 인원 2-7, 3만원 */ private void shopsAndShopReservations() { var owner = DataInitializerFactory.owner(); @@ -144,6 +168,11 @@ private void shopsAndShopReservations() { var shop4 = DataInitializerFactory.shop(owner.getId(), shopDetails, region2, shopAddress); shopRepository.saveAll(List.of(shop1, shop2, shop3, shop4)); + shop1.getShopPrice().updatePrice(MealType.LUNCH, 10000); + shop2.getShopPrice().updatePrice(MealType.LUNCH, 10000); + shop3.getShopPrice().updatePrice(MealType.LUNCH, 30000); + shop4.getShopPrice().updatePrice(MealType.LUNCH, 30000); + var shopReservation1 = DataInitializerFactory.shopReservation(shop1.getId(), 2, 5); var shopReservation2 = DataInitializerFactory.shopReservation(shop2.getId(), 2, 5); var shopReservation3 = DataInitializerFactory.shopReservation(shop3.getId(), 2, 7); From b2c0f16edc7ab304173b2807ebf4ce715e884277 Mon Sep 17 00:00:00 2001 From: heenahan Date: Wed, 20 Sep 2023 00:35:04 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[MDH-78]=20refactor=20:=20BooleanBuilder=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9C=BC=EB=A1=9C=20where=EC=A0=88=20?= =?UTF-8?q?=EA=B0=84=EB=9E=B5=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shop/persistence/ShopRepositoryImpl.java | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryImpl.java b/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryImpl.java index 529e820b..864d5ea2 100644 --- a/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryImpl.java +++ b/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryImpl.java @@ -14,6 +14,7 @@ import com.querydsl.core.types.dsl.*; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -78,8 +79,8 @@ public JPAQuery searchShopConditionCount(MultiValueMap con @Override public Page searchReservationShopByFilter(Pageable pageable, - LocalDate reservationDate, - LocalTime reservationTime, + @NonNull LocalDate reservationDate, + @NonNull LocalTime reservationTime, Integer personCount, String regionName, Integer minPrice, @@ -98,17 +99,13 @@ public Page searchReservationShopByFilter(Pageabl .from(shopReservationDateTimeSeat) .join(shopReservationDateTime) .on(shopReservationDateTimeSeat.shopReservationDateTime.id.eq(shopReservationDateTime.id)) - .where(shopReservationDateTime.reservationDate.eq(reservationDate) - .and(shopReservationDateTime.reservationTime.eq(reservationTime))) .join(shopReservation) .on(shopReservation.shopId.eq(shopReservationDateTime.shopReservation.shopId)) - .where(personLoeGoe(personCount)) .join(shop) .on(shopReservation.shopId.eq(shop.id)) .join(region) .on(shop.region.id.eq(region.id)) - .where(regionContains(regionName)) - .where(priceLoeGoe(minPrice, maxPrice)) + .where(booleanBuilder(reservationDate, reservationTime, personCount, regionName, minPrice, maxPrice)) .groupBy(shop.id) // 아이디로 group by .orderBy(getOrderSpecifiers(pageable)) .offset(pageable.getOffset()) @@ -120,17 +117,13 @@ public Page searchReservationShopByFilter(Pageabl .from(shopReservationDateTimeSeat) .join(shopReservationDateTime) .on(shopReservationDateTimeSeat.shopReservationDateTime.id.eq(shopReservationDateTime.id)) - .where(shopReservationDateTime.reservationDate.eq(reservationDate) - .and(shopReservationDateTime.reservationTime.eq(reservationTime))) .join(shopReservation) .on(shopReservation.shopId.eq(shopReservationDateTime.shopReservation.shopId)) - .where(personLoeGoe(personCount)) .join(shop) .on(shopReservation.shopId.eq(shop.id)) .join(region) .on(shop.region.id.eq(region.id)) - .where(regionContains(regionName)) - .where(priceLoeGoe(minPrice, maxPrice)); + .where(booleanBuilder(reservationDate, reservationTime, personCount, regionName, minPrice, maxPrice)); return PageableExecutionUtils.getPage(content, pageable, totalCount::fetchOne); } @@ -156,6 +149,24 @@ private NumberExpression seatStatusCaseBuilder() { .otherwise(0); } + private BooleanBuilder booleanBuilder(LocalDate reservationDate, + LocalTime reservationTime, + Integer personCount, + String regionName, + Integer minPrice, + Integer maxPrice) { + return new BooleanBuilder().and(reservationDateTimeEq(reservationDate, reservationTime)) + .and(personLoeGoe(personCount)) + .and(regionContains(regionName)) + .and(priceLoeGoe(minPrice, maxPrice)); + } + + private BooleanExpression reservationDateTimeEq(LocalDate reservationDate, LocalTime reservationTime) { + return shopReservationDateTime.reservationDate.eq(reservationDate).and( + shopReservationDateTime.reservationTime.eq(reservationTime) + ); + } + private BooleanExpression personLoeGoe(Integer personCount) { return personCount != null ? shopReservation.minimumPerson.loe(personCount) .and(shopReservation.maximumPerson.goe(personCount)) : null; From 730b3f7e992383e8eb95075a6b1c14a2ed3f9cc6 Mon Sep 17 00:00:00 2001 From: heenahan Date: Wed, 20 Sep 2023 14:16:26 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[MDH-78]=20docs=20:=20rest=20docs=EC=9D=98?= =?UTF-8?q?=20index.html=20gitignore=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ed28a93d..57fb779d 100644 --- a/.gitignore +++ b/.gitignore @@ -284,6 +284,7 @@ owner/src/main/resources user/src/main/resources ### ascii-doc ### -src/main/resources/static/docs/index.html +owner/src/main/resources/static/docs/owner-index.html +user/src/main/resources/static/docs/user-index.html # End of https://www.toptal.com/developers/gitignore/api/intellij,macos,java,gradle \ No newline at end of file From 9bb804899deb0384c1dfe1ea54e79791deb23ff1 Mon Sep 17 00:00:00 2001 From: heenahan Date: Wed, 20 Sep 2023 15:14:03 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[MDH-109]=20fix=20:=20=EC=B6=A9=EB=8F=8C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mdh/common/global/config/JpaConfig.java | 8 - .../shop/persistence/ShopQueryRepository.java | 18 --- .../persistence/ShopQueryRepositoryImpl.java | 140 ------------------ .../persistence/ShopRepositoryCustom.java | 13 ++ .../shop/persistence/ShopRepositoryImpl.java | 6 +- .../user/shop/application/ShopService.java | 1 + .../shop/presentation/ShopController.java | 14 ++ .../controller/ShopController.java | 33 ----- .../dto/ReservationShopSearchRequest.java | 2 +- .../shop/application/ShopServiceTest.java | 1 + 10 files changed, 33 insertions(+), 203 deletions(-) delete mode 100644 common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepository.java delete mode 100644 common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepositoryImpl.java delete mode 100644 user/src/main/java/com/mdh/user/shop/presentation/controller/ShopController.java rename user/src/main/java/com/mdh/user/shop/presentation/{controller => }/dto/ReservationShopSearchRequest.java (95%) diff --git a/common/src/main/java/com/mdh/common/global/config/JpaConfig.java b/common/src/main/java/com/mdh/common/global/config/JpaConfig.java index 5ec2bff4..fbf36cbb 100644 --- a/common/src/main/java/com/mdh/common/global/config/JpaConfig.java +++ b/common/src/main/java/com/mdh/common/global/config/JpaConfig.java @@ -1,9 +1,6 @@ package com.mdh.common.global.config; -import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.annotation.PostConstruct; -import jakarta.persistence.EntityManager; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @@ -17,9 +14,4 @@ public class JpaConfig { public void setTimeZone() { TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); } - - @Bean - public JPAQueryFactory jpaQueryFactory(EntityManager em) { - return new JPAQueryFactory(em); - } } \ No newline at end of file diff --git a/common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepository.java b/common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepository.java deleted file mode 100644 index db332cb3..00000000 --- a/common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.mdh.common.shop.persistence; - -import com.mdh.common.shop.persistence.dto.ReservationShopSearchQueryDto; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - -import java.time.LocalDate; -import java.time.LocalTime; - -public interface ShopQueryRepository { - Page searchReservationShopByFilter(Pageable pageable, - LocalDate reservationDate, - LocalTime reservationTime, - Integer personCount, - String regionName, - Integer minPrice, - Integer maxPrice); -} \ No newline at end of file diff --git a/common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepositoryImpl.java b/common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepositoryImpl.java deleted file mode 100644 index dbdad074..00000000 --- a/common/src/main/java/com/mdh/common/shop/persistence/ShopQueryRepositoryImpl.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.mdh.common.shop.persistence; - -import com.mdh.common.shop.domain.Shop; -import com.mdh.common.shop.persistence.dto.ReservationShopSearchQueryDto; -import com.querydsl.core.types.Order; -import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.core.types.Projections; -import com.querydsl.core.types.dsl.*; -import com.querydsl.jpa.impl.JPAQueryFactory; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.support.PageableExecutionUtils; - -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.ArrayList; - -import static com.mdh.common.reservation.QShopReservation.shopReservation; -import static com.mdh.common.reservation.QShopReservationDateTime.shopReservationDateTime; -import static com.mdh.common.reservation.QShopReservationDateTimeSeat.shopReservationDateTimeSeat; -import static com.mdh.common.shop.domain.QRegion.region; -import static com.mdh.common.shop.domain.QShop.shop; - -@RequiredArgsConstructor -public class ShopQueryRepositoryImpl implements ShopQueryRepository { - - private final JPAQueryFactory jpaQueryFactory; - - /** - * 보여줄 것? 매장 정보, 예약 날짜, 날짜의 가능한 좌석 수 - * 매장 - * join (매장 예약 날짜 join 매장 예약 좌석 where 날짜 = 날짜 & 시간 = 시간) - * group by 매장 - * order by 매장 예약 가능한 좌석 수(1) - */ - @Override - public Page searchReservationShopByFilter(Pageable pageable, - LocalDate reservationDate, - LocalTime reservationTime, - Integer personCount, - String regionName, - Integer minPrice, - Integer maxPrice) { - var content = jpaQueryFactory - .select(Projections.constructor(ReservationShopSearchQueryDto.class, - shop.id, - shop.name, - shop.description, - shop.shopType, - region.city, - region.district, - shop.shopPrice, - seatStatusCaseBuilder().sum().as("availableSeatCount") - )) - .from(shopReservationDateTimeSeat) - .join(shopReservationDateTime) - .on(shopReservationDateTimeSeat.shopReservationDateTime.id.eq(shopReservationDateTime.id)) - .where(shopReservationDateTime.reservationDate.eq(reservationDate) - .and(shopReservationDateTime.reservationTime.eq(reservationTime))) - .join(shopReservation) - .on(shopReservation.shopId.eq(shopReservationDateTime.shopReservation.shopId)) - .where(personLoeGoe(personCount)) - .join(shop) - .on(shopReservation.shopId.eq(shop.id)) - .join(region) - .on(shop.region.id.eq(region.id)) - .where(regionContains(regionName)) - .where(priceLoeGoe(minPrice, maxPrice)) - .groupBy(shop.id) // 아이디로 group by - .orderBy(getOrderSpecifiers(pageable)) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); - - var totalCount = jpaQueryFactory - .select(shop.countDistinct()) - .from(shopReservationDateTimeSeat) - .join(shopReservationDateTime) - .on(shopReservationDateTimeSeat.shopReservationDateTime.id.eq(shopReservationDateTime.id)) - .where(shopReservationDateTime.reservationDate.eq(reservationDate) - .and(shopReservationDateTime.reservationTime.eq(reservationTime))) - .join(shopReservation) - .on(shopReservation.shopId.eq(shopReservationDateTime.shopReservation.shopId)) - .where(personLoeGoe(personCount)) - .join(shop) - .on(shopReservation.shopId.eq(shop.id)) - .join(region) - .on(shop.region.id.eq(region.id)) - .where(regionContains(regionName)) - .where(priceLoeGoe(minPrice, maxPrice)); - - return PageableExecutionUtils.getPage(content, pageable, totalCount::fetchOne); - } - - private OrderSpecifier[] getOrderSpecifiers(Pageable pageable) { - var sort = pageable.getSort(); - var orderSpecifiers = new ArrayList<>(); - orderSpecifiers.add(new OrderSpecifier<>(Order.DESC, Expressions.stringPath("availableSeatCount"))); - var shopOrderSpecifiers = sort.get().map(o -> { - Order order = o.isAscending() ? Order.ASC : Order.DESC; - String property = o.getProperty(); - PathBuilder pathBuilder = new PathBuilder<>(Shop.class, "shop"); - return new OrderSpecifier(order, pathBuilder.getString(property)); - }).toList(); - orderSpecifiers.addAll(shopOrderSpecifiers); - return orderSpecifiers.stream().toArray(OrderSpecifier[]::new); - } - - private NumberExpression seatStatusCaseBuilder() { - return new CaseBuilder() - .when(shopReservationDateTimeSeat.seatStatus.eq(SeatStatus.AVAILABLE)) - .then(1) - .otherwise(0); - } - - private BooleanExpression personLoeGoe(Integer personCount) { - return personCount != null ? shopReservation.minimumPerson.loe(personCount) - .and(shopReservation.maximumPerson.goe(personCount)) : null; - } - - private BooleanExpression regionContains(String regionName) { - return regionName != null ? region.city.contains(regionName).or(region.district.contains(regionName)) : null; - } - - private BooleanExpression priceLoeGoe(Integer minPrice, Integer maxPrice) { - if (minPrice == null) return maxPriceGoe(maxPrice); - return minPriceLoe(minPrice).and(maxPriceGoe(maxPrice)); - } - - private BooleanExpression minPriceLoe(Integer minPrice) { - return minPrice != null ? shop.shopPrice.lunchMinPrice.loe(minPrice) - .or(shop.shopPrice.dinnerMinPrice.loe(minPrice)) : null; - } - - private BooleanExpression maxPriceGoe(Integer maxPrice) { - return maxPrice != null ? shop.shopPrice.lunchMaxPrice.goe(maxPrice) - .or(shop.shopPrice.dinnerMaxPrice.goe(maxPrice)) : null; - } -} \ No newline at end of file diff --git a/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryCustom.java b/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryCustom.java index 674149d1..e856ed8b 100644 --- a/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryCustom.java +++ b/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryCustom.java @@ -1,10 +1,15 @@ package com.mdh.common.shop.persistence; +import com.mdh.common.shop.persistence.dto.ReservationShopSearchQueryDto; import com.mdh.common.shop.persistence.dto.ShopQueryDto; import com.querydsl.jpa.impl.JPAQuery; +import lombok.NonNull; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.util.MultiValueMap; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; public interface ShopRepositoryCustom { @@ -12,4 +17,12 @@ public interface ShopRepositoryCustom { List searchShopCondition(MultiValueMap cond, Pageable pageable); JPAQuery searchShopConditionCount(MultiValueMap cond); + + public Page searchReservationShopByFilter(Pageable pageable, + @NonNull LocalDate reservationDate, + @NonNull LocalTime reservationTime, + Integer personCount, + String regionName, + Integer minPrice, + Integer maxPrice); } diff --git a/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryImpl.java b/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryImpl.java index 864d5ea2..f2af638a 100644 --- a/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryImpl.java +++ b/common/src/main/java/com/mdh/common/shop/persistence/ShopRepositoryImpl.java @@ -28,9 +28,9 @@ import java.util.Collections; import java.util.List; -import static com.mdh.common.reservation.QShopReservation.shopReservation; -import static com.mdh.common.reservation.QShopReservationDateTime.shopReservationDateTime; -import static com.mdh.common.reservation.QShopReservationDateTimeSeat.shopReservationDateTimeSeat; +import static com.mdh.common.reservation.domain.QShopReservation.shopReservation; +import static com.mdh.common.reservation.domain.QShopReservationDateTime.shopReservationDateTime; +import static com.mdh.common.reservation.domain.QShopReservationDateTimeSeat.shopReservationDateTimeSeat; import static com.mdh.common.shop.domain.QRegion.region; import static com.mdh.common.shop.domain.QShop.shop; import static com.mdh.common.shop.persistence.dto.ShopSearchCondParam.*; diff --git a/user/src/main/java/com/mdh/user/shop/application/ShopService.java b/user/src/main/java/com/mdh/user/shop/application/ShopService.java index b713bc38..ebc53f39 100644 --- a/user/src/main/java/com/mdh/user/shop/application/ShopService.java +++ b/user/src/main/java/com/mdh/user/shop/application/ShopService.java @@ -6,6 +6,7 @@ import com.mdh.user.shop.application.dto.ShopDetailInfoResponse; import com.mdh.user.shop.application.dto.ShopResponse; import com.mdh.user.shop.application.dto.ShopResponses; +import com.mdh.user.shop.presentation.dto.ReservationShopSearchRequest; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.support.PageableExecutionUtils; diff --git a/user/src/main/java/com/mdh/user/shop/presentation/ShopController.java b/user/src/main/java/com/mdh/user/shop/presentation/ShopController.java index 8a2feac1..979dd499 100644 --- a/user/src/main/java/com/mdh/user/shop/presentation/ShopController.java +++ b/user/src/main/java/com/mdh/user/shop/presentation/ShopController.java @@ -2,15 +2,21 @@ import com.mdh.user.global.ApiResponse; import com.mdh.user.shop.application.ShopService; +import com.mdh.user.shop.application.dto.ReservationShopSearchResponse; import com.mdh.user.shop.application.dto.ShopDetailInfoResponse; import com.mdh.user.shop.application.dto.ShopResponses; +import com.mdh.user.shop.presentation.dto.ReservationShopSearchRequest; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.*; +import java.util.Map; + @RequiredArgsConstructor @RestController @RequestMapping("/api/customer/v1/shops") @@ -27,6 +33,14 @@ public ResponseEntity> findByConditionWithWaiting( return new ResponseEntity<>(ApiResponse.ok(result), HttpStatus.OK); } + @GetMapping("/reservations") + public ResponseEntity> searchReservationShops( + @PageableDefault(sort = "createdDate", direction = Sort.Direction.ASC) Pageable pageable, + @RequestParam Map params) { + var reservationShopSearchResponse = shopService.searchReservationShop(pageable, ReservationShopSearchRequest.of(params)); + return ResponseEntity.ok(ApiResponse.ok(reservationShopSearchResponse)); + } + @GetMapping("/{shopId}") public ResponseEntity> findShopDetailsById(@PathVariable("shopId") Long shopId) { var result = shopService.findShopDetailsById(shopId); diff --git a/user/src/main/java/com/mdh/user/shop/presentation/controller/ShopController.java b/user/src/main/java/com/mdh/user/shop/presentation/controller/ShopController.java deleted file mode 100644 index 6993dfc2..00000000 --- a/user/src/main/java/com/mdh/user/shop/presentation/controller/ShopController.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.mdh.user.shop.presentation.controller; - -import com.mdh.user.global.ApiResponse; -import com.mdh.user.shop.application.ShopService; -import com.mdh.user.shop.application.dto.ReservationShopSearchResponse; -import com.mdh.user.shop.presentation.controller.dto.ReservationShopSearchRequest; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.web.PageableDefault; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Map; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/customer/v1/shops") -public class ShopController { - - private final ShopService shopService; - - @GetMapping("/reservations") - public ResponseEntity> searchReservationShops( - @PageableDefault(sort = "createdDate", direction = Sort.Direction.ASC) Pageable pageable, - @RequestParam Map params) { - var reservationShopSearchResponse = shopService.searchReservationShop(pageable, ReservationShopSearchRequest.of(params)); - return ResponseEntity.ok(ApiResponse.ok(reservationShopSearchResponse)); - } -} \ No newline at end of file diff --git a/user/src/main/java/com/mdh/user/shop/presentation/controller/dto/ReservationShopSearchRequest.java b/user/src/main/java/com/mdh/user/shop/presentation/dto/ReservationShopSearchRequest.java similarity index 95% rename from user/src/main/java/com/mdh/user/shop/presentation/controller/dto/ReservationShopSearchRequest.java rename to user/src/main/java/com/mdh/user/shop/presentation/dto/ReservationShopSearchRequest.java index a8cc6696..7577e477 100644 --- a/user/src/main/java/com/mdh/user/shop/presentation/controller/dto/ReservationShopSearchRequest.java +++ b/user/src/main/java/com/mdh/user/shop/presentation/dto/ReservationShopSearchRequest.java @@ -1,4 +1,4 @@ -package com.mdh.user.shop.presentation.controller.dto; +package com.mdh.user.shop.presentation.dto; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/user/src/test/java/com/mdh/user/shop/application/ShopServiceTest.java b/user/src/test/java/com/mdh/user/shop/application/ShopServiceTest.java index d4ac259f..fad91feb 100644 --- a/user/src/test/java/com/mdh/user/shop/application/ShopServiceTest.java +++ b/user/src/test/java/com/mdh/user/shop/application/ShopServiceTest.java @@ -5,6 +5,7 @@ import com.mdh.common.shop.persistence.dto.ReservationShopSearchQueryDto; import com.mdh.user.DataInitializerFactory; import com.mdh.user.shop.application.dto.ShopDetailInfoResponse; +import com.mdh.user.shop.presentation.dto.ReservationShopSearchRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith;