From eb653d7576c4d4921507a3c0042e398fecb84881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=85=B8=EA=B2=BD=EB=AF=BC?= Date: Tue, 5 Dec 2023 19:02:35 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=8B=9C=ED=97=98=EC=9E=A5=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/testcar/car/domains/track/Track.java | 7 ++ .../car/domains/track/TrackController.java | 68 +++++++++++++++++++ .../car/domains/track/TrackService.java | 66 ++++++++++++++++++ .../domains/track/exception/ErrorCode.java | 16 +++++ .../track/model/RegisterTrackRequest.java | 19 ++++++ .../domains/track/model/TrackResponse.java | 40 +++++++++++ .../track/model/vo/TrackFilterCondition.java | 16 +++++ .../repository/TrackCustomRepository.java | 10 +++ .../repository/TrackCustomRepositoryImpl.java | 36 ++++++++++ .../track/repository/TrackRepository.java | 12 ++++ 10 files changed, 290 insertions(+) create mode 100644 src/main/java/com/testcar/car/domains/track/TrackController.java create mode 100644 src/main/java/com/testcar/car/domains/track/TrackService.java create mode 100644 src/main/java/com/testcar/car/domains/track/exception/ErrorCode.java create mode 100644 src/main/java/com/testcar/car/domains/track/model/RegisterTrackRequest.java create mode 100644 src/main/java/com/testcar/car/domains/track/model/TrackResponse.java create mode 100644 src/main/java/com/testcar/car/domains/track/model/vo/TrackFilterCondition.java create mode 100644 src/main/java/com/testcar/car/domains/track/repository/TrackCustomRepository.java create mode 100644 src/main/java/com/testcar/car/domains/track/repository/TrackCustomRepositoryImpl.java create mode 100644 src/main/java/com/testcar/car/domains/track/repository/TrackRepository.java diff --git a/src/main/java/com/testcar/car/domains/track/Track.java b/src/main/java/com/testcar/car/domains/track/Track.java index 3fd6a45..9afbfdd 100644 --- a/src/main/java/com/testcar/car/domains/track/Track.java +++ b/src/main/java/com/testcar/car/domains/track/Track.java @@ -52,4 +52,11 @@ public Track(String name, String location, String description, Double length) { this.description = description; this.length = length; } + + public void update(Track track) { + this.name = track.getName(); + this.location = track.getLocation(); + this.description = track.getDescription(); + this.length = track.getLength(); + } } diff --git a/src/main/java/com/testcar/car/domains/track/TrackController.java b/src/main/java/com/testcar/car/domains/track/TrackController.java new file mode 100644 index 0000000..d82fbe8 --- /dev/null +++ b/src/main/java/com/testcar/car/domains/track/TrackController.java @@ -0,0 +1,68 @@ +package com.testcar.car.domains.track; + + +import com.testcar.car.common.annotation.RoleAllowed; +import com.testcar.car.domains.member.Role; +import com.testcar.car.domains.track.model.RegisterTrackRequest; +import com.testcar.car.domains.track.model.TrackResponse; +import com.testcar.car.domains.track.model.vo.TrackFilterCondition; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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("/tracks") +@RequiredArgsConstructor +public class TrackController { + private final TrackService trackService; + + @GetMapping + @RoleAllowed(role = Role.USER) + @Operation(summary = "[시험장 관리] 시험장 조회", description = "시험장을 조건에 맞게 조회합니다.") + public List getTracksByCondition(TrackFilterCondition condition) { + final List tracks = trackService.findAllByCondition(condition); + return tracks.stream().map(TrackResponse::from).toList(); + } + + @GetMapping("/{trackId}") + @RoleAllowed(role = Role.USER) + @Operation(summary = "[시험장 관리] 시험장 상세 정보", description = "시험장 상세 정보를 가져옵니다.") + public TrackResponse getTrackById(@PathVariable Long trackId) { + final Track track = trackService.findById(trackId); + return TrackResponse.from(track); + } + + @PostMapping("/register") + @RoleAllowed(role = Role.ADMIN) + @Operation(summary = "[시험장 관리] 시험장 등록", description = "(관리자) 새로운 시험장을 등록합니다.") + public TrackResponse register(@Valid @RequestBody RegisterTrackRequest request) { + final Track track = trackService.register(request); + return TrackResponse.from(track); + } + + @PatchMapping("/{trackId}") + @RoleAllowed(role = Role.ADMIN) + @Operation(summary = "[시험장 관리] 시험장 정보 수정", description = "(관리자) 시험장 정보를 수정합니다.") + public TrackResponse update( + @PathVariable Long trackId, @Valid @RequestBody RegisterTrackRequest request) { + final Track track = trackService.updateById(trackId, request); + return TrackResponse.from(track); + } + + @DeleteMapping("/{trackId}") + @RoleAllowed(role = Role.ADMIN) + @Operation(summary = "[시험장 관리] 시험장 삭제", description = "(관리자) 시험장을 삭제합니다.") + public TrackResponse delete(@PathVariable Long trackId) { + final Track track = trackService.deleteById(trackId); + return TrackResponse.from(track); + } +} diff --git a/src/main/java/com/testcar/car/domains/track/TrackService.java b/src/main/java/com/testcar/car/domains/track/TrackService.java new file mode 100644 index 0000000..a0b4217 --- /dev/null +++ b/src/main/java/com/testcar/car/domains/track/TrackService.java @@ -0,0 +1,66 @@ +package com.testcar.car.domains.track; + + +import com.testcar.car.common.exception.BadRequestException; +import com.testcar.car.common.exception.NotFoundException; +import com.testcar.car.domains.track.exception.ErrorCode; +import com.testcar.car.domains.track.model.RegisterTrackRequest; +import com.testcar.car.domains.track.model.vo.TrackFilterCondition; +import com.testcar.car.domains.track.repository.TrackRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class TrackService { + private final TrackRepository trackRepository; + + /** 시험장을 id로 조회합니다. */ + public Track findById(Long id) { + return trackRepository + .findByIdAndDeletedFalse(id) + .orElseThrow(() -> new NotFoundException(ErrorCode.TRACK_NOT_FOUND)); + } + + /** 시험장을 id로 조회합니다. */ + public List findAllByCondition(TrackFilterCondition condition) { + return trackRepository.findAllByCondition(condition); + } + + /** 새로운 시험장을 등록합니다. */ + public Track register(RegisterTrackRequest request) { + final Track car = createEntity(request); + return trackRepository.save(car); + } + + /** 시험장 정보를 업데이트 합니다. */ + public Track updateById(Long trackId, RegisterTrackRequest request) { + Track track = this.findById(trackId); + final Track updateTrack = this.createEntity(request); + track.update(updateTrack); + return trackRepository.save(track); + } + + /** 시험장을 삭제 처리 합니다. (soft delete) */ + public Track deleteById(Long trackId) { + final Track car = this.findById(trackId); + car.delete(); + return trackRepository.save(car); + } + + /** 영속되지 않은 시험장 엔티티를 생성합니다. */ + private Track createEntity(RegisterTrackRequest request) { + validateNameNotDuplicated(request.getName()); + return Track.builder().name(request.getName()).location(request.getLocation()).build(); + } + + /** 시험장명 중복을 검사합니다. */ + private void validateNameNotDuplicated(String name) { + if (trackRepository.existsByNameAndDeletedFalse(name)) { + throw new BadRequestException(ErrorCode.DUPLICATED_TRACK_NAME); + } + } +} diff --git a/src/main/java/com/testcar/car/domains/track/exception/ErrorCode.java b/src/main/java/com/testcar/car/domains/track/exception/ErrorCode.java new file mode 100644 index 0000000..66491d2 --- /dev/null +++ b/src/main/java/com/testcar/car/domains/track/exception/ErrorCode.java @@ -0,0 +1,16 @@ +package com.testcar.car.domains.track.exception; + + +import com.testcar.car.common.exception.BaseErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ErrorCode implements BaseErrorCode { + TRACK_NOT_FOUND("TRK001", "해당 시험장을 찾을 수 없습니다."), + DUPLICATED_TRACK_NAME("TRK002", "중복된 시험장명입니다."), + ; + private final String code; + private final String message; +} diff --git a/src/main/java/com/testcar/car/domains/track/model/RegisterTrackRequest.java b/src/main/java/com/testcar/car/domains/track/model/RegisterTrackRequest.java new file mode 100644 index 0000000..5f40a7f --- /dev/null +++ b/src/main/java/com/testcar/car/domains/track/model/RegisterTrackRequest.java @@ -0,0 +1,19 @@ +package com.testcar.car.domains.track.model; + + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import org.hibernate.validator.constraints.Length; + +@Getter +public class RegisterTrackRequest { + @NotBlank + @Length(max = 20) + @Schema(description = "시험장명", example = "서산주행시험장") + private String name; + + @NotBlank + @Schema(description = "위치", example = "충청남도 서산시 부석면") + private String location; +} diff --git a/src/main/java/com/testcar/car/domains/track/model/TrackResponse.java b/src/main/java/com/testcar/car/domains/track/model/TrackResponse.java new file mode 100644 index 0000000..1f6b4b0 --- /dev/null +++ b/src/main/java/com/testcar/car/domains/track/model/TrackResponse.java @@ -0,0 +1,40 @@ +package com.testcar.car.domains.track.model; + + +import com.testcar.car.domains.track.Track; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class TrackResponse { + @Schema(description = "시험장 ID", example = "1") + private Long id; + + @Schema(description = "시험명", example = "서산주행시험장") + private String name; + + @Schema(description = "위치", example = "충청남도 서산시 부석면") + private String location; + + @Schema(description = "경도", example = "132.123123") + private Double longitude; + + @Schema(description = "위도", example = "37.123123") + private Double latitude; + + @Schema(description = "시험장 특성", example = "평지") + private String description; + + public static TrackResponse from(Track track) { + return TrackResponse.builder() + .id(track.getId()) + .name(track.getName()) + .location(track.getLocation()) + .longitude(track.getLongitude()) + .latitude(track.getLatitude()) + .description(track.getDescription()) + .build(); + } +} diff --git a/src/main/java/com/testcar/car/domains/track/model/vo/TrackFilterCondition.java b/src/main/java/com/testcar/car/domains/track/model/vo/TrackFilterCondition.java new file mode 100644 index 0000000..ecbbe0e --- /dev/null +++ b/src/main/java/com/testcar/car/domains/track/model/vo/TrackFilterCondition.java @@ -0,0 +1,16 @@ +package com.testcar.car.domains.track.model.vo; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TrackFilterCondition { + @Schema(description = "시험장명", example = "null") + private String name; + + @Schema(description = "위치", example = "null") + private String location; +} diff --git a/src/main/java/com/testcar/car/domains/track/repository/TrackCustomRepository.java b/src/main/java/com/testcar/car/domains/track/repository/TrackCustomRepository.java new file mode 100644 index 0000000..5d22024 --- /dev/null +++ b/src/main/java/com/testcar/car/domains/track/repository/TrackCustomRepository.java @@ -0,0 +1,10 @@ +package com.testcar.car.domains.track.repository; + + +import com.testcar.car.domains.track.Track; +import com.testcar.car.domains.track.model.vo.TrackFilterCondition; +import java.util.List; + +public interface TrackCustomRepository { + List findAllByCondition(TrackFilterCondition condition); +} diff --git a/src/main/java/com/testcar/car/domains/track/repository/TrackCustomRepositoryImpl.java b/src/main/java/com/testcar/car/domains/track/repository/TrackCustomRepositoryImpl.java new file mode 100644 index 0000000..01ec899 --- /dev/null +++ b/src/main/java/com/testcar/car/domains/track/repository/TrackCustomRepositoryImpl.java @@ -0,0 +1,36 @@ +package com.testcar.car.domains.track.repository; + +import static com.testcar.car.domains.track.QTrack.track; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.testcar.car.common.entity.BaseQueryDslRepository; +import com.testcar.car.domains.track.Track; +import com.testcar.car.domains.track.model.vo.TrackFilterCondition; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class TrackCustomRepositoryImpl implements TrackCustomRepository, BaseQueryDslRepository { + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List findAllByCondition(TrackFilterCondition condition) { + return jpaQueryFactory + .selectFrom(track) + .where( + notDeleted(track), + trackNameContainsOrNull(condition.getName()), + locationContainsOrNull(condition.getLocation())) + .orderBy(track.name.asc()) + .fetch(); + } + + private BooleanExpression trackNameContainsOrNull(String name) { + return (name == null) ? null : track.name.contains(name); + } + + private BooleanExpression locationContainsOrNull(String location) { + return (location == null) ? null : track.location.contains(location); + } +} diff --git a/src/main/java/com/testcar/car/domains/track/repository/TrackRepository.java b/src/main/java/com/testcar/car/domains/track/repository/TrackRepository.java new file mode 100644 index 0000000..e6538a0 --- /dev/null +++ b/src/main/java/com/testcar/car/domains/track/repository/TrackRepository.java @@ -0,0 +1,12 @@ +package com.testcar.car.domains.track.repository; + + +import com.testcar.car.domains.track.Track; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TrackRepository extends JpaRepository, TrackCustomRepository { + Optional findByIdAndDeletedFalse(Long id); + + boolean existsByNameAndDeletedFalse(String name); +}