From 4ef90cf4e31746476e4e2ddbfbc03fe2a157e479 Mon Sep 17 00:00:00 2001 From: ChuYong <jakgon@naver.com> Date: Mon, 8 Jul 2024 11:04:59 +0900 Subject: [PATCH 1/3] chore: add validation dependency --- photo-service/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/photo-service/build.gradle.kts b/photo-service/build.gradle.kts index 318d900..6f699ea 100644 --- a/photo-service/build.gradle.kts +++ b/photo-service/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-data-r2dbc") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.flywaydb:flyway-core") implementation("org.flywaydb:flyway-mysql") implementation("org.springframework:spring-jdbc") From 21ab1366200b93548a25793c8fad444beea34f18 Mon Sep 17 00:00:00 2001 From: ChuYong <jakgon@naver.com> Date: Mon, 8 Jul 2024 13:05:58 +0900 Subject: [PATCH 2/3] feat: add validation to photo service --- .../kr/mafoo/photo/annotation/MatchEnum.java | 46 +++++++++++++++++++ .../java/kr/mafoo/photo/annotation/ULID.java | 25 ++++++++++ .../java/kr/mafoo/photo/api/AlbumApi.java | 9 ++++ .../java/kr/mafoo/photo/api/PhotoApi.java | 9 ++++ .../photo/controller/AlbumController.java | 3 +- .../dto/request/AlbumCreateRequest.java | 9 +++- .../dto/request/AlbumUpdateRequest.java | 10 +++- .../dto/request/PhotoCreateRequest.java | 2 + .../request/PhotoUpdateAlbumIdRequest.java | 2 + 9 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 photo-service/src/main/java/kr/mafoo/photo/annotation/MatchEnum.java create mode 100644 photo-service/src/main/java/kr/mafoo/photo/annotation/ULID.java diff --git a/photo-service/src/main/java/kr/mafoo/photo/annotation/MatchEnum.java b/photo-service/src/main/java/kr/mafoo/photo/annotation/MatchEnum.java new file mode 100644 index 0000000..4a98372 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/annotation/MatchEnum.java @@ -0,0 +1,46 @@ +package kr.mafoo.photo.annotation; + +import jakarta.validation.Constraint; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Constraint(validatedBy = {MatchEnum.EnumValidator.class}) +@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface MatchEnum { + String message() default "타입이 올바르지 않습니다"; + Class<?>[] groups() default {}; + Class<? extends Payload>[] payload() default {}; + Class<? extends java.lang.Enum<?>> enumClass(); + + class EnumValidator implements ConstraintValidator<MatchEnum, String> { + private MatchEnum annotation; + + @Override + public void initialize(MatchEnum constraintAnnotation) { + this.annotation = constraintAnnotation; + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) return false; + boolean result = false; + Object[] enumValues = this.annotation.enumClass().getEnumConstants(); + if (enumValues != null) { + for (Object enumValue : enumValues) { + if (value.equals(enumValue.toString())) { + result = true; + break; + } + } + } + return result; + } + } +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/annotation/ULID.java b/photo-service/src/main/java/kr/mafoo/photo/annotation/ULID.java new file mode 100644 index 0000000..28aed92 --- /dev/null +++ b/photo-service/src/main/java/kr/mafoo/photo/annotation/ULID.java @@ -0,0 +1,25 @@ +package kr.mafoo.photo.annotation; + +import jakarta.validation.Constraint; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {ULID.ULIDValidator.class}) +public @interface ULID { + String message() default "ULID 형식이 아닙니다"; + Class[] groups() default {}; + Class[] payload() default {}; + + class ULIDValidator implements ConstraintValidator<ULID, String> { + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) return false; + return value.matches("[0-7][0-9A-HJKMNP-TV-Z]{25}"); + } + } + +} diff --git a/photo-service/src/main/java/kr/mafoo/photo/api/AlbumApi.java b/photo-service/src/main/java/kr/mafoo/photo/api/AlbumApi.java index ed5f49d..9911780 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/api/AlbumApi.java +++ b/photo-service/src/main/java/kr/mafoo/photo/api/AlbumApi.java @@ -3,14 +3,18 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import kr.mafoo.photo.annotation.RequestMemberId; +import kr.mafoo.photo.annotation.ULID; import kr.mafoo.photo.controller.dto.request.AlbumCreateRequest; import kr.mafoo.photo.controller.dto.request.AlbumUpdateRequest; import kr.mafoo.photo.controller.dto.response.AlbumResponse; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +@Validated @Tag(name = "앨범 관련 API", description = "앨범 조회, 생성, 수정, 삭제 등 API") @RequestMapping("/v1/albums") public interface AlbumApi { @@ -27,6 +31,7 @@ Mono<AlbumResponse> getAlbum( @RequestMemberId String memberId, + @ULID @Parameter(description = "앨범 ID", example = "test_album_id") @PathVariable String albumId @@ -38,6 +43,7 @@ Mono<AlbumResponse> createAlbum( @RequestMemberId String memberId, + @Valid @RequestBody AlbumCreateRequest request ); @@ -48,10 +54,12 @@ Mono<AlbumResponse> updateAlbum( @RequestMemberId String memberId, + @ULID @Parameter(description = "앨범 ID", example = "test_album_id") @PathVariable String albumId, + @Valid @RequestBody AlbumUpdateRequest request ); @@ -62,6 +70,7 @@ Mono<Void> deleteAlbum( @RequestMemberId String memberId, + @ULID @Parameter(description = "앨범 ID", example = "test_album_id") @PathVariable String albumId diff --git a/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java b/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java index 84915f1..4bb1b21 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java +++ b/photo-service/src/main/java/kr/mafoo/photo/api/PhotoApi.java @@ -3,14 +3,18 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import kr.mafoo.photo.annotation.RequestMemberId; +import kr.mafoo.photo.annotation.ULID; import kr.mafoo.photo.controller.dto.request.PhotoCreateRequest; import kr.mafoo.photo.controller.dto.request.PhotoUpdateAlbumIdRequest; import kr.mafoo.photo.controller.dto.response.PhotoResponse; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +@Validated @Tag(name = "사진 관련 API", description = "사진 조회, 생성, 수정, 삭제 등 API") @RequestMapping("/v1/photos") public interface PhotoApi { @@ -20,6 +24,7 @@ Flux<PhotoResponse> getPhotos( @RequestMemberId String memberId, + @ULID @Parameter(description = "앨범 ID", example = "test_album_id") @RequestParam String albumId @@ -31,6 +36,7 @@ Mono<PhotoResponse> createPhoto( @RequestMemberId String memberId, + @Valid @RequestBody PhotoCreateRequest request ); @@ -41,10 +47,12 @@ Mono<PhotoResponse> updatePhotoAlbum( @RequestMemberId String memberId, + @ULID @Parameter(description = "사진 ID", example = "test_photo_id") @PathVariable String photoId, + @Valid @RequestBody PhotoUpdateAlbumIdRequest request ); @@ -55,6 +63,7 @@ Mono<Void> deletePhoto( @RequestMemberId String memberId, + @ULID @Parameter(description = "사진 ID", example = "test_photo_id") @PathVariable String photoId diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/AlbumController.java b/photo-service/src/main/java/kr/mafoo/photo/controller/AlbumController.java index 7743805..8910747 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/AlbumController.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/AlbumController.java @@ -48,9 +48,10 @@ public Mono<AlbumResponse> createAlbum( @Override public Mono<AlbumResponse> updateAlbum(String memberId, String albumId, AlbumUpdateRequest request) { + AlbumType albumType = AlbumType.valueOf(request.type()); return albumService .updateAlbumName(albumId, request.name(), memberId) - .then(albumService.updateAlbumType(albumId, request.type(), memberId)) + .then(albumService.updateAlbumType(albumId, albumType, memberId)) .map(AlbumResponse::fromEntity); } diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/AlbumCreateRequest.java b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/AlbumCreateRequest.java index a52cf45..bf4cf79 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/AlbumCreateRequest.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/AlbumCreateRequest.java @@ -1,13 +1,20 @@ package kr.mafoo.photo.controller.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import kr.mafoo.photo.annotation.MatchEnum; +import kr.mafoo.photo.domain.AlbumType; +import org.hibernate.validator.constraints.Length; @Schema(description = "앨범 생성 요청") public record AlbumCreateRequest( + @NotBlank + @Length(min = 1, max = 100) @Schema(description = "앨범 이름", example = "시금치파슷하") String name, - @Schema(description = "앨범 타입", example = "TYPE_A") + @MatchEnum(enumClass = AlbumType.class) + @Schema(description = "앨범 타입") String type ) { } diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/AlbumUpdateRequest.java b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/AlbumUpdateRequest.java index 8ec636a..d54470d 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/AlbumUpdateRequest.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/AlbumUpdateRequest.java @@ -1,14 +1,20 @@ package kr.mafoo.photo.controller.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import kr.mafoo.photo.annotation.MatchEnum; import kr.mafoo.photo.domain.AlbumType; +import org.hibernate.validator.constraints.Length; @Schema(description = "앨범 수정 요청") public record AlbumUpdateRequest( + @NotBlank + @Length(min = 1, max = 100) @Schema(description = "앨범 이름", example = "시금치파슷하") String name, - @Schema(description = "앨범 타입", example = "TYPE_A") - AlbumType type + @MatchEnum(enumClass = AlbumType.class) + @Schema(description = "앨범 타입") + String type ) { } diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoCreateRequest.java b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoCreateRequest.java index 6b0fa51..f4c80e6 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoCreateRequest.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoCreateRequest.java @@ -1,9 +1,11 @@ package kr.mafoo.photo.controller.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import org.hibernate.validator.constraints.URL; @Schema(description = "사진 생성 요청") public record PhotoCreateRequest( + @URL @Schema(description = "QR URL", example = "qr_url") String qrUrl ) { diff --git a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoUpdateAlbumIdRequest.java b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoUpdateAlbumIdRequest.java index b4bb1b6..81ab484 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoUpdateAlbumIdRequest.java +++ b/photo-service/src/main/java/kr/mafoo/photo/controller/dto/request/PhotoUpdateAlbumIdRequest.java @@ -1,9 +1,11 @@ package kr.mafoo.photo.controller.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import kr.mafoo.photo.annotation.ULID; @Schema(description = "사진 앨범 수정 요청") public record PhotoUpdateAlbumIdRequest( + @ULID @Schema(description = "앨범 ID", example = "test_album_id") String albumId ) { From 2999ab7fd7fd843084d838809c17b41c1b299071 Mon Sep 17 00:00:00 2001 From: ChuYong <jakgon@naver.com> Date: Mon, 8 Jul 2024 13:20:51 +0900 Subject: [PATCH 3/3] feat: handle validation exception --- .../kr/mafoo/photo/annotation/MatchEnum.java | 2 +- .../photo/config/WebExceptionHandler.java | 32 +++++++++++++++++-- .../kr/mafoo/photo/exception/ErrorCode.java | 3 +- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/photo-service/src/main/java/kr/mafoo/photo/annotation/MatchEnum.java b/photo-service/src/main/java/kr/mafoo/photo/annotation/MatchEnum.java index 4a98372..0bb4602 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/annotation/MatchEnum.java +++ b/photo-service/src/main/java/kr/mafoo/photo/annotation/MatchEnum.java @@ -14,7 +14,7 @@ @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface MatchEnum { - String message() default "타입이 올바르지 않습니다"; + String message() default "ENUM 타입이 올바르지 않습니다"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; Class<? extends java.lang.Enum<?>> enumClass(); diff --git a/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java b/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java index 46707a7..816fb53 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java +++ b/photo-service/src/main/java/kr/mafoo/photo/config/WebExceptionHandler.java @@ -1,12 +1,16 @@ package kr.mafoo.photo.config; +import jakarta.validation.ConstraintViolationException; import kr.mafoo.photo.controller.dto.response.ErrorResponse; import kr.mafoo.photo.exception.DomainException; +import kr.mafoo.photo.exception.ErrorCode; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.bind.support.WebExchangeBindException; -@ControllerAdvice +@RestControllerAdvice public class WebExceptionHandler { @ExceptionHandler(DomainException.class) public ResponseEntity<ErrorResponse> handleDomainException(DomainException exception) { @@ -14,4 +18,28 @@ public ResponseEntity<ErrorResponse> handleDomainException(DomainException excep .badRequest() .body(ErrorResponse.fromErrorCode(exception.getErrorCode())); } + + @ExceptionHandler({MethodArgumentNotValidException.class, + ConstraintViolationException.class, + WebExchangeBindException.class}) + public ResponseEntity<ErrorResponse> validException(Exception ex) { + String errorMessage = "입력값 검증 오류: "; + if (ex instanceof MethodArgumentNotValidException mex) { + errorMessage += mex.getBindingResult().getAllErrors().get(0).getDefaultMessage(); + } else if (ex instanceof ConstraintViolationException cvex) { + errorMessage += cvex.getConstraintViolations().iterator().next().getMessage(); + } else if (ex instanceof WebExchangeBindException wex) { + errorMessage += wex.getAllErrors().get(0).getDefaultMessage(); + } else { + errorMessage += "알 수 없는 오류"; + } + ErrorResponse response = new ErrorResponse( + ErrorCode.REQUEST_INPUT_NOT_VALID.getCode(), + errorMessage + ); + + return ResponseEntity + .badRequest() + .body(response); + } } diff --git a/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java b/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java index 063c1b0..f2045b0 100644 --- a/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java +++ b/photo-service/src/main/java/kr/mafoo/photo/exception/ErrorCode.java @@ -7,7 +7,8 @@ @RequiredArgsConstructor public enum ErrorCode { - REDIRECT_URI_NOT_FOUND("EX001", "리다이렉트 URI를 찾을 수 없습니다"), + REDIRECT_URI_NOT_FOUND("EX0001", "리다이렉트 URI를 찾을 수 없습니다"), + REQUEST_INPUT_NOT_VALID("EX0002", "입력 값이 올바르지 않습니다."), ALBUM_NOT_FOUND("AE0001", "앨범을 찾을 수 없습니다"), PHOTO_NOT_FOUND("PE0001", "사진을 찾을 수 없습니다"),