From 01c81a6ccad8a88a501c516a3cc1388a7bb15bae Mon Sep 17 00:00:00 2001 From: Parkjyun <98092394+Parkjyun@users.noreply.github.com> Date: Wed, 17 Jul 2024 20:12:11 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20=EC=A7=80=EB=8F=84=EB=B7=B0=20?= =?UTF-8?q?=ED=95=80=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EC=8B=9D=EB=8B=B9?= =?UTF-8?q?=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20api=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#117)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [dependency] add querydsl dependency * [chore] add Qclass path in .gitignore * [fix] add minPrice in PriceCategory.java * [refac] delete unused method in Store.java * [feat] create QueryDslConfig.java * [feat] create dtos for getting pins and list of stores * [feat] create repository for dynamic query when searching stores * [feat] create service layer for get stores pins and list api * [feat] create get stores pins and list api * [fix] fix error from rebase --- .gitignore | 3 + build.gradle | 15 ++++ .../api/store/controller/StoreController.java | 22 ++++-- .../api/store/parameter/PriceCategory.java | 7 +- .../api/store/service/StoreFinder.java | 8 ++ .../api/store/service/StoreQueryService.java | 12 +++ .../store/service/response/PinResponse.java | 14 ++++ .../service/response/StorePinsResponse.java | 10 +++ .../store/service/response/StoreResponse.java | 15 ++++ .../service/response/StoresResponse.java | 9 +++ .../response/UserStoreReportedResponse.java | 2 +- .../domain/config/QueryDslConfig.java | 19 +++++ .../domain/store/model/Store.java | 7 -- .../repository/CustomStoreRepository.java | 12 +++ .../repository/CustomStoreRepositoryImpl.java | 76 +++++++++++++++++++ .../store/repository/StoreRepository.java | 2 +- 16 files changed, 216 insertions(+), 17 deletions(-) create mode 100644 src/main/java/org/hankki/hankkiserver/api/store/service/response/PinResponse.java create mode 100644 src/main/java/org/hankki/hankkiserver/api/store/service/response/StorePinsResponse.java create mode 100644 src/main/java/org/hankki/hankkiserver/api/store/service/response/StoreResponse.java create mode 100644 src/main/java/org/hankki/hankkiserver/api/store/service/response/StoresResponse.java create mode 100644 src/main/java/org/hankki/hankkiserver/domain/config/QueryDslConfig.java create mode 100644 src/main/java/org/hankki/hankkiserver/domain/store/repository/CustomStoreRepository.java create mode 100644 src/main/java/org/hankki/hankkiserver/domain/store/repository/CustomStoreRepositoryImpl.java diff --git a/.gitignore b/.gitignore index e56e1c14..44abcc58 100644 --- a/.gitignore +++ b/.gitignore @@ -153,3 +153,6 @@ Temporary Items application-local.yml application-dev.yml + +# Qclass +/src/main/generated diff --git a/build.gradle b/build.gradle index 38cfa7b2..977947ea 100644 --- a/build.gradle +++ b/build.gradle @@ -62,12 +62,27 @@ dependencies { // AWS implementation("software.amazon.awssdk:s3:2.21.0") implementation("software.amazon.awssdk:bom:2.21.0") + + // QueryDSL + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" } tasks.named('test') { useJUnitPlatform() } +def generated = 'src/main/generated' +tasks.withType(JavaCompile).configureEach { + options.getGeneratedSourceOutputDirectory().set(file(generated)) +} + +clean { + delete file(generated) +} + tasks.register('copyYml', Copy) { copy { from './server-yml' diff --git a/src/main/java/org/hankki/hankkiserver/api/store/controller/StoreController.java b/src/main/java/org/hankki/hankkiserver/api/store/controller/StoreController.java index 96051604..ab041899 100644 --- a/src/main/java/org/hankki/hankkiserver/api/store/controller/StoreController.java +++ b/src/main/java/org/hankki/hankkiserver/api/store/controller/StoreController.java @@ -5,21 +5,19 @@ import org.hankki.hankkiserver.api.dto.HankkiResponse; import org.hankki.hankkiserver.api.store.controller.request.StoreDuplicateValidationRequest; import org.hankki.hankkiserver.api.store.controller.request.StorePostRequest; +import org.hankki.hankkiserver.api.store.parameter.PriceCategory; +import org.hankki.hankkiserver.api.store.parameter.SortOption; import org.hankki.hankkiserver.api.store.service.StoreCommandService; import org.hankki.hankkiserver.api.store.service.StoreQueryService; import org.hankki.hankkiserver.api.store.service.command.StorePostCommand; import org.hankki.hankkiserver.api.store.service.response.*; import org.hankki.hankkiserver.common.code.CommonSuccessCode; +import org.hankki.hankkiserver.domain.store.model.StoreCategory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.hankki.hankkiserver.auth.UserId; import org.hankki.hankkiserver.api.store.service.HeartCommandService; -import org.hankki.hankkiserver.api.store.service.StoreQueryService; -import org.hankki.hankkiserver.api.store.service.command.StorePostCommand; import org.hankki.hankkiserver.api.store.service.command.StoreValidationCommand; -import org.hankki.hankkiserver.api.store.service.response.*; -import org.hankki.hankkiserver.auth.UserId; -import org.hankki.hankkiserver.common.code.CommonSuccessCode; import org.hankki.hankkiserver.api.store.service.command.HeartDeleteCommand; import org.hankki.hankkiserver.api.store.service.command.HeartPostCommand; import org.springframework.web.bind.annotation.PostMapping; @@ -43,6 +41,20 @@ public HankkiResponse getStore(@PathVariable final Long storeI return HankkiResponse.success(CommonSuccessCode.OK, storeQueryService.getStoreInformation(storeId, userId)); } + @GetMapping("/stores/pins") + public HankkiResponse getStorePins(@RequestParam(required = false) final Long universityId, + @RequestParam(required = false) final StoreCategory storeCategory, + @RequestParam(required = false) final PriceCategory priceCategory, + @RequestParam(required = false) final SortOption sortOption) { + return HankkiResponse.success(CommonSuccessCode.OK, storeQueryService.getStorePins(universityId, storeCategory, priceCategory, sortOption)); + } + @GetMapping("/stores") + public HankkiResponse getStores(@RequestParam(required = false) final Long universityId, + @RequestParam(required = false) final StoreCategory storeCategory, + @RequestParam(required = false) final PriceCategory priceCategory, + @RequestParam(required = false) final SortOption sortOption) { + return HankkiResponse.success(CommonSuccessCode.OK, storeQueryService.getStores(universityId, storeCategory, priceCategory, sortOption)); + } @GetMapping("/stores/{id}/thumbnail") public HankkiResponse getStoreThumbnail(@PathVariable final Long id) { return HankkiResponse.success(CommonSuccessCode.OK, storeQueryService.getStoreThumbnail(id)); diff --git a/src/main/java/org/hankki/hankkiserver/api/store/parameter/PriceCategory.java b/src/main/java/org/hankki/hankkiserver/api/store/parameter/PriceCategory.java index 0c932e28..d61b1d8b 100644 --- a/src/main/java/org/hankki/hankkiserver/api/store/parameter/PriceCategory.java +++ b/src/main/java/org/hankki/hankkiserver/api/store/parameter/PriceCategory.java @@ -7,10 +7,11 @@ @RequiredArgsConstructor public enum PriceCategory { - K6("6000원 이하", "6000"), - k8("6000~8000원", "8000"); + K6("6000원 이하", 6000, 0), + K8("6000~8000원", 8000, 6000); private final String name; - private final String maxPrice; + private final int maxPrice; + private final int minPrice; } diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/StoreFinder.java b/src/main/java/org/hankki/hankkiserver/api/store/service/StoreFinder.java index b1f92156..f5a1a368 100644 --- a/src/main/java/org/hankki/hankkiserver/api/store/service/StoreFinder.java +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/StoreFinder.java @@ -1,12 +1,16 @@ package org.hankki.hankkiserver.api.store.service; import lombok.RequiredArgsConstructor; +import org.hankki.hankkiserver.api.store.parameter.PriceCategory; +import org.hankki.hankkiserver.api.store.parameter.SortOption; import org.hankki.hankkiserver.common.code.StoreErrorCode; import org.hankki.hankkiserver.common.exception.NotFoundException; import org.hankki.hankkiserver.domain.store.model.Store; +import org.hankki.hankkiserver.domain.store.model.StoreCategory; import org.hankki.hankkiserver.domain.store.repository.StoreRepository; import org.springframework.stereotype.Component; +import java.util.List; import java.util.Optional; @Component @@ -35,4 +39,8 @@ protected Optional findByLatitudeAndLongitude(final double latitude, fina protected boolean existsByLatitudeAndLongitude(final double latitude, final double longitude) { return storeRepository.existsByPoint_LatitudeAndPoint_Longitude(latitude, longitude); } + + public List findAllDynamicQuery(final Long universityId, final StoreCategory storeCategory, final PriceCategory priceCategory, final SortOption sortOption) { + return storeRepository.findStoreByCategoryAndLowestPriceAndUniversityIdAndIsDeletedFalseOrderBySortOptions(storeCategory, priceCategory, universityId, sortOption); + } } diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/StoreQueryService.java b/src/main/java/org/hankki/hankkiserver/api/store/service/StoreQueryService.java index 6c960658..d52342a4 100644 --- a/src/main/java/org/hankki/hankkiserver/api/store/service/StoreQueryService.java +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/StoreQueryService.java @@ -44,6 +44,18 @@ public StoreGetResponse getStoreInformation(final Long storeId, final Long userI getMenus(store)); } + @Transactional(readOnly = true) + public StorePinsResponse getStorePins(final Long universityId, final StoreCategory storeCategory, final PriceCategory priceCategory, final SortOption sortOption) { + return StorePinsResponse.of(storeFinder.findAllDynamicQuery(universityId, storeCategory, priceCategory, sortOption) + .stream().map(PinResponse::of).toList()); + } + + @Transactional(readOnly = true) + public StoresResponse getStores(final Long universityId, final StoreCategory storeCategory, final PriceCategory priceCategory, final SortOption sortOption) { + return StoresResponse.of(storeFinder.findAllDynamicQuery(universityId, storeCategory, priceCategory, sortOption) + .stream().map(StoreResponse::of).toList()); + } + public CategoriesResponse getCategories() { return new CategoriesResponse(Arrays.stream(StoreCategory.values()) .map(CategoryResponse::of) diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/response/PinResponse.java b/src/main/java/org/hankki/hankkiserver/api/store/service/response/PinResponse.java new file mode 100644 index 00000000..07c37b35 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/response/PinResponse.java @@ -0,0 +1,14 @@ +package org.hankki.hankkiserver.api.store.service.response; + +import org.hankki.hankkiserver.domain.store.model.Store; + +public record PinResponse( + double latitude, + double longitude, + long id, + String name +) { + public static PinResponse of(final Store store) { + return new PinResponse(store.getPoint().getLatitude(), store.getPoint().getLongitude(), store.getId(), store.getName()); + } +} diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/response/StorePinsResponse.java b/src/main/java/org/hankki/hankkiserver/api/store/service/response/StorePinsResponse.java new file mode 100644 index 00000000..cd7b11d9 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/response/StorePinsResponse.java @@ -0,0 +1,10 @@ +package org.hankki.hankkiserver.api.store.service.response; + +import java.util.List; + +public record StorePinsResponse(List pins) { + public static StorePinsResponse of (final List pins) { + return new StorePinsResponse(pins); + } + +} diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/response/StoreResponse.java b/src/main/java/org/hankki/hankkiserver/api/store/service/response/StoreResponse.java new file mode 100644 index 00000000..3b5c5d39 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/response/StoreResponse.java @@ -0,0 +1,15 @@ +package org.hankki.hankkiserver.api.store.service.response; + +import org.hankki.hankkiserver.domain.store.model.Store; + +public record StoreResponse (long id, + String imageUrl, + String category, + String name, + int lowestPrice, + int heartCount) { + + public static StoreResponse of(final Store store) { + return new StoreResponse(store.getId(), store.getImages().get(0).getImageUrl(), store.getCategory().getName(), store.getName(), store.getLowestPrice(), store.getHeartCount()); + } +} diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/response/StoresResponse.java b/src/main/java/org/hankki/hankkiserver/api/store/service/response/StoresResponse.java new file mode 100644 index 00000000..30a459e7 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/response/StoresResponse.java @@ -0,0 +1,9 @@ +package org.hankki.hankkiserver.api.store.service.response; + +import java.util.List; + +public record StoresResponse(List stores) { + public static StoresResponse of(final List response) { + return new StoresResponse(response); + } +} diff --git a/src/main/java/org/hankki/hankkiserver/api/user/service/response/UserStoreReportedResponse.java b/src/main/java/org/hankki/hankkiserver/api/user/service/response/UserStoreReportedResponse.java index a527d869..cadfd4c4 100644 --- a/src/main/java/org/hankki/hankkiserver/api/user/service/response/UserStoreReportedResponse.java +++ b/src/main/java/org/hankki/hankkiserver/api/user/service/response/UserStoreReportedResponse.java @@ -15,7 +15,7 @@ public static UserStoreReportedResponse of(Store store) { store.getId(), store.getName(), store.getCategory().getName(), - store.getImage(), + store.getImages().get(0).getImageUrl(), store.getLowestPrice(), store.getHeartCount()); } diff --git a/src/main/java/org/hankki/hankkiserver/domain/config/QueryDslConfig.java b/src/main/java/org/hankki/hankkiserver/domain/config/QueryDslConfig.java new file mode 100644 index 00000000..c0526d49 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/domain/config/QueryDslConfig.java @@ -0,0 +1,19 @@ +package org.hankki.hankkiserver.domain.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QueryDslConfig { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java b/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java index b4b10969..15656fc9 100644 --- a/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java +++ b/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java @@ -67,13 +67,6 @@ private Store (String name, Point point, String address, StoreCategory category, this.isDeleted = isDeleted; } - public String getImage() { - if (images.isEmpty()) { - return "default.com"; - } - return images.get(0).getImageUrl(); - } - public void decreaseHeartCount() { this.heartCount--; } diff --git a/src/main/java/org/hankki/hankkiserver/domain/store/repository/CustomStoreRepository.java b/src/main/java/org/hankki/hankkiserver/domain/store/repository/CustomStoreRepository.java new file mode 100644 index 00000000..94912947 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/domain/store/repository/CustomStoreRepository.java @@ -0,0 +1,12 @@ +package org.hankki.hankkiserver.domain.store.repository; + +import org.hankki.hankkiserver.api.store.parameter.PriceCategory; +import org.hankki.hankkiserver.api.store.parameter.SortOption; +import org.hankki.hankkiserver.domain.store.model.Store; +import org.hankki.hankkiserver.domain.store.model.StoreCategory; + +import java.util.List; + +public interface CustomStoreRepository { + List findStoreByCategoryAndLowestPriceAndUniversityIdAndIsDeletedFalseOrderBySortOptions(StoreCategory category, PriceCategory priceCategory, Long universityId, SortOption sortOptions); +} diff --git a/src/main/java/org/hankki/hankkiserver/domain/store/repository/CustomStoreRepositoryImpl.java b/src/main/java/org/hankki/hankkiserver/domain/store/repository/CustomStoreRepositoryImpl.java new file mode 100644 index 00000000..e71ec2be --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/domain/store/repository/CustomStoreRepositoryImpl.java @@ -0,0 +1,76 @@ +package org.hankki.hankkiserver.domain.store.repository; + +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.hankki.hankkiserver.api.store.parameter.PriceCategory; +import org.hankki.hankkiserver.api.store.parameter.SortOption; +import org.hankki.hankkiserver.domain.store.model.Store; +import org.hankki.hankkiserver.domain.store.model.StoreCategory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static org.hankki.hankkiserver.domain.store.model.QStore.store; +import static org.hankki.hankkiserver.domain.universitystore.model.QUniversityStore.universityStore; + +@RequiredArgsConstructor +public class CustomStoreRepositoryImpl implements CustomStoreRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List findStoreByCategoryAndLowestPriceAndUniversityIdAndIsDeletedFalseOrderBySortOptions(StoreCategory category, PriceCategory priceCategory, Long universityId, SortOption sortOptions) { + OrderSpecifier[] orderSpecifiers = createOrderSpecifier(sortOptions); + return jpaQueryFactory + .select(store) + .from(store) + .join(store.universityStores, universityStore).fetchJoin() + .where(eqCategory(category),eqUniversity(universityId), evaluatePriceCategory(priceCategory)) + .where(store.isDeleted.isFalse()) + .orderBy(orderSpecifiers) + .fetch(); + } + + private BooleanExpression eqUniversity(Long university) { + if (university == null) { + return null; + } + return store.universityStores.any().university.id.eq(university); + } + + private BooleanExpression eqCategory(StoreCategory category) { + if (category == null) { + return null; + } + return store.category.eq(category); + } + + private BooleanExpression evaluatePriceCategory(PriceCategory priceCategory) { + if (priceCategory == null) { + return null; + } + return store.lowestPrice.goe(priceCategory.getMinPrice()) + .and(store.lowestPrice.loe(priceCategory.getMaxPrice())); + } + + private OrderSpecifier[] createOrderSpecifier(SortOption sortOption) { + List orderSpecifiers = new ArrayList<>(); + if (Objects.isNull(sortOption)){ + orderSpecifiers.add(new OrderSpecifier(Order.DESC, store.createdAt)); + } + else if (SortOption.LATEST == sortOption){ + orderSpecifiers.add(new OrderSpecifier(Order.DESC, store.createdAt)); + } + else if (SortOption.LOWESTPRICE == sortOption){ + orderSpecifiers.add(new OrderSpecifier(Order.ASC, store.lowestPrice)); + } + else if (SortOption.RECOMMENDED == sortOption){ + orderSpecifiers.add(new OrderSpecifier(Order.DESC, store.heartCount)); + } + return orderSpecifiers.toArray(new OrderSpecifier[orderSpecifiers.size()]); + } +} diff --git a/src/main/java/org/hankki/hankkiserver/domain/store/repository/StoreRepository.java b/src/main/java/org/hankki/hankkiserver/domain/store/repository/StoreRepository.java index 15a7d5c0..dd3ed1dc 100644 --- a/src/main/java/org/hankki/hankkiserver/domain/store/repository/StoreRepository.java +++ b/src/main/java/org/hankki/hankkiserver/domain/store/repository/StoreRepository.java @@ -6,7 +6,7 @@ import java.util.Optional; -public interface StoreRepository extends JpaRepository { +public interface StoreRepository extends JpaRepository, CustomStoreRepository { @Query("select s from Store s where s.id = :id and s.isDeleted = false") Optional findByIdAndIsDeletedIsFalse(Long id);