Skip to content

Commit

Permalink
[Feat] 유효성 검사 및 에러 핸들링 구현 - #14
Browse files Browse the repository at this point in the history
  • Loading branch information
xunxxoie committed Nov 11, 2024
1 parent 3f4f812 commit 876524c
Show file tree
Hide file tree
Showing 10 changed files with 101 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
@RequestMapping("/api/todos")
public class TodoController {

// TODO dto에서 사용자 아이디 제거하고 RequestParams나 PathVariable로 받는 것으로 수정하기!
private final TodoService todoService;

@Operation(summary = "투두 생성하기")
Expand All @@ -34,50 +33,47 @@ public class TodoController {
@PostMapping
public ResponseEntity<ResponseDto<Long>> 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<ResponseDto<Boolean>> 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<ResponseDto<Boolean>> updateTodo(@RequestParam Long userId,
public ResponseEntity<ResponseDto<Long>> 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<ResponseDto<Boolean>> 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<ResponseDto<List<TodoResponseDto>>> getAllTodo(@RequestParam Long userId){
Expand All @@ -87,11 +83,11 @@ public ResponseEntity<ResponseDto<List<TodoResponseDto>>> getAllTodo(@RequestPar
@Operation(summary = "특정 날짜 투두 불러오기")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "일별 투두 가져오기 성공"),
@ApiResponse(responseCode = "401", description = "일별 투두 가져오기 실패 : 권한 없음")
@ApiResponse(responseCode = "403", description = "일별 투두 가져오기 실패 : 권한 없음")
})
@GetMapping("/date")
public ResponseEntity<ResponseDto<List<TodoResponseDto>>> 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() + " 투두 조회"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@Getter
@NoArgsConstructor
public class DayTodoRequestDto {

// TODO DateTimeFormat 에러 핸들링 추가(Custom Validator)
@DateTimeFormat(pattern = "yyyyMMdd")
private LocalDateTime requestDate;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
@NoArgsConstructor
public class TodoCompletedRequestDto {
@Schema(description = "투두 아이디", example = "1")
@NotBlank
@NotBlank(message = "투두 ID가 포함되지 않았습니다.")
private Long todoId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
@NoArgsConstructor
public class TodoDeleteRequestDto {
@Schema(description = "투두 아이디", example = "1")
@NotBlank
@NotBlank(message = "투두 ID가 포함되지 않았습니다.")
private Long todoId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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();
Expand All @@ -75,23 +72,25 @@ public Boolean updateCompletedTodo(Long userId, TodoCompletedRequestDto todoComp
}

public List<TodoResponseDto> getAllTodo(Long userId){
// TODO 배열이 비어있는 경우 처리
return todoRepository.findAllByUserId(userId).stream()
.map(TodoResponseDto::from)
.collect(Collectors.toList());
}

public List<TodoResponseDto> 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){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@Getter
@Builder
@RequiredArgsConstructor
public class ErrorResponse {
public class ErrorResponse<T> {
private final Integer error;
private final String message;
private final T message;
}
Original file line number Diff line number Diff line change
@@ -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<Object> handleRestApiException(RestApiException e){
public ResponseEntity<ErrorResponse<String>> handleRestApiException(RestApiException e){
ErrorCode errorCode = e.getErrorCode();
return handleExceptionInternal(errorCode);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse<List<String>>> handleValidException(MethodArgumentNotValidException e) {
BindingResult result = e.getBindingResult();
List<String> 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<Object> handleException(Exception e){
public ResponseEntity<ErrorResponse<String>> handleException(Exception e){
ErrorCode errorCode = CustomErrorCode.INTERNAL_SERVER_ERROR;
return handleExceptionInternal(errorCode);
}

private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode) {
private ResponseEntity<ErrorResponse<String>> handleExceptionInternal(ErrorCode errorCode) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(makeErrorResponse(errorCode));
}

public ErrorResponse makeErrorResponse(ErrorCode errorCode){
return ErrorResponse.builder()
private ResponseEntity<ErrorResponse<List<String>>> handleExceptionInternal(ErrorCode errorCode, List<String> message) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(makeErrorResponse(errorCode, message));
}

private ErrorResponse<String> makeErrorResponse(ErrorCode errorCode){
return ErrorResponse.<String>builder()
.error(errorCode.getCode())
.message(errorCode.getMessage())
.build();
}
}

private ErrorResponse<List<String>> makeErrorResponse(ErrorCode errorCode, List<String> message){
return ErrorResponse.<List<String>>builder()
.error(errorCode.getCode())
.message(message)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 876524c

Please sign in to comment.