diff --git a/build.gradle b/build.gradle index da6970ca..089da741 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/src/main/java/com/example/parking/api/parking/ParkingController.java b/src/main/java/com/example/parking/api/parking/ParkingController.java new file mode 100644 index 00000000..1a86adb9 --- /dev/null +++ b/src/main/java/com/example/parking/api/parking/ParkingController.java @@ -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 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); + } +} diff --git a/src/main/java/com/example/parking/application/SearchConditionMapper.java b/src/main/java/com/example/parking/application/SearchConditionMapper.java new file mode 100644 index 00000000..82c700d5 --- /dev/null +++ b/src/main/java/com/example/parking/application/SearchConditionMapper.java @@ -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 & SearchConditionAvailable> List toEnums(Class searchConditionAvailableClass, + List descriptions) { + return descriptions.stream() + .map(description -> toEnum(searchConditionAvailableClass, description)) + .toList(); + } + + public & SearchConditionAvailable> E toEnum(Class 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 & SearchConditionAvailable> List getValues( + Class searchConditionAvailableClass) { + E[] conditions = searchConditionAvailableClass.getEnumConstants(); + + return Arrays.stream(conditions) + .filter(condition -> condition != condition.getDefault()) + .map(SearchConditionAvailable::getDescription) + .toList(); + } +} diff --git a/src/main/java/com/example/parking/application/parking/ParkingFilteringService.java b/src/main/java/com/example/parking/application/parking/ParkingFilteringService.java new file mode 100644 index 00000000..a4dae272 --- /dev/null +++ b/src/main/java/com/example/parking/application/parking/ParkingFilteringService.java @@ -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 filterByCondition(List 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(); + } +} diff --git a/src/main/java/com/example/parking/application/parking/ParkingService.java b/src/main/java/com/example/parking/application/parking/ParkingService.java index 1cf6c155..5cab21e7 100644 --- a/src/main/java/com/example/parking/application/parking/ParkingService.java +++ b/src/main/java/com/example/parking/application/parking/ParkingService.java @@ -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; @@ -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 favorites = findMemberFavorites(memberId); + List parkingLots = findParkingLotsByOrderCondition(parkingSearchConditionRequest.getPriority(), + parkingQueryRequest, destination); + + // 조회조건 기반 필터링 + SearchingCondition searchingCondition = toSearchingCondition(parkingSearchConditionRequest); + List filteredParkingLots = parkingFilteringService.filterByCondition(parkingLots, searchingCondition, + now); + + // 응답 dto 변환 + List parkingResponses = collectParkingInfo(filteredParkingLots, + parkingSearchConditionRequest.getHours(), destination, favorites, now); + + return new ParkingLotsResponse(parkingResponses); + } + + private SearchingCondition toSearchingCondition(ParkingSearchConditionRequest request) { + List parkingTypes = searchConditionMapper.toEnums(ParkingType.class, request.getParkingTypes()); + List operationTypes = searchConditionMapper.toEnums(OperationType.class, + request.getOperationTypes()); + List 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 findMemberFavorites(Long memberId) { + if (memberId == null) { + return Collections.emptyList(); + } + return favoriteRepository.findByMemberId(Association.from(memberId)); + } + + private List 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 collectParkingInfo(List parkingLots, int hours, + Location destination, List memberFavorites, + LocalDateTime now) { + Set favoriteParkingIds = extractFavoriteParkingIds(memberFavorites); + return calculateParkingInfo(parkingLots, destination, hours, favoriteParkingIds, now); + } + + private List calculateParkingInfo(List parkingLots, Location destination, int hours, + Set 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 extractFavoriteParkingIds(List 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 parkingLots) { diff --git a/src/main/java/com/example/parking/application/parking/dto/ParkingLotsResponse.java b/src/main/java/com/example/parking/application/parking/dto/ParkingLotsResponse.java new file mode 100644 index 00000000..aa4f05cd --- /dev/null +++ b/src/main/java/com/example/parking/application/parking/dto/ParkingLotsResponse.java @@ -0,0 +1,44 @@ +package com.example.parking.application.parking.dto; + +import java.util.List; +import lombok.Getter; + +@Getter +public class ParkingLotsResponse { + + private List parkingLots; + + private ParkingLotsResponse() { + } + + public ParkingLotsResponse(List 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; + } + } +} diff --git a/src/main/java/com/example/parking/application/parking/dto/ParkingQueryRequest.java b/src/main/java/com/example/parking/application/parking/dto/ParkingQueryRequest.java new file mode 100644 index 00000000..db7323c4 --- /dev/null +++ b/src/main/java/com/example/parking/application/parking/dto/ParkingQueryRequest.java @@ -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; + } +} diff --git a/src/main/java/com/example/parking/application/parking/dto/ParkingSearchConditionRequest.java b/src/main/java/com/example/parking/application/parking/dto/ParkingSearchConditionRequest.java new file mode 100644 index 00000000..8ff8ccb3 --- /dev/null +++ b/src/main/java/com/example/parking/application/parking/dto/ParkingSearchConditionRequest.java @@ -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 operationTypes; + private final List parkingTypes; + private final String feeType; + private final List payTypes; + private final Integer hours; + private final String priority; + + public ParkingSearchConditionRequest(List operationTypes, List parkingTypes, String feeType, + List payTypes, int hours, String priority) { + this.operationTypes = operationTypes; + this.parkingTypes = parkingTypes; + this.feeType = feeType; + this.payTypes = payTypes; + this.hours = hours; + this.priority = priority; + } +} diff --git a/src/main/java/com/example/parking/application/searchcondition/SearchConditionService.java b/src/main/java/com/example/parking/application/searchcondition/SearchConditionService.java index dc36abe7..5649eb99 100644 --- a/src/main/java/com/example/parking/application/searchcondition/SearchConditionService.java +++ b/src/main/java/com/example/parking/application/searchcondition/SearchConditionService.java @@ -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; @@ -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); @@ -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 List toEnums(List descriptions, E... values) { - return descriptions.stream() - .map(description -> SearchConditionAvailable.find(description, values)) - .toList(); - } } diff --git a/src/main/java/com/example/parking/config/WebMvcConfig.java b/src/main/java/com/example/parking/config/WebMvcConfig.java index 36da6a9c..94b08500 100644 --- a/src/main/java/com/example/parking/config/WebMvcConfig.java +++ b/src/main/java/com/example/parking/config/WebMvcConfig.java @@ -1,8 +1,9 @@ package com.example.parking.config; import com.example.parking.config.argumentresolver.AuthArgumentResolver; +import com.example.parking.config.argumentresolver.parking.ParkingQueryArgumentResolver; +import com.example.parking.config.argumentresolver.parking.ParkingSearchConditionArgumentResolver; import com.example.parking.config.interceptor.AuthInterceptor; - import io.swagger.v3.oas.models.PathItem; import java.util.List; import lombok.RequiredArgsConstructor; @@ -19,6 +20,8 @@ public class WebMvcConfig implements WebMvcConfigurer { private final AuthInterceptor authInterceptor; private final AuthArgumentResolver authArgumentResolver; + private final ParkingQueryArgumentResolver parkingQueryArgumentResolver; + private final ParkingSearchConditionArgumentResolver parkingSearchConditionArgumentResolver; @Value("${cors.allowedOrigins}") private String[] allowedOrigins; @@ -32,13 +35,16 @@ public void addInterceptors(InterceptorRegistry registry) { "/swagger-resources/**", "/swagger-ui/**", "/signup", - "/signin" + "/signin", + "/parkings" )); } @Override public void addArgumentResolvers(List resolvers) { resolvers.add(authArgumentResolver); + resolvers.add(parkingQueryArgumentResolver); + resolvers.add(parkingSearchConditionArgumentResolver); } @Override diff --git a/src/main/java/com/example/parking/config/argumentresolver/AuthArgumentResolver.java b/src/main/java/com/example/parking/config/argumentresolver/AuthArgumentResolver.java index 86d3c042..8a4ae81f 100644 --- a/src/main/java/com/example/parking/config/argumentresolver/AuthArgumentResolver.java +++ b/src/main/java/com/example/parking/config/argumentresolver/AuthArgumentResolver.java @@ -27,7 +27,11 @@ public boolean supportsParameter(MethodParameter parameter) { @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + MemberAuth memberAuth = parameter.getParameterAnnotation(MemberAuth.class); String sessionId = webRequest.getHeader(JSESSIONID); + if (memberAuth.nullable() && sessionId == null) { + return null; + } MemberSession session = authService.findSession(sessionId); return session.getMemberId(); } diff --git a/src/main/java/com/example/parking/config/argumentresolver/MemberAuth.java b/src/main/java/com/example/parking/config/argumentresolver/MemberAuth.java index af6d7f49..4fdcee5a 100644 --- a/src/main/java/com/example/parking/config/argumentresolver/MemberAuth.java +++ b/src/main/java/com/example/parking/config/argumentresolver/MemberAuth.java @@ -8,4 +8,6 @@ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface MemberAuth { + + boolean nullable() default false; } diff --git a/src/main/java/com/example/parking/config/argumentresolver/parking/ParkingQuery.java b/src/main/java/com/example/parking/config/argumentresolver/parking/ParkingQuery.java new file mode 100644 index 00000000..e328d4e7 --- /dev/null +++ b/src/main/java/com/example/parking/config/argumentresolver/parking/ParkingQuery.java @@ -0,0 +1,11 @@ +package com.example.parking.config.argumentresolver.parking; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface ParkingQuery { +} diff --git a/src/main/java/com/example/parking/config/argumentresolver/parking/ParkingQueryArgumentResolver.java b/src/main/java/com/example/parking/config/argumentresolver/parking/ParkingQueryArgumentResolver.java new file mode 100644 index 00000000..674a7390 --- /dev/null +++ b/src/main/java/com/example/parking/config/argumentresolver/parking/ParkingQueryArgumentResolver.java @@ -0,0 +1,28 @@ +package com.example.parking.config.argumentresolver.parking; + +import com.example.parking.application.parking.dto.ParkingQueryRequest; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +public class ParkingQueryArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(ParkingQuery.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + return new ParkingQueryRequest( + Double.valueOf(webRequest.getParameter("latitude")), + Double.valueOf(webRequest.getParameter("longitude")), + Integer.parseInt(webRequest.getParameter("radius")) + ); + } +} diff --git a/src/main/java/com/example/parking/config/argumentresolver/parking/ParkingSearchCondition.java b/src/main/java/com/example/parking/config/argumentresolver/parking/ParkingSearchCondition.java new file mode 100644 index 00000000..275cf05e --- /dev/null +++ b/src/main/java/com/example/parking/config/argumentresolver/parking/ParkingSearchCondition.java @@ -0,0 +1,11 @@ +package com.example.parking.config.argumentresolver.parking; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface ParkingSearchCondition { +} diff --git a/src/main/java/com/example/parking/config/argumentresolver/parking/ParkingSearchConditionArgumentResolver.java b/src/main/java/com/example/parking/config/argumentresolver/parking/ParkingSearchConditionArgumentResolver.java new file mode 100644 index 00000000..282629e6 --- /dev/null +++ b/src/main/java/com/example/parking/config/argumentresolver/parking/ParkingSearchConditionArgumentResolver.java @@ -0,0 +1,78 @@ +package com.example.parking.config.argumentresolver.parking; + +import com.example.parking.application.SearchConditionMapper; +import com.example.parking.application.parking.dto.ParkingSearchConditionRequest; +import com.example.parking.domain.parking.OperationType; +import com.example.parking.domain.parking.ParkingType; +import com.example.parking.domain.parking.PayType; +import java.util.Arrays; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@RequiredArgsConstructor +@Component +public class ParkingSearchConditionArgumentResolver implements HandlerMethodArgumentResolver { + + private static final int BASE_HOURS = 1; + private static final String NOT_FREE = "유료"; + private static final String RECOMMEND_ORDER_CONDITION = "추천 순"; + + private final SearchConditionMapper searchConditionMapper; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(ParkingSearchCondition.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + + String[] operationTypes = webRequest.getParameterValues("operationTypes"); + String[] parkingTypes = webRequest.getParameterValues("parkingTypes"); + String feeType = webRequest.getParameter("feeTypes"); + String[] payTypes = webRequest.getParameterValues("payTypes"); + Integer hours = Integer.parseInt(webRequest.getParameter("hours")); + String priority = webRequest.getParameter("priority"); + + if (containsNull(operationTypes, parkingTypes, feeType, payTypes, hours)) { + return defaultRequest(); + } + + return new ParkingSearchConditionRequest( + toList(operationTypes), + toList(parkingTypes), + feeType, + toList(payTypes), + hours, + priority + ); + } + + private boolean containsNull(String[] operationTypes, String[] parkingTypes, String feeType, + String[] payTypes, Integer hours) { + return operationTypes == null || parkingTypes == null || feeType == null || payTypes == null || hours == null; + } + + private ParkingSearchConditionRequest defaultRequest() { + return new ParkingSearchConditionRequest( + searchConditionMapper.getValues(OperationType.class), + searchConditionMapper.getValues(ParkingType.class), + NOT_FREE, + searchConditionMapper.getValues(PayType.class), + BASE_HOURS, + RECOMMEND_ORDER_CONDITION + ); + } + + private List toList(String[] parameters) { + return Arrays.stream(parameters) + .toList(); + } +} diff --git a/src/main/java/com/example/parking/domain/favorite/FavoriteRepository.java b/src/main/java/com/example/parking/domain/favorite/FavoriteRepository.java index b3193dfc..81aa3862 100644 --- a/src/main/java/com/example/parking/domain/favorite/FavoriteRepository.java +++ b/src/main/java/com/example/parking/domain/favorite/FavoriteRepository.java @@ -1,7 +1,7 @@ package com.example.parking.domain.favorite; import com.example.parking.domain.member.Member; -import com.example.parking.domain.parking.ParkingId; +import com.example.parking.domain.parking.Parking; import com.example.parking.support.Association; import java.util.List; import org.springframework.data.repository.Repository; @@ -10,7 +10,7 @@ public interface FavoriteRepository extends Repository { Favorite save(Favorite favorite); - void deleteByMemberIdAndParkingId(Association memberId, Association parkingId); + void deleteByMemberIdAndParkingId(Association memberId, Association parkingId); List findByMemberId(Association memberId); } diff --git a/src/main/java/com/example/parking/domain/parking/BaseInformation.java b/src/main/java/com/example/parking/domain/parking/BaseInformation.java index fde5eaf7..8287650d 100644 --- a/src/main/java/com/example/parking/domain/parking/BaseInformation.java +++ b/src/main/java/com/example/parking/domain/parking/BaseInformation.java @@ -1,9 +1,11 @@ package com.example.parking.domain.parking; import static jakarta.persistence.EnumType.STRING; + import jakarta.persistence.Embeddable; import jakarta.persistence.Embedded; import jakarta.persistence.Enumerated; +import java.util.List; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -35,4 +37,18 @@ public BaseInformation(String name, String tel, String address, PayTypes payType this.parkingType = parkingType; this.operationType = operationType; } + + public boolean containsOperationType(List operationTypes) { + return operationTypes.stream() + .anyMatch(operationType -> this.operationType == operationType); + } + + public boolean containsParkingType(List parkingTypes) { + return parkingTypes.stream() + .anyMatch(parkingType -> this.parkingType == parkingType); + } + + public boolean containsPayType(List memberPayTypes) { + return this.payTypes.contains(memberPayTypes); + } } diff --git a/src/main/java/com/example/parking/domain/parking/Fee.java b/src/main/java/com/example/parking/domain/parking/Fee.java index 60b12a1f..2845b1cd 100644 --- a/src/main/java/com/example/parking/domain/parking/Fee.java +++ b/src/main/java/com/example/parking/domain/parking/Fee.java @@ -48,6 +48,10 @@ public static Fee min(Fee fee, Fee otherFee) { return otherFee; } + public boolean isFree() { + return fee == 0; + } + public boolean isValidFee() { return !NO_INFO.equals(this); } diff --git a/src/main/java/com/example/parking/domain/parking/Location.java b/src/main/java/com/example/parking/domain/parking/Location.java index 32711458..bc31f4b9 100644 --- a/src/main/java/com/example/parking/domain/parking/Location.java +++ b/src/main/java/com/example/parking/domain/parking/Location.java @@ -5,6 +5,10 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.PrecisionModel; @Getter @EqualsAndHashCode @@ -12,19 +16,19 @@ @Embeddable public class Location { - private static final Location NO_PROVIDE = new Location(-1.0, -1.0); + private static final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); + private static final Location NO_PROVIDE = Location.of(-1.0, -1.0); - private double longitude; - private double latitude; + private Point point; - private Location(double longitude, double latitude) { - this.longitude = longitude; - this.latitude = latitude; + private Location(Point point) { + this.point = point; } public static Location of(Double longitude, Double latitude) { try { - return new Location(longitude, latitude); + Point point = geometryFactory.createPoint(new Coordinate(longitude, latitude)); + return new Location(point); } catch (NullPointerException e) { return NO_PROVIDE; } @@ -32,9 +36,17 @@ public static Location of(Double longitude, Double latitude) { public static Location of(String longitude, String latitude) { try { - return new Location(Double.parseDouble(longitude), Double.parseDouble(latitude)); + return Location.of(Double.parseDouble(longitude), Double.parseDouble(latitude)); } catch (NumberFormatException | NullPointerException e) { return NO_PROVIDE; } } + + public double getLatitude() { + return point.getY(); + } + + public double getLongitude() { + return point.getX(); + } } diff --git a/src/main/java/com/example/parking/domain/parking/Parking.java b/src/main/java/com/example/parking/domain/parking/Parking.java index d29093b7..7653cb45 100644 --- a/src/main/java/com/example/parking/domain/parking/Parking.java +++ b/src/main/java/com/example/parking/domain/parking/Parking.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Objects; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -17,6 +18,8 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Parking extends AuditingEntity { + private static final int AVERAGE_WALKING_SPEED = 5; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -39,6 +42,18 @@ public class Parking extends AuditingEntity { @Embedded private FeePolicy feePolicy; + @Builder + private Parking(Long id, BaseInformation baseInformation, Location location, Space space, + FreeOperatingTime freeOperatingTime, OperatingTime operatingTime, FeePolicy feePolicy) { + this.id = id; + this.baseInformation = baseInformation; + this.location = location; + this.space = space; + this.freeOperatingTime = freeOperatingTime; + this.operatingTime = operatingTime; + this.feePolicy = feePolicy; + } + public Parking(BaseInformation baseInformation, Location location, Space space, FreeOperatingTime freeOperatingTime, OperatingTime operatingTime, FeePolicy feePolicy) { this.baseInformation = baseInformation; @@ -72,6 +87,45 @@ public void update(Location location) { this.location = location; } + public boolean containsOperationType(List operationTypes) { + return baseInformation.containsOperationType(operationTypes); + } + + public boolean containsParkingType(List parkingTypes) { + return baseInformation.containsParkingType(parkingTypes); + } + + public boolean containsPayType(List memberPayTypes) { + return baseInformation.containsPayType(memberPayTypes); + } + + public int calculateWalkingTime(Location destination) { + double distance = calculateDistanceToDestination(destination); + double averageWalkingTime = distance / AVERAGE_WALKING_SPEED; + return (int) Math.ceil(averageWalkingTime); + } + + private double calculateDistanceToDestination(Location destination) { + double parkingLongitude = this.location.getLongitude(); + double parkingLatitude = this.location.getLatitude(); + + double radius = 6371; // 지구 반지름(km) + double toRadian = Math.PI / 180; + + double deltaLatitude = Math.abs(parkingLatitude - destination.getLatitude()) * toRadian; + double deltaLongitude = Math.abs(parkingLongitude - destination.getLongitude()) * toRadian; + + double sinDeltaLat = Math.sin(deltaLatitude / 2); + double sinDeltaLng = Math.sin(deltaLongitude / 2); + double squareRoot = Math.sqrt( + sinDeltaLat * sinDeltaLat + + Math.cos(parkingLatitude * toRadian) * Math.cos(destination.getLatitude() * toRadian) + * sinDeltaLng + * sinDeltaLng); + + return 2 * radius * Math.asin(squareRoot); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/src/main/java/com/example/parking/domain/parking/ParkingFeeCalculator.java b/src/main/java/com/example/parking/domain/parking/ParkingFeeCalculator.java index 54f2e1b3..4c974cef 100644 --- a/src/main/java/com/example/parking/domain/parking/ParkingFeeCalculator.java +++ b/src/main/java/com/example/parking/domain/parking/ParkingFeeCalculator.java @@ -4,7 +4,9 @@ import java.time.LocalTime; import java.util.ArrayList; import java.util.List; +import org.springframework.stereotype.Component; +@Component public class ParkingFeeCalculator { public Fee calculateParkingFee(Parking parking, LocalDateTime beginTime, LocalDateTime endTime) { @@ -26,7 +28,8 @@ public Fee calculateParkingFee(Parking parking, LocalDateTime beginTime, LocalDa private List separateDate(LocalDateTime beginTime, LocalDateTime endTime) { if (isSameDate(beginTime, endTime)) { - return List.of(new DayParking(Day.from(beginTime.getDayOfWeek()), beginTime.toLocalTime(), endTime.toLocalTime())); + return List.of( + new DayParking(Day.from(beginTime.getDayOfWeek()), beginTime.toLocalTime(), endTime.toLocalTime())); } List dayParkingDates = new ArrayList<>(); diff --git a/src/main/java/com/example/parking/domain/parking/ParkingId.java b/src/main/java/com/example/parking/domain/parking/ParkingId.java deleted file mode 100644 index 4b6972e8..00000000 --- a/src/main/java/com/example/parking/domain/parking/ParkingId.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.parking.domain.parking; - -import jakarta.persistence.Embeddable; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Embeddable -@Getter -@NoArgsConstructor -public class ParkingId { - - private Long parkingId; - - public ParkingId(Long parkingId) { - this.parkingId = parkingId; - } -} diff --git a/src/main/java/com/example/parking/domain/parking/ParkingRepository.java b/src/main/java/com/example/parking/domain/parking/ParkingRepository.java deleted file mode 100644 index a9bb0728..00000000 --- a/src/main/java/com/example/parking/domain/parking/ParkingRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.parking.domain.parking; - -import com.example.parking.support.exception.DomainException; -import com.example.parking.support.exception.ExceptionInformation; -import java.util.Optional; -import java.util.Set; -import org.springframework.data.repository.Repository; - -public interface ParkingRepository extends Repository { - - Optional findById(Long id); - - default Parking getById(Long id) { - return findById(id).orElseThrow(() -> new DomainException(ExceptionInformation.INVALID_PARKING)); - } - - Set findAllByBaseInformationNameIn(Set names); - - void saveAll(Iterable parkingLots); -} diff --git a/src/main/java/com/example/parking/domain/parking/PayType.java b/src/main/java/com/example/parking/domain/parking/PayType.java index a1e8fee2..11e9f793 100644 --- a/src/main/java/com/example/parking/domain/parking/PayType.java +++ b/src/main/java/com/example/parking/domain/parking/PayType.java @@ -13,7 +13,7 @@ public enum PayType implements SearchConditionAvailable { private final String description; - PayType(final String description) { + PayType(String description) { this.description = description; } diff --git a/src/main/java/com/example/parking/domain/parking/PayTypes.java b/src/main/java/com/example/parking/domain/parking/PayTypes.java index d45eab1f..ac216367 100644 --- a/src/main/java/com/example/parking/domain/parking/PayTypes.java +++ b/src/main/java/com/example/parking/domain/parking/PayTypes.java @@ -2,6 +2,7 @@ import jakarta.persistence.Embeddable; import java.util.Collection; +import java.util.List; import java.util.stream.Collectors; import lombok.AccessLevel; import lombok.EqualsAndHashCode; @@ -34,4 +35,9 @@ public static PayTypes from(Collection payTypes) { .collect(Collectors.joining(DELIMITER)) ); } + + public boolean contains(List memberPayTypes) { + return memberPayTypes.stream() + .anyMatch(payType -> this.description.contains(payType.getDescription())); + } } diff --git a/src/main/java/com/example/parking/domain/parking/SearchingCondition.java b/src/main/java/com/example/parking/domain/parking/SearchingCondition.java new file mode 100644 index 00000000..820ddb7a --- /dev/null +++ b/src/main/java/com/example/parking/domain/parking/SearchingCondition.java @@ -0,0 +1,25 @@ +package com.example.parking.domain.parking; + +import com.example.parking.domain.searchcondition.FeeType; +import java.util.List; +import lombok.Getter; + +@Getter +public class SearchingCondition { + + private final List operationTypes; + private final List parkingTypes; + private final List payTypes; + private final FeeType feeType; + private final Integer hours; + + public SearchingCondition(List operationTypes, List parkingTypes, + List payTypes, + FeeType feeType, Integer hours) { + this.operationTypes = operationTypes; + this.parkingTypes = parkingTypes; + this.payTypes = payTypes; + this.feeType = feeType; + this.hours = hours; + } +} diff --git a/src/main/java/com/example/parking/domain/parking/dto/ParkingQueryCondition.java b/src/main/java/com/example/parking/domain/parking/dto/ParkingQueryCondition.java new file mode 100644 index 00000000..3e40c896 --- /dev/null +++ b/src/main/java/com/example/parking/domain/parking/dto/ParkingQueryCondition.java @@ -0,0 +1,23 @@ +package com.example.parking.domain.parking.dto; + +import com.example.parking.domain.parking.OperationType; +import com.example.parking.domain.parking.ParkingType; +import com.example.parking.domain.parking.PayTypes; +import lombok.Getter; + +@Getter +public class ParkingQueryCondition { + + private OperationType operationType; + private ParkingType parkingType; + private Boolean cardEnabled; + private PayTypes payTypes; + + public ParkingQueryCondition(OperationType operationType, ParkingType parkingType, Boolean cardEnabled, + PayTypes payTypes) { + this.operationType = operationType; + this.parkingType = parkingType; + this.cardEnabled = cardEnabled; + this.payTypes = payTypes; + } +} diff --git a/src/main/java/com/example/parking/domain/parking/repository/ParkingRepository.java b/src/main/java/com/example/parking/domain/parking/repository/ParkingRepository.java new file mode 100644 index 00000000..ebc27d45 --- /dev/null +++ b/src/main/java/com/example/parking/domain/parking/repository/ParkingRepository.java @@ -0,0 +1,43 @@ +package com.example.parking.domain.parking.repository; + +import com.example.parking.domain.parking.Parking; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.locationtech.jts.geom.Point; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; + +public interface ParkingRepository extends Repository { + + default Parking getById(Long id) { + return findById(id).orElseThrow(() -> new RuntimeException("익셉션 !!")); + } + + Optional findById(Long id); + + @Query(""" + SELECT p + FROM Parking p + WHERE ST_Contains(ST_Buffer(:point, :radius), p.location.point) + """ + ) + List findAroundParkingLots(@Param("point") Point point, @Param("radius") int radius); + + @Query(""" + SELECT p + FROM Parking p + WHERE ST_Contains(ST_Buffer(:point, :radius), p.location.point) + ORDER BY ST_DISTANCE_SPHERE(:point, p.location.point) + """ + ) + List findAroundParkingLotsOrderByDistance( + @Param("point") Point point, + @Param("radius") int radius + ); + + Set findAllByBaseInformationNameIn(Set parkingNames); + + void saveAll(Iterable parkingLots); +} diff --git a/src/main/java/com/example/parking/domain/searchcondition/SearchConditionAvailable.java b/src/main/java/com/example/parking/domain/searchcondition/SearchConditionAvailable.java index 61a16918..affea042 100644 --- a/src/main/java/com/example/parking/domain/searchcondition/SearchConditionAvailable.java +++ b/src/main/java/com/example/parking/domain/searchcondition/SearchConditionAvailable.java @@ -1,17 +1,8 @@ package com.example.parking.domain.searchcondition; -import java.util.Arrays; - public interface SearchConditionAvailable { String getDescription(); E getDefault(); - - static E find(String description, E... values) { - return Arrays.stream(values) - .filter(e -> description.startsWith(e.getDescription())) - .findAny() - .orElse(values[0].getDefault()); - } } diff --git a/src/main/java/com/example/parking/external/coordinate/dto/CoordinateResponse.java b/src/main/java/com/example/parking/external/coordinate/dto/CoordinateResponse.java index 2d94099e..e9b673f9 100644 --- a/src/main/java/com/example/parking/external/coordinate/dto/CoordinateResponse.java +++ b/src/main/java/com/example/parking/external/coordinate/dto/CoordinateResponse.java @@ -21,17 +21,14 @@ public CoordinateResponse(List exactLocations, Meta meta) { } @NoArgsConstructor + @Getter public static class ExactLocation { - private Double x; - private Double y; - public Double getLatitude() { - return y; - } + @JsonProperty("x") + private Double latitude; - public Double getLongitude() { - return x; - } + @JsonProperty("y") + private Double longitude; } @Getter diff --git a/src/main/java/com/example/parking/support/exception/ExceptionInformation.java b/src/main/java/com/example/parking/support/exception/ExceptionInformation.java index a489c337..bd3c994e 100644 --- a/src/main/java/com/example/parking/support/exception/ExceptionInformation.java +++ b/src/main/java/com/example/parking/support/exception/ExceptionInformation.java @@ -22,7 +22,8 @@ public enum ExceptionInformation { INVALID_CONNECT("주차장 API 연결 중 예외 발생"), COORDINATE_EXCEPTION("좌표 변환 중 예외 발생"), - INVALID_AUTH_CODE("존재하지 않는 인증코드 입니다."); + INVALID_AUTH_CODE("존재하지 않는 인증코드 입니다."), + INVALID_DESCRIPTION("해당하는 내용의 검색 조건이 존재하지 않습니다."); private final String message; diff --git a/src/test/java/com/example/parking/application/parking/ParkingFilteringServiceTest.java b/src/test/java/com/example/parking/application/parking/ParkingFilteringServiceTest.java new file mode 100644 index 00000000..36b74bcd --- /dev/null +++ b/src/test/java/com/example/parking/application/parking/ParkingFilteringServiceTest.java @@ -0,0 +1,163 @@ +package com.example.parking.application.parking; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.example.parking.domain.parking.BaseInformation; +import com.example.parking.domain.parking.Fee; +import com.example.parking.domain.parking.FeePolicy; +import com.example.parking.domain.parking.FreeOperatingTime; +import com.example.parking.domain.parking.OperationType; +import com.example.parking.domain.parking.Parking; +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.PayTypes; +import com.example.parking.domain.parking.SearchingCondition; +import com.example.parking.domain.parking.TimeUnit; +import com.example.parking.domain.searchcondition.FeeType; +import java.time.LocalDateTime; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class ParkingFilteringServiceTest { + + private final ParkingFilteringService parkingFilteringService = new ParkingFilteringService( + new ParkingFeeCalculator()); + + @Test + void 조회조건에_따라_주차장을_필터링한다1() { + // given + ParkingType parkingTypeCondition = ParkingType.MECHANICAL; + OperationType operationTypeCondition = OperationType.PUBLIC; + PayType wantPayType = PayType.CASH; + + Parking wantParking = Parking.builder() + .baseInformation(new BaseInformation("name", "tell", "address", + PayTypes.from(List.of(wantPayType)), + parkingTypeCondition, + operationTypeCondition)) + .build(); + + Parking notWantParking1 = Parking.builder() + .baseInformation(new BaseInformation("name", "tell", "address", + PayTypes.DEFAULT, + parkingTypeCondition, + OperationType.NO_INFO)) + .build(); + + Parking notWantParking2 = Parking.builder() + .baseInformation(new BaseInformation("name", "tell", "address", + PayTypes.DEFAULT, + ParkingType.OFF_STREET, + operationTypeCondition)) + .build(); + + // when + SearchingCondition searchingCondition = new SearchingCondition( + List.of(operationTypeCondition), + List.of(parkingTypeCondition), + List.of(wantPayType), + FeeType.PAID, 3); + + List filterList = parkingFilteringService.filterByCondition( + List.of(wantParking, notWantParking1, notWantParking2), + searchingCondition, + LocalDateTime.now() + ); + + // then + assertThat(filterList).hasSize(1); + } + + @Test + void 조회조건에_따라_주차장을_필터링한다2() { + // given + ParkingType wantParkingTypeCondition = ParkingType.ON_STREET; + OperationType wantOperationTypeCondition = OperationType.PUBLIC; + PayType wantPayType = PayType.CARD; + + Parking wantParking = Parking.builder() + .baseInformation(new BaseInformation("name", "tel", "address", + PayTypes.from(List.of(wantPayType)), + wantParkingTypeCondition, + wantOperationTypeCondition)) + .build(); + + Parking notWantParking1 = Parking.builder() + .baseInformation(new BaseInformation("name", "tel", "address", + PayTypes.DEFAULT, + ParkingType.MECHANICAL, + wantOperationTypeCondition)) + .build(); + + Parking notWantParking2 = Parking.builder() + .baseInformation(new BaseInformation("name", "tel", "address", + PayTypes.DEFAULT, + ParkingType.NO_INFO, + wantOperationTypeCondition)) + .build(); + + // when + SearchingCondition searchingCondition = new SearchingCondition( + List.of(wantOperationTypeCondition), + List.of(wantParkingTypeCondition), + List.of(wantPayType), + FeeType.PAID, 3); + + List result = parkingFilteringService.filterByCondition( + List.of(wantParking, notWantParking1, notWantParking2), + searchingCondition, + LocalDateTime.now() + ); + + // then + assertThat(result).hasSize(1); + } + + @Test + void 조회조건이_무료일_때_예상요금이_0인_주차장만_조회된다() { + // given - 하루종일 무료 주차장 2개, 유료 주차장 1개 + FeePolicy freeFeePolicy = new FeePolicy(Fee.ZERO, Fee.ZERO, TimeUnit.from(10), TimeUnit.from(10), Fee.ZERO); + + OperationType operationType = OperationType.PUBLIC; + ParkingType parkingType = ParkingType.MECHANICAL; + BaseInformation baseInformation = new BaseInformation("name", "tel", "address", + PayTypes.from(List.of(PayType.CARD)), + parkingType, + operationType + ); + Parking freeParking1 = Parking.builder() + .baseInformation(baseInformation) + .freeOperatingTime(FreeOperatingTime.ALWAYS_FREE) + .feePolicy(freeFeePolicy) + .build(); + + Parking freeParking2 = Parking.builder() + .baseInformation(baseInformation) + .freeOperatingTime(FreeOperatingTime.ALWAYS_FREE) + .feePolicy(freeFeePolicy) + .build(); + + FeePolicy paidFeePolicy = new FeePolicy(Fee.from(100), Fee.from(200), TimeUnit.from(1), TimeUnit.from(12), + Fee.from(1000)); + Parking paidParking = Parking.builder() + .baseInformation(baseInformation) + .freeOperatingTime(FreeOperatingTime.ALWAYS_PAY) + .feePolicy(paidFeePolicy) + .build(); + + // when - 검색조건이 Free 인 filterCondition 으로 주차장 필터링 + SearchingCondition searchingCondition = new SearchingCondition(List.of(operationType), List.of(parkingType), + List.of(PayType.CARD), FeeType.FREE, 3); + List filteredParkings = parkingFilteringService.filterByCondition( + List.of(freeParking1, freeParking2, paidParking), + searchingCondition, + LocalDateTime.now() + ); + + // then + Assertions.assertThat(filteredParkings).hasSize(2); + + } +} diff --git a/src/test/java/com/example/parking/domain/parking/LocationTest.java b/src/test/java/com/example/parking/domain/parking/LocationTest.java index 361dd388..11811709 100644 --- a/src/test/java/com/example/parking/domain/parking/LocationTest.java +++ b/src/test/java/com/example/parking/domain/parking/LocationTest.java @@ -1,15 +1,30 @@ package com.example.parking.domain.parking; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class LocationTest { + @Test + void 위도와_경도를_통해_Point객체를_가진_Location객체를_생성한다() { + double latitude = 37.4809626; + double longitude = 127.1216765; + + Location location = Location.of(longitude, latitude); + + assertAll( + () -> assertThat(location.getLongitude()).isEqualTo(longitude), + () -> assertThat(location.getLatitude()).isEqualTo(latitude) + ); + } + @CsvSource({"0.1, -", " , 0.2", "nil, nil"}) @ParameterizedTest void 이상한_값이_들어오면_특정_음수_좌표를_반환한다(String longitude, String latitude) { diff --git a/src/test/java/com/example/parking/domain/parking/ParkingTest.java b/src/test/java/com/example/parking/domain/parking/ParkingTest.java index 70fd7c7e..4b33b123 100644 --- a/src/test/java/com/example/parking/domain/parking/ParkingTest.java +++ b/src/test/java/com/example/parking/domain/parking/ParkingTest.java @@ -1,14 +1,46 @@ package com.example.parking.domain.parking; +import java.util.stream.Stream; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.util.stream.Stream; - class ParkingTest { + @Test + void 목적지와_68미터_떨어진_주차장_도보_예상시간_계산() { + // given (parking 과 destination 거리 68m) + int expectedTime = (int) Math.ceil(0.068 / 5); + Parking parking = Parking.builder() + .location(Location.of(127.1215865, 37.4811181)) + .build(); + Location destination = Location.of(127.1213647, 37.4817298); + + // when + int walkingTime = parking.calculateWalkingTime(destination); + + // then + Assertions.assertThat(walkingTime).isEqualTo(expectedTime); + } + + @Test + void 목적지와_333미터_떨어진_주차장_도보_예상시간_계산() { + // given (parking 과 destination 거리 68m) + int expectedTime = (int) Math.ceil(0.333 / 5); + Parking parking = Parking.builder() + .location(Location.of(127.1215865, 37.4811181)) + .build(); + Location destination = Location.of(127.1186224, 37.479259); + + // when + int walkingTime = parking.calculateWalkingTime(destination); + + // then + Assertions.assertThat(walkingTime).isEqualTo(expectedTime); + } + @ParameterizedTest @MethodSource("getParking") void 주어진_주차장_정보에_따라_하루_요금_계산을_한다(Parking parking, int payOfChargeMinutes, Fee expected) { diff --git a/src/test/java/com/example/parking/domain/parking/ParkingTypeTest.java b/src/test/java/com/example/parking/domain/parking/ParkingTypeTest.java deleted file mode 100644 index 967e172e..00000000 --- a/src/test/java/com/example/parking/domain/parking/ParkingTypeTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.parking.domain.parking; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import static org.junit.jupiter.params.provider.Arguments.arguments; - -import java.util.stream.Stream; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -@DisplayNameGeneration(ReplaceUnderscores.class) -class ParkingTypeTest { - - @ParameterizedTest(name = "찾으려는 주차장 정보: {0} | 예상 결과: {1}") - @MethodSource("parametersProvider") - void 주차장_유형_설명으로_조회(String description, ParkingType expected) { - //given, when - ParkingType actual = ParkingType.find(description); - - //then - assertThat(actual).isEqualTo(expected); - } - - static Stream parametersProvider() { - return Stream.of( - arguments("노외", ParkingType.OFF_STREET), - arguments("노외주차장", ParkingType.OFF_STREET), - arguments("노외 주차장", ParkingType.OFF_STREET), - arguments("노상", ParkingType.ON_STREET), - arguments("노상주차장", ParkingType.ON_STREET), - arguments("노상 주차장", ParkingType.ON_STREET), - arguments("기계", ParkingType.MECHANICAL), - arguments("기계식주차", ParkingType.MECHANICAL), - arguments("기계식 주차장", ParkingType.MECHANICAL), - arguments("-", ParkingType.NO_INFO), - arguments("", ParkingType.NO_INFO), - arguments("차영호 주차장", ParkingType.NO_INFO) - ); - } -} diff --git a/src/test/java/com/example/parking/domain/parking/PayTypesTest.java b/src/test/java/com/example/parking/domain/parking/PayTypesTest.java index f16bd17d..d668fa87 100644 --- a/src/test/java/com/example/parking/domain/parking/PayTypesTest.java +++ b/src/test/java/com/example/parking/domain/parking/PayTypesTest.java @@ -8,10 +8,12 @@ import static org.junit.jupiter.params.provider.Arguments.arguments; import java.util.Collection; +import java.util.List; import java.util.Set; import java.util.stream.Stream; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -29,6 +31,24 @@ class PayTypesTest { assertThat(actual.getDescription()).isEqualTo(expected); } + @Test + void 결제방식_포함_시_true() { + //given, when + PayTypes actual = PayTypes.from(List.of(CARD, CASH)); + + //then + assertThat(actual.contains(List.of(CARD))).isTrue(); + } + + @Test + void 결제방식_미포함_시_false() { + //given, when + PayTypes actual = PayTypes.from(List.of(CARD, CASH)); + + //then + assertThat(actual.contains(List.of(BANK_TRANSFER))).isFalse(); + } + static Stream parametersProvider() { final String DELIMITER = ", "; return Stream.of( diff --git a/src/test/java/com/example/parking/domain/searchcondition/SearchConditionAvailableTest.java b/src/test/java/com/example/parking/domain/searchcondition/SearchConditionAvailableTest.java deleted file mode 100644 index 127d7eb2..00000000 --- a/src/test/java/com/example/parking/domain/searchcondition/SearchConditionAvailableTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.example.parking.domain.searchcondition; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.params.provider.Arguments.arguments; - -import com.example.parking.domain.parking.OperationType; -import com.example.parking.domain.parking.ParkingType; -import com.example.parking.domain.parking.PayType; -import java.util.stream.Stream; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -class SearchConditionAvailableTest { - - @ParameterizedTest - @MethodSource("parametersProvider") - void 설명과_enum_values_로_해당하는_값을_찾고_없으면_default를_반환한다(String description, - E expected, E... values) { - //given, when - E actual = SearchConditionAvailable.find(description, values); - - //then - assertThat(actual).isEqualTo(expected); - } - - static Stream parametersProvider() { - return Stream.of( - arguments("민영 주차장", OperationType.PRIVATE, OperationType.values()), - arguments("공영", OperationType.PUBLIC, OperationType.values()), - arguments("노상", ParkingType.ON_STREET, ParkingType.values()), - arguments("노외 주차장", ParkingType.OFF_STREET, ParkingType.values()), - arguments("무료", FeeType.FREE, FeeType.values()), - arguments("유료", FeeType.PAID, FeeType.values()), - arguments("계좌", PayType.BANK_TRANSFER, PayType.values()), - arguments("~", PayType.NO_INFO, PayType.values()), - arguments("가격순", Priority.PRICE, Priority.values()), - arguments("아무거나 입력", Priority.RECOMMENDATION, Priority.values()) - ); - } -} diff --git a/src/test/java/com/example/parking/domain/searchcondition/SearchConditionMapperTest.java b/src/test/java/com/example/parking/domain/searchcondition/SearchConditionMapperTest.java new file mode 100644 index 00000000..948796ae --- /dev/null +++ b/src/test/java/com/example/parking/domain/searchcondition/SearchConditionMapperTest.java @@ -0,0 +1,104 @@ +package com.example.parking.domain.searchcondition; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import com.example.parking.application.SearchConditionMapper; +import com.example.parking.domain.parking.OperationType; +import com.example.parking.domain.parking.ParkingType; +import com.example.parking.domain.parking.PayType; +import com.example.parking.support.exception.ClientException; +import com.example.parking.support.exception.ExceptionInformation; +import java.util.List; +import java.util.stream.Stream; +import org.junit.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class SearchConditionMapperTest { + + private static final SearchConditionMapper searchConditionMapper = new SearchConditionMapper(); + + @ParameterizedTest + @MethodSource("parametersProvider1") + & SearchConditionAvailable> void enum_class와_설명으로_해당하는_값을_반환한다(Class clazz, + String description, + E expected) { + //given, when + E actual = searchConditionMapper.toEnum(clazz, description); + + //then + assertThat(actual).isEqualTo(expected); + } + + static Stream parametersProvider1() { + return Stream.of( + arguments(OperationType.class, "민영 주차장", OperationType.PRIVATE), + arguments(OperationType.class, "공영", OperationType.PUBLIC), + arguments(ParkingType.class, "노상", ParkingType.ON_STREET), + arguments(ParkingType.class, "노외 주차장", ParkingType.OFF_STREET), + arguments(FeeType.class, "무료", FeeType.FREE), + arguments(FeeType.class, "유료", FeeType.PAID), + arguments(PayType.class, "계좌", PayType.BANK_TRANSFER), + arguments(Priority.class, "가격순", Priority.PRICE) + ); + } + + @Test + void 변환시_해당하는_값이_없으면_클라이언트_예외를_반환한다() { + //given, when, then + assertThatThrownBy(() -> searchConditionMapper.toEnum(PayType.class, "아무거나 입력")) + .isInstanceOf(ClientException.class) + .hasMessage(ExceptionInformation.INVALID_DESCRIPTION.getMessage()); + } + + @ParameterizedTest + @MethodSource("parametersProvider2") + & SearchConditionAvailable> void enum_class와_설명들로_해당하는_값들을_반환한다(Class clazz, + List descriptions, + List expected) { + //given, when + List actual = searchConditionMapper.toEnums(clazz, descriptions); + + //then + assertThat(actual).containsExactlyInAnyOrderElementsOf(expected); + } + + static Stream parametersProvider2() { + return Stream.of( + arguments(OperationType.class, List.of("민영 주차장", "공영 주차장"), + List.of(OperationType.PRIVATE, OperationType.PUBLIC)), + arguments(OperationType.class, List.of("공영"), List.of(OperationType.PUBLIC)), + arguments(ParkingType.class, List.of("노상", "기계"), + List.of(ParkingType.ON_STREET, ParkingType.MECHANICAL)), + arguments(ParkingType.class, List.of("노외 주차장"), List.of(ParkingType.OFF_STREET)), + arguments(FeeType.class, List.of("무료", "유료"), List.of(FeeType.FREE, FeeType.PAID)), + arguments(FeeType.class, List.of("유료"), List.of(FeeType.PAID)), + arguments(PayType.class, List.of("계좌", "현금"), List.of(PayType.BANK_TRANSFER, PayType.CASH)), + arguments(Priority.class, List.of("가격순"), List.of(Priority.PRICE)) + ); + } + + @ParameterizedTest + @MethodSource("parametersProvider3") + & SearchConditionAvailable> void 해당하는_enum_class의_기본_값을_제외한_값들을_가져온다(Class clazz, + List expected) { + //given, when + List actual = searchConditionMapper.getValues(clazz); + + //then + assertThat(actual).containsExactlyInAnyOrderElementsOf(expected); + } + + static Stream parametersProvider3() { + return Stream.of( + arguments(OperationType.class, List.of("공영", "민영")), + arguments(ParkingType.class, List.of("노상", "기계", "노외")), + arguments(FeeType.class, List.of("무료", "유료")), + arguments(PayType.class, List.of("카드", "계좌", "현금")), + arguments(Priority.class, List.of("가까운순", "가격순")) + ); + } +} diff --git a/src/test/java/com/example/parking/fake/BasicParkingRepository.java b/src/test/java/com/example/parking/fake/BasicParkingRepository.java index a3772736..eb44f02a 100644 --- a/src/test/java/com/example/parking/fake/BasicParkingRepository.java +++ b/src/test/java/com/example/parking/fake/BasicParkingRepository.java @@ -8,11 +8,12 @@ import com.example.parking.domain.parking.OperatingTime; 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.ParkingType; import com.example.parking.domain.parking.PayTypes; import com.example.parking.domain.parking.Space; import com.example.parking.domain.parking.TimeUnit; +import com.example.parking.domain.parking.repository.ParkingRepository; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -20,6 +21,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import org.locationtech.jts.geom.Point; public class BasicParkingRepository implements ParkingRepository, BasicRepository { @@ -31,6 +33,16 @@ public Optional findById(Long id) { return Optional.of(store.get(id)); } + @Override + public List findAroundParkingLots(Point point, int radius) { + return Collections.EMPTY_LIST; + } + + @Override + public List findAroundParkingLotsOrderByDistance(Point point, int radius) { + return Collections.EMPTY_LIST; + } + @Override public Set findAllByBaseInformationNameIn(Set names) { return store.values() diff --git a/src/test/java/com/example/parking/fake/FakeFavoriteRepository.java b/src/test/java/com/example/parking/fake/FakeFavoriteRepository.java new file mode 100644 index 00000000..263edab0 --- /dev/null +++ b/src/test/java/com/example/parking/fake/FakeFavoriteRepository.java @@ -0,0 +1,26 @@ +package com.example.parking.fake; + +import com.example.parking.domain.favorite.Favorite; +import com.example.parking.domain.favorite.FavoriteRepository; +import com.example.parking.domain.member.Member; +import com.example.parking.domain.parking.Parking; +import com.example.parking.support.Association; +import java.util.List; + +public class FakeFavoriteRepository implements FavoriteRepository { + + @Override + public Favorite save(Favorite favorite) { + return null; + } + + @Override + public void deleteByMemberIdAndParkingId(Association memberId, Association parkingId) { + + } + + @Override + public List findByMemberId(Association memberId) { + return null; + } +} diff --git a/src/test/java/com/example/parking/fake/FakeParkingService.java b/src/test/java/com/example/parking/fake/FakeParkingService.java index 58c307d8..3c69ca39 100644 --- a/src/test/java/com/example/parking/fake/FakeParkingService.java +++ b/src/test/java/com/example/parking/fake/FakeParkingService.java @@ -1,13 +1,17 @@ package com.example.parking.fake; +import com.example.parking.application.SearchConditionMapper; +import com.example.parking.application.parking.ParkingFilteringService; import com.example.parking.application.parking.ParkingService; +import com.example.parking.domain.parking.ParkingFeeCalculator; public class FakeParkingService extends ParkingService { private final BasicParkingRepository repository; public FakeParkingService(BasicParkingRepository repository) { - super(repository); + super(repository, new ParkingFilteringService(new ParkingFeeCalculator()), + new FakeFavoriteRepository(), new SearchConditionMapper(), new ParkingFeeCalculator()); this.repository = repository; } diff --git a/src/test/java/com/example/parking/fake/FakeSearchConditionRepository.java b/src/test/java/com/example/parking/fake/FakeSearchConditionRepository.java new file mode 100644 index 00000000..df13129e --- /dev/null +++ b/src/test/java/com/example/parking/fake/FakeSearchConditionRepository.java @@ -0,0 +1,17 @@ +package com.example.parking.fake; + +import com.example.parking.domain.searchcondition.SearchCondition; +import com.example.parking.domain.searchcondition.SearchConditionRepository; +import java.util.Optional; + +public class FakeSearchConditionRepository implements SearchConditionRepository { + @Override + public Optional findByMemberId(Long memberId) { + return Optional.empty(); + } + + @Override + public void save(SearchCondition searchCondition) { + + } +}