Skip to content

Commit

Permalink
feat: 예약 순위 기능 추가 (#65)
Browse files Browse the repository at this point in the history
* feat: 시험 차량 예약 순위 기능 추가

* feat: 시험장 예약 순위 기능 추가

* test: 순위 조회 메서드 테스트 추가
  • Loading branch information
gengminy authored Dec 17, 2023
1 parent 0bce20b commit 7682bf1
Show file tree
Hide file tree
Showing 17 changed files with 267 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
import com.testcar.car.common.annotation.RoleAllowed;
import com.testcar.car.common.response.PageResponse;
import com.testcar.car.domains.carReservation.entity.CarReservation;
import com.testcar.car.domains.carReservation.model.CarReservationRankingResponse;
import com.testcar.car.domains.carReservation.model.CarReservationRequest;
import com.testcar.car.domains.carReservation.model.CarReservationResponse;
import com.testcar.car.domains.carReservation.model.ReturnCarReservationRequest;
import com.testcar.car.domains.carReservation.model.dto.CarReservationDto;
import com.testcar.car.domains.carReservation.model.vo.CarReservationCountVo;
import com.testcar.car.domains.carReservation.model.vo.CarReservationFilterCondition;
import com.testcar.car.domains.member.entity.Member;
import com.testcar.car.domains.member.entity.Role;
Expand Down Expand Up @@ -55,6 +57,15 @@ public PageResponse<CarReservationResponse> getCarReservationsByCondition(
return PageResponse.from(carReservations.map(CarReservationResponse::from));
}

@GetMapping("/reservations/rank")
@RoleAllowed(role = Role.USER)
@Operation(summary = "[대여 이력] 시험차량 대여 이력 순위", description = "시험차량 대여 이력의 순위를 5위까지 조회합니다.")
public List<CarReservationRankingResponse> getCarReservationsByLast7Days() {
final List<CarReservationCountVo> carReservations =
carReservationService.findAllByLast7DaysRank();
return carReservations.stream().map(CarReservationRankingResponse::from).toList();
}

@PatchMapping("/return")
@RoleAllowed(role = Role.USER)
@Operation(summary = "[대여 이력] 시험차량 반납", description = "시험 차량을 반납합니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.testcar.car.domains.carReservation;

import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;

import com.testcar.car.common.exception.BadRequestException;
import com.testcar.car.common.exception.NotFoundException;
Expand All @@ -9,6 +11,7 @@
import com.testcar.car.domains.carReservation.model.CarReservationRequest;
import com.testcar.car.domains.carReservation.model.ReturnCarReservationRequest;
import com.testcar.car.domains.carReservation.model.dto.CarReservationDto;
import com.testcar.car.domains.carReservation.model.vo.CarReservationCountVo;
import com.testcar.car.domains.carReservation.model.vo.CarReservationFilterCondition;
import com.testcar.car.domains.carReservation.repository.CarReservationRepository;
import com.testcar.car.domains.carStock.CarStockService;
Expand All @@ -18,6 +21,8 @@
import com.testcar.car.domains.member.entity.Member;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand Down Expand Up @@ -50,6 +55,24 @@ public List<CarReservation> findAllByMemberAndIds(Member member, List<Long> ids)
return carReservations;
}

/** 일주일 이내 대여된 차량 예약 순위를 조회합니다. */
public List<CarReservationCountVo> findAllByLast7DaysRank() {
final LocalDateTime now = LocalDateTime.now();
final LocalDateTime weekAgo = now.minusDays(7);
List<CarReservationDto> reservations =
carReservationRepository.findAllByCreatedAtBetween(weekAgo, now);
final Map<String, Long> countMap =
reservations.stream()
.map(CarReservationDto::getCarName)
.collect(groupingBy(Function.identity(), counting()));

return countMap.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(5)
.map(CarReservationCountVo::from)
.toList();
}

/** 시험차량을 예약합니다. */
public CarReservation reserve(Member member, CarReservationRequest request) {
final CarStock carStock = carStockService.findById(request.getCarStockId());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.testcar.car.domains.carReservation.model;


import com.testcar.car.domains.carReservation.model.vo.CarReservationCountVo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class CarReservationRankingResponse {
@Schema(description = "차량명", example = "아반떼")
private final String name;

@Schema(description = "대여 횟수", example = "20")
private final long count;

public static CarReservationRankingResponse from(CarReservationCountVo carReservationCountVo) {
return CarReservationRankingResponse.builder()
.name(carReservationCountVo.getName())
.count(carReservationCountVo.getCount())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.testcar.car.domains.carReservation.model.vo;


import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Map.Entry;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class CarReservationCountVo {
@Schema(description = "차량명", example = "아반떼")
private String name;

@Schema(description = "대여 횟수", example = "20")
private long count;

public static CarReservationCountVo from(Entry<String, Long> entry) {
return CarReservationCountVo.builder().name(entry.getKey()).count(entry.getValue()).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ Page<CarReservationDto> findAllPageByCondition(
List<CarReservation> findAllWithCarStockByIdInAndMemberId(List<Long> ids, Long memberId);

List<CarReservation> findAllByExpiredAtAndStatusReserved(LocalDateTime expiredAt);

List<CarReservationDto> findAllByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,21 @@ public List<CarReservation> findAllByExpiredAtAndStatusReserved(LocalDateTime ex
.fetch();
}

@Override
public List<CarReservationDto> findAllByCreatedAtBetween(
LocalDateTime start, LocalDateTime end) {
return jpaQueryFactory
.select(
Projections.constructor(
CarReservationDto.class,
carReservation,
carReservation.carStock.car.name,
carReservation.carStock.stockNumber))
.from(carReservation)
.where(notDeleted(carReservation), carReservation.createdAt.between(start, end))
.fetch();
}

private BooleanExpression carNameContainsOrNull(String name) {
return name == null ? null : carReservation.carStock.car.name.contains(name);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
import com.testcar.car.domains.trackReservation.entity.TrackReservation;
import com.testcar.car.domains.trackReservation.entity.TrackReservationSlot;
import com.testcar.car.domains.trackReservation.model.TrackReservationDetailResponse;
import com.testcar.car.domains.trackReservation.model.TrackReservationRankingResponse;
import com.testcar.car.domains.trackReservation.model.TrackReservationRequest;
import com.testcar.car.domains.trackReservation.model.TrackReservationResponse;
import com.testcar.car.domains.trackReservation.model.TrackReservationSlotResponse;
import com.testcar.car.domains.trackReservation.model.vo.TrackReservationCountVo;
import com.testcar.car.domains.trackReservation.model.vo.TrackReservationFilterCondition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand Down Expand Up @@ -46,6 +48,15 @@ public List<TrackReservationResponse> getTrackReservationsByCondition(
return trackReservations.stream().map(TrackReservationResponse::from).toList();
}

@GetMapping("/reservations/rank")
@RoleAllowed(role = Role.USER)
@Operation(summary = "[예약 이력] 시험장 전체 예약 순위", description = "대여된 시험장 순위를 5개 가져옵니다.")
public List<TrackReservationRankingResponse> getTrackReservationsRank() {
final List<TrackReservationCountVo> trackReservations =
trackReservationService.findAllByLast7DaysRank();
return trackReservations.stream().map(TrackReservationRankingResponse::from).toList();
}

@GetMapping("/reservations/{trackReservationId}")
@RoleAllowed(role = Role.USER)
@Operation(summary = "[예약 이력] 내 시험장 예약 이력 상세 조회", description = "시험장 예약 이력 상세 정보를 조회합니다.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static com.testcar.car.domains.trackReservation.exception.ErrorCode.RESERVATION_ALREADY_CANCELED;
import static com.testcar.car.domains.trackReservation.exception.ErrorCode.TRACK_RESERVATION_NOT_FOUND;
import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;

import com.testcar.car.common.exception.BadRequestException;
import com.testcar.car.common.exception.NotFoundException;
Expand All @@ -12,11 +14,15 @@
import com.testcar.car.domains.trackReservation.entity.TrackReservation;
import com.testcar.car.domains.trackReservation.entity.TrackReservationSlot;
import com.testcar.car.domains.trackReservation.model.TrackReservationRequest;
import com.testcar.car.domains.trackReservation.model.vo.TrackReservationCountVo;
import com.testcar.car.domains.trackReservation.model.vo.TrackReservationFilterCondition;
import com.testcar.car.domains.trackReservation.repository.TrackReservationRepository;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -42,6 +48,24 @@ public List<TrackReservation> findAllByMemberAndCondition(
return trackReservationRepository.findAllByMemberIdAndCondition(member.getId(), condition);
}

/** 최근 7일간 가장 많이 대여된 시험장 5개를 가져옵니다. */
public List<TrackReservationCountVo> findAllByLast7DaysRank() {
final LocalDateTime now = LocalDateTime.now();
final LocalDateTime weekAgo = now.minusDays(7);
List<TrackReservation> reservations =
trackReservationRepository.findAllByCreatedAtBetween(weekAgo, now);
final Map<String, Long> countMap =
reservations.stream()
.map(reservation -> reservation.getTrack().getName())
.collect(groupingBy(Function.identity(), counting()));

return countMap.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(5)
.map(TrackReservationCountVo::from)
.toList();
}

/** 시험장의 해당 일자 예약 정보를 모두 조회합니다. */
public Set<TrackReservationSlot> findUnavailableReservationSlots(Long trackId, LocalDate date) {
return trackReservationSlotService.findAllByTrackIdAndDate(trackId, date);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.testcar.car.domains.trackReservation.model;


import com.testcar.car.domains.trackReservation.model.vo.TrackReservationCountVo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class TrackReservationRankingResponse {
@Schema(description = "시험장명", example = "직선주행장")
private final String name;

@Schema(description = "대여 횟수", example = "20")
private final long count;

public static TrackReservationRankingResponse from(
TrackReservationCountVo trackReservationCountVo) {
return TrackReservationRankingResponse.builder()
.name(trackReservationCountVo.getName())
.count(trackReservationCountVo.getCount())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.testcar.car.domains.trackReservation.model.vo;


import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Map.Entry;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class TrackReservationCountVo {
@Schema(description = "시험장명", example = "직선주행장")
private String name;

@Schema(description = "대여 횟수", example = "20")
private long count;

public static TrackReservationCountVo from(Entry<String, Long> entry) {
return TrackReservationCountVo.builder()
.name(entry.getKey())
.count(entry.getValue())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ List<TrackReservation> findAllByMemberIdAndCondition(
Long memberId, TrackReservationFilterCondition condition);

List<TrackReservation> findAllBySlotExpiredAtAndStatusReserved(LocalDateTime expiredAt);

List<TrackReservation> findAllByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ public List<TrackReservation> findAllBySlotExpiredAtAndStatusReserved(LocalDateT
.fetch();
}

@Override
public List<TrackReservation> findAllByCreatedAtBetween(
LocalDateTime start, LocalDateTime end) {
return jpaQueryFactory
.selectFrom(trackReservation)
.leftJoin(trackReservation.track, track)
.fetchJoin()
.where(notDeleted(trackReservation), trackReservation.createdAt.between(start, end))
.fetch();
}

private BooleanExpression trackNameContainsOrNull(String name) {
return (name == null) ? null : track.name.contains(name);
}
Expand Down
13 changes: 13 additions & 0 deletions src/test/java/com/testcar/car/common/CarEntityFactory.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.testcar.car.common;

import static com.testcar.car.common.Constant.ANOTHER_CAR_NAME;
import static com.testcar.car.common.Constant.CAR_DISPLACEMENT;
import static com.testcar.car.common.Constant.CAR_NAME;
import static com.testcar.car.common.Constant.CAR_STOCK_NUMBER;
Expand All @@ -26,6 +27,10 @@ public static Car createCar() {
return createCarBuilder().build();
}

public static Car createAnotherCar() {
return createCarBuilder().name(ANOTHER_CAR_NAME).build();
}

public static CarBuilder createCarBuilder() {
return Car.builder().name(CAR_NAME).displacement(CAR_DISPLACEMENT).type(CAR_TYPE);
}
Expand All @@ -34,6 +39,10 @@ public static CarStock createCarStock() {
return createCarStockBuilder().build();
}

public static CarStock createAnotherCarStock() {
return createCarStockBuilder().car(createAnotherCar()).build();
}

public static CarStockBuilder createCarStockBuilder() {
return CarStock.builder()
.car(createCar())
Expand All @@ -45,6 +54,10 @@ public static CarReservation createCarReservation() {
return createCarReservationBuilder().build();
}

public static CarReservation createAnotherCarReservation() {
return createCarReservationBuilder().carStock(createAnotherCarStock()).build();
}

public static CarReservationBuilder createCarReservationBuilder() {
return CarReservation.builder()
.member(MemberEntityFactory.createMember())
Expand Down
8 changes: 8 additions & 0 deletions src/test/java/com/testcar/car/common/DtoFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import static com.testcar.car.common.TrackEntityFactory.createTrack;

import com.testcar.car.domains.car.entity.Car;
import com.testcar.car.domains.carReservation.entity.CarReservation;
import com.testcar.car.domains.carReservation.model.dto.CarReservationDto;
import com.testcar.car.domains.carStock.entity.CarStock;
import com.testcar.car.domains.carTest.entity.CarTest;
import com.testcar.car.domains.carTest.model.vo.CarTestDto;
Expand All @@ -17,6 +19,12 @@
public class DtoFactory {
private DtoFactory() {}

public static CarReservationDto createCarReservationDto(CarReservation carReservation) {
final CarStock carStock = carReservation.getCarStock();
final Car car = carStock.getCar();
return new CarReservationDto(carReservation, car.getName(), carStock.getStockNumber());
}

public static CarTestDto createCarTestDto(CarTest carTest) {
final Member member = MemberEntityFactory.createMember();
final Department department = member.getDepartment();
Expand Down
7 changes: 7 additions & 0 deletions src/test/java/com/testcar/car/common/TrackEntityFactory.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.testcar.car.common;

import static com.testcar.car.common.Constant.ANOTHER_TRACK_NAME;
import static com.testcar.car.common.Constant.ANOTHER_TRACK_RESERVATION_SLOT_EXPIRED_AT;
import static com.testcar.car.common.Constant.ANOTHER_TRACK_RESERVATION_SLOT_STARTED_AT;
import static com.testcar.car.common.Constant.TRACK_DESCRIPTION;
Expand Down Expand Up @@ -37,6 +38,12 @@ public static TrackReservation createTrackReservation() {
return createTrackReservationBuilder().build();
}

public static TrackReservation createAnotherTrackReservation() {
return createTrackReservationBuilder()
.track(createTrackBuilder().name(ANOTHER_TRACK_NAME).build())
.build();
}

public static TrackReservationBuilder createTrackReservationBuilder() {
return TrackReservation.builder()
.member(MemberEntityFactory.createMember())
Expand Down
Loading

0 comments on commit 7682bf1

Please sign in to comment.