Skip to content

Commit

Permalink
Merge pull request #34 from 9oormthon-univ/develop
Browse files Browse the repository at this point in the history
feat: 카테고리 별 가게 목록 조회 구현
  • Loading branch information
LEEJaeHyeok97 authored Nov 18, 2024
2 parents 432dcc7 + e42b307 commit ac83537
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package com.jangburich.domain.store.domain.controller;

import com.jangburich.domain.store.domain.Category;
import com.jangburich.domain.store.domain.dto.condition.StoreSearchCondition;
import com.jangburich.domain.store.domain.dto.condition.StoreSearchConditionWithType;
import com.jangburich.domain.store.domain.dto.response.SearchStoresResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.jangburich.domain.oauth.domain.CustomOAuthUser;
Expand All @@ -27,8 +35,32 @@
@RequiredArgsConstructor
@RequestMapping("/store")
public class StoreController {

private final StoreService storeService;

@Operation(summary = "카테고리 별 가게 목록 조회", description = "카테고리 별로 가게 목록을 조회합니다.")
@GetMapping
public ResponseCustom<Page<SearchStoresResponse>> searchByCategory(
Authentication authentication,
@RequestParam(required = false, defaultValue = "3") Integer searchRadius,
@RequestParam(required = false, defaultValue = "ALL") Category category,
@ModelAttribute StoreSearchCondition storeSearchCondition,
Pageable pageable
) {
return ResponseCustom.OK(storeService.searchByCategory(authentication, searchRadius, category, storeSearchCondition, pageable));
}

@Operation(summary = "매장 찾기(검색)", description = "검색어와 매장 유형에 맞는 매장을 검색합니다.")
@GetMapping("/search")
public ResponseCustom<Page<SearchStoresResponse>> searchStores(
Authentication authentication,
@RequestParam(required = false, defaultValue = "") String keyword,
@ModelAttribute StoreSearchConditionWithType storeSearchConditionWithType,
Pageable pageable
) {
return ResponseCustom.OK(storeService.searchStores(authentication, keyword, storeSearchConditionWithType, pageable));
}

@Operation(summary = "가게 등록", description = "신규 파트너 가게를 등록합니다.")
@PostMapping("/create")
public ResponseCustom<Message> createStore(Authentication authentication,
Expand Down Expand Up @@ -63,7 +95,7 @@ public ResponseCustom<Message> updateStore(Authentication authentication, @PathV
}

@Operation(summary = "가게 정보 조회", description = "가게 상세 정보를 조회합니다.")
@GetMapping("")
@GetMapping("/{storeId}")
public ResponseCustom<StoreGetResponseDTO> getStoreInfo(Authentication authentication) {
CustomOAuthUser customOAuth2User = (CustomOAuthUser)authentication.getPrincipal();
return ResponseCustom.OK(storeService.getStoreInfo(customOAuth2User));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.jangburich.domain.store.domain.dto.condition;

public record StoreSearchCondition(
Double lat,
Double lon
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.jangburich.domain.store.domain.dto.condition;

import com.querydsl.core.annotations.QueryProjection;

public record StoreSearchConditionWithType(
Boolean isReservable,
Boolean isOperational,
Boolean isPrepaid,
Boolean isPostpaid
) {

@QueryProjection
public StoreSearchConditionWithType(Boolean isReservable, Boolean isOperational, Boolean isPrepaid,
Boolean isPostpaid) {
this.isReservable = isReservable;
this.isOperational = isOperational;
this.isPrepaid = isPrepaid;
this.isPostpaid = isPostpaid;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.jangburich.domain.store.domain.dto.response;

import com.jangburich.domain.store.domain.Category;
import com.querydsl.core.annotations.QueryProjection;
import lombok.Builder;

@Builder
public record SearchStoresResponse(
Long storeId,
String name,
Boolean isFavorite,
Category category,
Double distance,
String businessStatus,
String closeTime,
String phoneNumber,
String imageUrl
) {

@QueryProjection
public SearchStoresResponse(Long storeId, String name, Boolean isFavorite, Category category, Double distance,
String businessStatus, String closeTime, String phoneNumber, String imageUrl) {
this.storeId = storeId;
this.name = name;
this.isFavorite = isFavorite;
this.category = category;
this.distance = distance;
this.businessStatus = businessStatus;
this.closeTime = closeTime;
this.phoneNumber = phoneNumber;
this.imageUrl = imageUrl;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.jangburich.domain.store.domain.repository;

import com.jangburich.domain.store.domain.Category;
import com.jangburich.domain.store.domain.dto.condition.StoreSearchCondition;
import com.jangburich.domain.store.domain.dto.condition.StoreSearchConditionWithType;
import com.jangburich.domain.store.domain.dto.response.SearchStoresResponse;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface StoreQueryDslRepository {
Page<SearchStoresResponse> findStoresByCategory(Long userId, Integer searchRadius, Category category, StoreSearchCondition storeSearchCondition, Pageable pageable);

Page<SearchStoresResponse> findStores(Long userId, String keyword, StoreSearchConditionWithType storeSearchConditionWithType, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.jangburich.domain.store.domain.repository;

import static com.jangburich.domain.store.domain.QStore.store;

import com.jangburich.domain.store.domain.Category;
import com.jangburich.domain.store.domain.dto.condition.StoreSearchCondition;
import com.jangburich.domain.store.domain.dto.condition.StoreSearchConditionWithType;
import com.jangburich.domain.store.domain.dto.response.QSearchStoresResponse;
import com.jangburich.domain.store.domain.dto.response.SearchStoresResponse;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
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;

@RequiredArgsConstructor
@Repository
public class StoreQueryDslRepositoryImpl implements StoreQueryDslRepository {

private static final int RADIUS_OF_EARTH_KM = 6371;

private final JPAQueryFactory queryFactory;

@Override
public Page<SearchStoresResponse> findStoresByCategory(Long userId, Integer searchRadius, Category category,
StoreSearchCondition storeSearchCondition, Pageable pageable) {
double myCurrentLat = storeSearchCondition.lat();
double myCurrentLon = storeSearchCondition.lon();

List<SearchStoresResponse> results = queryFactory
.select(new QSearchStoresResponse(store.id, store.name, Expressions.FALSE, store.category,
Expressions.constant(1.0), Expressions.constant("open"),
store.closeTime.stringValue(), store.contactNumber, store.representativeImage))
.from(store)
.where(
store.category.eq(category),
withinSearchRadius(myCurrentLat, myCurrentLon, searchRadius, store.latitude, store.longitude)
)
.orderBy(store.id.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

JPAQuery<Long> countQuery = queryFactory
.select(store.count())
.from(store)
.where(
store.category.eq(category),
withinSearchRadius(myCurrentLat, myCurrentLon, searchRadius, store.latitude, store.longitude)
);

return PageableExecutionUtils.getPage(results, pageable, () -> countQuery.fetch().size());
}

@Override
public Page<SearchStoresResponse> findStores(Long userId, String keyword,
StoreSearchConditionWithType storeSearchConditionWithType,
Pageable pageable) {
Boolean reservable = storeSearchConditionWithType.isReservable();
Boolean operational = storeSearchConditionWithType.isOperational();
Boolean prepaid = storeSearchConditionWithType.isPrepaid();
Boolean postpaid = storeSearchConditionWithType.isPostpaid();

List<SearchStoresResponse> results = queryFactory
.select(new QSearchStoresResponse(store.id, store.name, Expressions.FALSE, store.category,
Expressions.constant(1.0), Expressions.constant("open"),
store.closeTime.stringValue(), store.contactNumber, store.representativeImage))
.from(store)
.where(
store.reservationAvailable.eq(reservable),
store.name.contains(keyword)
)
.orderBy(store.id.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

JPAQuery<Long> countQuery = queryFactory
.select(store.count())
.from(store)
.where(
store.reservationAvailable.eq(reservable),
store.name.contains(keyword)
);

return PageableExecutionUtils.getPage(results, pageable, () -> countQuery.fetch().size());
}

// TO DO
private BooleanExpression isStoreCurrentlyOpen(String openTime, String closeTime) {
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");

String currentTimeStr = LocalTime.now().format(timeFormatter);
LocalTime currentTime = LocalTime.parse(currentTimeStr, timeFormatter);

LocalTime open = LocalTime.parse(openTime, timeFormatter);
LocalTime close = LocalTime.parse(closeTime, timeFormatter);

return Expressions.booleanTemplate("{0} >= {1} and {0} < {2}", currentTime, openTime, closeTime);
}

private BooleanExpression withinSearchRadius(double userLat, double userLng, int searchRadius,
com.querydsl.core.types.dsl.NumberPath<Double> storeLat,
com.querydsl.core.types.dsl.NumberPath<Double> storeLng) {
return Expressions.numberTemplate(Double.class,
"({0} * acos(cos(radians({1})) * cos(radians({2})) * cos(radians({3}) - radians({4})) + sin(radians({1})) * sin(radians({2}))))",
RADIUS_OF_EARTH_KM, userLat, storeLat, storeLng, userLng)
.loe(searchRadius);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
import com.jangburich.domain.store.domain.Store;

@Repository
public interface StoreRepository extends JpaRepository<Store, Long> {
public interface StoreRepository extends JpaRepository<Store, Long>, StoreQueryDslRepository {
Optional<Store> findByOwner(Owner owner);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package com.jangburich.domain.store.domain.service;

import com.jangburich.domain.store.domain.Category;
import com.jangburich.domain.store.domain.dto.condition.StoreSearchCondition;
import com.jangburich.domain.store.domain.dto.condition.StoreSearchConditionWithType;
import com.jangburich.domain.store.domain.dto.response.SearchStoresResponse;
import com.jangburich.utils.parser.AuthenticationParser;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -133,4 +141,24 @@ public StoreGetResponseDTO getStoreInfo(CustomOAuthUser customOAuth2User) {

return new StoreGetResponseDTO().of(store);
}

public Page<SearchStoresResponse> searchByCategory(final Authentication authentication,
final Integer searchRadius,
final Category category,
final StoreSearchCondition storeSearchCondition,
final Pageable pageable) {
String parsed = AuthenticationParser.parseUserId(authentication);
User user = userRepository.findByProviderId(parsed)
.orElseThrow(() -> new DefaultNullPointerException(ErrorCode.INVALID_AUTHENTICATION));
return storeRepository.findStoresByCategory(user.getUserId(), searchRadius, category, storeSearchCondition, pageable);
}

public Page<SearchStoresResponse> searchStores(final Authentication authentication, final String keyword,
final StoreSearchConditionWithType storeSearchConditionWithType,
final Pageable pageable) {
String parsed = AuthenticationParser.parseUserId(authentication);
User user = userRepository.findByProviderId(parsed)
.orElseThrow(() -> new DefaultNullPointerException(ErrorCode.INVALID_AUTHENTICATION));
return storeRepository.findStores(user.getUserId(), keyword, storeSearchConditionWithType, pageable);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.jangburich.domain.store.exception;

public class ResourceNotFoundException extends RuntimeException {

public ResourceNotFoundException(String message) {
super(message);
}
}

0 comments on commit ac83537

Please sign in to comment.