From 876524c9de3fc02c94d1576eb48b1e6abd371620 Mon Sep 17 00:00:00 2001 From: xunxxoie Date: Mon, 11 Nov 2024 18:34:15 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=EB=B0=8F=20=EC=97=90=EB=9F=AC=20=ED=95=B8?= =?UTF-8?q?=EB=93=A4=EB=A7=81=20=EA=B5=AC=ED=98=84=20-=20#14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../todo/controller/TodoController.java | 28 +++++----- .../todo/dto/req/DayTodoRequestDto.java | 2 +- .../todo/dto/req/TodoCompletedRequestDto.java | 2 +- .../todo/dto/req/TodoDeleteRequestDto.java | 2 +- .../todo/domain/todo/dto/req/TodoDto.java | 12 ++--- .../todo/dto/req/TodoUpdateRequestDto.java | 15 +++--- .../todo/domain/todo/service/TodoService.java | 35 ++++++------- .../todo/todo/global/error/ErrorResponse.java | 4 +- .../global/error/GlobalExceptionHandler.java | 52 +++++++++++++++---- .../global/error/code/CustomErrorCode.java | 11 +++- 10 files changed, 101 insertions(+), 62 deletions(-) diff --git a/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/controller/TodoController.java b/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/controller/TodoController.java index c49fee7..67767eb 100644 --- a/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/controller/TodoController.java +++ b/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/controller/TodoController.java @@ -23,7 +23,6 @@ @RequestMapping("/api/todos") public class TodoController { - // TODO dto에서 사용자 아이디 제거하고 RequestParams나 PathVariable로 받는 것으로 수정하기! private final TodoService todoService; @Operation(summary = "투두 생성하기") @@ -34,50 +33,47 @@ public class TodoController { @PostMapping public ResponseEntity> saveTodo(@RequestParam Long userId, @Valid@RequestBody TodoDto request){ - Long todoId = todoService.saveTodo(userId, request); - return ResponseEntity.status(HttpStatus.CREATED).body(ResponseDto.of(todoId, "투두 생성 성공")); + + return ResponseEntity.status(HttpStatus.CREATED).body(ResponseDto.of(todoService.saveTodo(userId, request), "투두 생성 성공")); } @Operation(summary = "투두 삭제하기") @ApiResponses({ @ApiResponse(responseCode = "200", description = "투두 삭제 성공"), - @ApiResponse(responseCode = "401", description = "투두 삭제 실패 : 권한 없음") + @ApiResponse(responseCode = "403", description = "투두 삭제 실패 : 권한 없음") }) @DeleteMapping public ResponseEntity> deleteTodo(@RequestParam Long userId, @Valid @RequestBody TodoDeleteRequestDto request){ - Boolean isSucceed = todoService.deleteTodo(userId, request); - return ResponseEntity.status(HttpStatus.OK).body(ResponseDto.of(isSucceed, request.getTodoId().toString())); + return ResponseEntity.status(HttpStatus.OK).body(ResponseDto.of(todoService.deleteTodo(userId, request), "투두 삭제 성공")); } @Operation(summary = "투두 전체 수정하기") @ApiResponses({ @ApiResponse(responseCode = "200", description = "투두 수정 성공"), - @ApiResponse(responseCode = "401", description = "투두 수정 실패 : 권한 없음") + @ApiResponse(responseCode = "403", description = "투두 수정 실패 : 권한 없음") }) @PutMapping - public ResponseEntity> updateTodo(@RequestParam Long userId, + public ResponseEntity> updateTodo(@RequestParam Long userId, @Valid@RequestBody TodoUpdateRequestDto request){ - Boolean isSucceed = todoService.updateTodo(userId, request); - return ResponseEntity.status(HttpStatus.OK).body(ResponseDto.of(isSucceed, request.getTitle())); + return ResponseEntity.status(HttpStatus.OK).body(ResponseDto.of(todoService.updateTodo(userId, request), request.getTitle())); } @Operation(summary = "투두 완료여부 수정하기") @ApiResponses({ @ApiResponse(responseCode = "200", description = "투두 완료여부 수정 성공"), - @ApiResponse(responseCode = "401", description = "투두 완료여부 수정 실패 : 권한 없음") + @ApiResponse(responseCode = "403", description = "투두 완료여부 수정 실패 : 권한 없음") }) @PutMapping("/complete") public ResponseEntity> updateCompletedTodo(@RequestParam Long userId, @Valid@RequestBody TodoCompletedRequestDto request){ - Boolean isSucceed = todoService.updateCompletedTodo(userId, request); - return ResponseEntity.status(HttpStatus.OK).body(ResponseDto.of(isSucceed, request.getTitle())); + return ResponseEntity.status(HttpStatus.OK).body(ResponseDto.of(todoService.updateCompletedTodo(userId, request), request.getTodoId().toString())); } @Operation(summary = "전체 투두 불러오기") @ApiResponses({ @ApiResponse(responseCode = "200", description = "투두 가져오기 성공"), - @ApiResponse(responseCode = "401", description = "투두 가져오기 실패 : 권한 없음") + @ApiResponse(responseCode = "403", description = "투두 가져오기 실패 : 권한 없음") }) @GetMapping public ResponseEntity>> getAllTodo(@RequestParam Long userId){ @@ -87,11 +83,11 @@ public ResponseEntity>> getAllTodo(@RequestPar @Operation(summary = "특정 날짜 투두 불러오기") @ApiResponses({ @ApiResponse(responseCode = "200", description = "일별 투두 가져오기 성공"), - @ApiResponse(responseCode = "401", description = "일별 투두 가져오기 실패 : 권한 없음") + @ApiResponse(responseCode = "403", description = "일별 투두 가져오기 실패 : 권한 없음") }) @GetMapping("/date") public ResponseEntity>> getDayTodo(@RequestParam Long userId, @Valid @RequestBody DayTodoRequestDto request){ - return ResponseEntity.status(HttpStatus.OK).body(ResponseDto.of(todoService.getDayTodo(userId, request), request.getRequestDate().toString() + "의 투두 조회")); + return ResponseEntity.status(HttpStatus.OK).body(ResponseDto.of(todoService.getDayTodo(userId, request), request.getRequestDate().toString() + " 투두 조회")); } } \ No newline at end of file diff --git a/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/DayTodoRequestDto.java b/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/DayTodoRequestDto.java index fa93c09..a511c35 100644 --- a/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/DayTodoRequestDto.java +++ b/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/DayTodoRequestDto.java @@ -13,7 +13,7 @@ @Getter @NoArgsConstructor public class DayTodoRequestDto { - + // TODO DateTimeFormat 에러 핸들링 추가(Custom Validator) @DateTimeFormat(pattern = "yyyyMMdd") private LocalDateTime requestDate; } diff --git a/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoCompletedRequestDto.java b/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoCompletedRequestDto.java index e97c452..05b2461 100644 --- a/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoCompletedRequestDto.java +++ b/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoCompletedRequestDto.java @@ -10,6 +10,6 @@ @NoArgsConstructor public class TodoCompletedRequestDto { @Schema(description = "투두 아이디", example = "1") - @NotBlank + @NotBlank(message = "투두 ID가 포함되지 않았습니다.") private Long todoId; } diff --git a/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoDeleteRequestDto.java b/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoDeleteRequestDto.java index 07a53c2..b328bce 100644 --- a/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoDeleteRequestDto.java +++ b/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoDeleteRequestDto.java @@ -10,6 +10,6 @@ @NoArgsConstructor public class TodoDeleteRequestDto { @Schema(description = "투두 아이디", example = "1") - @NotBlank + @NotBlank(message = "투두 ID가 포함되지 않았습니다.") private Long todoId; } diff --git a/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoDto.java b/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoDto.java index c78ac89..991c7c4 100644 --- a/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoDto.java +++ b/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoDto.java @@ -16,20 +16,20 @@ public class TodoDto { @Schema(description = "제목", example = "지형이랑 카페가서 공부하기") - @NotBlank + @NotBlank(message = "제목이 입력되지 않았습니다.") private String title; @Schema(description = "내용", example = "카페 꼼마에서 오전 11시 만남") - @Size(max = 100) + @Size(max = 100, message = "내용의 최대 길이는 100입니다.") private String description; @Schema(description = "마감일", example = "YYYY-MM-DD") - @NotBlank + @NotBlank(message = "마감일이 지정되지 않았습니다.") private LocalDateTime dueDate; @Schema(description = "우선순위", example = "1") - @NotBlank - @Min(value = 0, message = "최소 = 0(매우매우 중요)") - @Max(value = 4, message = "최대 = 4(매우매우 안중요)") + @NotBlank(message = "우선순위가 입력되지 않았습니다.") + @Min(value = 0, message = "우선순위의 최소값은 0입니다.") + @Max(value = 4, message = "우선순위의 최대값은 4입니다.") private Integer priority; } diff --git a/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoUpdateRequestDto.java b/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoUpdateRequestDto.java index 38b5a25..169dad6 100644 --- a/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoUpdateRequestDto.java +++ b/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/dto/req/TodoUpdateRequestDto.java @@ -14,17 +14,20 @@ @Getter @NoArgsConstructor public class TodoUpdateRequestDto { - @NotBlank + @NotBlank(message = "투두 ID가 입력되지 않았습니다.") + private Long todoId; + + @NotBlank(message = "제목이 입력되지 않았습니다.") private String title; - @Size(max = 100) + @Size(max = 100, message = "내용의 최대 길이는 100입니다.") private String description; - @NotBlank + @NotBlank(message = "마감일이 지정되지 않았습니다.") private LocalDateTime dueDate; - @NotBlank - @Min(value = 0, message = "최소 = 0(매우매우 중요)") - @Max(value = 4, message = "최대 = 4(매우매우 안중요)") + @NotBlank(message = "우선순위가 입력되지 않았습니다.") + @Min(value = 0, message = "우선순위의 최소값은 0입니다.") + @Max(value = 4, message = "우선순위의 최대값은 4입니다.") private Integer priority; } diff --git a/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/service/TodoService.java b/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/service/TodoService.java index e1adcaf..6b297c1 100644 --- a/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/service/TodoService.java +++ b/contents/joonseo/todo/src/main/java/org/todo/todo/domain/todo/service/TodoService.java @@ -8,6 +8,8 @@ import org.todo.todo.domain.user.entity.UserEntity; import org.todo.todo.domain.user.repository.UserRepository; import org.todo.todo.domain.todo.dto.res.TodoResponseDto; +import org.todo.todo.global.error.code.CustomErrorCode; +import org.todo.todo.global.error.exception.RestApiException; import java.util.List; import java.util.Objects; @@ -34,10 +36,8 @@ public Long saveTodo(Long userId, TodoDto todoDto){ } public Boolean deleteTodo(Long userId, TodoDeleteRequestDto todoDeleteRequestDto){ - // 삭제하려는 유저와 투두 작성자 일치 여부 확인 - // TODO 에러핸들링 추가 - TodoEntity todo = todoRepository.findById(todoDeleteRequestDto.getTodoId()) - .orElseThrow(null); + + TodoEntity todo = findTodo(todoDeleteRequestDto.getTodoId()); if(checkAuthorization(todo, userId)) { todoRepository.delete(todo); @@ -47,24 +47,21 @@ public Boolean deleteTodo(Long userId, TodoDeleteRequestDto todoDeleteRequestDto return false; } - public Boolean updateTodo(Long userId, TodoUpdateRequestDto todoUpdateRequestDto){ - // 수정하려는 유저와 투두 작성자 일치 여부 확인 - // TODO 에러핸들링 추가 - TodoEntity todo = todoRepository.findByTitle(todoUpdateRequestDto.getTitle()) - .orElseThrow(null); + public Long updateTodo(Long userId, TodoUpdateRequestDto todoUpdateRequestDto){ + + TodoEntity todo = findTodo(todoUpdateRequestDto.getTodoId()); if(checkAuthorization(todo, userId)){ todo.updateTodo(todoUpdateRequestDto.getTitle(), todoUpdateRequestDto.getDescription(), todoUpdateRequestDto.getDueDate(), todoUpdateRequestDto.getPriority()); - return true; + return todo.getId(); } - return false; + return -1L; } public Boolean updateCompletedTodo(Long userId, TodoCompletedRequestDto todoCompletedRequestDto){ - // TODO 에러핸들링 추가 - TodoEntity todo = todoRepository.findById(todoCompletedRequestDto.getTodoId()) - .orElseThrow(null); + + TodoEntity todo = findTodo(todoCompletedRequestDto.getTodoId()); if(checkAuthorization(todo, userId)) { todo.updateCompleted(); @@ -75,23 +72,25 @@ public Boolean updateCompletedTodo(Long userId, TodoCompletedRequestDto todoComp } public List getAllTodo(Long userId){ - // TODO 배열이 비어있는 경우 처리 return todoRepository.findAllByUserId(userId).stream() .map(TodoResponseDto::from) .collect(Collectors.toList()); } public List getDayTodo(Long userId, DayTodoRequestDto dayTodoRequestDto){ - // TODO 배열이 비어있는 경우 처리 return todoRepository.findAllByCreatedAtAndUserId(dayTodoRequestDto.getRequestDate(),userId).stream() .map(TodoResponseDto::from) .collect(Collectors.toList()); } public UserEntity findUser(Long userId){ - // TODO 에러핸들링 추가 return userRepository.findById(userId) - .orElseThrow(null); + .orElseThrow(() -> new RestApiException(CustomErrorCode.PERMISSION_DENIED)); + } + + public TodoEntity findTodo(Long todoId) { + return todoRepository.findById(todoId) + .orElseThrow(() -> new RestApiException(CustomErrorCode.TODO_NOT_FOUND)); } public Boolean checkAuthorization(TodoEntity todo, Long userId){ diff --git a/contents/joonseo/todo/src/main/java/org/todo/todo/global/error/ErrorResponse.java b/contents/joonseo/todo/src/main/java/org/todo/todo/global/error/ErrorResponse.java index 53626b1..1d75094 100644 --- a/contents/joonseo/todo/src/main/java/org/todo/todo/global/error/ErrorResponse.java +++ b/contents/joonseo/todo/src/main/java/org/todo/todo/global/error/ErrorResponse.java @@ -7,7 +7,7 @@ @Getter @Builder @RequiredArgsConstructor -public class ErrorResponse { +public class ErrorResponse { private final Integer error; - private final String message; + private final T message; } diff --git a/contents/joonseo/todo/src/main/java/org/todo/todo/global/error/GlobalExceptionHandler.java b/contents/joonseo/todo/src/main/java/org/todo/todo/global/error/GlobalExceptionHandler.java index 512b425..5dcb929 100644 --- a/contents/joonseo/todo/src/main/java/org/todo/todo/global/error/GlobalExceptionHandler.java +++ b/contents/joonseo/todo/src/main/java/org/todo/todo/global/error/GlobalExceptionHandler.java @@ -1,38 +1,72 @@ package org.todo.todo.global.error; -import org.springframework.http.HttpStatus; -import org.springframework.http.HttpStatusCode; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.todo.todo.global.error.code.CustomErrorCode; import org.todo.todo.global.error.code.ErrorCode; import org.todo.todo.global.error.exception.RestApiException; +import java.util.ArrayList; +import java.util.List; + +@Slf4j @RestControllerAdvice -public class GlobalExceptionHandler{ +public class GlobalExceptionHandler { @ExceptionHandler(RestApiException.class) - public ResponseEntity handleRestApiException(RestApiException e){ + public ResponseEntity> handleRestApiException(RestApiException e){ ErrorCode errorCode = e.getErrorCode(); return handleExceptionInternal(errorCode); } + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity>> handleValidException(MethodArgumentNotValidException e) { + BindingResult result = e.getBindingResult(); + List errorMessages = new ArrayList<>(); + + log.error("@Valid Exception occur with below parameter"); + for (FieldError error : result.getFieldErrors()){ + String errorMessage = "[ " + error.getField() + " ]" + + "[ " + error.getDefaultMessage() + " ]" + + "[ " + error.getRejectedValue() + " ]"; + errorMessages.add(errorMessage); + } + + return handleExceptionInternal(CustomErrorCode.INVALID_PARAMS, errorMessages); + } + @ExceptionHandler(Exception.class) - public ResponseEntity handleException(Exception e){ + public ResponseEntity> handleException(Exception e){ ErrorCode errorCode = CustomErrorCode.INTERNAL_SERVER_ERROR; return handleExceptionInternal(errorCode); } - private ResponseEntity handleExceptionInternal(ErrorCode errorCode) { + private ResponseEntity> handleExceptionInternal(ErrorCode errorCode) { return ResponseEntity.status(errorCode.getHttpStatus()) .body(makeErrorResponse(errorCode)); } - public ErrorResponse makeErrorResponse(ErrorCode errorCode){ - return ErrorResponse.builder() + private ResponseEntity>> handleExceptionInternal(ErrorCode errorCode, List message) { + return ResponseEntity.status(errorCode.getHttpStatus()) + .body(makeErrorResponse(errorCode, message)); + } + + private ErrorResponse makeErrorResponse(ErrorCode errorCode){ + return ErrorResponse.builder() .error(errorCode.getCode()) .message(errorCode.getMessage()) .build(); } -} + + private ErrorResponse> makeErrorResponse(ErrorCode errorCode, List message){ + return ErrorResponse.>builder() + .error(errorCode.getCode()) + .message(message) + .build(); + } +} \ No newline at end of file diff --git a/contents/joonseo/todo/src/main/java/org/todo/todo/global/error/code/CustomErrorCode.java b/contents/joonseo/todo/src/main/java/org/todo/todo/global/error/code/CustomErrorCode.java index d21024f..83b3bd2 100644 --- a/contents/joonseo/todo/src/main/java/org/todo/todo/global/error/code/CustomErrorCode.java +++ b/contents/joonseo/todo/src/main/java/org/todo/todo/global/error/code/CustomErrorCode.java @@ -7,10 +7,17 @@ @Getter @RequiredArgsConstructor public enum CustomErrorCode implements ErrorCode{ - INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 500, "Internal Server Error") + // common error + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 500, "Internal Server Error"), + // todos error + TODO_NOT_FOUND(HttpStatus.NOT_FOUND, 404, "Todo not found"), + // user error + USER_NOT_FOUND(HttpStatus.NOT_FOUND, 404, "User not found"), + PERMISSION_DENIED(HttpStatus.FORBIDDEN, 403, "Permission denied"), + // validation + INVALID_PARAMS(HttpStatus.BAD_REQUEST, 400, "Validation Failed") ; - private final HttpStatus httpStatus; private final Integer code; private final String message;