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", "사진을 찾을 수 없습니다"),