▸ 오늘의 코드카타
▸ 투두앱[todoParty] 리팩토링
▸ Global 예외처리
▸ Entity, DTO 테스트
2024년 2월 21일 - [프로그래머스] 27 : H-Index | n^2 배열 자르기
알고리즘 문제를 풀면서 문제에서 나온 그대로 풀지 않아도 된다는 것을 깨달았다.
문제에서 2차원 배열을 만드는게 나왔다고 해서 풀이도 그대로 2차원 배열을 만들지 않아도 됐다. 문제의 정답을 얻기 위해 불필요한 부분을 제외하고 필요한 부분만 계산하는 것이 중요하다는 것을 깨달았다.
🎥 전에 만들었던 투두앱 [todoParty] 을 추가 기능 구현하고 심화 주차에 배운 테스트 코드 작성과 예외처리를 적용해보았다.
▶️ 기본 CRU 기능
▶️ 로그인된 유저만 할일카드 생성, 수정할 수 있음
▶️ 할일카드 완료 상태로 만들 수 있음
▶️ 할일카드 삭제 기능 구현
▶️ 댓글 기능 구현
▶️ 로그인된 유저가 작성한 할일카드 조회 기능 구현
▶️ 생성, 수정, 삭제시 새로고침 기능 추가하기
▶️ Global 예외 처리
▶️ DTO, Entity 테스트 추가하기
▶️ Controller 테스트 추가하기
▶️ Service 테스트 추가하기
▶️ Repository 테스트 추가하기
▶️ Mockito 사용해서 테스트용 객체 만들어보기
▶️ Spring Boot 통합 테스트 추가하기
//유저의 전체 할일카드 조회
@ResponseBody
@GetMapping("/myTodos")
public List<TodoResponseDto> getPostsByUserId(@AuthenticationPrincipal UserDetailsImpl userDetails){
return todoService.getTodosByUserId(userDetails);
}
//유저의 전체 할일카드 조회
public List<TodoResponseDto> getTodosByUserId(UserDetailsImpl userDetails) {
List<Todo> todos = todoRepository.findByUserId(userDetails.getUser().getId());
List<TodoResponseDto> responseDtoList = new ArrayList<>();
for (Todo todo : todos) {
responseDtoList.add(new TodoResponseDto(todo));
}
return responseDtoList;
}
public interface TodoRepository extends JpaRepository<Todo, Long> {
List<Todo> findByUserId(Long userId);
}
@RestController
@RequestMapping("/api/comments")
public class CommentController {
private final CommentService commentService;
public CommentController(CommentService commentService) {
this.commentService = commentService;
}
//댓글 작성 기능
@PostMapping("/todo/{todoId}/create")
public CommentResponseDto createComment(@PathVariable Long todoId, @RequestBody CommentRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails){
return commentService.createComment(todoId, requestDto, userDetails);
}
}
@Entity
@Getter
@NoArgsConstructor
@Table(name = "comments")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String content;
@Column
private String username;
@Column
private LocalDateTime createDate;
@Column
private Long todoId;
public Comment(Long todoId, CommentRequestDto requestDto, UserDetailsImpl userDetails){
this.todoId = todoId;
this.content = requestDto.getContent();
this.username = userDetails.getUsername();
this.createDate = LocalDateTime.now();
}
}
@Getter
@Setter
public class CommentRequestDto {
private String content;
}
@Getter
@Setter
@AllArgsConstructor
public class CommentResponseDto {
private Long id;
private String content;
private String username;
private LocalDateTime createDate;
private Long todoId;
public CommentResponseDto(Comment comment){
this.id = comment.getId();
this.content = comment.getContent();
this.username = comment.getUsername();
this.createDate = comment.getCreateDate();
this.todoId = comment.getTodoId();
}
}
@Service
public class CommentService {
private final CommentRepository commentRepository;
public CommentService(CommentRepository commentRepository) {
this.commentRepository = commentRepository;
}
public CommentResponseDto createComment(Long todoId, CommentRequestDto requestDto, UserDetailsImpl userDetails) {
Comment comment = new Comment(todoId, requestDto, userDetails);
commentRepository.save(comment);
return new CommentResponseDto(comment);
}
}
public interface CommentRepository extends JpaRepository<Comment, Long> {
}
//할일카드의 모든 댓글 조회 기능
@GetMapping("{todoId}")
public List<CommentResponseDto> getComments(@PathVariable Long todoId){
return commentService.getComments(todoId);
}
public List<CommentResponseDto> getComments(Long todoId) {
List<Comment> comments = commentRepository.findByTodoIdOrderByCreateDate(todoId);
List<CommentResponseDto> responseDtoList = new ArrayList<>();
for(Comment comment : comments){
responseDtoList.add(new CommentResponseDto(comment));
}
return responseDtoList;
}
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByTodoIdOrderByCreateDate(Long todoId);
}
//댓글 수정 기능
@Transactional
@PutMapping("/{commentId}")
public CommentResponseDto updateComment(@PathVariable Long commentId, @RequestBody CommentRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails){
return commentService.updateComment(commentId, requestDto, userDetails);
}
public CommentResponseDto updateComment(Long commentId, CommentRequestDto requestDto, UserDetailsImpl userDetails) {
Comment comment = commentRepository.findById(commentId).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 댓글입니다."));
if(!userDetails.getUsername().equals(comment.getUsername())){
throw new RejectedExecutionException("할일카드의 작성자만 수정이 가능합니다.");
}
comment.update(requestDto);
return new CommentResponseDto(comment);
}
public void update(CommentRequestDto requestDto) {
this.content = requestDto.getContent();
}
//댓글 삭제 기능
@Transactional
@DeleteMapping("/{commentId}")
public void deleteComment(@PathVariable Long commentId, @AuthenticationPrincipal UserDetailsImpl userDetails){
commentService.deleteComment(commentId, userDetails);
}
public void deleteComment(Long commentId, UserDetailsImpl userDetails) {
Comment comment = commentRepository.findById(commentId).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 댓글입니다."));
if(!userDetails.getUsername().equals(comment.getUsername())){
throw new RejectedExecutionException("댓글의 작성자만 삭제가 가능합니다.");
}
commentRepository.delete(comment);
}
//할일카드 삭제
@Transactional
@DeleteMapping("/{todoId}")
public String deleteTodo(@PathVariable Long todoId, @AuthenticationPrincipal UserDetailsImpl userDetails){
todoService.deleteTodo(todoId, userDetails);
return "redirect:/api/todos/myTodos";
}
void deleteByTodoId(Long todoId);
public void deleteCommentByTodoId(Long todoId) {
commentRepository.deleteByTodoId(todoId);
}
public void deleteTodo(Long todoId, UserDetailsImpl userDetails) {
Todo todo = todoRepository.findById(todoId).orElseThrow(()-> new IllegalArgumentException("존재하지 않는 할일카드 입니다."));
if(!userDetails.getUsername().equals(todo.getUsername())){
throw new RejectedExecutionException("게시물의 작성자만 삭제가 가능합니다.");
}
commentService.deleteCommentByTodoId(todoId);
todoRepository.delete(todo);
}
@PostMapping
public String postTodo(@RequestBody TodoRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails){
//UserDetailsImpl에 Getter 추가
TodoResponseDto todoResponseDto = todoService.postTodo(requestDto, userDetails);
return "redirect:/api/todos/myTodos";
}
//할일카드 수정
@PutMapping("/{todoId}")
public String putTodo(@PathVariable Long todoId, @RequestBody TodoRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails){
//UserDetailsImpl에 Getter 추가
todoService.updateTodo(todoId, requestDto, userDetails);
return "redirect:/api/todos/myTodos";
}
//할일카드 삭제
@Transactional
@DeleteMapping("/{todoId}")
public String deleteTodo(@PathVariable Long todoId, @AuthenticationPrincipal UserDetailsImpl userDetails){
todoService.deleteTodo(todoId, userDetails);
return "redirect:/api/todos/myTodos";
}
@Getter
@AllArgsConstructor
public class RestApiException {
private String errorMessage;
private int statusCode;
}
RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({IllegalArgumentException.class})
public ResponseEntity<RestApiException> handleIllegalArgumentException(IllegalArgumentException ex) {
RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.BAD_REQUEST.value());
return new ResponseEntity<>(
// HTTP body
restApiException,
// HTTP status code
HttpStatus.BAD_REQUEST
);
}
@ExceptionHandler({RejectedExecutionException.class})
public ResponseEntity<RestApiException> handleRejectedExecutionException(RejectedExecutionException ex){
RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.BAD_REQUEST.value());
return new ResponseEntity<>(
restApiException,
HttpStatus.BAD_REQUEST
);
}
}
not.found.comment=해당 댓글이 존재하지 않습니다.
public class CommentNotFoundException extends IllegalArgumentException{
public CommentNotFoundException(String message){
super(message);
}
}
private final MessageSource messageSource;
...
public CommentResponseDto updateComment(Long commentId, CommentRequestDto requestDto, UserDetailsImpl userDetails) {
Comment comment = commentRepository.findById(commentId).orElseThrow(() -> new CommentNotFoundException(messageSource.getMessage(
"not.found.comment",
null,
"Not Found Comment",
Locale.getDefault()
))
);
if(!userDetails.getUsername().equals(comment.getUsername())){
throw new RejectedExecutionException("댓글의 작성자만 수정이 가능합니다.");
}
comment.update(requestDto);
return new CommentResponseDto(comment);
}
public void deleteComment(Long commentId, UserDetailsImpl userDetails) {
Comment comment = commentRepository.findById(commentId).orElseThrow(() -> new CommentNotFoundException(messageSource.getMessage(
"not.found.comment",
null,
"Not Found Comment",
Locale.getDefault()
))
);
if(!userDetails.getUsername().equals(comment.getUsername())){
throw new RejectedExecutionException("댓글의 작성자만 삭제가 가능합니다.");
}
commentRepository.delete(comment);
}
@ExceptionHandler({MethodArgumentNotValidException.class})
public ResponseEntity<RestApiException> handleValidationException(MethodArgumentNotValidException ex){
RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.BAD_REQUEST.value());
return new ResponseEntity<>(
restApiException,
HttpStatus.BAD_REQUEST
);
}
e.getBindingResult().getFieldErrors().get(0).getDefaultMessage()
//여기서 e는 에러
@ExceptionHandler({MethodArgumentNotValidException.class})
public ResponseEntity<RestApiException> handleValidationException(MethodArgumentNotValidException ex){
RestApiException restApiException = new RestApiException(ex.getBindingResult().getFieldErrors().get(0).getDefaultMessage(), HttpStatus.BAD_REQUEST.value());
return new ResponseEntity<>(
restApiException,
HttpStatus.BAD_REQUEST
);
}
import static org.junit.jupiter.api.Assertions.assertEquals;
public class EntityTest {
@Test
@DisplayName("Todo update 메서드 - title, content 수정")
void Test1(){
//given
String title = "제목";
String content = "내용";
Todo todo = new Todo();
todo.setTitle(title);
todo.setContent(content);
String modifiedTitle = "수정 제목";
String modifiedContent = "수정 내용";
TodoRequestDto todoRequestDto = new TodoRequestDto();
todoRequestDto.setTitle(modifiedTitle);
todoRequestDto.setContent(modifiedContent);
//when
todo.update(todoRequestDto);
//then
assertEquals(todo.getTitle(),"수정 제목");
assertEquals(todo.getContent(), "수정 내용");
}
@Test
@DisplayName("Comment update 메서드 - content 수정")
void Test2(){
//given
String content = "내용";
Comment comment = new Comment();
comment.setContent(content);
String modifiedContent = "수정 내용";
CommentRequestDto commentRequestDto = new CommentRequestDto();
commentRequestDto.setContent(modifiedContent);
//when
comment.update(commentRequestDto);
//then
assertEquals(comment.getContent(),"수정 내용");
}
}