From 652d7acd75feec5694934921148bdde6cb06f3fc Mon Sep 17 00:00:00 2001 From: bum12ark Date: Thu, 3 Mar 2022 16:08:55 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(store-service):=20Just-Pickup=20?= =?UTF-8?q?=EA=B0=80=EA=B2=8C=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - slice 를 이용하여 더보기 버튼 로직 추가 - 쿼리에서 haversine 공식을 사용하여 가까운 거리순으로 가져오도록 구현 --- .../storeservice/StoreServiceApplication.java | 53 ++++++++++++ .../storeservice/domain/map/entity/Map.java | 11 +++ .../store/dto/SearchStoreCondition.java | 15 ++++ .../domain/store/dto/SearchStoreResult.java | 26 ++++++ .../domain/store/entity/Store.java | 13 ++- .../repository/StoreRepositoryCustom.java | 80 +++++++++++++++++++ .../domain/store/service/StoreService.java | 10 +++ .../store/service/StoreServiceImpl.java | 21 +++++ .../domain/store/web/StoreController.java | 63 +++++++++++++++ 9 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 store-service/src/main/java/com/justpickup/storeservice/domain/store/dto/SearchStoreCondition.java create mode 100644 store-service/src/main/java/com/justpickup/storeservice/domain/store/dto/SearchStoreResult.java create mode 100644 store-service/src/main/java/com/justpickup/storeservice/domain/store/repository/StoreRepositoryCustom.java create mode 100644 store-service/src/main/java/com/justpickup/storeservice/domain/store/service/StoreService.java create mode 100644 store-service/src/main/java/com/justpickup/storeservice/domain/store/service/StoreServiceImpl.java create mode 100644 store-service/src/main/java/com/justpickup/storeservice/domain/store/web/StoreController.java diff --git a/store-service/src/main/java/com/justpickup/storeservice/StoreServiceApplication.java b/store-service/src/main/java/com/justpickup/storeservice/StoreServiceApplication.java index e0e9bd9..bf755d1 100644 --- a/store-service/src/main/java/com/justpickup/storeservice/StoreServiceApplication.java +++ b/store-service/src/main/java/com/justpickup/storeservice/StoreServiceApplication.java @@ -1,8 +1,17 @@ package com.justpickup.storeservice; +import com.justpickup.storeservice.domain.map.entity.Map; +import com.justpickup.storeservice.domain.store.entity.Store; +import com.justpickup.storeservice.domain.store.repository.StoreRepository; +import com.justpickup.storeservice.global.entity.Address; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; +import org.springframework.context.annotation.Bean; + +import java.util.ArrayList; +import java.util.List; @SpringBootApplication @EnableEurekaClient @@ -12,4 +21,48 @@ public class StoreServiceApplication { SpringApplication.run(StoreServiceApplication.class, args); } + @Bean + CommandLineRunner run(StoreRepository storeRepository) { + return args -> { + List stores = new ArrayList<>(); + + stores.add( + Store.of( + new Address("서울시", "마포구 도화동", "201-20"), + Map.of(37.5398271003404, 126.94769672415691), + 1L, + "커피온리 마포역점" + ) + ); + + stores.add( + Store.of( + new Address("서울시", "마포구 도화동", "50-10"), + Map.of(37.54010719003089, 126.94556661330861), + 2L, + "만랩커피 마포점" + ) + ); + + stores.add( + Store.of( + new Address("서울시", "마포구 도화동", "555"), + Map.of(37.539797393793755, 126.9453578838543), + 3L, + "이디야커피 마포오벨리스크점" + ) + ); + + stores.add( + Store.of( + new Address("서울시", "영등포구 도림로", "31길 2"), + Map.of(37.493033141569505, 126.89593667847592), + 4L, + "이디야커피 대림역점" + ) + ); + + storeRepository.saveAll(stores); + }; + } } diff --git a/store-service/src/main/java/com/justpickup/storeservice/domain/map/entity/Map.java b/store-service/src/main/java/com/justpickup/storeservice/domain/map/entity/Map.java index 384a429..9cd62b7 100644 --- a/store-service/src/main/java/com/justpickup/storeservice/domain/map/entity/Map.java +++ b/store-service/src/main/java/com/justpickup/storeservice/domain/map/entity/Map.java @@ -14,4 +14,15 @@ public class Map extends BaseEntity { @Id @GeneratedValue @Column(name = "map_id") private Long id; + + private Double latitude; + + private Double longitude; + + public static Map of (double latitude, double longitude) { + Map map = new Map(); + map.latitude = latitude; + map.longitude = longitude; + return map; + } } diff --git a/store-service/src/main/java/com/justpickup/storeservice/domain/store/dto/SearchStoreCondition.java b/store-service/src/main/java/com/justpickup/storeservice/domain/store/dto/SearchStoreCondition.java new file mode 100644 index 0000000..30cd935 --- /dev/null +++ b/store-service/src/main/java/com/justpickup/storeservice/domain/store/dto/SearchStoreCondition.java @@ -0,0 +1,15 @@ +package com.justpickup.storeservice.domain.store.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import javax.validation.constraints.NotNull; + +@Getter @AllArgsConstructor +public class SearchStoreCondition { + @NotNull(message = "필수 값입니다.") + private double latitude; + @NotNull(message = "필수 값입니다.") + private double longitude; + private String storeName; +} diff --git a/store-service/src/main/java/com/justpickup/storeservice/domain/store/dto/SearchStoreResult.java b/store-service/src/main/java/com/justpickup/storeservice/domain/store/dto/SearchStoreResult.java new file mode 100644 index 0000000..fbe9f1a --- /dev/null +++ b/store-service/src/main/java/com/justpickup/storeservice/domain/store/dto/SearchStoreResult.java @@ -0,0 +1,26 @@ +package com.justpickup.storeservice.domain.store.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.text.DecimalFormat; + +@Getter @AllArgsConstructor +public class SearchStoreResult { + private Long storeId; + private String storeName; + private Double distanceMeter; + + public String convertDistanceToString() { + // km 으로 표시 + if (distanceMeter >= 1000) { + double km = distanceMeter * 0.001; + String format = new DecimalFormat("0.0").format(km); + // ex) 1.7km + return format + "km"; + } + + // ex) 621m + return new DecimalFormat("0").format(distanceMeter) + "m"; + } +} diff --git a/store-service/src/main/java/com/justpickup/storeservice/domain/store/entity/Store.java b/store-service/src/main/java/com/justpickup/storeservice/domain/store/entity/Store.java index d30fb05..7dba721 100644 --- a/store-service/src/main/java/com/justpickup/storeservice/domain/store/entity/Store.java +++ b/store-service/src/main/java/com/justpickup/storeservice/domain/store/entity/Store.java @@ -29,6 +29,8 @@ public class Store extends BaseEntity { private LocalDateTime businessEndTime; + private String name; + private String phoneNumber; @Embedded @@ -37,7 +39,7 @@ public class Store extends BaseEntity { @Embedded private Photo photo; - @OneToOne(fetch = LAZY) + @OneToOne(fetch = LAZY, cascade = CascadeType.ALL) @JoinColumn(name = "map_id") private Map map; @@ -60,4 +62,13 @@ public class Store extends BaseEntity { items.add(item); item.setStore(this); } + + public static Store of(Address address, Map map, Long userId, String name) { + Store store = new Store(); + store.address = address; + store.map = map; + store.userId = userId; + store.name = name; + return store; + } } diff --git a/store-service/src/main/java/com/justpickup/storeservice/domain/store/repository/StoreRepositoryCustom.java b/store-service/src/main/java/com/justpickup/storeservice/domain/store/repository/StoreRepositoryCustom.java new file mode 100644 index 0000000..c51e77e --- /dev/null +++ b/store-service/src/main/java/com/justpickup/storeservice/domain/store/repository/StoreRepositoryCustom.java @@ -0,0 +1,80 @@ +package com.justpickup.storeservice.domain.store.repository; + +import com.justpickup.storeservice.domain.store.dto.SearchStoreCondition; +import com.justpickup.storeservice.domain.store.dto.SearchStoreResult; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static com.justpickup.storeservice.domain.store.entity.QStore.store; +import static com.querydsl.core.types.dsl.MathExpressions.*; + +@Repository +@RequiredArgsConstructor +@Slf4j +public class StoreRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + public SliceImpl findSearchStoreScroll(SearchStoreCondition condition, Pageable pageable) { + Expression latitude = Expressions.constant(condition.getLatitude()); + Expression longitude = Expressions.constant(condition.getLongitude()); + + NumberPath distanceAlias = Expressions.numberPath(Double.class, "distance"); + int earthRadius = 6371; + + NumberExpression haversineDistance = acos( + cos(radians(latitude)) + .multiply(cos(radians(store.map.latitude))) + .multiply( + cos(radians(store.map.longitude) + .subtract(radians(longitude))) + ) + .add( + sin(radians(latitude)) + .multiply(sin(radians(store.map.latitude))) + ) + ) + .multiply(Expressions.constant(earthRadius * 1000)); + + List content = queryFactory.select( + Projections.constructor(SearchStoreResult.class, + store.id, + store.name, + haversineDistance.as(distanceAlias)) + ) + .from(store) + .join(store.map) + .where( + storeNameContains(condition.getStoreName()) + ) + .orderBy(distanceAlias.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) + .fetch(); + + boolean hasNext = false; + if (content.size() > pageable.getPageSize()) { + content.remove(pageable.getPageSize()); + hasNext = true; + } + + return new SliceImpl<>(content, pageable, hasNext); + } + + private BooleanExpression storeNameContains(String storeName) { + return storeName == null ? null : store.name.contains(storeName); + } + +} diff --git a/store-service/src/main/java/com/justpickup/storeservice/domain/store/service/StoreService.java b/store-service/src/main/java/com/justpickup/storeservice/domain/store/service/StoreService.java new file mode 100644 index 0000000..463b7bf --- /dev/null +++ b/store-service/src/main/java/com/justpickup/storeservice/domain/store/service/StoreService.java @@ -0,0 +1,10 @@ +package com.justpickup.storeservice.domain.store.service; + +import com.justpickup.storeservice.domain.store.dto.SearchStoreCondition; +import com.justpickup.storeservice.domain.store.dto.SearchStoreResult; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.SliceImpl; + +public interface StoreService { + SliceImpl findSearchStoreScroll(SearchStoreCondition condition, Pageable pageable); +} diff --git a/store-service/src/main/java/com/justpickup/storeservice/domain/store/service/StoreServiceImpl.java b/store-service/src/main/java/com/justpickup/storeservice/domain/store/service/StoreServiceImpl.java new file mode 100644 index 0000000..071a2ca --- /dev/null +++ b/store-service/src/main/java/com/justpickup/storeservice/domain/store/service/StoreServiceImpl.java @@ -0,0 +1,21 @@ +package com.justpickup.storeservice.domain.store.service; + +import com.justpickup.storeservice.domain.store.dto.SearchStoreCondition; +import com.justpickup.storeservice.domain.store.dto.SearchStoreResult; +import com.justpickup.storeservice.domain.store.repository.StoreRepositoryCustom; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class StoreServiceImpl implements StoreService { + + private final StoreRepositoryCustom storeRepositoryCustom; + + @Override + public SliceImpl findSearchStoreScroll(SearchStoreCondition condition, Pageable pageable) { + return storeRepositoryCustom.findSearchStoreScroll(condition, pageable); + } +} diff --git a/store-service/src/main/java/com/justpickup/storeservice/domain/store/web/StoreController.java b/store-service/src/main/java/com/justpickup/storeservice/domain/store/web/StoreController.java new file mode 100644 index 0000000..a63359a --- /dev/null +++ b/store-service/src/main/java/com/justpickup/storeservice/domain/store/web/StoreController.java @@ -0,0 +1,63 @@ +package com.justpickup.storeservice.domain.store.web; + +import com.justpickup.storeservice.domain.store.dto.SearchStoreCondition; +import com.justpickup.storeservice.domain.store.dto.SearchStoreResult; +import com.justpickup.storeservice.domain.store.service.StoreService; +import com.justpickup.storeservice.global.dto.Result; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequiredArgsConstructor +public class StoreController { + + private final StoreService storeService; + + @GetMapping("/searchStore") + public ResponseEntity searchStore(@Valid SearchStoreCondition condition, + @PageableDefault(page = 0, size = 2) Pageable pageable) { + SliceImpl searchStoreScroll = storeService.findSearchStoreScroll(condition, pageable); + + SearchStoreResponse searchStoreResponse = + new SearchStoreResponse(searchStoreScroll.getContent(), searchStoreScroll.hasNext()); + + return ResponseEntity.status(HttpStatus.OK) + .body(Result.createSuccessResult(searchStoreResponse)); + } + + @Data @NoArgsConstructor + static class SearchStoreResponse { + private List stores; + private boolean hasNext; + + @Data @AllArgsConstructor + static class StoreDto { + private Long id; + private String name; + private String distance; + } + + public SearchStoreResponse(List content, boolean hasNext) { + this.stores = content.stream() + .map(result -> + new StoreDto( + result.getStoreId(), result.getStoreName(), result.convertDistanceToString()) + ) + .collect(Collectors.toList()); + this.hasNext = hasNext; + } + } +} From 005e347cad38962f9f2176fd05d2b43b8370b7f7 Mon Sep 17 00:00:00 2001 From: bum12ark Date: Thu, 3 Mar 2022 19:38:05 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(customer-vue):=20Just=20pickup=20?= =?UTF-8?q?=EB=A7=A4=EC=9E=A5=20=EA=B2=80=EC=83=89=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 매장 검색 페이지 더보기 버튼 로직 구현 - 페이지 로딩 시 로딩 바 추가 --- customer-vue/package.json | 1 + customer-vue/src/api/store.js | 15 ++ customer-vue/src/router/router.js | 5 + customer-vue/src/views/Layout/HomeLayout.vue | 11 +- customer-vue/src/views/SearchStore.vue | 147 +++++++++++++++++++ 5 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 customer-vue/src/api/store.js create mode 100644 customer-vue/src/views/SearchStore.vue diff --git a/customer-vue/package.json b/customer-vue/package.json index 5dba678..0977d00 100644 --- a/customer-vue/package.json +++ b/customer-vue/package.json @@ -8,6 +8,7 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "axios": "^0.26.0", "core-js": "^3.8.3", "vue": "^2.6.14", "vue-router": "^3.5.3", diff --git a/customer-vue/src/api/store.js b/customer-vue/src/api/store.js new file mode 100644 index 0000000..df0c8dc --- /dev/null +++ b/customer-vue/src/api/store.js @@ -0,0 +1,15 @@ +import axios from "axios"; + +export default { + requestSearchStore(latitude, longitude, storeName, page) { + const options = { + params: { + latitude: latitude, + longitude: longitude, + storeName: storeName, + page: page + } + } + return axios.get("http://localhost:8000/store-service/searchStore", options); + } +} \ No newline at end of file diff --git a/customer-vue/src/router/router.js b/customer-vue/src/router/router.js index c621f62..c029e50 100644 --- a/customer-vue/src/router/router.js +++ b/customer-vue/src/router/router.js @@ -15,6 +15,11 @@ const routes = [ path: "/home", name: 'home', component: () => import('../views/HomeView') + }, + { + path: "/search", + name: 'search-store', + component: () => import('../views/SearchStore') } ] } diff --git a/customer-vue/src/views/Layout/HomeLayout.vue b/customer-vue/src/views/Layout/HomeLayout.vue index c9ed6dc..e0d8b4d 100644 --- a/customer-vue/src/views/Layout/HomeLayout.vue +++ b/customer-vue/src/views/Layout/HomeLayout.vue @@ -1,8 +1,8 @@