diff --git a/todolist/src/main/java/study/todolist/controller/TodoController.java b/todolist/src/main/java/study/todolist/controller/TodoController.java new file mode 100644 index 0000000..9150257 --- /dev/null +++ b/todolist/src/main/java/study/todolist/controller/TodoController.java @@ -0,0 +1,75 @@ +package study.todolist.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import study.todolist.dto.TodoDto; +import study.todolist.global.Envelope; +import study.todolist.entity.TodoList; +import study.todolist.service.TodoService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/todo") +public class TodoController { + + private final TodoService todoService; + + + // 생성 + @PostMapping("/create") + public ResponseEntity createTodo(@RequestBody TodoDto.Request request){ + + Long id = todoService.createTodo(request.getTitle()); + + return ResponseEntity.ok(Envelope.toEnvelope(todoService.findById(id))); + } + + // 단건조회 + @GetMapping("/find/{id}") + public ResponseEntity findById(@PathVariable("id") Long id){ + + TodoList todo = todoService.findById(id); + Envelope response = Envelope.toEnvelope(todo); + + return ResponseEntity.ok(response); + } + + // 전체조회 + @GetMapping("/find/all") + public ResponseEntity findAll(){ + + Envelope envelope = Envelope.toEnvelope(todoService.findAll()); + + return ResponseEntity.ok(envelope); + } + + // 수정 + @PatchMapping("/update/{id}") + public ResponseEntity updateTodo(@PathVariable Long id, + @RequestBody TodoDto.Request request){ + + todoService.updateTitle(id, request.getTitle()); + + return ResponseEntity.ok(Envelope.toEnvelope(todoService.findById(id))); + } + + // check + @PatchMapping("/check/{id}") + public ResponseEntity checkTodo(@PathVariable Long id){ + + todoService.updateCheck(id); + + return ResponseEntity.ok(Envelope.toEnvelope(todoService.findById(id))); + } + + // 삭제 + @DeleteMapping("/delete/{id}") + public ResponseEntity deleteTodo(@PathVariable Long id){ + + todoService.delete(id); + + return ResponseEntity.ok(true); + } +} + diff --git a/todolist/src/main/java/study/todolist/dto/TodoDto.java b/todolist/src/main/java/study/todolist/dto/TodoDto.java new file mode 100644 index 0000000..89e4c1d --- /dev/null +++ b/todolist/src/main/java/study/todolist/dto/TodoDto.java @@ -0,0 +1,33 @@ +package study.todolist.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import study.todolist.entity.TodoList; + +public class TodoDto { + + @Data + public static class Request{ + private String title; + } + + @Data @Builder + @NoArgsConstructor @AllArgsConstructor + public static class Response{ + private Long id; + private String title; + private boolean check; + + public static Response of(TodoList todoList) { + + return Response.builder() + .id(todoList.getId()) + .title(todoList.getTitle()) + .check(todoList.isChecked()) + .build(); + } + } +} + diff --git a/todolist/src/main/java/study/todolist/entity/TodoList.java b/todolist/src/main/java/study/todolist/entity/TodoList.java new file mode 100644 index 0000000..d30a399 --- /dev/null +++ b/todolist/src/main/java/study/todolist/entity/TodoList.java @@ -0,0 +1,32 @@ +package study.todolist.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter @Builder +@NoArgsConstructor +@AllArgsConstructor +public class TodoList{ + + private Long id; + private String title; + private boolean checked; + + public void updateId(Long id){ + this.id = id; + } + + public void updateTitle(String title){ + this.title = title; + } + + public void updateChecked(){ + if (this.checked) + this.checked = false; + + else this.checked = true; + } +} + diff --git a/todolist/src/main/java/study/todolist/global/Envelope.java b/todolist/src/main/java/study/todolist/global/Envelope.java new file mode 100644 index 0000000..be0adbe --- /dev/null +++ b/todolist/src/main/java/study/todolist/global/Envelope.java @@ -0,0 +1,45 @@ +package study.todolist.global; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import study.todolist.dto.TodoDto; +import study.todolist.entity.TodoList; + +import java.util.List; +import java.util.stream.Collectors; + +@Getter @Builder +@NoArgsConstructor @AllArgsConstructor +public class Envelope { + + private T data; + private String error; + private String message; + + public static Envelope toEnvelope(TodoList data){ + + TodoDto.Response response = TodoDto.Response.builder() + .id(data.getId()) + .title(data.getTitle()) + .check(data.isChecked()) + .build(); + + return Envelope.builder() + .data(response) + .build(); + } + + public static Envelope toEnvelope(List data){ + + List list = data.stream() + .map(TodoDto.Response::of) + .collect(Collectors.toList()); + + return Envelope.builder() + .data(list) + .build(); + } +} \ No newline at end of file diff --git a/todolist/src/main/java/study/todolist/global/error/ErrorCode.java b/todolist/src/main/java/study/todolist/global/error/ErrorCode.java new file mode 100644 index 0000000..bd6ae4b --- /dev/null +++ b/todolist/src/main/java/study/todolist/global/error/ErrorCode.java @@ -0,0 +1,23 @@ +package study.todolist.global.error; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum ErrorCode { + + NOT_FOUND_TODO(HttpStatus.NOT_FOUND, "T-001", "TODO를 찾을 수 없습니다.") + ; + + + ErrorCode(HttpStatus httpStatus, String errorCode, String message) { + this.httpStatus = httpStatus; + this.errorCode = errorCode; + this.message = message; + } + + private HttpStatus httpStatus; + private String errorCode; + private String message; +} + diff --git a/todolist/src/main/java/study/todolist/global/error/ErrorResponse.java b/todolist/src/main/java/study/todolist/global/error/ErrorResponse.java new file mode 100644 index 0000000..148e257 --- /dev/null +++ b/todolist/src/main/java/study/todolist/global/error/ErrorResponse.java @@ -0,0 +1,55 @@ +package study.todolist.global.error; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; + +import java.util.List; + +@Getter +@Builder +public class ErrorResponse { + + private String errorCode; + private String errorMessage; + + public static ErrorResponse of(String errorCode, String errorMessage){ + + return ErrorResponse.builder() + .errorCode(errorCode) + .errorMessage(errorMessage) + .build(); + } + + public static ErrorResponse of(String errorCode, BindingResult bindingResult){ + + return ErrorResponse.builder() + .errorCode(errorCode) + .errorMessage(createErrorMessage(bindingResult)) + .build(); + } + + private static String createErrorMessage(BindingResult bindingResult) { + + StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + + List fieldErrors = bindingResult.getFieldErrors(); + + for (FieldError error : fieldErrors){ + + if (!isFirst){ + sb.append(", "); + } else isFirst = false; + + sb.append("["); + sb.append(error.getField()); + sb.append("]"); + sb.append(error.getDefaultMessage()); + } + + return sb.toString(); + } +} + diff --git a/todolist/src/main/java/study/todolist/global/error/GlobalExceptionHandler.java b/todolist/src/main/java/study/todolist/global/error/GlobalExceptionHandler.java new file mode 100644 index 0000000..40dca4a --- /dev/null +++ b/todolist/src/main/java/study/todolist/global/error/GlobalExceptionHandler.java @@ -0,0 +1,42 @@ +package study.todolist.global.error; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import study.todolist.global.Envelope; +import study.todolist.global.error.exception.EntityNotFoundException; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * EntityNotFoundException 발생 메시지 처리 + */ + @ExceptionHandler(EntityNotFoundException.class) + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e){ + log.error("EntityNotFoundException", e); + + Envelope envelope = Envelope.builder() + .error(e.getErrorCode().toString()) + .message(e.getMessage()) + .build(); + + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(envelope); + } + + /** + * 나머지 예외 발생 + */ + @ExceptionHandler(Exception.class) + protected ResponseEntity handleException(Exception e){ + log.error("Exception", e); + ErrorResponse errorResponse = ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR.toString(), e.getMessage()); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorResponse); + } +} diff --git a/todolist/src/main/java/study/todolist/global/error/exception/BusinessException.java b/todolist/src/main/java/study/todolist/global/error/exception/BusinessException.java new file mode 100644 index 0000000..85bb614 --- /dev/null +++ b/todolist/src/main/java/study/todolist/global/error/exception/BusinessException.java @@ -0,0 +1,15 @@ +package study.todolist.global.error.exception; + +import lombok.Getter; +import study.todolist.global.error.ErrorCode; + +@Getter +public class BusinessException extends RuntimeException{ + + private ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode){ + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} \ No newline at end of file diff --git a/todolist/src/main/java/study/todolist/global/error/exception/EntityNotFoundException.java b/todolist/src/main/java/study/todolist/global/error/exception/EntityNotFoundException.java new file mode 100644 index 0000000..b76f1fe --- /dev/null +++ b/todolist/src/main/java/study/todolist/global/error/exception/EntityNotFoundException.java @@ -0,0 +1,11 @@ +package study.todolist.global.error.exception; + +import study.todolist.global.error.ErrorCode; + +public class EntityNotFoundException extends BusinessException{ + + public EntityNotFoundException(ErrorCode errorCode) { + super(errorCode); + } +} + diff --git a/todolist/src/main/java/study/todolist/repository/MemoryDBRepository.java b/todolist/src/main/java/study/todolist/repository/MemoryDBRepository.java new file mode 100644 index 0000000..692a6c2 --- /dev/null +++ b/todolist/src/main/java/study/todolist/repository/MemoryDBRepository.java @@ -0,0 +1,15 @@ +package study.todolist.repository; + +import java.util.List; +import java.util.Optional; + +public interface MemoryDBRepository { + + T save(T entity); + + Optional findById(Long id); + + void delete(Long id); + + List findAll(); +} diff --git a/todolist/src/main/java/study/todolist/repository/TodoRepository.java b/todolist/src/main/java/study/todolist/repository/TodoRepository.java new file mode 100644 index 0000000..fb79288 --- /dev/null +++ b/todolist/src/main/java/study/todolist/repository/TodoRepository.java @@ -0,0 +1,43 @@ +package study.todolist.repository; + +import org.springframework.stereotype.Repository; +import study.todolist.entity.TodoList; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Repository +public class TodoRepository implements MemoryDBRepository { + + private List DB = new ArrayList<>(); + Long id = 0L; + + @Override + public TodoList save(TodoList entity) { + entity.updateId(++id); + DB.add(entity); + + return entity; + } + + @Override + public Optional findById(Long id) { + + return DB.stream() + .filter(todo -> todo.getId().equals(id)) + .findFirst(); + } + + @Override + public void delete(Long id) { + + DB.removeIf(todo -> todo.getId().equals(id)); + } + + @Override + public List findAll() { + + return DB; + } +} diff --git a/todolist/src/main/java/study/todolist/service/TodoService.java b/todolist/src/main/java/study/todolist/service/TodoService.java new file mode 100644 index 0000000..b4ac384 --- /dev/null +++ b/todolist/src/main/java/study/todolist/service/TodoService.java @@ -0,0 +1,62 @@ +package study.todolist.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import study.todolist.entity.TodoList; +import study.todolist.global.error.ErrorCode; +import study.todolist.global.error.exception.EntityNotFoundException; +import study.todolist.repository.TodoRepository; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class TodoService { + + private final TodoRepository todoRepository; + + // 단건조회 + public TodoList findById(Long id){ + + return todoRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.NOT_FOUND_TODO)); + } + // 전체조회 + public List findAll(){ + + return todoRepository.findAll(); + } + + // 생성 + public Long createTodo(String title){ + + TodoList todo = TodoList.builder() + .title(title) + .build(); + + return todoRepository.save(todo).getId(); + } + + // 수정 (수행 여부) + public void updateCheck(Long id){ + + TodoList findTodo = findById(id); + + findTodo.updateChecked(); + } + + // 수정 (내용) + public Long updateTitle(Long id, String title){ + + TodoList findTodo = findById(id); + + findTodo.updateTitle(title); + + return id; + } + + public void delete(Long id){ + + todoRepository.delete(id); + } +} diff --git a/todolist/src/test/java/study/todolist/controller/TodoControllerTest.java b/todolist/src/test/java/study/todolist/controller/TodoControllerTest.java new file mode 100644 index 0000000..9bef48d --- /dev/null +++ b/todolist/src/test/java/study/todolist/controller/TodoControllerTest.java @@ -0,0 +1,102 @@ +package study.todolist.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import study.todolist.dto.TodoDto; +import study.todolist.entity.TodoList; +import study.todolist.service.TodoService; + +import java.util.ArrayList; +import java.util.List; + +@WebMvcTest(TodoController.class) +class TodoControllerTest { + + @MockBean + private TodoService todoService; + + @Autowired + private TodoController todoController; + + private MockMvc mockMvc; + + private ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + public void initMockMvc() { + mockMvc = MockMvcBuilders + .standaloneSetup(todoController) + .build(); + } + @Test + @DisplayName("단건조회") + public void 단건조회() throws Exception { + + // given + Long id = 1L; + TodoList todo = new TodoList(id, "title", false); + + // stub + BDDMockito.given(todoService.findById(id)).willReturn(todo); + + // when + mockMvc.perform(MockMvcRequestBuilders.get("/api/todo/find/" + id) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()); + + // then + BDDMockito.verify(todoService).findById(id); + } + + @Test + @DisplayName("전체조회") + public void 전체조회() throws Exception { + + // given + List response = new ArrayList<>(); + response.add(new TodoList(1L, "First", false)); + response.add(new TodoList(2L, "Second", false)); + response.add(new TodoList(3L, "Third", false)); + + // stub + BDDMockito.given(todoService.findAll()).willReturn(response); + + // when + mockMvc.perform(MockMvcRequestBuilders.get("/api/todo/find/all") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()); + + // then + BDDMockito.verify(todoService).findAll(); + } + + @Test + @DisplayName("삭제") + public void 삭제() throws Exception { + + // given + Long id = 1L; + + // stub + BDDMockito.willDoNothing().given(todoService).delete(id); + + // when + mockMvc.perform(MockMvcRequestBuilders.delete("/api/todo/delete/" + id) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()); + + // then + BDDMockito.verify(todoService).delete(id); + } +} \ No newline at end of file