From e7febbfc2f211eb5212947ede45d3b507a5a022a Mon Sep 17 00:00:00 2001 From: Jang99u Date: Thu, 2 May 2024 17:25:00 +0900 Subject: [PATCH 01/27] =?UTF-8?q?Feat:=20customException=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ice/spot/dto/global/ExceptionDto.java | 16 +++++++ .../ice/spot/exeption/CommonException.java | 10 ++++ .../java/ice/spot/exeption/ErrorCode.java | 40 ++++++++++++++++ .../spot/exeption/GlobalExceptionHandler.java | 48 +++++++++++++++++++ 4 files changed, 114 insertions(+) create mode 100644 src/main/java/ice/spot/dto/global/ExceptionDto.java create mode 100644 src/main/java/ice/spot/exeption/CommonException.java create mode 100644 src/main/java/ice/spot/exeption/ErrorCode.java create mode 100644 src/main/java/ice/spot/exeption/GlobalExceptionHandler.java diff --git a/src/main/java/ice/spot/dto/global/ExceptionDto.java b/src/main/java/ice/spot/dto/global/ExceptionDto.java new file mode 100644 index 0000000..7017212 --- /dev/null +++ b/src/main/java/ice/spot/dto/global/ExceptionDto.java @@ -0,0 +1,16 @@ +package ice.spot.dto.global; + +import ice.spot.exeption.ErrorCode; + +public record ExceptionDto( + Integer code, + String message +) { + public ExceptionDto(ErrorCode errorCode) { + this(errorCode.getCode(), errorCode.getMessage()); + } + + public static ExceptionDto of(ErrorCode errorCode) { + return new ExceptionDto(errorCode); + } +} diff --git a/src/main/java/ice/spot/exeption/CommonException.java b/src/main/java/ice/spot/exeption/CommonException.java new file mode 100644 index 0000000..c735e2b --- /dev/null +++ b/src/main/java/ice/spot/exeption/CommonException.java @@ -0,0 +1,10 @@ +package ice.spot.exeption; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class CommonException extends RuntimeException { + private final ErrorCode errorCode; +} diff --git a/src/main/java/ice/spot/exeption/ErrorCode.java b/src/main/java/ice/spot/exeption/ErrorCode.java new file mode 100644 index 0000000..3eda73b --- /dev/null +++ b/src/main/java/ice/spot/exeption/ErrorCode.java @@ -0,0 +1,40 @@ +package ice.spot.exeption; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ErrorCode { + //400 + WRONG_ENTRY_POINT(40000, HttpStatus.BAD_REQUEST, "잘못된 접근입니다"), + MISSING_REQUEST_PARAMETER(40001, HttpStatus.BAD_REQUEST, "필수 요청 파라미터가 누락되었습니다."), + INVALID_PARAMETER_FORMAT(40002, HttpStatus.BAD_REQUEST, "요청에 유효하지 않은 인자 형식입니다."), + BAD_REQUEST_JSON(40003, HttpStatus.BAD_REQUEST, "잘못된 JSON 형식입니다."), + + //401 + INVALID_HEADER_VALUE(40100, HttpStatus.UNAUTHORIZED, "올바르지 않은 헤더값입니다."), + EXPIRED_TOKEN_ERROR(40101, HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."), + INVALID_TOKEN_ERROR(40102, HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."), + TOKEN_MALFORMED_ERROR(40103, HttpStatus.UNAUTHORIZED, "토큰이 올바르지 않습니다."), + TOKEN_TYPE_ERROR(40104, HttpStatus.UNAUTHORIZED, "토큰 타입이 일치하지 않거나 비어있습니다."), + TOKEN_UNSUPPORTED_ERROR(40105, HttpStatus.UNAUTHORIZED, "지원하지않는 토큰입니다."), + TOKEN_GENERATION_ERROR(40106, HttpStatus.UNAUTHORIZED, "토큰 생성에 실패하였습니다."), + TOKEN_UNKNOWN_ERROR(40107, HttpStatus.UNAUTHORIZED, "알 수 없는 토큰입니다."), + LOGIN_FAILURE(40108, HttpStatus.UNAUTHORIZED, "로그인에 실패했습니다"), + + //403 + FORBIDDEN_ROLE(40300, HttpStatus.FORBIDDEN, "권한이 존재하지 않습니다."), + + //404 + NOT_FOUND_USER(40400, HttpStatus.NOT_FOUND, "존재하지 않는 사용자입니다."), + + //500 + INTERNAL_SERVER_ERROR(50000, HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다"), + ; + + private final Integer code; + private final HttpStatus httpStatus; + private final String message; +} diff --git a/src/main/java/ice/spot/exeption/GlobalExceptionHandler.java b/src/main/java/ice/spot/exeption/GlobalExceptionHandler.java new file mode 100644 index 0000000..e8e105a --- /dev/null +++ b/src/main/java/ice/spot/exeption/GlobalExceptionHandler.java @@ -0,0 +1,48 @@ +package ice.spot.exeption; + +import ice.spot.dto.global.ResponseDto; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + // 지원되지 않는 HTTP 메소드를 사용할 때 발생하는 exception + @ExceptionHandler(value = {NoHandlerFoundException.class, HttpRequestMethodNotSupportedException.class}) + public ResponseDto handleNoPageFoundException(Exception e) { + log.error("handleNoPageFoundException() in GlobalExceptionHandler throw NoHandlerFoundException : {}", e.getMessage()); + return ResponseDto.fail(new CommonException(ErrorCode.WRONG_ENTRY_POINT)); + } + + // 메소드의 인자 타입이 일치하지 않을 때 발생하는 exception + @ExceptionHandler(value = {MethodArgumentTypeMismatchException.class}) + public ResponseDto handleArgumentNotValidException(MethodArgumentTypeMismatchException e) { + log.error("handleArgumentNotValidException() in GlobalExceptionHandler throw MethodArgumentTypeMismatchException : {}", e.getMessage()); + return ResponseDto.fail(e); + } + + // 필수 파라미터가 누락되었을 때 발생하는 exception + @ExceptionHandler(value = {MissingServletRequestParameterException.class}) + public ResponseDto handleArgumentNotValidException(MissingServletRequestParameterException e) { + log.error("handleArgumentNotValidException() in GlobalExceptionHandler throw MethodArgumentNotValidException : {}", e.getMessage()); + return ResponseDto.fail(e); + } + + // 커스텀 exception + @ExceptionHandler(value = {CommonException.class}) + public ResponseDto handleCustomException(CommonException e){ + return ResponseDto.fail(e); + } + + // 서버 exception + @ExceptionHandler(value = {Exception.class}) + public ResponseDto handleServerException(Exception e){ + log.info("occurred exception in handleServerError = {}", e.getMessage()); + return ResponseDto.fail(new CommonException(ErrorCode.INTERNAL_SERVER_ERROR)); + } +} From 476790882c8c6cca8e0e4ed0da9972f1d97a0c68 Mon Sep 17 00:00:00 2001 From: Jang99u Date: Thu, 2 May 2024 17:25:16 +0900 Subject: [PATCH 02/27] =?UTF-8?q?Feat:=20responseDto=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ice/spot/dto/global/ResponseDto.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/main/java/ice/spot/dto/global/ResponseDto.java diff --git a/src/main/java/ice/spot/dto/global/ResponseDto.java b/src/main/java/ice/spot/dto/global/ResponseDto.java new file mode 100644 index 0000000..939e0d6 --- /dev/null +++ b/src/main/java/ice/spot/dto/global/ResponseDto.java @@ -0,0 +1,60 @@ +package ice.spot.dto.global; + +import ice.spot.exeption.CommonException; +import ice.spot.exeption.ErrorCode; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import net.minidev.json.annotate.JsonIgnore; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +public record ResponseDto ( + @JsonIgnore HttpStatus httpStatus, + boolean success, + @Nullable T data, + @Nullable ExceptionDto exceptionDto +) { + public static ResponseDto ok(T data){ + return new ResponseDto<>( + HttpStatus.OK, + true, + data, + null + ); + } + public static ResponseDto created(Boolean data){ + return new ResponseDto<>( + HttpStatus.CREATED, + true, + data, + null + ); + } + public static ResponseDto fail(@NotNull CommonException e){ + return new ResponseDto<>( + e.getErrorCode().getHttpStatus(), + false, + null, + new ExceptionDto(e.getErrorCode()) + ); + } + + public static ResponseDto fail(final MissingServletRequestParameterException e) { + return new ResponseDto<>( + HttpStatus.BAD_REQUEST, + false, + null, + new ExceptionDto(ErrorCode.MISSING_REQUEST_PARAMETER) + ); + } + + public static ResponseDto fail(final MethodArgumentTypeMismatchException e) { + return new ResponseDto<>( + HttpStatus.INTERNAL_SERVER_ERROR, + false, + null, + new ExceptionDto(ErrorCode.INVALID_PARAMETER_FORMAT) + ); + } +} From 6ff9b154fcd27e99adcaeedd0923b4ed102b8eb2 Mon Sep 17 00:00:00 2001 From: Jang99u Date: Fri, 3 May 2024 19:27:53 +0900 Subject: [PATCH 03/27] =?UTF-8?q?Feat:=20=EB=82=B4=20=EA=B7=BC=EC=B2=98=20?= =?UTF-8?q?=ED=82=A5=EB=B3=B4=EB=93=9C=20=EC=A3=BC=EC=B0=A8=EC=9E=A5=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spot/controller/ParkingLotController.java | 21 +++++++++ .../response/ParkingLotResponse.java | 27 +++++++++++ .../spot/repository/ParkingLotRepository.java | 7 +++ .../ice/spot/service/ParkingLotService.java | 46 +++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 src/main/java/ice/spot/controller/ParkingLotController.java create mode 100644 src/main/java/ice/spot/dto/parkingLot/response/ParkingLotResponse.java create mode 100644 src/main/java/ice/spot/repository/ParkingLotRepository.java create mode 100644 src/main/java/ice/spot/service/ParkingLotService.java 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/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/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/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); + } +} From fdbc0613bbb8080b46f940d053affb6ccfad316e Mon Sep 17 00:00:00 2001 From: Jang99u Date: Sun, 5 May 2024 17:18:33 +0900 Subject: [PATCH 04/27] =?UTF-8?q?Fix:=20BoardingRecord=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ice/spot/domain/BoardingRecord.java | 31 +++++-------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/src/main/java/ice/spot/domain/BoardingRecord.java b/src/main/java/ice/spot/domain/BoardingRecord.java index b46aa20..f0d6e9a 100644 --- a/src/main/java/ice/spot/domain/BoardingRecord.java +++ b/src/main/java/ice/spot/domain/BoardingRecord.java @@ -6,6 +6,7 @@ import lombok.NoArgsConstructor; import java.time.LocalDate; +import java.time.LocalDateTime; @Entity @Getter @@ -20,23 +21,11 @@ public class BoardingRecord { @Column(name = "image_url") private String imageUrl; - @Column(name = "depart_at") - private LocalDate departAt; + @Column(name = "distance") + private Double distance; - @Column(name = "arrive_at") - private LocalDate arriveAt; - - @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 = "time") + private Integer time; @ManyToOne @JoinColumn(name = "user_id") @@ -46,14 +35,10 @@ public class BoardingRecord { @JoinColumn(name = "parking_lot_id", referencedColumnName = "id") private ParkingLot parkingLot; - public BoardingRecord(String imageUrl, LocalDate departAt, LocalDate arriveAt, Double departLat, Double departLon, Double arriveLat, Double arriveLon, User user, ParkingLot parkingLot) { + public BoardingRecord(String imageUrl, Double distance, Integer time, 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; + this.distance = distance; + this.time = time; this.user = user; this.parkingLot = parkingLot; } From 0db2509bd4a039311d7275c891832399cf121884 Mon Sep 17 00:00:00 2001 From: JeongHeumChoi Date: Sun, 5 May 2024 23:20:06 +0900 Subject: [PATCH 05/27] =?UTF-8?q?Feat:=20Spring=20Security,=20JWT=20Cookie?= =?UTF-8?q?,=20=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ice/spot/annotation/UserId.java | 11 +++ .../java/ice/spot/config/WebMVCConfig.java | 33 +++++++ .../java/ice/spot/constant/Constants.java | 16 +++ .../ice/spot/controller/AuthController.java | 27 +++++ .../ice/spot/dto/request/OauthSignUpDto.java | 9 ++ .../ice/spot/dto/response/JwtTokenDto.java | 18 ++++ .../java/ice/spot/dto/type/EProvider.java | 13 +++ src/main/java/ice/spot/dto/type/ERole.java | 16 +++ .../interceptor/post/ResponseInterceptor.java | 32 ++++++ .../pre/UserIdArgumentResolver.java | 36 +++++++ .../interceptor/pre/UserIdInterceptor.java | 22 +++++ .../ice/spot/repository/UserRepository.java | 47 +++++++++ .../spot/security/config/PasswordConfig.java | 14 +++ .../spot/security/config/SecurityConfig.java | 75 ++++++++++++++ .../filter/JwtAuthenticationFilter.java | 70 +++++++++++++ .../security/filter/JwtExceptionFilter.java | 67 +++++++++++++ .../exception/CustomAccessDeniedHandler.java | 24 +++++ ...CustomAuthenticationEntryPointHandler.java | 30 ++++++ .../handler/login/Oauth2FailureHandler.java | 25 +++++ .../handler/login/Oauth2SuccessHandler.java | 45 +++++++++ .../logout/CustomLogoutProcessHandler.java | 29 ++++++ .../logout/CustomLogoutResultHandler.java | 37 +++++++ .../security/info/AuthenticationResponse.java | 72 ++++++++++++++ .../ice/spot/security/info/JwtUserInfo.java | 6 ++ .../security/info/KakaoOauth2UserInfo.java | 16 +++ .../ice/spot/security/info/UserPrincipal.java | 99 +++++++++++++++++++ .../security/info/factory/Oauth2UserInfo.java | 13 +++ .../info/factory/Oauth2UserInfoFactory.java | 22 +++++ .../provider/JwtAuthenticationManager.java | 22 +++++ .../provider/JwtAuthenticationProvider.java | 59 +++++++++++ .../CustomOauth2UserDetailService.java | 64 ++++++++++++ .../service/CustomUserDetailService.java | 38 +++++++ .../java/ice/spot/service/AuthService.java | 34 +++++++ src/main/java/ice/spot/util/CookieUtil.java | 85 ++++++++++++++++ src/main/java/ice/spot/util/HeaderUtil.java | 22 +++++ src/main/java/ice/spot/util/JwtUtil.java | 70 +++++++++++++ 36 files changed, 1318 insertions(+) create mode 100644 src/main/java/ice/spot/annotation/UserId.java create mode 100644 src/main/java/ice/spot/config/WebMVCConfig.java create mode 100644 src/main/java/ice/spot/constant/Constants.java create mode 100644 src/main/java/ice/spot/controller/AuthController.java create mode 100644 src/main/java/ice/spot/dto/request/OauthSignUpDto.java create mode 100644 src/main/java/ice/spot/dto/response/JwtTokenDto.java create mode 100644 src/main/java/ice/spot/dto/type/EProvider.java create mode 100644 src/main/java/ice/spot/dto/type/ERole.java create mode 100644 src/main/java/ice/spot/interceptor/post/ResponseInterceptor.java create mode 100644 src/main/java/ice/spot/interceptor/pre/UserIdArgumentResolver.java create mode 100644 src/main/java/ice/spot/interceptor/pre/UserIdInterceptor.java create mode 100644 src/main/java/ice/spot/repository/UserRepository.java create mode 100644 src/main/java/ice/spot/security/config/PasswordConfig.java create mode 100644 src/main/java/ice/spot/security/config/SecurityConfig.java create mode 100644 src/main/java/ice/spot/security/filter/JwtAuthenticationFilter.java create mode 100644 src/main/java/ice/spot/security/filter/JwtExceptionFilter.java create mode 100644 src/main/java/ice/spot/security/handler/exception/CustomAccessDeniedHandler.java create mode 100644 src/main/java/ice/spot/security/handler/exception/CustomAuthenticationEntryPointHandler.java create mode 100644 src/main/java/ice/spot/security/handler/login/Oauth2FailureHandler.java create mode 100644 src/main/java/ice/spot/security/handler/login/Oauth2SuccessHandler.java create mode 100644 src/main/java/ice/spot/security/handler/logout/CustomLogoutProcessHandler.java create mode 100644 src/main/java/ice/spot/security/handler/logout/CustomLogoutResultHandler.java create mode 100644 src/main/java/ice/spot/security/info/AuthenticationResponse.java create mode 100644 src/main/java/ice/spot/security/info/JwtUserInfo.java create mode 100644 src/main/java/ice/spot/security/info/KakaoOauth2UserInfo.java create mode 100644 src/main/java/ice/spot/security/info/UserPrincipal.java create mode 100644 src/main/java/ice/spot/security/info/factory/Oauth2UserInfo.java create mode 100644 src/main/java/ice/spot/security/info/factory/Oauth2UserInfoFactory.java create mode 100644 src/main/java/ice/spot/security/provider/JwtAuthenticationManager.java create mode 100644 src/main/java/ice/spot/security/provider/JwtAuthenticationProvider.java create mode 100644 src/main/java/ice/spot/security/service/CustomOauth2UserDetailService.java create mode 100644 src/main/java/ice/spot/security/service/CustomUserDetailService.java create mode 100644 src/main/java/ice/spot/service/AuthService.java create mode 100644 src/main/java/ice/spot/util/CookieUtil.java create mode 100644 src/main/java/ice/spot/util/HeaderUtil.java create mode 100644 src/main/java/ice/spot/util/JwtUtil.java diff --git a/src/main/java/ice/spot/annotation/UserId.java b/src/main/java/ice/spot/annotation/UserId.java new file mode 100644 index 0000000..2c3f090 --- /dev/null +++ b/src/main/java/ice/spot/annotation/UserId.java @@ -0,0 +1,11 @@ +package ice.spot.annotation; + +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 UserId { +} diff --git a/src/main/java/ice/spot/config/WebMVCConfig.java b/src/main/java/ice/spot/config/WebMVCConfig.java new file mode 100644 index 0000000..9b0cf84 --- /dev/null +++ b/src/main/java/ice/spot/config/WebMVCConfig.java @@ -0,0 +1,33 @@ +package ice.spot.config; + +import ice.spot.constant.Constants; +import ice.spot.interceptor.pre.UserIdArgumentResolver; +import ice.spot.interceptor.pre.UserIdInterceptor; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class WebMVCConfig implements WebMvcConfigurer { + private final UserIdArgumentResolver userIdArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + WebMvcConfigurer.super.addArgumentResolvers(resolvers); + resolvers.add(this.userIdArgumentResolver); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new UserIdInterceptor()) + .addPathPatterns("/**") + .excludePathPatterns(Constants.NO_NEED_AUTH); + } +} diff --git a/src/main/java/ice/spot/constant/Constants.java b/src/main/java/ice/spot/constant/Constants.java new file mode 100644 index 0000000..8b85e53 --- /dev/null +++ b/src/main/java/ice/spot/constant/Constants.java @@ -0,0 +1,16 @@ +package ice.spot.constant; + +import java.util.List; + +public class Constants { + public static String CLAIM_USER_ID = "uuid"; + public static String CLAIM_USER_ROLE = "role"; + public static String PREFIX_BEARER = "Bearer "; + public static String PREFIX_AUTH = "authorization"; + public static String ACCESS_COOKIE_NAME = "access_token"; + public static String REFRESH_COOKIE_NAME = "refresh_token"; + public static List NO_NEED_AUTH = List.of( + "/api/auth/sign-up", + "/api/auth/sign-in" + ); +} diff --git a/src/main/java/ice/spot/controller/AuthController.java b/src/main/java/ice/spot/controller/AuthController.java new file mode 100644 index 0000000..173b2a9 --- /dev/null +++ b/src/main/java/ice/spot/controller/AuthController.java @@ -0,0 +1,27 @@ +package ice.spot.controller; + +import ice.spot.annotation.UserId; +import ice.spot.dto.global.ResponseDto; +import ice.spot.dto.request.OauthSignUpDto; +import ice.spot.service.AuthService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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; + +@Slf4j +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @PostMapping("/oauth2/sign-up") + public ResponseDto signUp(@UserId Long userId, @RequestBody OauthSignUpDto oauthSignUpDto) { + authService.signUp(userId, oauthSignUpDto); + return ResponseDto.ok(null); + } +} diff --git a/src/main/java/ice/spot/dto/request/OauthSignUpDto.java b/src/main/java/ice/spot/dto/request/OauthSignUpDto.java new file mode 100644 index 0000000..1d7c0b2 --- /dev/null +++ b/src/main/java/ice/spot/dto/request/OauthSignUpDto.java @@ -0,0 +1,9 @@ +package ice.spot.dto.request; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record OauthSignUpDto( + @JsonProperty("nickname") + String nickname +) { +} diff --git a/src/main/java/ice/spot/dto/response/JwtTokenDto.java b/src/main/java/ice/spot/dto/response/JwtTokenDto.java new file mode 100644 index 0000000..ecd8d33 --- /dev/null +++ b/src/main/java/ice/spot/dto/response/JwtTokenDto.java @@ -0,0 +1,18 @@ +package ice.spot.dto.response; + +import lombok.Builder; + +import java.io.Serializable; + +@Builder +public record JwtTokenDto( + String accessToken, + String refreshToken +) implements Serializable { + public static JwtTokenDto of(String accessToken, String refreshToken) { + return JwtTokenDto.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/src/main/java/ice/spot/dto/type/EProvider.java b/src/main/java/ice/spot/dto/type/EProvider.java new file mode 100644 index 0000000..235e685 --- /dev/null +++ b/src/main/java/ice/spot/dto/type/EProvider.java @@ -0,0 +1,13 @@ +package ice.spot.dto.type; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum EProvider { + + KAKAO("KAKAO"); + + private final String name; +} diff --git a/src/main/java/ice/spot/dto/type/ERole.java b/src/main/java/ice/spot/dto/type/ERole.java new file mode 100644 index 0000000..da41f59 --- /dev/null +++ b/src/main/java/ice/spot/dto/type/ERole.java @@ -0,0 +1,16 @@ +package ice.spot.dto.type; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ERole { + + GUEST("GUEST", "ROLE_GUEST"), + USER("USER", "ROLE_USER"), + ADMIN("ADMIN", "ROLE_ADMIN"); + + private final String role; + private final String securityRole; +} diff --git a/src/main/java/ice/spot/interceptor/post/ResponseInterceptor.java b/src/main/java/ice/spot/interceptor/post/ResponseInterceptor.java new file mode 100644 index 0000000..0d2a52f --- /dev/null +++ b/src/main/java/ice/spot/interceptor/post/ResponseInterceptor.java @@ -0,0 +1,32 @@ +package ice.spot.interceptor.post; + +import ice.spot.dto.global.ResponseDto; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +@RestControllerAdvice +public class ResponseInterceptor implements ResponseBodyAdvice { + + @Override + public boolean supports(MethodParameter returnType, Class converterType) { + return true; + } + + @Override + public Object beforeBodyWrite( + Object body, + MethodParameter returnType, + MediaType selectedContentType, + Class selectedConverterType, + ServerHttpRequest request, + ServerHttpResponse response + ) { + if (returnType.getParameterType().equals(ResponseDto.class)) + response.setStatusCode(((ResponseDto) body).httpStatus()); + return body; + } +} diff --git a/src/main/java/ice/spot/interceptor/pre/UserIdArgumentResolver.java b/src/main/java/ice/spot/interceptor/pre/UserIdArgumentResolver.java new file mode 100644 index 0000000..46670bb --- /dev/null +++ b/src/main/java/ice/spot/interceptor/pre/UserIdArgumentResolver.java @@ -0,0 +1,36 @@ +package ice.spot.interceptor.pre; + +import ice.spot.annotation.UserId; +import ice.spot.exeption.CommonException; +import ice.spot.exeption.ErrorCode; +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.context.request.WebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +public class UserIdArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(Long.class) + && parameter.hasParameterAnnotation(UserId.class); + } + + @Override + public Object resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) throws Exception { + final Object userId = webRequest + .getAttribute("USER_ID", WebRequest.SCOPE_REQUEST); + if (userId == null) + throw new CommonException(ErrorCode.INVALID_HEADER_VALUE); + return Long.valueOf(userId.toString()); + } +} diff --git a/src/main/java/ice/spot/interceptor/pre/UserIdInterceptor.java b/src/main/java/ice/spot/interceptor/pre/UserIdInterceptor.java new file mode 100644 index 0000000..d8796b8 --- /dev/null +++ b/src/main/java/ice/spot/interceptor/pre/UserIdInterceptor.java @@ -0,0 +1,22 @@ +package ice.spot.interceptor.pre; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.servlet.HandlerInterceptor; + +public class UserIdInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler + ) throws Exception { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + request.setAttribute("USER_ID", authentication.getName()); + + return HandlerInterceptor.super.preHandle(request, response, handler); + } +} diff --git a/src/main/java/ice/spot/repository/UserRepository.java b/src/main/java/ice/spot/repository/UserRepository.java new file mode 100644 index 0000000..434046a --- /dev/null +++ b/src/main/java/ice/spot/repository/UserRepository.java @@ -0,0 +1,47 @@ +package ice.spot.repository; + +import ice.spot.domain.User; +import ice.spot.dto.type.ERole; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + @Query("select u.id as id, u.role as role, u.password as password from User u where u.serialId = :serialId") + Optional findUserSecurityFromBySerialId(String serialId); + @Query("select u.id as id, u.role as role, u.password as password from User u where u.id = :id") + Optional findUserSecurityFromById(Long id); + Optional findByIdAndRefreshToken(Long id, String refreshToken); + @Modifying(clearAutomatically = true) + @Query("update User u set u.refreshToken = :refreshToken where u.id = :userId") + void updateRefreshToken(Long userId, String refreshToken); + Optional findById(Long userId); + + interface UserSecurityForm { + + Long getId(); + ERole getRole(); + String getPassword(); + + static UserSecurityForm invoke(User user){ + return new UserSecurityForm() { + @Override + public Long getId() { + return user.getId(); + } + + @Override + public ERole getRole() { + return user.getRole(); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + }; + } + } +} diff --git a/src/main/java/ice/spot/security/config/PasswordConfig.java b/src/main/java/ice/spot/security/config/PasswordConfig.java new file mode 100644 index 0000000..d648a36 --- /dev/null +++ b/src/main/java/ice/spot/security/config/PasswordConfig.java @@ -0,0 +1,14 @@ +package ice.spot.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +@Configuration +public class PasswordConfig { + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/ice/spot/security/config/SecurityConfig.java b/src/main/java/ice/spot/security/config/SecurityConfig.java new file mode 100644 index 0000000..ef21d48 --- /dev/null +++ b/src/main/java/ice/spot/security/config/SecurityConfig.java @@ -0,0 +1,75 @@ +package ice.spot.security.config; + +import ice.spot.security.filter.JwtAuthenticationFilter; +import ice.spot.security.filter.JwtExceptionFilter; +import ice.spot.security.handler.exception.CustomAccessDeniedHandler; +import ice.spot.security.handler.exception.CustomAuthenticationEntryPointHandler; +import ice.spot.security.handler.login.Oauth2FailureHandler; +import ice.spot.security.handler.login.Oauth2SuccessHandler; +import ice.spot.security.handler.logout.CustomLogoutProcessHandler; +import ice.spot.security.handler.logout.CustomLogoutResultHandler; +import ice.spot.security.provider.JwtAuthenticationManager; +import ice.spot.security.service.CustomOauth2UserDetailService; +import ice.spot.util.JwtUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.logout.LogoutFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final Oauth2SuccessHandler oauth2SuccessHandler; + private final Oauth2FailureHandler oauth2FailureHandler; + private final CustomOauth2UserDetailService customOauth2UserDetailService; + private final CustomLogoutProcessHandler customLogoutProcessHandler; + private final CustomLogoutResultHandler customLogoutResultHandler; + private final CustomAccessDeniedHandler customAccessDeniedHandler; + private final CustomAuthenticationEntryPointHandler customAuthenticationEntryPointHandler; + private final JwtUtil jwtUtil; + private final JwtAuthenticationManager jwtAuthenticationManager; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + .csrf(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .sessionManagement((session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + ) + .authorizeHttpRequests(request -> + request + .requestMatchers("/api/oauth2/sign-up").permitAll() + .requestMatchers("/api/**").hasAnyRole("USER") + .anyRequest().authenticated() + ) + .oauth2Login(oauth2 -> oauth2 + .successHandler(oauth2SuccessHandler) + .failureHandler(oauth2FailureHandler) + .userInfoEndpoint(it -> it.userService(customOauth2UserDetailService)) + ) + .logout(logout -> logout + .logoutUrl("/api/users/logout") + .addLogoutHandler(customLogoutProcessHandler) + .logoutSuccessHandler(customLogoutResultHandler) + ) + .exceptionHandling(exception -> exception + .accessDeniedHandler(customAccessDeniedHandler) + .authenticationEntryPoint(customAuthenticationEntryPointHandler) + ) + .addFilterBefore( + new JwtAuthenticationFilter(jwtUtil, jwtAuthenticationManager), LogoutFilter.class + ) + .addFilterBefore( + new JwtExceptionFilter(), JwtAuthenticationFilter.class + ) + .getOrBuild(); + } +} diff --git a/src/main/java/ice/spot/security/filter/JwtAuthenticationFilter.java b/src/main/java/ice/spot/security/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..b4626e6 --- /dev/null +++ b/src/main/java/ice/spot/security/filter/JwtAuthenticationFilter.java @@ -0,0 +1,70 @@ +package ice.spot.security.filter; + +import ice.spot.constant.Constants; +import ice.spot.dto.type.ERole; +import ice.spot.exeption.CommonException; +import ice.spot.exeption.ErrorCode; +import ice.spot.security.info.JwtUserInfo; +import ice.spot.security.provider.JwtAuthenticationManager; +import ice.spot.util.HeaderUtil; +import ice.spot.util.JwtUtil; +import io.jsonwebtoken.Claims; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtUtil jwtUtil; + private final JwtAuthenticationManager jwtAuthenticationManager; + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + return Constants.NO_NEED_AUTH.contains(request.getRequestURI()); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String token = HeaderUtil.refineHeader(request, Constants.PREFIX_AUTH, Constants.PREFIX_BEARER) + .orElseThrow(() -> new CommonException(ErrorCode.INVALID_HEADER_VALUE)); + + Claims claims = jwtUtil.validateToken(token); + log.info("claim: getUserId() = {}", claims.get(Constants.CLAIM_USER_ID, Long.class)); + + JwtUserInfo jwtUserInfo = new JwtUserInfo( + claims.get(Constants.CLAIM_USER_ID, Long.class), + ERole.valueOf(claims.get(Constants.CLAIM_USER_ROLE, String.class)) + ); + + // 인증 받지 않은 인증용 객체 + UsernamePasswordAuthenticationToken unAuthenticatedToken = new UsernamePasswordAuthenticationToken( + jwtUserInfo, null, null + ); + + // 인증 받은 후의 인증 객체 + UsernamePasswordAuthenticationToken authenticatedToken + = (UsernamePasswordAuthenticationToken) jwtAuthenticationManager.authenticate(unAuthenticatedToken); + log.info("인증 성공"); + + // 사용자의 IP등 세부 정보 인증 정보에 추가 + authenticatedToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(authenticatedToken); + SecurityContextHolder.setContext(securityContext); + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/ice/spot/security/filter/JwtExceptionFilter.java b/src/main/java/ice/spot/security/filter/JwtExceptionFilter.java new file mode 100644 index 0000000..7a8cdb0 --- /dev/null +++ b/src/main/java/ice/spot/security/filter/JwtExceptionFilter.java @@ -0,0 +1,67 @@ +package ice.spot.security.filter; + +import ice.spot.constant.Constants; +import ice.spot.exeption.CommonException; +import ice.spot.exeption.ErrorCode; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +public class JwtExceptionFilter extends OncePerRequestFilter { + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (SecurityException e) { + log.error("FilterException throw SecurityException Exception : {}", e.getMessage()); + request.setAttribute("exception", ErrorCode.FORBIDDEN_ROLE); + filterChain.doFilter(request, response); + } catch (MalformedJwtException e) { + log.error("FilterException throw MalformedJwtException Exception : {}", e.getMessage()); + request.setAttribute("exception", ErrorCode.TOKEN_MALFORMED_ERROR); + filterChain.doFilter(request, response); + } catch (IllegalArgumentException e) { + log.error("FilterException throw IllegalArgumentException Exception : {}", e.getMessage()); + request.setAttribute("exception", ErrorCode.TOKEN_TYPE_ERROR); + filterChain.doFilter(request, response); + } catch (ExpiredJwtException e) { + log.error("FilterException throw ExpiredJwtException Exception : {}", e.getMessage()); + request.setAttribute("exception", ErrorCode.EXPIRED_TOKEN_ERROR); + filterChain.doFilter(request, response); + } catch (UnsupportedJwtException e) { + log.error("FilterException throw UnsupportedJwtException Exception : {}", e.getMessage()); + request.setAttribute("exception", ErrorCode.TOKEN_UNSUPPORTED_ERROR); + filterChain.doFilter(request, response); + } catch (JwtException e) { + log.error("FilterException throw JwtException Exception : {}", e.getMessage()); + request.setAttribute("exception", ErrorCode.TOKEN_UNKNOWN_ERROR); + filterChain.doFilter(request, response); + } catch (CommonException e) { + log.error("FilterException throw CommonException Exception : {}", e.getMessage()); + request.setAttribute("exception", e.getErrorCode()); + filterChain.doFilter(request, response); + } catch (Exception e) { + log.error("FilterException throw Exception Exception : {}", e.getMessage()); + request.setAttribute("exception", ErrorCode.INTERNAL_SERVER_ERROR); + filterChain.doFilter(request, response); + } + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + return Constants.NO_NEED_AUTH.contains(request.getRequestURI()); + } +} \ No newline at end of file diff --git a/src/main/java/ice/spot/security/handler/exception/CustomAccessDeniedHandler.java b/src/main/java/ice/spot/security/handler/exception/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..fa25844 --- /dev/null +++ b/src/main/java/ice/spot/security/handler/exception/CustomAccessDeniedHandler.java @@ -0,0 +1,24 @@ +package ice.spot.security.handler.exception; + +import ice.spot.exeption.ErrorCode; +import ice.spot.security.info.AuthenticationResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException { + AuthenticationResponse.makeFailureResponse(response, ErrorCode.FORBIDDEN_ROLE); + } +} diff --git a/src/main/java/ice/spot/security/handler/exception/CustomAuthenticationEntryPointHandler.java b/src/main/java/ice/spot/security/handler/exception/CustomAuthenticationEntryPointHandler.java new file mode 100644 index 0000000..df9e2af --- /dev/null +++ b/src/main/java/ice/spot/security/handler/exception/CustomAuthenticationEntryPointHandler.java @@ -0,0 +1,30 @@ +package ice.spot.security.handler.exception; + +import ice.spot.exeption.ErrorCode; +import ice.spot.security.info.AuthenticationResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomAuthenticationEntryPointHandler implements AuthenticationEntryPoint { + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authenticationException + ) throws IOException { + // filter 단에서 발생한 에러 핸들링 + ErrorCode errorCode = (ErrorCode) request.getAttribute("exception"); + if (errorCode == null) { + AuthenticationResponse.makeFailureResponse(response, ErrorCode.WRONG_ENTRY_POINT); + return; + } + AuthenticationResponse.makeFailureResponse(response, errorCode); + } +} diff --git a/src/main/java/ice/spot/security/handler/login/Oauth2FailureHandler.java b/src/main/java/ice/spot/security/handler/login/Oauth2FailureHandler.java new file mode 100644 index 0000000..b9d2b5c --- /dev/null +++ b/src/main/java/ice/spot/security/handler/login/Oauth2FailureHandler.java @@ -0,0 +1,25 @@ +package ice.spot.security.handler.login; + +import ice.spot.exeption.ErrorCode; +import ice.spot.security.info.AuthenticationResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class Oauth2FailureHandler implements AuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception + ) throws IOException, ServletException { + AuthenticationResponse.makeFailureResponse(response, ErrorCode.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/ice/spot/security/handler/login/Oauth2SuccessHandler.java b/src/main/java/ice/spot/security/handler/login/Oauth2SuccessHandler.java new file mode 100644 index 0000000..0ae5bc4 --- /dev/null +++ b/src/main/java/ice/spot/security/handler/login/Oauth2SuccessHandler.java @@ -0,0 +1,45 @@ +package ice.spot.security.handler.login; + +import ice.spot.dto.response.JwtTokenDto; +import ice.spot.repository.UserRepository; +import ice.spot.security.info.AuthenticationResponse; +import ice.spot.security.info.UserPrincipal; +import ice.spot.util.JwtUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class Oauth2SuccessHandler implements AuthenticationSuccessHandler { + + @Value("${server.domain}") + private String domain; + private final JwtUtil jwtUtil; + private final UserRepository userRepository; + + @Override + @Transactional + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) throws IOException, ServletException { + UserPrincipal principal = (UserPrincipal) authentication.getPrincipal(); + JwtTokenDto jwtTokenDto = jwtUtil.generateTokens(principal.getUserId(), principal.getRole()); + + userRepository.updateRefreshToken(principal.getUserId(), jwtTokenDto.refreshToken()); + + AuthenticationResponse.makeLoginSuccessResponse(response, domain, jwtTokenDto, jwtUtil.getRefreshExpiration()); + + response.sendRedirect("https://" + domain); + } +} diff --git a/src/main/java/ice/spot/security/handler/logout/CustomLogoutProcessHandler.java b/src/main/java/ice/spot/security/handler/logout/CustomLogoutProcessHandler.java new file mode 100644 index 0000000..80e07d6 --- /dev/null +++ b/src/main/java/ice/spot/security/handler/logout/CustomLogoutProcessHandler.java @@ -0,0 +1,29 @@ +package ice.spot.security.handler.logout; + +import ice.spot.repository.UserRepository; +import ice.spot.security.info.UserPrincipal; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@RequiredArgsConstructor +public class CustomLogoutProcessHandler implements LogoutHandler { + + private final UserRepository userRepository; + + @Override + @Transactional + public void logout( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) { + UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); + userRepository.updateRefreshToken(userPrincipal.getUserId(), null); + } +} diff --git a/src/main/java/ice/spot/security/handler/logout/CustomLogoutResultHandler.java b/src/main/java/ice/spot/security/handler/logout/CustomLogoutResultHandler.java new file mode 100644 index 0000000..9b2cf4b --- /dev/null +++ b/src/main/java/ice/spot/security/handler/logout/CustomLogoutResultHandler.java @@ -0,0 +1,37 @@ +package ice.spot.security.handler.logout; + +import ice.spot.exeption.ErrorCode; +import ice.spot.security.info.AuthenticationResponse; +import ice.spot.util.CookieUtil; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Slf4j +@Component +public class CustomLogoutResultHandler implements LogoutSuccessHandler { + + @Value("${server.domain}") + private String domain; + + @Override + public void onLogoutSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) throws IOException, ServletException { + if (authentication == null) { + log.info("인증 정보가 존재하지 않습니다. authentication is null."); + AuthenticationResponse.makeFailureResponse(response, ErrorCode.NOT_FOUND_USER); + } + CookieUtil.logoutCookie(request, response, domain); + AuthenticationResponse.makeSuccessResponse(response); + } +} diff --git a/src/main/java/ice/spot/security/info/AuthenticationResponse.java b/src/main/java/ice/spot/security/info/AuthenticationResponse.java new file mode 100644 index 0000000..01d9076 --- /dev/null +++ b/src/main/java/ice/spot/security/info/AuthenticationResponse.java @@ -0,0 +1,72 @@ +package ice.spot.security.info; + +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.util.CookieUtil; +import jakarta.servlet.http.HttpServletResponse; +import net.minidev.json.JSONValue; +import org.springframework.http.HttpStatus; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class AuthenticationResponse { + + public static void makeLoginSuccessResponse( + HttpServletResponse response, + String domain, + JwtTokenDto jwtTokenDto, + Integer refreshExpiration + ) throws IOException { + CookieUtil.addCookie( + response, + domain, + Constants.ACCESS_COOKIE_NAME, + jwtTokenDto.accessToken() + ); + CookieUtil.addSecureCookie( + response, + domain, + Constants.REFRESH_COOKIE_NAME, + jwtTokenDto.refreshToken(), + refreshExpiration + ); + + makeSuccessResponse(response); + } + + public static void makeSuccessResponse( + HttpServletResponse response + ) throws IOException { + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + + response.setStatus(HttpStatus.OK.value()); + + Map body = new HashMap<>(); + body.put("success", "true"); + body.put("data", null); + body.put("error", null); + + response.getWriter().write(JSONValue.toJSONString(body)); + } + + public static void makeFailureResponse( + HttpServletResponse response, + ErrorCode errorCode + ) throws IOException { + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.setStatus(errorCode.getHttpStatus().value()); + + Map body= new HashMap<>(); + body.put("success", false); + body.put("data", null); + body.put("error", ExceptionDto.of(errorCode)); + + response.getWriter().write(JSONValue.toJSONString(body)); + } +} diff --git a/src/main/java/ice/spot/security/info/JwtUserInfo.java b/src/main/java/ice/spot/security/info/JwtUserInfo.java new file mode 100644 index 0000000..1a1b987 --- /dev/null +++ b/src/main/java/ice/spot/security/info/JwtUserInfo.java @@ -0,0 +1,6 @@ +package ice.spot.security.info; + +import ice.spot.dto.type.ERole; + +public record JwtUserInfo(Long userId, ERole role) { +} diff --git a/src/main/java/ice/spot/security/info/KakaoOauth2UserInfo.java b/src/main/java/ice/spot/security/info/KakaoOauth2UserInfo.java new file mode 100644 index 0000000..456eaf2 --- /dev/null +++ b/src/main/java/ice/spot/security/info/KakaoOauth2UserInfo.java @@ -0,0 +1,16 @@ +package ice.spot.security.info; + +import ice.spot.security.info.factory.Oauth2UserInfo; + +import java.util.Map; + +public class KakaoOauth2UserInfo extends Oauth2UserInfo { + public KakaoOauth2UserInfo(Map attributes) { + super(attributes); + } + + @Override + public String getId() { + return attributes.get("id").toString(); + } +} diff --git a/src/main/java/ice/spot/security/info/UserPrincipal.java b/src/main/java/ice/spot/security/info/UserPrincipal.java new file mode 100644 index 0000000..651376b --- /dev/null +++ b/src/main/java/ice/spot/security/info/UserPrincipal.java @@ -0,0 +1,99 @@ +package ice.spot.security.info; + +import ice.spot.dto.type.ERole; +import ice.spot.repository.UserRepository; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +@Getter +@Builder +@RequiredArgsConstructor +public class UserPrincipal implements UserDetails, OAuth2User { + + private final Long userId; + private final String password; + private final ERole role; + private final Map attributes; + private final Collection authorities; + + public static UserPrincipal create(UserRepository.UserSecurityForm securityForm) { + return UserPrincipal.builder() + .userId(securityForm.getId()) + .password(securityForm.getPassword()) + .role(securityForm.getRole()) + .attributes(Collections.emptyMap()) + .authorities(Collections.singleton( + new SimpleGrantedAuthority(securityForm.getRole().getSecurityRole())) + ) + .build(); + } + + public static UserPrincipal create( + UserRepository.UserSecurityForm securityForm, + Map attributes + ) { + return UserPrincipal.builder() + .userId(securityForm.getId()) + .password(securityForm.getPassword()) + .role(securityForm.getRole()) + .attributes(attributes) + .authorities(Collections.singleton( + new SimpleGrantedAuthority(securityForm.getRole().getSecurityRole())) + ) + .build(); + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public Collection getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.userId.toString(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public String getName() { + return userId.toString(); + } +} diff --git a/src/main/java/ice/spot/security/info/factory/Oauth2UserInfo.java b/src/main/java/ice/spot/security/info/factory/Oauth2UserInfo.java new file mode 100644 index 0000000..00c9389 --- /dev/null +++ b/src/main/java/ice/spot/security/info/factory/Oauth2UserInfo.java @@ -0,0 +1,13 @@ +package ice.spot.security.info.factory; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Map; + +@Getter +@RequiredArgsConstructor +public abstract class Oauth2UserInfo { + protected final Map attributes; + public abstract String getId(); +} diff --git a/src/main/java/ice/spot/security/info/factory/Oauth2UserInfoFactory.java b/src/main/java/ice/spot/security/info/factory/Oauth2UserInfoFactory.java new file mode 100644 index 0000000..b93358b --- /dev/null +++ b/src/main/java/ice/spot/security/info/factory/Oauth2UserInfoFactory.java @@ -0,0 +1,22 @@ +package ice.spot.security.info.factory; + +import ice.spot.dto.type.EProvider; +import ice.spot.security.info.KakaoOauth2UserInfo; + +import java.util.Map; + +public class Oauth2UserInfoFactory { + + public static Oauth2UserInfo getOauth2UserInfo( + EProvider provider, + Map attributes + ) { + Oauth2UserInfo ret; + switch (provider) { + case KAKAO -> ret = new KakaoOauth2UserInfo(attributes); + default -> throw new IllegalAccessError("잘못된 제공자입니다."); + } + + return ret; + } +} diff --git a/src/main/java/ice/spot/security/provider/JwtAuthenticationManager.java b/src/main/java/ice/spot/security/provider/JwtAuthenticationManager.java new file mode 100644 index 0000000..4ecc927 --- /dev/null +++ b/src/main/java/ice/spot/security/provider/JwtAuthenticationManager.java @@ -0,0 +1,22 @@ +package ice.spot.security.provider; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationManager implements AuthenticationManager { + + private final JwtAuthenticationProvider jwtAuthenticationProvider; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + log.info("AuthenticationManager 진입"); + return jwtAuthenticationProvider.authenticate(authentication); + } +} diff --git a/src/main/java/ice/spot/security/provider/JwtAuthenticationProvider.java b/src/main/java/ice/spot/security/provider/JwtAuthenticationProvider.java new file mode 100644 index 0000000..b07dddd --- /dev/null +++ b/src/main/java/ice/spot/security/provider/JwtAuthenticationProvider.java @@ -0,0 +1,59 @@ +package ice.spot.security.provider; + +import ice.spot.security.info.JwtUserInfo; +import ice.spot.security.info.UserPrincipal; +import ice.spot.security.service.CustomUserDetailService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationProvider implements AuthenticationProvider { + + private final BCryptPasswordEncoder bCryptPasswordEncoder; + private final CustomUserDetailService customUserDetailService; + + @Override + public Authentication authenticate( + Authentication authentication + ) throws AuthenticationException { + log.info("AuthenticationProvider 진입 성공"); + if (authentication.getPrincipal().getClass().equals(String.class)) { + // form login 요청인 경우 + log.info("로그인 로직 인증 과정"); + return authOfLogin(authentication); + } else { + log.info("로그인 한 사용자 검증 과정"); + return authOfAfterLogin((JwtUserInfo) authentication.getPrincipal()); + } + } + + private Authentication authOfLogin(Authentication authentication) { + // DB에 저장된 실제 데이터 + UserPrincipal userPrincipal = customUserDetailService + .loadUserByUsername(authentication.getPrincipal().toString()); + + // 비밀번호 검증 로직 + if (!bCryptPasswordEncoder.matches(authentication.getCredentials().toString(), userPrincipal.getPassword())) + throw new UsernameNotFoundException("비밀번호가 일치하지 않습니다 ! "); + return new UsernamePasswordAuthenticationToken(userPrincipal, null, userPrincipal.getAuthorities()); + } + + private Authentication authOfAfterLogin(JwtUserInfo userInfo){ + UserPrincipal userPrincipal = customUserDetailService.loadUserById(userInfo.userId()); + return new UsernamePasswordAuthenticationToken(userPrincipal, null, userPrincipal.getAuthorities()); + } + + @Override + public boolean supports(Class authentication) { + return authentication.equals(UsernamePasswordAuthenticationToken.class); + } +} diff --git a/src/main/java/ice/spot/security/service/CustomOauth2UserDetailService.java b/src/main/java/ice/spot/security/service/CustomOauth2UserDetailService.java new file mode 100644 index 0000000..1a684da --- /dev/null +++ b/src/main/java/ice/spot/security/service/CustomOauth2UserDetailService.java @@ -0,0 +1,64 @@ +package ice.spot.security.service; + +import ice.spot.domain.User; +import ice.spot.dto.type.EProvider; +import ice.spot.dto.type.ERole; +import ice.spot.repository.UserRepository; +import ice.spot.security.info.UserPrincipal; +import ice.spot.security.info.factory.Oauth2UserInfo; +import ice.spot.security.info.factory.Oauth2UserInfoFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CustomOauth2UserDetailService extends DefaultOAuth2UserService { + + private final UserRepository userRepository; + private final BCryptPasswordEncoder bCryptPasswordEncoder; + + @Override + public OAuth2User loadUser( + OAuth2UserRequest userRequest + ) throws OAuth2AuthenticationException { + // provider 가져오기 + EProvider provider = EProvider.valueOf( + userRequest.getClientRegistration().getRegistrationId().toUpperCase() + ); + log.info("oauth 제공자 정보 가져오기 성공, 제공자 = {}", provider); + // 사용자 정보 가져오기 + Oauth2UserInfo oauth2UserInfo = Oauth2UserInfoFactory + .getOauth2UserInfo(provider, super.loadUser(userRequest).getAttributes()); + log.info("oauth 사용자 정보 가져오기 성공"); + log.info("attributes = {}", oauth2UserInfo.getAttributes().toString()); + + UserRepository.UserSecurityForm securityForm = userRepository + .findUserSecurityFromBySerialId(oauth2UserInfo.getId()) + .orElseGet(() -> { + log.info("새로운 사용자 접근, 저장 로직 진입"); + User newUser = userRepository.save( + User.builder() + .serialId(oauth2UserInfo.getId()) + .password( + bCryptPasswordEncoder + .encode(UUID.randomUUID().toString()) + ) + .provider(provider) + .role(ERole.GUEST) + .build() + ); + return UserRepository.UserSecurityForm.invoke(newUser); + }); + log.info("oauth2 사용자 조회 성공"); + return UserPrincipal.create(securityForm, oauth2UserInfo.getAttributes()); + } +} diff --git a/src/main/java/ice/spot/security/service/CustomUserDetailService.java b/src/main/java/ice/spot/security/service/CustomUserDetailService.java new file mode 100644 index 0000000..91ff7f5 --- /dev/null +++ b/src/main/java/ice/spot/security/service/CustomUserDetailService.java @@ -0,0 +1,38 @@ +package ice.spot.security.service; + +import ice.spot.exeption.CommonException; +import ice.spot.exeption.ErrorCode; +import ice.spot.repository.UserRepository; +import ice.spot.security.info.UserPrincipal; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CustomUserDetailService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserPrincipal loadUserByUsername( + String username + ) throws UsernameNotFoundException { + UserRepository.UserSecurityForm userSecurityForm = userRepository + .findUserSecurityFromBySerialId(username) + .orElseThrow(() -> new UsernameNotFoundException("존재하지 않는 아이디입니다.")); + log.info(("아이디 기반 조회 성공")); + return UserPrincipal.create(userSecurityForm); + } + + public UserPrincipal loadUserById(Long id) { + UserRepository.UserSecurityForm userSecurityForm = userRepository.findUserSecurityFromById(id) + .orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_USER)); + log.info("user id 기반 조회 성공"); + + return UserPrincipal.create(userSecurityForm); + } +} diff --git a/src/main/java/ice/spot/service/AuthService.java b/src/main/java/ice/spot/service/AuthService.java new file mode 100644 index 0000000..c94a9a1 --- /dev/null +++ b/src/main/java/ice/spot/service/AuthService.java @@ -0,0 +1,34 @@ +package ice.spot.service; + +import ice.spot.annotation.UserId; +import ice.spot.domain.User; +import ice.spot.dto.global.ResponseDto; +import ice.spot.dto.request.OauthSignUpDto; +import ice.spot.exeption.CommonException; +import ice.spot.exeption.ErrorCode; +import ice.spot.repository.UserRepository; +import ice.spot.util.JwtUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuthService { + + private final JwtUtil jwtUtil; + private final UserRepository userRepository; + private final BCryptPasswordEncoder bCryptPasswordEncoder; + + @Transactional + public void signUp(Long userId, OauthSignUpDto oauthSignUpDto){ + User oauthUser = userRepository.findById(userId) + .orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_USER)); + + oauthUser.register(oauthSignUpDto.nickname()); + } + +} diff --git a/src/main/java/ice/spot/util/CookieUtil.java b/src/main/java/ice/spot/util/CookieUtil.java new file mode 100644 index 0000000..5bd314b --- /dev/null +++ b/src/main/java/ice/spot/util/CookieUtil.java @@ -0,0 +1,85 @@ +package ice.spot.util; + +import ice.spot.constant.Constants; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.ResponseCookie; + +public class CookieUtil { + + public static void addCookie( + HttpServletResponse response, + String domain, + String key, + String value + ) { + ResponseCookie cookie = ResponseCookie.from(key, value) + .path("/") + .domain(domain) + .httpOnly(false) + .secure(true) + .build(); + + response.addHeader("Set-Cookie", cookie.toString()); + } + + public static void logoutCookie( + HttpServletRequest request, + HttpServletResponse response, + String domain + ) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) + return; + + for (Cookie cookie : cookies) { + boolean isAccessCookie = cookie.getName().equals(Constants.ACCESS_COOKIE_NAME); + boolean isRefreshCookie = cookie.getName().equals(Constants.REFRESH_COOKIE_NAME); + + if (isAccessCookie || isRefreshCookie) { + ResponseCookie tempCookie = ResponseCookie.from(cookie.getName(), cookie.getValue()) + .path("/") + .domain(domain) + .secure(true) + .maxAge(0) + .httpOnly(isRefreshCookie) + .build(); + response.addHeader("Set-Cookie", tempCookie.toString()); + } + } + } + + public static void addSecureCookie( + HttpServletResponse response, + String domain, + String key, + String value, + Integer maxAge + ) { + Cookie cookie = new Cookie(key, value); + cookie.setPath("/"); + cookie.setDomain(domain); + cookie.setSecure(true); + cookie.setHttpOnly(true); + cookie.setMaxAge(maxAge); + response.addCookie(cookie); + } + + public static void deleteCookie( + HttpServletRequest request, + HttpServletResponse response, + String name + ) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) + return; + + for (Cookie cookie : cookies) + if (cookie.getName().equals(name)) { + cookie.setMaxAge(0); + cookie.setPath("/"); + response.addCookie(cookie); + } + } +} diff --git a/src/main/java/ice/spot/util/HeaderUtil.java b/src/main/java/ice/spot/util/HeaderUtil.java new file mode 100644 index 0000000..4484455 --- /dev/null +++ b/src/main/java/ice/spot/util/HeaderUtil.java @@ -0,0 +1,22 @@ +package ice.spot.util; + +import ice.spot.exeption.CommonException; +import ice.spot.exeption.ErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.util.StringUtils; + +import java.util.Optional; + +public class HeaderUtil { + + public static Optional refineHeader( + HttpServletRequest request, + String headerName, + String prefix + ) { + String headerValue = request.getHeader(headerName); + if (!StringUtils.hasText(headerValue) || !headerValue.startsWith(prefix)) + throw new CommonException(ErrorCode.INVALID_HEADER_VALUE); + return Optional.of(headerValue.substring(prefix.length())); + } +} diff --git a/src/main/java/ice/spot/util/JwtUtil.java b/src/main/java/ice/spot/util/JwtUtil.java new file mode 100644 index 0000000..13bc692 --- /dev/null +++ b/src/main/java/ice/spot/util/JwtUtil.java @@ -0,0 +1,70 @@ +package ice.spot.util; + +import ice.spot.constant.Constants; +import ice.spot.dto.response.JwtTokenDto; +import ice.spot.dto.type.ERole; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.Getter; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; + +@Component +public class JwtUtil implements InitializingBean { + + @Value("${jwt.secret}") + private String secretKey; + + @Value("${jwt.access-token.expiration}") + @Getter + private Integer accessExpiration; + + @Value("${jwt.refresh-token.expiration}") + @Getter + private Integer refreshExpiration; + + private Key key; + + @Override + public void afterPropertiesSet() throws Exception { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + public Claims validateToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public String generateToken(Long id, ERole role, Integer expiration) { + Claims claims = Jwts.claims(); + claims.put(Constants.CLAIM_USER_ID, id); + if (role != null) + claims.put(Constants.CLAIM_USER_ROLE, role); + + return Jwts.builder() + .setHeaderParam(Header.JWT_TYPE, Header.JWT_TYPE) + .setClaims(claims) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(key) + .compact(); + } + + public JwtTokenDto generateTokens(Long id, ERole role) { + return JwtTokenDto.of( + generateToken(id, role, accessExpiration), + generateToken(id, role, refreshExpiration) + ); + } +} From 83b0ab573d550a33ad7d7f2c7a56a8e5c002fda9 Mon Sep 17 00:00:00 2001 From: JeongHeumChoi Date: Sun, 5 May 2024 23:20:17 +0900 Subject: [PATCH 06/27] =?UTF-8?q?Feat:=20Spring=20Security,=20JWT=20Cookie?= =?UTF-8?q?,=20=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ice/spot/domain/User.java | 33 +++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/main/java/ice/spot/domain/User.java b/src/main/java/ice/spot/domain/User.java index 34e3c95..5c66be8 100644 --- a/src/main/java/ice/spot/domain/User.java +++ b/src/main/java/ice/spot/domain/User.java @@ -1,9 +1,13 @@ package ice.spot.domain; +import ice.spot.dto.type.EProvider; +import ice.spot.dto.type.ERole; import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicUpdate; import java.time.LocalDate; import java.util.ArrayList; @@ -11,8 +15,9 @@ @Entity @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "user") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@DynamicUpdate public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -31,17 +36,41 @@ public class User { @Column(name = "point") private Long point; + @Column(name = "role", nullable = false) + @Enumerated(EnumType.STRING) + private ERole role; + + @Column(name = "provider", nullable = false) + @Enumerated(EnumType.STRING) + private EProvider provider; + @Column(name = "created_at") private LocalDate createdAt; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List boardingRecords = new ArrayList<>(); - public User(String serialId, String password, String nickname, Long point) { + @Column(name = "refresh_token") + private String refreshToken; + + @Builder + public User(String serialId, String password, String nickname, ERole role, EProvider provider, Long point) { this.serialId = serialId; this.password = password; this.nickname = nickname; + this.role = role; + this.provider = provider; this.point = point; this.createdAt = LocalDate.now(); } + + public void register(String nickname) { + this.nickname = nickname; + this.createdAt = LocalDate.now(); + this.role = ERole.USER; + } + + public void updateRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } } From f29ef023792568479354c42ce9ada60207c9bdd3 Mon Sep 17 00:00:00 2001 From: JeongHeumChoi Date: Sun, 5 May 2024 23:20:31 +0900 Subject: [PATCH 07/27] =?UTF-8?q?Fix:=20=EC=BD=94=EB=93=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ice/spot/exeption/CommonException.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ice/spot/exeption/CommonException.java b/src/main/java/ice/spot/exeption/CommonException.java index c735e2b..427c4c6 100644 --- a/src/main/java/ice/spot/exeption/CommonException.java +++ b/src/main/java/ice/spot/exeption/CommonException.java @@ -7,4 +7,5 @@ @RequiredArgsConstructor public class CommonException extends RuntimeException { private final ErrorCode errorCode; + public String getMessage() { return this.errorCode.getMessage(); } } From 31f231e10d7afd1903c177065184cfc88e11830a Mon Sep 17 00:00:00 2001 From: JeongHeumChoi Date: Sun, 5 May 2024 23:21:16 +0900 Subject: [PATCH 08/27] =?UTF-8?q?Refactor:=20=EC=95=84=EC=A7=81=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ice/spot/service/AuthService.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/ice/spot/service/AuthService.java b/src/main/java/ice/spot/service/AuthService.java index c94a9a1..0483a9e 100644 --- a/src/main/java/ice/spot/service/AuthService.java +++ b/src/main/java/ice/spot/service/AuthService.java @@ -1,16 +1,12 @@ package ice.spot.service; -import ice.spot.annotation.UserId; import ice.spot.domain.User; -import ice.spot.dto.global.ResponseDto; import ice.spot.dto.request.OauthSignUpDto; import ice.spot.exeption.CommonException; import ice.spot.exeption.ErrorCode; import ice.spot.repository.UserRepository; -import ice.spot.util.JwtUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,9 +15,7 @@ @RequiredArgsConstructor public class AuthService { - private final JwtUtil jwtUtil; private final UserRepository userRepository; - private final BCryptPasswordEncoder bCryptPasswordEncoder; @Transactional public void signUp(Long userId, OauthSignUpDto oauthSignUpDto){ From 4810467b1a386db82a79076b90a8b31571f579c6 Mon Sep 17 00:00:00 2001 From: JeongHeumChoi Date: Mon, 6 May 2024 16:03:43 +0900 Subject: [PATCH 09/27] =?UTF-8?q?Feat:=20oauth2=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index ca94c48..5d24f36 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,7 @@ dependencies { // spring security implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // spring boot implementation 'org.springframework.boot:spring-boot-starter-web' From bdc6b6d600f6329109af69df35cca282c18e50f4 Mon Sep 17 00:00:00 2001 From: JeongHeumChoi Date: Mon, 6 May 2024 16:03:59 +0900 Subject: [PATCH 10/27] =?UTF-8?q?Chore:=20Credentials=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spot-server-properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spot-server-properties b/spot-server-properties index 6bf7ffc..88515af 160000 --- a/spot-server-properties +++ b/spot-server-properties @@ -1 +1 @@ -Subproject commit 6bf7ffcb4dcbb7e8d1797c2b1df3d77944593e89 +Subproject commit 88515afcdc708913af815f585bce9affa47980ab From e1e1dc6fb34e32896638ba7edc96036a494a077a Mon Sep 17 00:00:00 2001 From: JeongHeumChoi <79458446+JeongHeumChoi@users.noreply.github.com> Date: Mon, 6 May 2024 20:10:49 +0900 Subject: [PATCH 11/27] =?UTF-8?q?=E2=9C=A8=20[Feature]=20-=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EB=82=A0=EC=A7=9C=20=EC=84=A4=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=EC=9C=A0=EC=A0=80=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: 스프링 서버 시간 한국으로 설정 * Feat: 사용자 닉네임 지정 로직 추가 * Fix: 초기 포인트 설정 --- src/main/java/ice/spot/SpotApplication.java | 8 ++++++++ src/main/java/ice/spot/domain/User.java | 5 +++-- .../java/ice/spot/security/info/KakaoOauth2UserInfo.java | 5 +++++ .../ice/spot/security/info/factory/Oauth2UserInfo.java | 1 + .../security/service/CustomOauth2UserDetailService.java | 3 ++- 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/main/java/ice/spot/SpotApplication.java b/src/main/java/ice/spot/SpotApplication.java index 8ba15cb..cb29d3d 100644 --- a/src/main/java/ice/spot/SpotApplication.java +++ b/src/main/java/ice/spot/SpotApplication.java @@ -1,11 +1,19 @@ package ice.spot; +import jakarta.annotation.PostConstruct; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import java.util.TimeZone; + @SpringBootApplication public class SpotApplication { + @PostConstruct + public void init() { + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); + } + public static void main(String[] args) { SpringApplication.run(SpotApplication.class, args); } diff --git a/src/main/java/ice/spot/domain/User.java b/src/main/java/ice/spot/domain/User.java index 5c66be8..9e32a50 100644 --- a/src/main/java/ice/spot/domain/User.java +++ b/src/main/java/ice/spot/domain/User.java @@ -54,13 +54,13 @@ public class User { private String refreshToken; @Builder - public User(String serialId, String password, String nickname, ERole role, EProvider provider, Long point) { + public User(String serialId, String password, String nickname, ERole role, EProvider provider) { this.serialId = serialId; this.password = password; this.nickname = nickname; this.role = role; this.provider = provider; - this.point = point; + this.point = 0L; this.createdAt = LocalDate.now(); } @@ -68,6 +68,7 @@ public void register(String nickname) { this.nickname = nickname; this.createdAt = LocalDate.now(); this.role = ERole.USER; + this.point = 0L; } public void updateRefreshToken(String refreshToken) { diff --git a/src/main/java/ice/spot/security/info/KakaoOauth2UserInfo.java b/src/main/java/ice/spot/security/info/KakaoOauth2UserInfo.java index 456eaf2..cc1f656 100644 --- a/src/main/java/ice/spot/security/info/KakaoOauth2UserInfo.java +++ b/src/main/java/ice/spot/security/info/KakaoOauth2UserInfo.java @@ -13,4 +13,9 @@ public KakaoOauth2UserInfo(Map attributes) { public String getId() { return attributes.get("id").toString(); } + + @Override + public String getNickname() { + return attributes.get("nickname").toString(); + } } diff --git a/src/main/java/ice/spot/security/info/factory/Oauth2UserInfo.java b/src/main/java/ice/spot/security/info/factory/Oauth2UserInfo.java index 00c9389..187000d 100644 --- a/src/main/java/ice/spot/security/info/factory/Oauth2UserInfo.java +++ b/src/main/java/ice/spot/security/info/factory/Oauth2UserInfo.java @@ -10,4 +10,5 @@ public abstract class Oauth2UserInfo { protected final Map attributes; public abstract String getId(); + public abstract String getNickname(); } diff --git a/src/main/java/ice/spot/security/service/CustomOauth2UserDetailService.java b/src/main/java/ice/spot/security/service/CustomOauth2UserDetailService.java index 1a684da..9736397 100644 --- a/src/main/java/ice/spot/security/service/CustomOauth2UserDetailService.java +++ b/src/main/java/ice/spot/security/service/CustomOauth2UserDetailService.java @@ -52,8 +52,9 @@ public OAuth2User loadUser( bCryptPasswordEncoder .encode(UUID.randomUUID().toString()) ) + .nickname(oauth2UserInfo.getNickname()) .provider(provider) - .role(ERole.GUEST) + .role(ERole.USER) .build() ); return UserRepository.UserSecurityForm.invoke(newUser); From a681714e775cb2a558880e72d8f48258d31f848b Mon Sep 17 00:00:00 2001 From: JeongHeumChoi <79458446+JeongHeumChoi@users.noreply.github.com> Date: Mon, 6 May 2024 20:57:35 +0900 Subject: [PATCH 12/27] =?UTF-8?q?Fix:=20nickname=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ice/spot/security/info/KakaoOauth2UserInfo.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/ice/spot/security/info/KakaoOauth2UserInfo.java b/src/main/java/ice/spot/security/info/KakaoOauth2UserInfo.java index cc1f656..51fd67f 100644 --- a/src/main/java/ice/spot/security/info/KakaoOauth2UserInfo.java +++ b/src/main/java/ice/spot/security/info/KakaoOauth2UserInfo.java @@ -16,6 +16,7 @@ public String getId() { @Override public String getNickname() { - return attributes.get("nickname").toString(); + Map properties = (Map) attributes.get("properties"); + return properties.get("nickname").toString(); } } From 5895a7705cb6f0a4458199e5191a5deea33fa0fc Mon Sep 17 00:00:00 2001 From: Jang99u Date: Mon, 6 May 2024 22:36:48 +0900 Subject: [PATCH 13/27] =?UTF-8?q?Fix:=20BoardingRecord=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ice/spot/domain/BoardingRecord.java | 43 ++++++------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/src/main/java/ice/spot/domain/BoardingRecord.java b/src/main/java/ice/spot/domain/BoardingRecord.java index b46aa20..8667ca0 100644 --- a/src/main/java/ice/spot/domain/BoardingRecord.java +++ b/src/main/java/ice/spot/domain/BoardingRecord.java @@ -2,11 +2,10 @@ import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDate; - @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -17,26 +16,11 @@ public class BoardingRecord { @Column(name = "id") private Long id; - @Column(name = "image_url") - private String imageUrl; - - @Column(name = "depart_at") - private LocalDate departAt; - - @Column(name = "arrive_at") - private LocalDate arriveAt; - - @Column(name = "depart_lat") - private Double departLat; + @Column(name = "distance") + private Double distance; - @Column(name = "depart_lon") - private Double departLon; - - @Column(name = "arrive_lat") - private Double arriveLat; - - @Column(name = "arrive_lon") - private Double arriveLon; + @Column(name = "time") + private Integer time; @ManyToOne @JoinColumn(name = "user_id") @@ -46,15 +30,16 @@ public class BoardingRecord { @JoinColumn(name = "parking_lot_id", referencedColumnName = "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", referencedColumnName = "id") + private Image image; + + @Builder + public BoardingRecord(Double distance, Integer time, User user, ParkingLot parkingLot, Image image) { + this.distance = distance; + this.time = time; this.user = user; this.parkingLot = parkingLot; + this.image = image; } } From 2facd1e5a5897125eefaf727ef0cfc576dbfed31 Mon Sep 17 00:00:00 2001 From: Jang99u Date: Tue, 7 May 2024 00:20:32 +0900 Subject: [PATCH 14/27] =?UTF-8?q?Feat:=20=EB=94=94=ED=8E=9C=EB=8D=98?= =?UTF-8?q?=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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) { From d25eb02a01c30721e931c7d840fcdfc8144fd49a Mon Sep 17 00:00:00 2001 From: Jang99u Date: Tue, 7 May 2024 00:20:47 +0900 Subject: [PATCH 15/27] =?UTF-8?q?Feat:=20ErrorCode=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ice/spot/exeption/ErrorCode.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/ice/spot/exeption/ErrorCode.java b/src/main/java/ice/spot/exeption/ErrorCode.java index 3eda73b..2df9bd6 100644 --- a/src/main/java/ice/spot/exeption/ErrorCode.java +++ b/src/main/java/ice/spot/exeption/ErrorCode.java @@ -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, "올바르지 않은 헤더값입니다."), From a8e20f02de1d74df984cd6c13175a25a29f5d4f3 Mon Sep 17 00:00:00 2001 From: Jang99u Date: Tue, 7 May 2024 00:26:32 +0900 Subject: [PATCH 16/27] =?UTF-8?q?Feat:=20Image=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ice/spot/domain/Image.java | 51 ++++++++ .../dto/image/request/ImageCheckRequest.java | 6 + .../dto/image/request/ImageSaveRequest.java | 13 ++ .../dto/image/response/ImageResponse.java | 16 +++ .../ice/spot/repository/ImageRepository.java | 7 ++ .../java/ice/spot/service/ImageService.java | 111 ++++++++++++++++++ 6 files changed, 204 insertions(+) create mode 100644 src/main/java/ice/spot/domain/Image.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/repository/ImageRepository.java create mode 100644 src/main/java/ice/spot/service/ImageService.java 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..3c23ab9 --- /dev/null +++ b/src/main/java/ice/spot/domain/Image.java @@ -0,0 +1,51 @@ +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) + 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/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/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/service/ImageService.java b/src/main/java/ice/spot/service/ImageService.java new file mode 100644 index 0000000..aa1d228 --- /dev/null +++ b/src/main/java/ice/spot/service/ImageService.java @@ -0,0 +1,111 @@ +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.ParkingLotResult; +import ice.spot.dto.image.request.ImageCheckRequest; +import ice.spot.dto.image.response.ImageResponse; +import ice.spot.dto.image.request.ImageSaveRequest; +import ice.spot.exeption.CommonException; +import ice.spot.exeption.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(ImageSaveRequest imageSaveRequest) { + MultipartFile multipartFile = imageSaveRequest.getImage(); + String originalName = multipartFile.getOriginalFilename(); + Image image = new Image(originalName); + String filename = image.getStoredName(); + + try { + 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); + } catch(IOException e) { + + } + + 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(); + + log.info("태그이름"); + log.info(imageResponse.getPredictions().get(0).tagName()); + + 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; + } + } + + @Transactional + public void deleteImage() { + Image image = imageRepository.findById(2L).orElseThrow(); + imageRepository.delete(image); + } +} \ No newline at end of file From 74a58ee1e60c71c0e77d10ba9c5641b2d5a3446f Mon Sep 17 00:00:00 2001 From: Jang99u Date: Tue, 7 May 2024 00:27:00 +0900 Subject: [PATCH 17/27] =?UTF-8?q?Feat:=20WebClientConfig=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ice/spot/config/WebClientConfig.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/java/ice/spot/config/WebClientConfig.java 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(); + } +} From d342989e8fcc101315d5618bd68614413ab3e6a6 Mon Sep 17 00:00:00 2001 From: Jang99u Date: Tue, 7 May 2024 00:27:07 +0900 Subject: [PATCH 18/27] =?UTF-8?q?Feat:=20S3ClientConfig=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/ice/spot/config/S3Config.java | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/ice/spot/config/S3Config.java 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 From 865eadc9cadf63a0dccea0b87601ca8ee5a8c770 Mon Sep 17 00:00:00 2001 From: Jang99u Date: Tue, 7 May 2024 00:27:30 +0900 Subject: [PATCH 19/27] =?UTF-8?q?Feat:=20BoardingRecord=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/BoardingRecordController.java | 25 ++++++++++++++++ .../request/BoardingRecordRequest.java | 11 +++++++ .../response/BoardingRecordResponse.java | 29 +++++++++++++++++++ .../repository/BoardingRecordRepository.java | 7 +++++ .../spot/service/BoardingRecordService.java | 29 +++++++++++++++++++ 5 files changed, 101 insertions(+) create mode 100644 src/main/java/ice/spot/controller/BoardingRecordController.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/BoardingRecordResponse.java create mode 100644 src/main/java/ice/spot/repository/BoardingRecordRepository.java create mode 100644 src/main/java/ice/spot/service/BoardingRecordService.java 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..5c9ca4d --- /dev/null +++ b/src/main/java/ice/spot/controller/BoardingRecordController.java @@ -0,0 +1,25 @@ +package ice.spot.controller; + +import ice.spot.dto.boardingrecord.request.BoardingRecordRequest; +import ice.spot.dto.global.ResponseDto; +import ice.spot.dto.image.request.ImageSaveRequest; +import ice.spot.service.ImageService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class BoardingRecordController { + + private final ImageService imageService; + + @PostMapping("/boarding-record") + public ResponseDto saveBoardingRecord( + @ModelAttribute ImageSaveRequest imageSaveRequest, @RequestBody BoardingRecordRequest boardingRecordRequest + ) { + Long imageId = imageService.saveImage(imageSaveRequest); + + return null; + } +} 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..f30194e --- /dev/null +++ b/src/main/java/ice/spot/dto/boardingrecord/request/BoardingRecordRequest.java @@ -0,0 +1,11 @@ +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/BoardingRecordResponse.java b/src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordResponse.java new file mode 100644 index 0000000..e816e47 --- /dev/null +++ b/src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordResponse.java @@ -0,0 +1,29 @@ +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, + + @JsonProperty("is_parking_lot") + Boolean isParkingLot +) { + public static BoardingRecordResponse of(String createdAt, String image, Double distance, Integer time, Integer point, Boolean isParkingLot) { + return new BoardingRecordResponse(createdAt, image, distance, time, point, isParkingLot); + } +} 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/service/BoardingRecordService.java b/src/main/java/ice/spot/service/BoardingRecordService.java new file mode 100644 index 0000000..d70cf24 --- /dev/null +++ b/src/main/java/ice/spot/service/BoardingRecordService.java @@ -0,0 +1,29 @@ +package ice.spot.service; + +import ice.spot.dto.boardingrecord.response.BoardingRecordResponse; +import ice.spot.repository.BoardingRecordRepository; +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 WebClient webClient; + + @Transactional + public Boolean saveBoardingRecord() { + return null; + } + + @Transactional(readOnly = true) + public List boardingRecordList(Long userId) { + return null; + } +} From 328b74a8bfa01905795619f5ca438598ae912641 Mon Sep 17 00:00:00 2001 From: Jang99u Date: Tue, 7 May 2024 00:28:12 +0900 Subject: [PATCH 20/27] =?UTF-8?q?Feat:=20=EC=A3=BC=EC=B0=A8=EC=9E=A5=20?= =?UTF-8?q?=EC=98=88=EC=B8=A1=20=EA=B2=B0=EA=B3=BC=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/ice/spot/domain/ParkingLotResult.java | 17 +++++++++++++++++ .../dto/image/response/PredictionResponse.java | 14 ++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 src/main/java/ice/spot/domain/ParkingLotResult.java create mode 100644 src/main/java/ice/spot/dto/image/response/PredictionResponse.java diff --git a/src/main/java/ice/spot/domain/ParkingLotResult.java b/src/main/java/ice/spot/domain/ParkingLotResult.java new file mode 100644 index 0000000..3c299a2 --- /dev/null +++ b/src/main/java/ice/spot/domain/ParkingLotResult.java @@ -0,0 +1,17 @@ +package ice.spot.domain; + +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/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 +) { +} From 13712d23773882bec7c470720712c67c63d64716 Mon Sep 17 00:00:00 2001 From: Jang99u Date: Tue, 7 May 2024 01:36:29 +0900 Subject: [PATCH 21/27] =?UTF-8?q?Feat:=20=ED=83=91=EC=8A=B9=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=A0=80=EC=9E=A5=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/BoardingRecordController.java | 16 ++++++--- .../request/BoardingRecordRequest.java | 1 - .../spot/service/BoardingRecordService.java | 36 +++++++++++++++++-- .../java/ice/spot/service/ImageService.java | 7 ++-- 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/main/java/ice/spot/controller/BoardingRecordController.java b/src/main/java/ice/spot/controller/BoardingRecordController.java index 5c9ca4d..03e3a1b 100644 --- a/src/main/java/ice/spot/controller/BoardingRecordController.java +++ b/src/main/java/ice/spot/controller/BoardingRecordController.java @@ -1,25 +1,33 @@ 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.dto.image.request.ImageSaveRequest; +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; +@Slf4j @RestController @RequiredArgsConstructor @RequestMapping("/api") public class BoardingRecordController { private final ImageService imageService; + private final BoardingRecordService boardingRecordService; @PostMapping("/boarding-record") public ResponseDto saveBoardingRecord( - @ModelAttribute ImageSaveRequest imageSaveRequest, @RequestBody BoardingRecordRequest boardingRecordRequest + @UserId Long userId, + @RequestPart(value = "image", required = false) MultipartFile multipartFile, + @RequestPart(value = "dto") BoardingRecordRequest boardingRecordRequest ) { - Long imageId = imageService.saveImage(imageSaveRequest); + Long imageId = imageService.saveImage(multipartFile); - return null; + return ResponseDto.created(boardingRecordService + .saveBoardingRecord(userId, imageId, boardingRecordRequest)); } } diff --git a/src/main/java/ice/spot/dto/boardingrecord/request/BoardingRecordRequest.java b/src/main/java/ice/spot/dto/boardingrecord/request/BoardingRecordRequest.java index f30194e..cd35784 100644 --- a/src/main/java/ice/spot/dto/boardingrecord/request/BoardingRecordRequest.java +++ b/src/main/java/ice/spot/dto/boardingrecord/request/BoardingRecordRequest.java @@ -5,7 +5,6 @@ @Builder public record BoardingRecordRequest( Double distance, - Integer time ) { } diff --git a/src/main/java/ice/spot/service/BoardingRecordService.java b/src/main/java/ice/spot/service/BoardingRecordService.java index d70cf24..834de05 100644 --- a/src/main/java/ice/spot/service/BoardingRecordService.java +++ b/src/main/java/ice/spot/service/BoardingRecordService.java @@ -1,13 +1,26 @@ package ice.spot.service; +import ice.spot.annotation.UserId; +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.BoardingRecordResponse; +import ice.spot.exeption.CommonException; +import ice.spot.exeption.ErrorCode; import ice.spot.repository.BoardingRecordRepository; +import ice.spot.repository.ImageRepository; +import ice.spot.repository.ParkingLotRepository; +import ice.spot.repository.UserRepository; +import ice.spot.security.service.CustomOauth2UserDetailService; 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.IllformedLocaleException; import java.util.List; @Slf4j @@ -15,11 +28,30 @@ @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() { - return null; + 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()) + .user(user) + .image(image) + .parkingLot(parkingLot) + .build()); + + return Boolean.TRUE; } @Transactional(readOnly = true) diff --git a/src/main/java/ice/spot/service/ImageService.java b/src/main/java/ice/spot/service/ImageService.java index aa1d228..4a9df83 100644 --- a/src/main/java/ice/spot/service/ImageService.java +++ b/src/main/java/ice/spot/service/ImageService.java @@ -35,8 +35,8 @@ public class ImageService { private final WebClient webClient; @Transactional - public Long saveImage(ImageSaveRequest imageSaveRequest) { - MultipartFile multipartFile = imageSaveRequest.getImage(); + public Long saveImage(MultipartFile multipartFile) { + String originalName = multipartFile.getOriginalFilename(); Image image = new Image(originalName); String filename = image.getStoredName(); @@ -88,9 +88,6 @@ public ParkingLotResult checkImage(ImageCheckRequest imageCheckRequest) { .bodyToMono(ImageResponse.class); ImageResponse imageResponse = responseMono.block(); - log.info("태그이름"); - log.info(imageResponse.getPredictions().get(0).tagName()); - String hisgestProbabilityTag = imageResponse.getPredictions().get(0).tagName(); String secondProbabilityTag = imageResponse.getPredictions().get(1).tagName(); From e0ee0046c849872d91c3afb822378d3c3d184a99 Mon Sep 17 00:00:00 2001 From: Jang99u Date: Tue, 7 May 2024 19:00:11 +0900 Subject: [PATCH 22/27] =?UTF-8?q?Fix:=20=EB=94=94=EB=A0=89=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/BoardingRecordController.java | 6 ++-- .../java/ice/spot/domain/BoardingRecord.java | 4 +-- src/main/java/ice/spot/domain/Image.java | 1 + .../domain/{ => type}/ParkingLotResult.java | 2 +- .../ice/spot/dto/global/ExceptionDto.java | 2 +- .../java/ice/spot/dto/global/ResponseDto.java | 4 +-- .../CommonException.java | 2 +- .../{exeption => exception}/ErrorCode.java | 2 +- .../GlobalExceptionHandler.java | 2 +- .../pre/UserIdArgumentResolver.java | 4 +-- .../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 | 7 ++--- .../java/ice/spot/service/ImageService.java | 31 ++++++------------- src/main/java/ice/spot/util/HeaderUtil.java | 4 +-- 22 files changed, 43 insertions(+), 54 deletions(-) rename src/main/java/ice/spot/domain/{ => type}/ParkingLotResult.java (93%) rename src/main/java/ice/spot/{exeption => exception}/CommonException.java (90%) rename src/main/java/ice/spot/{exeption => exception}/ErrorCode.java (98%) rename src/main/java/ice/spot/{exeption => exception}/GlobalExceptionHandler.java (98%) diff --git a/src/main/java/ice/spot/controller/BoardingRecordController.java b/src/main/java/ice/spot/controller/BoardingRecordController.java index 03e3a1b..82c107a 100644 --- a/src/main/java/ice/spot/controller/BoardingRecordController.java +++ b/src/main/java/ice/spot/controller/BoardingRecordController.java @@ -10,6 +10,8 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; + @Slf4j @RestController @RequiredArgsConstructor @@ -20,11 +22,11 @@ public class BoardingRecordController { private final BoardingRecordService boardingRecordService; @PostMapping("/boarding-record") - public ResponseDto saveBoardingRecord( + 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 diff --git a/src/main/java/ice/spot/domain/BoardingRecord.java b/src/main/java/ice/spot/domain/BoardingRecord.java index 8667ca0..10346e0 100644 --- a/src/main/java/ice/spot/domain/BoardingRecord.java +++ b/src/main/java/ice/spot/domain/BoardingRecord.java @@ -27,11 +27,11 @@ public class BoardingRecord { private User user; @OneToOne - @JoinColumn(name = "parking_lot_id", referencedColumnName = "id") + @JoinColumn(name = "parking_lot_id") private ParkingLot parkingLot; @OneToOne - @JoinColumn(name = "image_id", referencedColumnName = "id") + @JoinColumn(name = "image_id") private Image image; @Builder diff --git a/src/main/java/ice/spot/domain/Image.java b/src/main/java/ice/spot/domain/Image.java index 3c23ab9..71c246e 100644 --- a/src/main/java/ice/spot/domain/Image.java +++ b/src/main/java/ice/spot/domain/Image.java @@ -14,6 +14,7 @@ public class Image { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") private Long id; @Column(name = "origin_name") diff --git a/src/main/java/ice/spot/domain/ParkingLotResult.java b/src/main/java/ice/spot/domain/type/ParkingLotResult.java similarity index 93% rename from src/main/java/ice/spot/domain/ParkingLotResult.java rename to src/main/java/ice/spot/domain/type/ParkingLotResult.java index 3c299a2..ec48743 100644 --- a/src/main/java/ice/spot/domain/ParkingLotResult.java +++ b/src/main/java/ice/spot/domain/type/ParkingLotResult.java @@ -1,4 +1,4 @@ -package ice.spot.domain; +package ice.spot.domain.type; import lombok.AllArgsConstructor; import lombok.Getter; 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/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 98% rename from src/main/java/ice/spot/exeption/ErrorCode.java rename to src/main/java/ice/spot/exception/ErrorCode.java index 2df9bd6..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; 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/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 index 834de05..268f65f 100644 --- a/src/main/java/ice/spot/service/BoardingRecordService.java +++ b/src/main/java/ice/spot/service/BoardingRecordService.java @@ -1,26 +1,23 @@ package ice.spot.service; -import ice.spot.annotation.UserId; 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.BoardingRecordResponse; -import ice.spot.exeption.CommonException; -import ice.spot.exeption.ErrorCode; +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 ice.spot.security.service.CustomOauth2UserDetailService; 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.IllformedLocaleException; import java.util.List; @Slf4j diff --git a/src/main/java/ice/spot/service/ImageService.java b/src/main/java/ice/spot/service/ImageService.java index 4a9df83..ea6ad90 100644 --- a/src/main/java/ice/spot/service/ImageService.java +++ b/src/main/java/ice/spot/service/ImageService.java @@ -4,12 +4,11 @@ import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.ObjectMetadata; import ice.spot.domain.Image; -import ice.spot.domain.ParkingLotResult; +import ice.spot.domain.type.ParkingLotResult; import ice.spot.dto.image.request.ImageCheckRequest; import ice.spot.dto.image.response.ImageResponse; -import ice.spot.dto.image.request.ImageSaveRequest; -import ice.spot.exeption.CommonException; -import ice.spot.exeption.ErrorCode; +import ice.spot.exception.CommonException; +import ice.spot.exception.ErrorCode; import ice.spot.repository.ImageRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -35,24 +34,20 @@ public class ImageService { private final WebClient webClient; @Transactional - public Long saveImage(MultipartFile multipartFile) { + public Long saveImage(MultipartFile multipartFile) throws IOException { String originalName = multipartFile.getOriginalFilename(); Image image = new Image(originalName); String filename = image.getStoredName(); - try { - ObjectMetadata objectMetadata = new ObjectMetadata(); - objectMetadata.setContentType(multipartFile.getContentType()); - objectMetadata.setContentLength(multipartFile.getInputStream().available()); - - amazonS3Client.putObject(bucketName, filename, multipartFile.getInputStream(), objectMetadata); + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(multipartFile.getContentType()); + objectMetadata.setContentLength(multipartFile.getInputStream().available()); - String accessUrl = amazonS3Client.getUrl(bucketName, filename).toString(); - image.setAccessUrl(accessUrl); - } catch(IOException e) { + 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) { @@ -99,10 +94,4 @@ public ParkingLotResult checkImage(ImageCheckRequest imageCheckRequest) { return ParkingLotResult.NOT_FOUND_KICKBOARD; } } - - @Transactional - public void deleteImage() { - Image image = imageRepository.findById(2L).orElseThrow(); - imageRepository.delete(image); - } } \ No newline at end of file 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; From 193d9a7c2e3411377a3f08bb0a5af9815d9bce6d Mon Sep 17 00:00:00 2001 From: Jang99u Date: Tue, 7 May 2024 22:17:59 +0900 Subject: [PATCH 23/27] =?UTF-8?q?Feat:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=84=B0=EB=A7=81=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/BoardingRecordController.java | 5 ++++ .../java/ice/spot/domain/BoardingRecord.java | 14 +++++++-- src/main/java/ice/spot/domain/ParkingLot.java | 7 +++-- src/main/java/ice/spot/domain/User.java | 4 +++ .../response/BoardingRecordListResponse.java | 14 +++++++++ .../response/BoardingRecordResponse.java | 9 ++---- .../dto/user/response/PersonResponse.java | 11 +++++++ .../spot/service/BoardingRecordService.java | 29 +++++++++++++++++-- 8 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordListResponse.java create mode 100644 src/main/java/ice/spot/dto/user/response/PersonResponse.java diff --git a/src/main/java/ice/spot/controller/BoardingRecordController.java b/src/main/java/ice/spot/controller/BoardingRecordController.java index 82c107a..28bfc37 100644 --- a/src/main/java/ice/spot/controller/BoardingRecordController.java +++ b/src/main/java/ice/spot/controller/BoardingRecordController.java @@ -32,4 +32,9 @@ public ResponseDto saveBoardingRecord ( 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/domain/BoardingRecord.java b/src/main/java/ice/spot/domain/BoardingRecord.java index 10346e0..33abcd8 100644 --- a/src/main/java/ice/spot/domain/BoardingRecord.java +++ b/src/main/java/ice/spot/domain/BoardingRecord.java @@ -6,6 +6,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -22,11 +24,17 @@ public class BoardingRecord { @Column(name = "time") private Integer time; + @Column(name = "point") + private Integer point; + + @Column(name = "createdAt") + private LocalDateTime createdAt; + @ManyToOne @JoinColumn(name = "user_id") private User user; - @OneToOne + @ManyToOne @JoinColumn(name = "parking_lot_id") private ParkingLot parkingLot; @@ -35,9 +43,11 @@ public class BoardingRecord { private Image image; @Builder - public BoardingRecord(Double distance, Integer time, User user, ParkingLot parkingLot, Image image) { + 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/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/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 index e816e47..08eea96 100644 --- a/src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordResponse.java +++ b/src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordResponse.java @@ -18,12 +18,9 @@ public record BoardingRecordResponse( Integer time, @JsonProperty("point") - Integer point, - - @JsonProperty("is_parking_lot") - Boolean isParkingLot + Integer point ) { - public static BoardingRecordResponse of(String createdAt, String image, Double distance, Integer time, Integer point, Boolean isParkingLot) { - return new BoardingRecordResponse(createdAt, image, distance, time, point, isParkingLot); + 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/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/service/BoardingRecordService.java b/src/main/java/ice/spot/service/BoardingRecordService.java index 268f65f..325f31e 100644 --- a/src/main/java/ice/spot/service/BoardingRecordService.java +++ b/src/main/java/ice/spot/service/BoardingRecordService.java @@ -5,7 +5,9 @@ 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; @@ -43,16 +45,39 @@ public Boolean saveBoardingRecord(Long userId, Long imageId, BoardingRecordReque 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 List boardingRecordList(Long userId) { - return null; + 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; } } From a3aca24a31006a34706b0e8a9a2e4d7c58f13d80 Mon Sep 17 00:00:00 2001 From: JeongHeumChoi <79458446+JeongHeumChoi@users.noreply.github.com> Date: Wed, 8 May 2024 17:29:38 +0900 Subject: [PATCH 24/27] =?UTF-8?q?Chore:=20Credentials=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20(#29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spot-server-properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spot-server-properties b/spot-server-properties index 88515af..a440066 160000 --- a/spot-server-properties +++ b/spot-server-properties @@ -1 +1 @@ -Subproject commit 88515afcdc708913af815f585bce9affa47980ab +Subproject commit a440066410fac97d04913f4e9eb36b60e199565f From a537187eb0540e48620913bae178c9cfd85eeaf2 Mon Sep 17 00:00:00 2001 From: JeongHeumChoi Date: Wed, 8 May 2024 21:24:37 +0900 Subject: [PATCH 25/27] =?UTF-8?q?!HOTFIX:=20=EC=84=9C=EB=B8=8C=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=BB=A8=ED=94=8C=EB=A6=AD=ED=8A=B8=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spot-server-properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spot-server-properties b/spot-server-properties index a440066..0626579 160000 --- a/spot-server-properties +++ b/spot-server-properties @@ -1 +1 @@ -Subproject commit a440066410fac97d04913f4e9eb36b60e199565f +Subproject commit 062657935b3be59a83309b7ab827391e69105bc6 From 001b632c49f9130f8985010d82fa601850b13307 Mon Sep 17 00:00:00 2001 From: JeongHeumChoi <79458446+JeongHeumChoi@users.noreply.github.com> Date: Wed, 8 May 2024 21:28:58 +0900 Subject: [PATCH 26/27] =?UTF-8?q?=E2=9C=A8=20[Feature]=20-=20Reissue=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#31)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Chore: Credentials 수정 * Feat: Reissue 토큰 기능 구현 * Chore: Credentials 추가 --- .../ice/spot/controller/AuthController.java | 29 +++++++++++++++++++ .../java/ice/spot/service/AuthService.java | 14 +++++++++ 2 files changed, 43 insertions(+) diff --git a/src/main/java/ice/spot/controller/AuthController.java b/src/main/java/ice/spot/controller/AuthController.java index 173b2a9..49153ae 100644 --- a/src/main/java/ice/spot/controller/AuthController.java +++ b/src/main/java/ice/spot/controller/AuthController.java @@ -1,11 +1,20 @@ package ice.spot.controller; import ice.spot.annotation.UserId; +import ice.spot.constant.Constants; import ice.spot.dto.global.ResponseDto; import ice.spot.dto.request.OauthSignUpDto; +import ice.spot.dto.response.JwtTokenDto; +import ice.spot.exception.CommonException; +import ice.spot.exception.ErrorCode; import ice.spot.service.AuthService; +import ice.spot.util.CookieUtil; +import ice.spot.util.HeaderUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -17,6 +26,9 @@ @RequiredArgsConstructor public class AuthController { + @Value("${server.domain}") + private String domain; + private final AuthService authService; @PostMapping("/oauth2/sign-up") @@ -24,4 +36,21 @@ public ResponseDto signUp(@UserId Long userId, @RequestBody OauthSignUpDto oa authService.signUp(userId, oauthSignUpDto); return ResponseDto.ok(null); } + + @PostMapping("/auth/reissue") + public ResponseDto reissue( + HttpServletRequest request, + HttpServletResponse response, + @UserId Long userId){ + log.info("controller 진입 성공"); + String refreshToken = HeaderUtil.refineHeader(request, Constants.PREFIX_AUTH, Constants.PREFIX_BEARER) + .orElseThrow(() -> new CommonException(ErrorCode.INVALID_HEADER_VALUE)); + log.info("헤더값 조회 성공"); + JwtTokenDto jwtTokenDto = authService.reGenerateTokens(userId, refreshToken); + + CookieUtil.addCookie(response, domain, Constants.ACCESS_COOKIE_NAME, jwtTokenDto.accessToken()); + CookieUtil.addSecureCookie(response, domain, Constants.REFRESH_COOKIE_NAME, jwtTokenDto.refreshToken(), 60 * 60 * 24 * 14); + + return ResponseDto.ok(jwtTokenDto); + } } diff --git a/src/main/java/ice/spot/service/AuthService.java b/src/main/java/ice/spot/service/AuthService.java index e4a89bc..4b7d553 100644 --- a/src/main/java/ice/spot/service/AuthService.java +++ b/src/main/java/ice/spot/service/AuthService.java @@ -2,9 +2,11 @@ import ice.spot.domain.User; import ice.spot.dto.request.OauthSignUpDto; +import ice.spot.dto.response.JwtTokenDto; import ice.spot.exception.CommonException; import ice.spot.exception.ErrorCode; import ice.spot.repository.UserRepository; +import ice.spot.util.JwtUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -16,6 +18,7 @@ public class AuthService { private final UserRepository userRepository; + private final JwtUtil jwtUtil; @Transactional public void signUp(Long userId, OauthSignUpDto oauthSignUpDto){ @@ -25,4 +28,15 @@ public void signUp(Long userId, OauthSignUpDto oauthSignUpDto){ oauthUser.register(oauthSignUpDto.nickname()); } + @Transactional + public JwtTokenDto reGenerateTokens(Long userId, String refreshToken){ + log.info("re generate tokens 진입성공"); + User loginUser = userRepository.findByIdAndRefreshToken(userId, refreshToken) + .orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_USER)); + log.info("유저 조회 성공"); + JwtTokenDto jwtTokenDto = jwtUtil.generateTokens(loginUser.getId(), loginUser.getRole()); + + loginUser.updateRefreshToken(jwtTokenDto.refreshToken()); + return jwtTokenDto; + } } From 702f10bcd828ca7a0e48f68c015d64d23af4cd89 Mon Sep 17 00:00:00 2001 From: Jang99u Date: Thu, 9 May 2024 00:07:36 +0900 Subject: [PATCH 27/27] =?UTF-8?q?Fix:=20JsonProperty=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/BoardingRecordListResponse.java | 3 +++ .../response/ParkingLotResponseList.java | 18 ++++++++++++++++++ .../ice/spot/service/ParkingLotService.java | 5 +++-- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ice/spot/dto/parkingLot/response/ParkingLotResponseList.java diff --git a/src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordListResponse.java b/src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordListResponse.java index f6bf320..b2ace95 100644 --- a/src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordListResponse.java +++ b/src/main/java/ice/spot/dto/boardingrecord/response/BoardingRecordListResponse.java @@ -1,5 +1,6 @@ package ice.spot.dto.boardingrecord.response; +import com.fasterxml.jackson.annotation.JsonProperty; import ice.spot.dto.user.response.PersonResponse; import lombok.Builder; @@ -7,8 +8,10 @@ @Builder public record BoardingRecordListResponse( + @JsonProperty("person") PersonResponse personResponse, + @JsonProperty("records") List boardingRecordResponseList ) { } diff --git a/src/main/java/ice/spot/dto/parkingLot/response/ParkingLotResponseList.java b/src/main/java/ice/spot/dto/parkingLot/response/ParkingLotResponseList.java new file mode 100644 index 0000000..cee92e8 --- /dev/null +++ b/src/main/java/ice/spot/dto/parkingLot/response/ParkingLotResponseList.java @@ -0,0 +1,18 @@ +package ice.spot.dto.parkingLot.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +import java.util.List; + +@Builder +public record ParkingLotResponseList( + @JsonProperty("parkingLotList") + List parkingLotResponseList +) { + public static ParkingLotResponseList of(final List parkingLotResponse) { + return ParkingLotResponseList.builder() + .parkingLotResponseList(parkingLotResponse) + .build(); + } +} diff --git a/src/main/java/ice/spot/service/ParkingLotService.java b/src/main/java/ice/spot/service/ParkingLotService.java index 730b4bb..83ec06f 100644 --- a/src/main/java/ice/spot/service/ParkingLotService.java +++ b/src/main/java/ice/spot/service/ParkingLotService.java @@ -1,6 +1,7 @@ package ice.spot.service; import ice.spot.dto.parkingLot.response.ParkingLotResponse; +import ice.spot.dto.parkingLot.response.ParkingLotResponseList; import ice.spot.repository.ParkingLotRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -19,7 +20,7 @@ public class ParkingLotService { private final ParkingLotRepository parkingLotRepository; @Transactional(readOnly = true) - public List parkingLotList(Double lat, Double lon) { + public ParkingLotResponseList parkingLotList(Double lat, Double lon) { List parkingLotResponseList = new ArrayList<>(parkingLotRepository.findAll().stream() .map(parkingLot -> ParkingLotResponse.builder() @@ -41,6 +42,6 @@ public int compare(ParkingLotResponse o1, ParkingLotResponse o2) { } }); - return parkingLotResponseList.subList(0, 5); + return ParkingLotResponseList.of(parkingLotResponseList.subList(0, 5)); } }