Skip to content

Commit

Permalink
반경 내 주차장 조회 기능 구현 (#13)
Browse files Browse the repository at this point in the history
* feat: jsonProperty 설정

* feat: hibernate 공간 데이터 의존성 추가

* feat: Location 의 필드를 공간데이터 타입 Point 로 수정

* feat: querydsl 의존성 추가

* feat: querydsl 의존성 제거

* feat: 주차장 목록 조회 쿼리 파라미터 전용 argument resolver 구현

* feat: 반경 내 주차장 조회 쿼리 작성

* feat: argument resolver 등록

* feat: 반경 내 주차장 조회 controller, service(정렬, 필터링) 구현

* feat: builder 추가

* feat: 필터 기능하는 ParkingDomainService 추가

* feat: 사용하지 않는 코드 제거

* feat: 사용하지 않는 repository 삭제

* feat: 요금 계산 bean 추가

* feat: 주차장 목록 조회 시 사용자 조회 조건 필터링 구현

* feat: 메서드 분리

* feat: 주차장 도보 예상 시간 로직 테스트 작성

* refactor: 주석 제거

* feat: 주차장 목록 조회 시 조회 조건 엔티티로 대체 가능한 query param 제거

* feat: 주차장 검색 조건 request 생성

* feat: 검색 조건 변환 기능 구현

* feat: 반경조회 비회원 조회 구현

* feat: memberId 아규먼트 리졸버 구현

* feat: feeType 타입 변경

* feat: enum 의 value list 변환 로직 SearchConditionAvailable 로 이동

* feat: String 과 일치하는 enum List 변환 로직 SearchConditionAvailable 로 이동

* feat: find 메서드 구현

* feat: 무료인지 확인하는 메서드 구현

* feat: 검색조건 유.무료에 맞는 필터링 기능 구현

* refactor: ParkingDomainService -> ParkingApplicationService 네이밍 변경

* test: 유.무료 필터링 테스트

* refactor: 네이밍 수정

* feat: 비회원 기능 회원 인증 시 sessionId null 반환하도록 수정

* feat: 불필요한 메서드 제거

* feat: 충돌 해결

* feat: swagger 추가

* refactor: 서비스 계층에서 String <-> SearchConditionAvailable Enum 변환 로직 개선

* fix: 잘못 사용되고 있는 값 객체 제거

* feat: 응답 dto 변환 로직 이동

* refactor: 네이밍 수정

* feat: 현재 시간 변수 생성 위치 수정

---------

Co-authored-by: This2sho <zx00018@naver.com>
  • Loading branch information
youngh0 and This2sho authored Apr 3, 2024
1 parent e169da2 commit 3741d54
Show file tree
Hide file tree
Showing 43 changed files with 1,066 additions and 168 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation group: 'org.hibernate.orm', name: 'hibernate-spatial', version: '6.3.1.Final'

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.example.parking.api.parking;

import com.example.parking.application.parking.ParkingService;
import com.example.parking.application.parking.dto.ParkingLotsResponse;
import com.example.parking.application.parking.dto.ParkingQueryRequest;
import com.example.parking.application.parking.dto.ParkingSearchConditionRequest;
import com.example.parking.config.argumentresolver.MemberAuth;
import com.example.parking.config.argumentresolver.parking.ParkingQuery;
import com.example.parking.config.argumentresolver.parking.ParkingSearchCondition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "주차장 컨트롤러")
@RequiredArgsConstructor
@RestController
public class ParkingController {

private final ParkingService parkingService;

@Operation(summary = "주차장 반경 조회", description = "주차장 반경 조회")
@GetMapping("/parkings")
public ResponseEntity<ParkingLotsResponse> find(
@ParkingQuery ParkingQueryRequest parkingQueryRequest,
@ParkingSearchCondition ParkingSearchConditionRequest parkingSearchConditionRequest,
@Parameter(hidden = true) @MemberAuth(nullable = true) Long parkingMemberId
) {
ParkingLotsResponse parkingLots = parkingService.findParkingLots(parkingQueryRequest,
parkingSearchConditionRequest, parkingMemberId);
return ResponseEntity.ok(parkingLots);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.parking.application;

import com.example.parking.domain.searchcondition.SearchConditionAvailable;
import com.example.parking.support.exception.ClientException;
import com.example.parking.support.exception.ExceptionInformation;
import java.util.Arrays;
import java.util.List;
import org.springframework.stereotype.Component;

@Component
public class SearchConditionMapper {

public <E extends Enum<E> & SearchConditionAvailable> List<E> toEnums(Class<E> searchConditionAvailableClass,
List<String> descriptions) {
return descriptions.stream()
.map(description -> toEnum(searchConditionAvailableClass, description))
.toList();
}

public <E extends Enum<E> & SearchConditionAvailable> E toEnum(Class<E> searchConditionAvailableClass,
String description) {
E[] conditions = searchConditionAvailableClass.getEnumConstants();

return Arrays.stream(conditions)
.filter(condition -> description.startsWith(condition.getDescription()))
.findAny()
.orElseThrow(() -> new ClientException(ExceptionInformation.INVALID_DESCRIPTION));
}

public <E extends Enum<E> & SearchConditionAvailable> List<String> getValues(
Class<E> searchConditionAvailableClass) {
E[] conditions = searchConditionAvailableClass.getEnumConstants();

return Arrays.stream(conditions)
.filter(condition -> condition != condition.getDefault())
.map(SearchConditionAvailable::getDescription)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.parking.application.parking;

import com.example.parking.domain.parking.Parking;
import com.example.parking.domain.parking.ParkingFeeCalculator;
import com.example.parking.domain.parking.SearchingCondition;
import com.example.parking.domain.searchcondition.FeeType;
import java.time.LocalDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class ParkingFilteringService {

private final ParkingFeeCalculator parkingFeeCalculator;

public List<Parking> filterByCondition(List<Parking> parkingLots, SearchingCondition searchingCondition,
LocalDateTime now) {
return parkingLots.stream()
.filter(parking -> checkFeeTypeIsPaid(searchingCondition) || checkFeeTypeAndFeeFree(searchingCondition,
now, parking))
.filter(parking -> parking.containsOperationType(searchingCondition.getOperationTypes()))
.filter(parking -> parking.containsParkingType(searchingCondition.getParkingTypes()))
.filter(parking -> parking.containsPayType(searchingCondition.getPayTypes()))
.toList();
}

private boolean checkFeeTypeIsPaid(SearchingCondition searchingCondition) {
return searchingCondition.getFeeType() == FeeType.PAID;
}

private boolean checkFeeTypeAndFeeFree(SearchingCondition searchingCondition, LocalDateTime now, Parking parking) {
return searchingCondition.getFeeType() == FeeType.FREE && parkingFeeCalculator.calculateParkingFee(parking, now,
now.plusHours(searchingCondition.getHours())).isFree();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
package com.example.parking.application.parking;

import com.example.parking.application.SearchConditionMapper;
import com.example.parking.application.parking.dto.ParkingLotsResponse;
import com.example.parking.application.parking.dto.ParkingLotsResponse.ParkingResponse;
import com.example.parking.application.parking.dto.ParkingQueryRequest;
import com.example.parking.application.parking.dto.ParkingSearchConditionRequest;
import com.example.parking.domain.favorite.Favorite;
import com.example.parking.domain.favorite.FavoriteRepository;
import com.example.parking.domain.parking.Fee;
import com.example.parking.domain.parking.Location;
import com.example.parking.domain.parking.OperationType;
import com.example.parking.domain.parking.Parking;
import com.example.parking.domain.parking.ParkingRepository;
import com.example.parking.domain.parking.ParkingFeeCalculator;
import com.example.parking.domain.parking.ParkingType;
import com.example.parking.domain.parking.PayType;
import com.example.parking.domain.parking.SearchingCondition;
import com.example.parking.domain.parking.repository.ParkingRepository;
import com.example.parking.domain.searchcondition.FeeType;
import com.example.parking.support.Association;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -12,7 +31,102 @@
@Service
public class ParkingService {

private static final String DISTANCE_ORDER_CONDITION = "가까운 순";

private final ParkingRepository parkingRepository;
private final ParkingFilteringService parkingFilteringService;
private final FavoriteRepository favoriteRepository;
private final SearchConditionMapper searchConditionMapper;
private final ParkingFeeCalculator parkingFeeCalculator;

@Transactional(readOnly = true)
public ParkingLotsResponse findParkingLots(ParkingQueryRequest parkingQueryRequest,
ParkingSearchConditionRequest parkingSearchConditionRequest,
Long memberId) {
LocalDateTime now = LocalDateTime.now();
Location destination = Location.of(parkingQueryRequest.getLongitude(), parkingQueryRequest.getLatitude());

// 반경 주차장 조회
List<Favorite> favorites = findMemberFavorites(memberId);
List<Parking> parkingLots = findParkingLotsByOrderCondition(parkingSearchConditionRequest.getPriority(),
parkingQueryRequest, destination);

// 조회조건 기반 필터링
SearchingCondition searchingCondition = toSearchingCondition(parkingSearchConditionRequest);
List<Parking> filteredParkingLots = parkingFilteringService.filterByCondition(parkingLots, searchingCondition,
now);

// 응답 dto 변환
List<ParkingResponse> parkingResponses = collectParkingInfo(filteredParkingLots,
parkingSearchConditionRequest.getHours(), destination, favorites, now);

return new ParkingLotsResponse(parkingResponses);
}

private SearchingCondition toSearchingCondition(ParkingSearchConditionRequest request) {
List<ParkingType> parkingTypes = searchConditionMapper.toEnums(ParkingType.class, request.getParkingTypes());
List<OperationType> operationTypes = searchConditionMapper.toEnums(OperationType.class,
request.getOperationTypes());
List<PayType> payTypes = searchConditionMapper.toEnums(PayType.class, request.getPayTypes());
FeeType feeType = searchConditionMapper.toEnum(FeeType.class, request.getFeeType());

return new SearchingCondition(operationTypes, parkingTypes, payTypes, feeType, request.getHours());
}

private List<Favorite> findMemberFavorites(Long memberId) {
if (memberId == null) {
return Collections.emptyList();
}
return favoriteRepository.findByMemberId(Association.from(memberId));
}

private List<Parking> findParkingLotsByOrderCondition(String priority, ParkingQueryRequest parkingQueryRequest,
Location middleLocation) {
if (priority.equals(DISTANCE_ORDER_CONDITION)) {
return parkingRepository.findAroundParkingLotsOrderByDistance(middleLocation.getPoint(),
parkingQueryRequest.getRadius());
}
return parkingRepository.findAroundParkingLots(middleLocation.getPoint(), parkingQueryRequest.getRadius());
}

private List<ParkingResponse> collectParkingInfo(List<Parking> parkingLots, int hours,
Location destination, List<Favorite> memberFavorites,
LocalDateTime now) {
Set<Long> favoriteParkingIds = extractFavoriteParkingIds(memberFavorites);
return calculateParkingInfo(parkingLots, destination, hours, favoriteParkingIds, now);
}

private List<ParkingResponse> calculateParkingInfo(List<Parking> parkingLots, Location destination, int hours,
Set<Long> favoriteParkingIds, LocalDateTime now) {
return parkingLots.stream()
.map(parking -> toParkingResponse(
parking,
parkingFeeCalculator.calculateParkingFee(parking, now, now.plusHours(hours)),
parking.calculateWalkingTime(destination),
favoriteParkingIds.contains(parking.getId())
)
).toList();
}

private Set<Long> extractFavoriteParkingIds(List<Favorite> memberFavorites) {
return memberFavorites.stream()
.map(Favorite::getParkingId)
.map(Association::getId)
.collect(Collectors.toSet());
}

private ParkingResponse toParkingResponse(Parking parking, Fee fee, int walkingTime, boolean isFavorite) {
return new ParkingResponse(
parking.getId(),
parking.getBaseInformation().getName(),
fee.getFee(),
walkingTime,
parking.getBaseInformation().getParkingType().getDescription(),
isFavorite,
parking.getLocation().getLatitude(),
parking.getLocation().getLongitude()
);
}

@Transactional
public void saveAll(List<Parking> parkingLots) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.example.parking.application.parking.dto;

import java.util.List;
import lombok.Getter;

@Getter
public class ParkingLotsResponse {

private List<ParkingResponse> parkingLots;

private ParkingLotsResponse() {
}

public ParkingLotsResponse(List<ParkingResponse> parkingLots) {
this.parkingLots = parkingLots;
}

@Getter
public static class ParkingResponse {
private Long parkingId;
private String parkingName;
private Integer estimatedFee;
private Integer estimatedWalkingTime;
private String parkingType;
private Boolean isFavorite;
private Double latitude;
private Double longitude;

private ParkingResponse() {
}

public ParkingResponse(Long parkingId, String parkingName, Integer estimatedFee, Integer estimatedWalkingTime,
String parkingType, Boolean isFavorite, Double latitude, Double longitude) {
this.parkingId = parkingId;
this.parkingName = parkingName;
this.estimatedFee = estimatedFee;
this.estimatedWalkingTime = estimatedWalkingTime;
this.parkingType = parkingType;
this.isFavorite = isFavorite;
this.latitude = latitude;
this.longitude = longitude;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.parking.application.parking.dto;

import lombok.Getter;

@Getter
public class ParkingQueryRequest {

private final Double latitude;
private final Double longitude;
private final Integer radius;

public ParkingQueryRequest(Double latitude, Double longitude, Integer radius) {
this.latitude = latitude;
this.longitude = longitude;
this.radius = radius;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.example.parking.application.parking.dto;

import java.util.List;
import lombok.Getter;

@Getter
public class ParkingSearchConditionRequest {

private final List<String> operationTypes;
private final List<String> parkingTypes;
private final String feeType;
private final List<String> payTypes;
private final Integer hours;
private final String priority;

public ParkingSearchConditionRequest(List<String> operationTypes, List<String> parkingTypes, String feeType,
List<String> payTypes, int hours, String priority) {
this.operationTypes = operationTypes;
this.parkingTypes = parkingTypes;
this.feeType = feeType;
this.payTypes = payTypes;
this.hours = hours;
this.priority = priority;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.parking.application.searchcondition;

import com.example.parking.application.SearchConditionMapper;
import com.example.parking.application.searchcondition.dto.SearchConditionDto;
import com.example.parking.domain.parking.OperationType;
import com.example.parking.domain.parking.ParkingType;
Expand All @@ -22,6 +23,7 @@
public class SearchConditionService {

private final SearchConditionRepository searchConditionRepository;
private final SearchConditionMapper searchConditionMapper;

public SearchConditionDto findSearchCondition(Long memberId) {
SearchCondition searchCondition = searchConditionRepository.getByMemberId(memberId);
Expand Down Expand Up @@ -58,18 +60,12 @@ public void updateSearchCondition(Long memberId, SearchConditionDto searchCondit
private SearchCondition createSearchCondition(Long memberId, SearchConditionDto searchConditionDto) {
return new SearchCondition(
Association.from(memberId),
toEnums(searchConditionDto.getOperationType(), OperationType.values()),
toEnums(searchConditionDto.getParkingType(), ParkingType.values()),
toEnums(searchConditionDto.getFeeType(), FeeType.values()),
toEnums(searchConditionDto.getPayType(), PayType.values()),
SearchConditionAvailable.find(searchConditionDto.getPriority(), Priority.values()),
searchConditionMapper.toEnums(OperationType.class, searchConditionDto.getOperationType()),
searchConditionMapper.toEnums(ParkingType.class, searchConditionDto.getParkingType()),
searchConditionMapper.toEnums(FeeType.class, searchConditionDto.getFeeType()),
searchConditionMapper.toEnums(PayType.class, searchConditionDto.getPayType()),
searchConditionMapper.toEnum(Priority.class, searchConditionDto.getPriority()),
Hours.from(searchConditionDto.getHours())
);
}

public <E extends SearchConditionAvailable> List<E> toEnums(List<String> descriptions, E... values) {
return descriptions.stream()
.map(description -> SearchConditionAvailable.find(description, values))
.toList();
}
}
Loading

0 comments on commit 3741d54

Please sign in to comment.