From d626bdcec87917e18821892bb829d948bf8ec510 Mon Sep 17 00:00:00 2001 From: JeongHeumChoi <79458446+JeongHeumChoi@users.noreply.github.com> Date: Wed, 8 May 2024 17:23:27 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20[Deploy]=20-=20API=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: customException 구현 * Feat: responseDto 구현 * Feat: 내 근처 킥보드 주차장 찾기 API 구현 * Fix: BoardingRecord 도메인 수정 * Feat: Spring Security, JWT Cookie, 소셜 로그인 기능 구현 * Feat: Spring Security, JWT Cookie, 소셜 로그인 기능 구현 * Fix: 코드 에러 수정 * Refactor: 아직 사용하지 않는 코드 삭제 * Feat: oauth2 의존성 추가 * Chore: Credentials 추가 * ✨ [Feature] - 서버 날짜 설정 및 유저 닉네임 설정 (#15) * Feat: 스프링 서버 시간 한국으로 설정 * Feat: 사용자 닉네임 지정 로직 추가 * Fix: 초기 포인트 설정 * Fix: nickname 에러 수정 (#18) * Fix: BoardingRecord 도메인 수정 * Feat: 디펜던시 추가 * Feat: ErrorCode 추가 * Feat: Image API 구현 * Feat: WebClientConfig 구현 * Feat: S3ClientConfig 구현 * Feat: BoardingRecord API 구현 * Feat: 주차장 예측 결과 관련 기능 추가 * Feat: 탑승 기록 저장 API 구현 * Fix: 디렉토리 경로 수정 * Feat: 코드 리팩터링 진행 --------- Co-authored-by: Jang99u Co-authored-by: 민장규 <133879283+Jang99u@users.noreply.github.com> --- build.gradle | 10 +- src/main/java/ice/spot/config/S3Config.java | 32 ++++++ .../java/ice/spot/config/WebClientConfig.java | 22 +++++ .../controller/BoardingRecordController.java | 40 ++++++++ .../spot/controller/ParkingLotController.java | 21 ++++ .../java/ice/spot/domain/BoardingRecord.java | 51 +++++----- src/main/java/ice/spot/domain/Image.java | 52 ++++++++++ src/main/java/ice/spot/domain/ParkingLot.java | 7 +- src/main/java/ice/spot/domain/User.java | 4 + .../spot/domain/type/ParkingLotResult.java | 17 ++++ .../request/BoardingRecordRequest.java | 10 ++ .../response/BoardingRecordListResponse.java | 14 +++ .../response/BoardingRecordResponse.java | 26 +++++ .../ice/spot/dto/global/ExceptionDto.java | 2 +- .../java/ice/spot/dto/global/ResponseDto.java | 4 +- .../dto/image/request/ImageCheckRequest.java | 6 ++ .../dto/image/request/ImageSaveRequest.java | 13 +++ .../dto/image/response/ImageResponse.java | 16 +++ .../image/response/PredictionResponse.java | 14 +++ .../response/ParkingLotResponse.java | 27 ++++++ .../dto/user/response/PersonResponse.java | 11 +++ .../CommonException.java | 2 +- .../{exeption => exception}/ErrorCode.java | 4 +- .../GlobalExceptionHandler.java | 2 +- .../pre/UserIdArgumentResolver.java | 4 +- .../repository/BoardingRecordRepository.java | 7 ++ .../ice/spot/repository/ImageRepository.java | 7 ++ .../spot/repository/ParkingLotRepository.java | 7 ++ .../filter/JwtAuthenticationFilter.java | 4 +- .../security/filter/JwtExceptionFilter.java | 4 +- .../exception/CustomAccessDeniedHandler.java | 2 +- ...CustomAuthenticationEntryPointHandler.java | 2 +- .../handler/login/Oauth2FailureHandler.java | 2 +- .../logout/CustomLogoutResultHandler.java | 2 +- .../security/info/AuthenticationResponse.java | 2 +- .../service/CustomUserDetailService.java | 4 +- .../java/ice/spot/service/AuthService.java | 4 +- .../spot/service/BoardingRecordService.java | 83 ++++++++++++++++ .../java/ice/spot/service/ImageService.java | 97 +++++++++++++++++++ .../ice/spot/service/ParkingLotService.java | 46 +++++++++ src/main/java/ice/spot/util/HeaderUtil.java | 4 +- 41 files changed, 633 insertions(+), 55 deletions(-) create mode 100644 src/main/java/ice/spot/config/S3Config.java create mode 100644 src/main/java/ice/spot/config/WebClientConfig.java create mode 100644 src/main/java/ice/spot/controller/BoardingRecordController.java create mode 100644 src/main/java/ice/spot/controller/ParkingLotController.java create mode 100644 src/main/java/ice/spot/domain/Image.java create mode 100644 src/main/java/ice/spot/domain/type/ParkingLotResult.java create mode 100644 src/main/java/ice/spot/dto/boardingrecord/request/BoardingRecordRequest.java create mode 100644 src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordListResponse.java create mode 100644 src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordResponse.java create mode 100644 src/main/java/ice/spot/dto/image/request/ImageCheckRequest.java create mode 100644 src/main/java/ice/spot/dto/image/request/ImageSaveRequest.java create mode 100644 src/main/java/ice/spot/dto/image/response/ImageResponse.java create mode 100644 src/main/java/ice/spot/dto/image/response/PredictionResponse.java create mode 100644 src/main/java/ice/spot/dto/parkingLot/response/ParkingLotResponse.java create mode 100644 src/main/java/ice/spot/dto/user/response/PersonResponse.java rename src/main/java/ice/spot/{exeption => exception}/CommonException.java (90%) rename src/main/java/ice/spot/{exeption => exception}/ErrorCode.java (88%) rename src/main/java/ice/spot/{exeption => exception}/GlobalExceptionHandler.java (98%) create mode 100644 src/main/java/ice/spot/repository/BoardingRecordRepository.java create mode 100644 src/main/java/ice/spot/repository/ImageRepository.java create mode 100644 src/main/java/ice/spot/repository/ParkingLotRepository.java create mode 100644 src/main/java/ice/spot/service/BoardingRecordService.java create mode 100644 src/main/java/ice/spot/service/ImageService.java create mode 100644 src/main/java/ice/spot/service/ParkingLotService.java diff --git a/build.gradle b/build.gradle index 5d24f36..d6e4ba3 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ repositories { } dependencies { - //database + // database implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'com.mysql:mysql-connector-j' @@ -46,8 +46,14 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' - //json + // json implementation 'net.minidev:json-smart:2.4.7' + + // s3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + // WebClient + implementation 'org.springframework.boot:spring-boot-starter-webflux' } task copyGitSubmodule(type: Copy) { diff --git a/src/main/java/ice/spot/config/S3Config.java b/src/main/java/ice/spot/config/S3Config.java new file mode 100644 index 0000000..bc123b4 --- /dev/null +++ b/src/main/java/ice/spot/config/S3Config.java @@ -0,0 +1,32 @@ +package ice.spot.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .withRegion(region) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/ice/spot/config/WebClientConfig.java b/src/main/java/ice/spot/config/WebClientConfig.java new file mode 100644 index 0000000..b6b9601 --- /dev/null +++ b/src/main/java/ice/spot/config/WebClientConfig.java @@ -0,0 +1,22 @@ +package ice.spot.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient(WebClient.Builder builder) { + return builder + .baseUrl("https://detectingkickboard11-prediction.cognitiveservices.azure.com") + .defaultHeaders(httpHeaders -> { + httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + httpHeaders.add("Prediction-Key", "17482dedf9834e0c958d0e88a3bf940d"); + }) + .build(); + } +} diff --git a/src/main/java/ice/spot/controller/BoardingRecordController.java b/src/main/java/ice/spot/controller/BoardingRecordController.java new file mode 100644 index 0000000..28bfc37 --- /dev/null +++ b/src/main/java/ice/spot/controller/BoardingRecordController.java @@ -0,0 +1,40 @@ +package ice.spot.controller; + +import ice.spot.annotation.UserId; +import ice.spot.dto.boardingrecord.request.BoardingRecordRequest; +import ice.spot.dto.global.ResponseDto; +import ice.spot.service.BoardingRecordService; +import ice.spot.service.ImageService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class BoardingRecordController { + + private final ImageService imageService; + private final BoardingRecordService boardingRecordService; + + @PostMapping("/boarding-record") + public ResponseDto saveBoardingRecord ( + @UserId Long userId, + @RequestPart(value = "image", required = false) MultipartFile multipartFile, + @RequestPart(value = "dto") BoardingRecordRequest boardingRecordRequest + ) throws IOException { + Long imageId = imageService.saveImage(multipartFile); + + return ResponseDto.created(boardingRecordService + .saveBoardingRecord(userId, imageId, boardingRecordRequest)); + } + + @GetMapping("/boarding-record") + public ResponseDto getBoardingRecord (@UserId Long userId) { + return ResponseDto.ok(boardingRecordService.boardingRecordList(userId)); + } +} diff --git a/src/main/java/ice/spot/controller/ParkingLotController.java b/src/main/java/ice/spot/controller/ParkingLotController.java new file mode 100644 index 0000000..b5d6c0b --- /dev/null +++ b/src/main/java/ice/spot/controller/ParkingLotController.java @@ -0,0 +1,21 @@ +package ice.spot.controller; + +import ice.spot.dto.global.ResponseDto; +import ice.spot.service.ParkingLotService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class ParkingLotController { + private final ParkingLotService parkingLotService; + + @GetMapping("/kickboard") + public ResponseDto parkingLotList(@RequestParam Double lat, @RequestParam Double lon) { + return ResponseDto.ok(parkingLotService.parkingLotList(lat, lon)); + } +} \ No newline at end of file diff --git a/src/main/java/ice/spot/domain/BoardingRecord.java b/src/main/java/ice/spot/domain/BoardingRecord.java index b46aa20..33abcd8 100644 --- a/src/main/java/ice/spot/domain/BoardingRecord.java +++ b/src/main/java/ice/spot/domain/BoardingRecord.java @@ -2,10 +2,11 @@ import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDate; +import java.time.LocalDateTime; @Entity @Getter @@ -17,44 +18,38 @@ public class BoardingRecord { @Column(name = "id") private Long id; - @Column(name = "image_url") - private String imageUrl; + @Column(name = "distance") + private Double distance; - @Column(name = "depart_at") - private LocalDate departAt; + @Column(name = "time") + private Integer time; - @Column(name = "arrive_at") - private LocalDate arriveAt; + @Column(name = "point") + private Integer point; - @Column(name = "depart_lat") - private Double departLat; - - @Column(name = "depart_lon") - private Double departLon; - - @Column(name = "arrive_lat") - private Double arriveLat; - - @Column(name = "arrive_lon") - private Double arriveLon; + @Column(name = "createdAt") + private LocalDateTime createdAt; @ManyToOne @JoinColumn(name = "user_id") private User user; - @OneToOne - @JoinColumn(name = "parking_lot_id", referencedColumnName = "id") + @ManyToOne + @JoinColumn(name = "parking_lot_id") private ParkingLot parkingLot; - public BoardingRecord(String imageUrl, LocalDate departAt, LocalDate arriveAt, Double departLat, Double departLon, Double arriveLat, Double arriveLon, User user, ParkingLot parkingLot) { - this.imageUrl = imageUrl; - this.departAt = departAt; - this.arriveAt = arriveAt; - this.departLat = departLat; - this.departLon = departLon; - this.arriveLat = arriveLat; - this.arriveLon = arriveLon; + @OneToOne + @JoinColumn(name = "image_id") + private Image image; + + @Builder + public BoardingRecord(Double distance, Integer time, Integer point, User user, ParkingLot parkingLot, Image image) { + this.distance = distance; + this.time = time; + this.point = point; + this.createdAt = LocalDateTime.now(); this.user = user; this.parkingLot = parkingLot; + this.image = image; } } diff --git a/src/main/java/ice/spot/domain/Image.java b/src/main/java/ice/spot/domain/Image.java new file mode 100644 index 0000000..71c246e --- /dev/null +++ b/src/main/java/ice/spot/domain/Image.java @@ -0,0 +1,52 @@ +package ice.spot.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "image") +public class Image { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "origin_name") + private String originName; + + @Column(name = "stored_name") + private String storedName; + + @Column(name = "image_url") + private String imageUrl; + + @OneToOne(mappedBy = "image") + private BoardingRecord boardingRecord; + + public Image(String originName) { + this.originName = originName; + this.storedName = getFileName(originName); + this.imageUrl = ""; + } + + public void setAccessUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public String extractExtension(String originName) { + int index = originName.lastIndexOf('.'); + + return originName.substring(index, originName.length()); + } + + public String getFileName(String originName) { + return UUID.randomUUID() + "." + extractExtension(originName); + } + +} diff --git a/src/main/java/ice/spot/domain/ParkingLot.java b/src/main/java/ice/spot/domain/ParkingLot.java index 574011c..0bde2cc 100644 --- a/src/main/java/ice/spot/domain/ParkingLot.java +++ b/src/main/java/ice/spot/domain/ParkingLot.java @@ -5,6 +5,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.ArrayList; +import java.util.List; + @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -27,6 +30,6 @@ public class ParkingLot { @Column(name = "detail_address") private String detailAddress; - @OneToOne(mappedBy = "parkingLot") - private BoardingRecord boardingRecord; + @OneToMany(mappedBy = "parkingLot") + private List boardingRecords = new ArrayList<>(); } diff --git a/src/main/java/ice/spot/domain/User.java b/src/main/java/ice/spot/domain/User.java index 9e32a50..203fb77 100644 --- a/src/main/java/ice/spot/domain/User.java +++ b/src/main/java/ice/spot/domain/User.java @@ -74,4 +74,8 @@ public void register(String nickname) { public void updateRefreshToken(String refreshToken) { this.refreshToken = refreshToken; } + + public void plusPoint() { + this.point += 100L; + } } diff --git a/src/main/java/ice/spot/domain/type/ParkingLotResult.java b/src/main/java/ice/spot/domain/type/ParkingLotResult.java new file mode 100644 index 0000000..ec48743 --- /dev/null +++ b/src/main/java/ice/spot/domain/type/ParkingLotResult.java @@ -0,0 +1,17 @@ +package ice.spot.domain.type; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ParkingLotResult { + + CORRECT_PARKING_LOT(0, "올바른 주차구역에 주차되었습니다."), + WRONG_PARKING_LOT(1, "올바르지 않은 주차구역입니다."), + NOT_FOUND_KICKBOARD(2, "킥보드가 인식되지 않습니다."), + ; + + private final Integer code; + private final String message; +} diff --git a/src/main/java/ice/spot/dto/boardingrecord/request/BoardingRecordRequest.java b/src/main/java/ice/spot/dto/boardingrecord/request/BoardingRecordRequest.java new file mode 100644 index 0000000..cd35784 --- /dev/null +++ b/src/main/java/ice/spot/dto/boardingrecord/request/BoardingRecordRequest.java @@ -0,0 +1,10 @@ +package ice.spot.dto.boardingrecord.request; + +import lombok.Builder; + +@Builder +public record BoardingRecordRequest( + Double distance, + Integer time +) { +} diff --git a/src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordListResponse.java b/src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordListResponse.java new file mode 100644 index 0000000..f6bf320 --- /dev/null +++ b/src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordListResponse.java @@ -0,0 +1,14 @@ +package ice.spot.dto.boardingrecord.response; + +import ice.spot.dto.user.response.PersonResponse; +import lombok.Builder; + +import java.util.List; + +@Builder +public record BoardingRecordListResponse( + PersonResponse personResponse, + + List boardingRecordResponseList +) { +} diff --git a/src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordResponse.java b/src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordResponse.java new file mode 100644 index 0000000..08eea96 --- /dev/null +++ b/src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordResponse.java @@ -0,0 +1,26 @@ +package ice.spot.dto.boardingrecord.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@Builder +public record BoardingRecordResponse( + @JsonProperty("created_at") + String createdAt, + + @JsonProperty("image") + String image, + + @JsonProperty("distance") + Double distance, + + @JsonProperty("time") + Integer time, + + @JsonProperty("point") + Integer point +) { + public static BoardingRecordResponse of(String createdAt, String image, Double distance, Integer time, Integer point) { + return new BoardingRecordResponse(createdAt, image, distance, time, point); + } +} diff --git a/src/main/java/ice/spot/dto/global/ExceptionDto.java b/src/main/java/ice/spot/dto/global/ExceptionDto.java index 7017212..949fce8 100644 --- a/src/main/java/ice/spot/dto/global/ExceptionDto.java +++ b/src/main/java/ice/spot/dto/global/ExceptionDto.java @@ -1,6 +1,6 @@ package ice.spot.dto.global; -import ice.spot.exeption.ErrorCode; +import ice.spot.exception.ErrorCode; public record ExceptionDto( Integer code, diff --git a/src/main/java/ice/spot/dto/global/ResponseDto.java b/src/main/java/ice/spot/dto/global/ResponseDto.java index 939e0d6..5460f8b 100644 --- a/src/main/java/ice/spot/dto/global/ResponseDto.java +++ b/src/main/java/ice/spot/dto/global/ResponseDto.java @@ -1,7 +1,7 @@ package ice.spot.dto.global; -import ice.spot.exeption.CommonException; -import ice.spot.exeption.ErrorCode; +import ice.spot.exception.CommonException; +import ice.spot.exception.ErrorCode; import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; import net.minidev.json.annotate.JsonIgnore; diff --git a/src/main/java/ice/spot/dto/image/request/ImageCheckRequest.java b/src/main/java/ice/spot/dto/image/request/ImageCheckRequest.java new file mode 100644 index 0000000..678c3f3 --- /dev/null +++ b/src/main/java/ice/spot/dto/image/request/ImageCheckRequest.java @@ -0,0 +1,6 @@ +package ice.spot.dto.image.request; + +public record ImageCheckRequest( + String url +) { +} diff --git a/src/main/java/ice/spot/dto/image/request/ImageSaveRequest.java b/src/main/java/ice/spot/dto/image/request/ImageSaveRequest.java new file mode 100644 index 0000000..8367492 --- /dev/null +++ b/src/main/java/ice/spot/dto/image/request/ImageSaveRequest.java @@ -0,0 +1,13 @@ +package ice.spot.dto.image.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.springframework.web.multipart.MultipartFile; + +@AllArgsConstructor +@Getter +@Setter +public class ImageSaveRequest { + private MultipartFile image; +} \ No newline at end of file diff --git a/src/main/java/ice/spot/dto/image/response/ImageResponse.java b/src/main/java/ice/spot/dto/image/response/ImageResponse.java new file mode 100644 index 0000000..4d8cc7c --- /dev/null +++ b/src/main/java/ice/spot/dto/image/response/ImageResponse.java @@ -0,0 +1,16 @@ +package ice.spot.dto.image.response; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class ImageResponse { + private String id; + private String project; + private String iteration; + private String created; + private List predictions; +} diff --git a/src/main/java/ice/spot/dto/image/response/PredictionResponse.java b/src/main/java/ice/spot/dto/image/response/PredictionResponse.java new file mode 100644 index 0000000..bff1c1e --- /dev/null +++ b/src/main/java/ice/spot/dto/image/response/PredictionResponse.java @@ -0,0 +1,14 @@ +package ice.spot.dto.image.response; + +import jakarta.persistence.Column; + +public record PredictionResponse( + Double probability, + + @Column(name = "tag_id") + String tagId, + + @Column(name = "tag_name") + String tagName +) { +} diff --git a/src/main/java/ice/spot/dto/parkingLot/response/ParkingLotResponse.java b/src/main/java/ice/spot/dto/parkingLot/response/ParkingLotResponse.java new file mode 100644 index 0000000..f722c92 --- /dev/null +++ b/src/main/java/ice/spot/dto/parkingLot/response/ParkingLotResponse.java @@ -0,0 +1,27 @@ +package ice.spot.dto.parkingLot.response; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@Builder +public record ParkingLotResponse( + @JsonProperty("address") + String address, + + @JsonProperty("detail_address") + String detailAddress, + + @JsonProperty("lat") + Double lat, + + @JsonProperty("lon") + Double lon, + + @JsonIgnore + Double distance +) { + public static ParkingLotResponse of(String address, String detailAddress, Double lat, Double lon, Double distance) { + return new ParkingLotResponse(address, detailAddress, lat, lon, distance); + } +} diff --git a/src/main/java/ice/spot/dto/user/response/PersonResponse.java b/src/main/java/ice/spot/dto/user/response/PersonResponse.java new file mode 100644 index 0000000..72ac830 --- /dev/null +++ b/src/main/java/ice/spot/dto/user/response/PersonResponse.java @@ -0,0 +1,11 @@ +package ice.spot.dto.user.response; + +import lombok.Builder; + +@Builder +public record PersonResponse( + String nickname, + Integer recordCount, + Long point +) { +} diff --git a/src/main/java/ice/spot/exeption/CommonException.java b/src/main/java/ice/spot/exception/CommonException.java similarity index 90% rename from src/main/java/ice/spot/exeption/CommonException.java rename to src/main/java/ice/spot/exception/CommonException.java index 427c4c6..c7dd9c7 100644 --- a/src/main/java/ice/spot/exeption/CommonException.java +++ b/src/main/java/ice/spot/exception/CommonException.java @@ -1,4 +1,4 @@ -package ice.spot.exeption; +package ice.spot.exception; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/ice/spot/exeption/ErrorCode.java b/src/main/java/ice/spot/exception/ErrorCode.java similarity index 88% rename from src/main/java/ice/spot/exeption/ErrorCode.java rename to src/main/java/ice/spot/exception/ErrorCode.java index 3eda73b..39304df 100644 --- a/src/main/java/ice/spot/exeption/ErrorCode.java +++ b/src/main/java/ice/spot/exception/ErrorCode.java @@ -1,4 +1,4 @@ -package ice.spot.exeption; +package ice.spot.exception; import lombok.AllArgsConstructor; import lombok.Getter; @@ -12,6 +12,8 @@ public enum ErrorCode { MISSING_REQUEST_PARAMETER(40001, HttpStatus.BAD_REQUEST, "필수 요청 파라미터가 누락되었습니다."), INVALID_PARAMETER_FORMAT(40002, HttpStatus.BAD_REQUEST, "요청에 유효하지 않은 인자 형식입니다."), BAD_REQUEST_JSON(40003, HttpStatus.BAD_REQUEST, "잘못된 JSON 형식입니다."), + INVALID_PARKING_LOT(40004, HttpStatus.BAD_REQUEST, "올바르지 않은 주차구역입니다."), + INVALID_KICKBOARD_REQUEST(40405, HttpStatus.BAD_REQUEST, "킥보드가 인식되지 않습니다."), //401 INVALID_HEADER_VALUE(40100, HttpStatus.UNAUTHORIZED, "올바르지 않은 헤더값입니다."), diff --git a/src/main/java/ice/spot/exeption/GlobalExceptionHandler.java b/src/main/java/ice/spot/exception/GlobalExceptionHandler.java similarity index 98% rename from src/main/java/ice/spot/exeption/GlobalExceptionHandler.java rename to src/main/java/ice/spot/exception/GlobalExceptionHandler.java index e8e105a..2689cf5 100644 --- a/src/main/java/ice/spot/exeption/GlobalExceptionHandler.java +++ b/src/main/java/ice/spot/exception/GlobalExceptionHandler.java @@ -1,4 +1,4 @@ -package ice.spot.exeption; +package ice.spot.exception; import ice.spot.dto.global.ResponseDto; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/ice/spot/interceptor/pre/UserIdArgumentResolver.java b/src/main/java/ice/spot/interceptor/pre/UserIdArgumentResolver.java index 46670bb..dbe4a7f 100644 --- a/src/main/java/ice/spot/interceptor/pre/UserIdArgumentResolver.java +++ b/src/main/java/ice/spot/interceptor/pre/UserIdArgumentResolver.java @@ -1,8 +1,8 @@ package ice.spot.interceptor.pre; import ice.spot.annotation.UserId; -import ice.spot.exeption.CommonException; -import ice.spot.exeption.ErrorCode; +import ice.spot.exception.CommonException; +import ice.spot.exception.ErrorCode; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; import org.springframework.web.bind.support.WebDataBinderFactory; diff --git a/src/main/java/ice/spot/repository/BoardingRecordRepository.java b/src/main/java/ice/spot/repository/BoardingRecordRepository.java new file mode 100644 index 0000000..e001d83 --- /dev/null +++ b/src/main/java/ice/spot/repository/BoardingRecordRepository.java @@ -0,0 +1,7 @@ +package ice.spot.repository; + +import ice.spot.domain.BoardingRecord; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BoardingRecordRepository extends JpaRepository { +} diff --git a/src/main/java/ice/spot/repository/ImageRepository.java b/src/main/java/ice/spot/repository/ImageRepository.java new file mode 100644 index 0000000..6c0d9f3 --- /dev/null +++ b/src/main/java/ice/spot/repository/ImageRepository.java @@ -0,0 +1,7 @@ +package ice.spot.repository; + +import ice.spot.domain.Image; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ImageRepository extends JpaRepository { +} diff --git a/src/main/java/ice/spot/repository/ParkingLotRepository.java b/src/main/java/ice/spot/repository/ParkingLotRepository.java new file mode 100644 index 0000000..cc18aaf --- /dev/null +++ b/src/main/java/ice/spot/repository/ParkingLotRepository.java @@ -0,0 +1,7 @@ +package ice.spot.repository; + +import ice.spot.domain.ParkingLot; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ParkingLotRepository extends JpaRepository { +} diff --git a/src/main/java/ice/spot/security/filter/JwtAuthenticationFilter.java b/src/main/java/ice/spot/security/filter/JwtAuthenticationFilter.java index b4626e6..c898030 100644 --- a/src/main/java/ice/spot/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/ice/spot/security/filter/JwtAuthenticationFilter.java @@ -2,8 +2,8 @@ import ice.spot.constant.Constants; import ice.spot.dto.type.ERole; -import ice.spot.exeption.CommonException; -import ice.spot.exeption.ErrorCode; +import ice.spot.exception.CommonException; +import ice.spot.exception.ErrorCode; import ice.spot.security.info.JwtUserInfo; import ice.spot.security.provider.JwtAuthenticationManager; import ice.spot.util.HeaderUtil; diff --git a/src/main/java/ice/spot/security/filter/JwtExceptionFilter.java b/src/main/java/ice/spot/security/filter/JwtExceptionFilter.java index 7a8cdb0..217b985 100644 --- a/src/main/java/ice/spot/security/filter/JwtExceptionFilter.java +++ b/src/main/java/ice/spot/security/filter/JwtExceptionFilter.java @@ -1,8 +1,8 @@ package ice.spot.security.filter; import ice.spot.constant.Constants; -import ice.spot.exeption.CommonException; -import ice.spot.exeption.ErrorCode; +import ice.spot.exception.CommonException; +import ice.spot.exception.ErrorCode; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.MalformedJwtException; diff --git a/src/main/java/ice/spot/security/handler/exception/CustomAccessDeniedHandler.java b/src/main/java/ice/spot/security/handler/exception/CustomAccessDeniedHandler.java index fa25844..b061379 100644 --- a/src/main/java/ice/spot/security/handler/exception/CustomAccessDeniedHandler.java +++ b/src/main/java/ice/spot/security/handler/exception/CustomAccessDeniedHandler.java @@ -1,6 +1,6 @@ package ice.spot.security.handler.exception; -import ice.spot.exeption.ErrorCode; +import ice.spot.exception.ErrorCode; import ice.spot.security.info.AuthenticationResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/src/main/java/ice/spot/security/handler/exception/CustomAuthenticationEntryPointHandler.java b/src/main/java/ice/spot/security/handler/exception/CustomAuthenticationEntryPointHandler.java index df9e2af..54f4c0e 100644 --- a/src/main/java/ice/spot/security/handler/exception/CustomAuthenticationEntryPointHandler.java +++ b/src/main/java/ice/spot/security/handler/exception/CustomAuthenticationEntryPointHandler.java @@ -1,6 +1,6 @@ package ice.spot.security.handler.exception; -import ice.spot.exeption.ErrorCode; +import ice.spot.exception.ErrorCode; import ice.spot.security.info.AuthenticationResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/src/main/java/ice/spot/security/handler/login/Oauth2FailureHandler.java b/src/main/java/ice/spot/security/handler/login/Oauth2FailureHandler.java index b9d2b5c..55b9d9a 100644 --- a/src/main/java/ice/spot/security/handler/login/Oauth2FailureHandler.java +++ b/src/main/java/ice/spot/security/handler/login/Oauth2FailureHandler.java @@ -1,6 +1,6 @@ package ice.spot.security.handler.login; -import ice.spot.exeption.ErrorCode; +import ice.spot.exception.ErrorCode; import ice.spot.security.info.AuthenticationResponse; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/ice/spot/security/handler/logout/CustomLogoutResultHandler.java b/src/main/java/ice/spot/security/handler/logout/CustomLogoutResultHandler.java index 9b2cf4b..17c9924 100644 --- a/src/main/java/ice/spot/security/handler/logout/CustomLogoutResultHandler.java +++ b/src/main/java/ice/spot/security/handler/logout/CustomLogoutResultHandler.java @@ -1,6 +1,6 @@ package ice.spot.security.handler.logout; -import ice.spot.exeption.ErrorCode; +import ice.spot.exception.ErrorCode; import ice.spot.security.info.AuthenticationResponse; import ice.spot.util.CookieUtil; import jakarta.servlet.ServletException; diff --git a/src/main/java/ice/spot/security/info/AuthenticationResponse.java b/src/main/java/ice/spot/security/info/AuthenticationResponse.java index 01d9076..44300f7 100644 --- a/src/main/java/ice/spot/security/info/AuthenticationResponse.java +++ b/src/main/java/ice/spot/security/info/AuthenticationResponse.java @@ -3,7 +3,7 @@ import ice.spot.constant.Constants; import ice.spot.dto.global.ExceptionDto; import ice.spot.dto.response.JwtTokenDto; -import ice.spot.exeption.ErrorCode; +import ice.spot.exception.ErrorCode; import ice.spot.util.CookieUtil; import jakarta.servlet.http.HttpServletResponse; import net.minidev.json.JSONValue; diff --git a/src/main/java/ice/spot/security/service/CustomUserDetailService.java b/src/main/java/ice/spot/security/service/CustomUserDetailService.java index 91ff7f5..94c8cfd 100644 --- a/src/main/java/ice/spot/security/service/CustomUserDetailService.java +++ b/src/main/java/ice/spot/security/service/CustomUserDetailService.java @@ -1,7 +1,7 @@ package ice.spot.security.service; -import ice.spot.exeption.CommonException; -import ice.spot.exeption.ErrorCode; +import ice.spot.exception.CommonException; +import ice.spot.exception.ErrorCode; import ice.spot.repository.UserRepository; import ice.spot.security.info.UserPrincipal; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/ice/spot/service/AuthService.java b/src/main/java/ice/spot/service/AuthService.java index 0483a9e..e4a89bc 100644 --- a/src/main/java/ice/spot/service/AuthService.java +++ b/src/main/java/ice/spot/service/AuthService.java @@ -2,8 +2,8 @@ import ice.spot.domain.User; import ice.spot.dto.request.OauthSignUpDto; -import ice.spot.exeption.CommonException; -import ice.spot.exeption.ErrorCode; +import ice.spot.exception.CommonException; +import ice.spot.exception.ErrorCode; import ice.spot.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/ice/spot/service/BoardingRecordService.java b/src/main/java/ice/spot/service/BoardingRecordService.java new file mode 100644 index 0000000..325f31e --- /dev/null +++ b/src/main/java/ice/spot/service/BoardingRecordService.java @@ -0,0 +1,83 @@ +package ice.spot.service; + +import ice.spot.domain.BoardingRecord; +import ice.spot.domain.Image; +import ice.spot.domain.ParkingLot; +import ice.spot.domain.User; +import ice.spot.dto.boardingrecord.request.BoardingRecordRequest; +import ice.spot.dto.boardingrecord.response.BoardingRecordListResponse; +import ice.spot.dto.boardingrecord.response.BoardingRecordResponse; +import ice.spot.dto.user.response.PersonResponse; +import ice.spot.exception.CommonException; +import ice.spot.exception.ErrorCode; +import ice.spot.repository.BoardingRecordRepository; +import ice.spot.repository.ImageRepository; +import ice.spot.repository.ParkingLotRepository; +import ice.spot.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class BoardingRecordService { + private final BoardingRecordRepository boardingRecordRepository; + private final UserRepository userRepository; + private final ImageRepository imageRepository; + private final ParkingLotRepository parkingLotRepository; + private final WebClient webClient; + private final AuthService authService; + + @Transactional + public Boolean saveBoardingRecord(Long userId, Long imageId, BoardingRecordRequest boardingRecordRequest) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_USER)); + Image image = imageRepository.findById(imageId) + .orElseThrow(IllegalArgumentException::new); + ParkingLot parkingLot = parkingLotRepository.findById(1L) + .orElseThrow(IllegalArgumentException::new); + + boardingRecordRepository.save(BoardingRecord.builder() + .distance(boardingRecordRequest.distance()) + .time(boardingRecordRequest.time()) + .point(100) + .user(user) + .image(image) + .parkingLot(parkingLot) + .build()); + + user.plusPoint(); + + return Boolean.TRUE; + } + + @Transactional(readOnly = true) + public BoardingRecordListResponse boardingRecordList(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_USER)); + + BoardingRecordListResponse boardingRecordListResponse = BoardingRecordListResponse.builder() + .personResponse(PersonResponse.builder() + .nickname(user.getNickname()) + .recordCount(user.getBoardingRecords().size()) + .point(user.getPoint()) + .build()) + .boardingRecordResponseList(user.getBoardingRecords().stream() + .map(boardingRecord -> + BoardingRecordResponse.builder() + .createdAt(boardingRecord.getCreatedAt().toString()) + .image(boardingRecord.getImage().getImageUrl()) + .distance(boardingRecord.getDistance()) + .time(boardingRecord.getTime()) + .point(boardingRecord.getPoint()) + .build()) + .toList()) + .build(); + return boardingRecordListResponse; + } +} diff --git a/src/main/java/ice/spot/service/ImageService.java b/src/main/java/ice/spot/service/ImageService.java new file mode 100644 index 0000000..ea6ad90 --- /dev/null +++ b/src/main/java/ice/spot/service/ImageService.java @@ -0,0 +1,97 @@ +package ice.spot.service; + +import com.amazonaws.SdkClientException; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; +import ice.spot.domain.Image; +import ice.spot.domain.type.ParkingLotResult; +import ice.spot.dto.image.request.ImageCheckRequest; +import ice.spot.dto.image.response.ImageResponse; +import ice.spot.exception.CommonException; +import ice.spot.exception.ErrorCode; +import ice.spot.repository.ImageRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.io.IOException; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ImageService { + + @Value("${cloud.aws.s3.bucket}") + private String bucketName; + + private final AmazonS3Client amazonS3Client; + private final ImageRepository imageRepository; + private final WebClient webClient; + + @Transactional + public Long saveImage(MultipartFile multipartFile) throws IOException { + + String originalName = multipartFile.getOriginalFilename(); + Image image = new Image(originalName); + String filename = image.getStoredName(); + + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(multipartFile.getContentType()); + objectMetadata.setContentLength(multipartFile.getInputStream().available()); + + amazonS3Client.putObject(bucketName, filename, multipartFile.getInputStream(), objectMetadata); + + String accessUrl = amazonS3Client.getUrl(bucketName, filename).toString(); + image.setAccessUrl(accessUrl); + + ParkingLotResult parkingLotResult = checkImage(new ImageCheckRequest(image.getImageUrl())); + if(parkingLotResult == ParkingLotResult.CORRECT_PARKING_LOT) { + imageRepository.save(image); + } else if (parkingLotResult == ParkingLotResult.WRONG_PARKING_LOT) { + deleteImage(image.getStoredName()); + throw new CommonException(ErrorCode.INVALID_PARKING_LOT); + } else if (parkingLotResult == ParkingLotResult.NOT_FOUND_KICKBOARD){ + deleteImage(image.getStoredName()); + throw new CommonException(ErrorCode.INVALID_KICKBOARD_REQUEST); + } + + return image.getId(); + } + + @Transactional + public void deleteImage(String fileName) { + try { + amazonS3Client.deleteObject(bucketName, fileName); + } catch (SdkClientException e) { + + } + } + + @Transactional + public ParkingLotResult checkImage(ImageCheckRequest imageCheckRequest) { + String url = "/customvision/v3.0/Prediction/a7fb39e6-90a5-43a9-bee6-366150f3a1ad/classify/iterations/detecting_illegal_parking/url"; + + Mono responseMono = webClient.post() + .uri(url) + .bodyValue(imageCheckRequest) + .retrieve() + .bodyToMono(ImageResponse.class); + ImageResponse imageResponse = responseMono.block(); + + String hisgestProbabilityTag = imageResponse.getPredictions().get(0).tagName(); + String secondProbabilityTag = imageResponse.getPredictions().get(1).tagName(); + + if(hisgestProbabilityTag.equals("kickboard") && secondProbabilityTag.equals("parkinglot")) { + return ParkingLotResult.CORRECT_PARKING_LOT; + } else if (hisgestProbabilityTag.equals("kickboard") && !secondProbabilityTag.equals("parkinglot")) { + return ParkingLotResult.WRONG_PARKING_LOT; + } else { + return ParkingLotResult.NOT_FOUND_KICKBOARD; + } + } +} \ No newline at end of file diff --git a/src/main/java/ice/spot/service/ParkingLotService.java b/src/main/java/ice/spot/service/ParkingLotService.java new file mode 100644 index 0000000..730b4bb --- /dev/null +++ b/src/main/java/ice/spot/service/ParkingLotService.java @@ -0,0 +1,46 @@ +package ice.spot.service; + +import ice.spot.dto.parkingLot.response.ParkingLotResponse; +import ice.spot.repository.ParkingLotRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ParkingLotService { + private final ParkingLotRepository parkingLotRepository; + + @Transactional(readOnly = true) + public List parkingLotList(Double lat, Double lon) { + + List parkingLotResponseList = new ArrayList<>(parkingLotRepository.findAll().stream() + .map(parkingLot -> ParkingLotResponse.builder() + .address(parkingLot.getAddress()) + .detailAddress(parkingLot.getDetailAddress()) + .lat(parkingLot.getLat()) + .lon(parkingLot.getLon()) + .distance(Math.sqrt(Math.pow(parkingLot.getLat() - lat, 2) + Math.pow(parkingLot.getLon() - lon, 2))) + .build()) + .toList()); + Collections.sort(parkingLotResponseList, new Comparator() { + @Override + public int compare(ParkingLotResponse o1, ParkingLotResponse o2) { + if(o1.distance() > o2.distance()) { + return 1; + } else { + return -1; + } + } + }); + + return parkingLotResponseList.subList(0, 5); + } +} diff --git a/src/main/java/ice/spot/util/HeaderUtil.java b/src/main/java/ice/spot/util/HeaderUtil.java index 4484455..9b4cc54 100644 --- a/src/main/java/ice/spot/util/HeaderUtil.java +++ b/src/main/java/ice/spot/util/HeaderUtil.java @@ -1,7 +1,7 @@ package ice.spot.util; -import ice.spot.exeption.CommonException; -import ice.spot.exeption.ErrorCode; +import ice.spot.exception.CommonException; +import ice.spot.exception.ErrorCode; import jakarta.servlet.http.HttpServletRequest; import org.springframework.util.StringUtils;