Skip to content

Commit

Permalink
feat: 차량 관리 기능 구현 #7 (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
gengminy authored Dec 5, 2023
1 parent 7c57677 commit 8935b92
Show file tree
Hide file tree
Showing 12 changed files with 413 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,53 @@


import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.DateTimePath;
import com.querydsl.core.types.dsl.PathBuilder;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.data.domain.Sort;

public interface BaseQueryDslRepository {
default BooleanExpression notDeleted(EntityPath<? extends BaseEntity> entityPath) {
QBaseEntity qBaseEntity = new QBaseEntity(entityPath);
return qBaseEntity.deleted.isFalse();
}

default BooleanExpression dateTimeBetween(
DateTimePath<LocalDateTime> entityPath, LocalDateTime begin, LocalDateTime end) {
BooleanExpression beginExpression = (begin != null) ? entityPath.goe(begin) : null;
BooleanExpression endExpression = (end != null) ? entityPath.loe(end) : null;

if (beginExpression != null && endExpression != null) {
return beginExpression.and(endExpression);
} else if (beginExpression != null) {
return beginExpression;
}
return endExpression;
}

default OrderSpecifier<?>[] getOrders(EntityPath<?> qEntity, Sort sort) {
return sort.stream().map(it -> getOrder(qEntity, it)).toArray(OrderSpecifier[]::new);
}

default OrderSpecifier<?>[] getOrders(
EntityPath<?> qEntity, Sort sort, OrderSpecifier<?>... orderSpecifiers) {
List<OrderSpecifier<?>> orderComparator = new ArrayList<>(Arrays.asList(orderSpecifiers));
sort.stream().forEach(it -> orderComparator.add(getOrder(qEntity, it)));
return orderComparator.toArray(OrderSpecifier[]::new);
}

@SuppressWarnings("all")
private OrderSpecifier<?> getOrder(EntityPath<?> qEntity, Sort.Order order) {
final Order direction = order.isAscending() ? Order.ASC : Order.DESC;
final String property = order.getProperty();
PathBuilder<?> pathBuilder =
new PathBuilder<>(qEntity.getType(), qEntity.getMetadata().getName());
return new OrderSpecifier(direction, pathBuilder.get(property));
}
}
16 changes: 11 additions & 5 deletions src/main/java/com/testcar/car/domains/car/Car.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -30,19 +29,26 @@ public class Car extends BaseEntity {
@Column(length = 50, nullable = false)
private String name;

// 출시일자
// 배기량
@Column(nullable = false)
private LocalDateTime releasedAt;
private Double displacement;

// 차종
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Type type;

@Builder
public Car(String name, LocalDateTime releasedAt, Type type) {
public Car(String name, Double displacement, Type type) {
this.name = name;
this.releasedAt = releasedAt;
this.displacement = displacement;
this.type = type;
}

public Car update(Car car) {
this.name = car.getName();
this.displacement = car.getDisplacement();
this.type = car.getType();
return this;
}
}
74 changes: 74 additions & 0 deletions src/main/java/com/testcar/car/domains/car/CarController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.testcar.car.domains.car;


import com.testcar.car.common.annotation.RoleAllowed;
import com.testcar.car.common.response.PageResponse;
import com.testcar.car.domains.car.model.CarResponse;
import com.testcar.car.domains.car.model.RegisterCarRequest;
import com.testcar.car.domains.car.model.vo.CarFilterCondition;
import com.testcar.car.domains.member.Role;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/cars")
@RequiredArgsConstructor
public class CarController {
private final CarService carService;

@GetMapping
@RoleAllowed(role = Role.USER)
@Operation(summary = "[차량 관리] 차량 조회 필터", description = "조건에 맞는 모든 차량을 페이지네이션으로 조회합니다.")
public PageResponse<CarResponse> getMembersByCondition(
@ParameterObject @ModelAttribute CarFilterCondition condition,
@ParameterObject Pageable pageable) {
final Page<Car> cars = carService.findAllPageByCondition(condition, pageable);
return PageResponse.from(cars.map(CarResponse::from));
}

@GetMapping("/{carId}")
@RoleAllowed(role = Role.USER)
@Operation(summary = "[차량 관리] 사용자 상세 정보", description = "차량 상세 정보를 가져옵니다.")
public CarResponse getMemberById(@PathVariable Long carId) {
final Car car = carService.findById(carId);
return CarResponse.from(car);
}

@PostMapping("/register")
@RoleAllowed(role = Role.ADMIN)
@Operation(summary = "[차량 관리] 차량 등록", description = "(관리자) 새로운 차량을 등록합니다.")
public CarResponse register(@Valid @RequestBody RegisterCarRequest request) {
final Car car = carService.register(request);
return CarResponse.from(car);
}

@PatchMapping("/{carId}")
@RoleAllowed(role = Role.ADMIN)
@Operation(summary = "[차량 관리] 차량 정보 수정", description = "(관리자) 차량 정보를 수정합니다.")
public CarResponse update(
@PathVariable Long carId, @Valid @RequestBody RegisterCarRequest request) {
final Car car = carService.updateById(carId, request);
return CarResponse.from(car);
}

@DeleteMapping("/{carId}")
@RoleAllowed(role = Role.ADMIN)
@Operation(summary = "[차량 관리] 차량 삭제", description = "(관리자) 차량을 삭제합니다.")
public CarResponse withdraw(@PathVariable Long carId) {
final Car car = carService.deleteById(carId);
return CarResponse.from(car);
}
}
71 changes: 71 additions & 0 deletions src/main/java/com/testcar/car/domains/car/CarService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.testcar.car.domains.car;


import com.testcar.car.common.exception.BadRequestException;
import com.testcar.car.common.exception.NotFoundException;
import com.testcar.car.domains.car.exception.ErrorCode;
import com.testcar.car.domains.car.model.RegisterCarRequest;
import com.testcar.car.domains.car.model.vo.CarFilterCondition;
import com.testcar.car.domains.car.repository.CarRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class CarService {
private final CarRepository carRepository;

/** 차량을 id로 조회합니다. */
public Car findById(Long id) {
return carRepository
.findByIdAndDeletedFalse(id)
.orElseThrow(() -> new NotFoundException(ErrorCode.CAR_NOT_FOUND));
}

/** 차량을 조건에 맞게 조회합니다. */
public Page<Car> findAllPageByCondition(CarFilterCondition condition, Pageable pageable) {
return carRepository.findAllPageByCondition(condition, pageable);
}

/** 새로운 차량을 등록합니다. */
public Car register(RegisterCarRequest request) {
final Car car = createEntity(request);
return carRepository.save(car);
}

/** 차량 정보를 업데이트 합니다. */
public Car updateById(Long carId, RegisterCarRequest request) {
final Car car = this.findById(carId);
final Car updateMember = createEntity(request);
car.update(updateMember);
return carRepository.save(car);
}

/** 차량을 삭제 처리 합니다. (soft delete) */
public Car deleteById(Long carId) {
final Car car = this.findById(carId);
car.delete();
return carRepository.save(car);
}

/** 영속되지 않은 차량 엔티티를 생성합니다. */
private Car createEntity(RegisterCarRequest request) {
validateNameNotDuplicated(request.getName());
return Car.builder()
.name(request.getName())
.type(request.getType())
.displacement(request.getDisplacement())
.build();
}

/** 차량명 중복을 검사합니다. */
private void validateNameNotDuplicated(String name) {
if (carRepository.existsByNameAndDeletedFalse(name)) {
throw new BadRequestException(ErrorCode.DUPLICATED_CAR_NAME);
}
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/testcar/car/domains/car/exception/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.testcar.car.domains.car.exception;


import com.testcar.car.common.exception.BaseErrorCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum ErrorCode implements BaseErrorCode {
CAR_NOT_FOUND("CAR001", "해당 차량을 찾을 수 없습니다."),
DUPLICATED_CAR_NAME("CAR002", "중복된 차량명입니다."),
;
private final String code;
private final String message;
}
40 changes: 40 additions & 0 deletions src/main/java/com/testcar/car/domains/car/model/CarResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.testcar.car.domains.car.model;


import com.testcar.car.common.annotation.DateTimeFormat;
import com.testcar.car.domains.car.Car;
import com.testcar.car.domains.car.Type;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class CarResponse {
@Schema(description = "차량 ID", example = "1")
private Long id;

@Schema(description = "차량명", example = "아반떼")
private String name;

@Schema(description = "차종", example = "SEDAN", implementation = Type.class)
private Type type;

@Schema(description = "배기량", example = "1.6")
private Double displacement;

@DateTimeFormat
@Schema(description = "등록일시", example = "2021-01-01 12:33:22")
private LocalDateTime createdAt;

public static CarResponse from(Car car) {
return CarResponse.builder()
.id(car.getId())
.name(car.getName())
.type(car.getType())
.displacement(car.getDisplacement())
.createdAt(car.getCreatedAt())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.testcar.car.domains.car.model;


import com.testcar.car.domains.car.Type;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Getter;
import org.hibernate.validator.constraints.Length;

@Getter
public class RegisterCarRequest {
@NotBlank
@Length(max = 20)
@Schema(description = "차량명", example = "아반떼")
private String name;

@NotNull
@Schema(description = "차종", example = "SEDAN", implementation = Type.class)
private Type type;

@NotNull
@Positive
@Schema(description = "배기량", example = "1.6")
private Double displacement;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.testcar.car.domains.car.model.vo;


import com.testcar.car.common.annotation.DateTimeFormat;
import com.testcar.car.domains.car.Type;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class CarFilterCondition {
@Schema(description = "차량명", example = "null")
private String name;

@Schema(description = "차종", example = "null", implementation = Type.class)
private Type type;

@DateTimeFormat
@Schema(description = "등록일자 시작일", example = "null")
private LocalDateTime startDate;

@DateTimeFormat
@Schema(description = "등록일자 종료일", example = "null")
private LocalDateTime endDate;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.testcar.car.domains.car.repository;


import com.testcar.car.domains.car.Car;
import com.testcar.car.domains.car.model.vo.CarFilterCondition;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface CarCustomRepository {
Optional<Car> findById(Long id);

Page<Car> findAllPageByCondition(CarFilterCondition condition, Pageable pageable);
}
Loading

0 comments on commit 8935b92

Please sign in to comment.