이번에는 댓글기능을 추가하였다.
단순하게 CRUD 기능을 추가하였기에 크게 어렵지 않은 내용이다.
우선 시큐리티 접근 권한 설정을 추가하였다.
SecurityConfig
.antMatchers(HttpMethod.GET, "/api/comments").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
.antMatchers(HttpMethod.POST, "/api/comments").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
.antMatchers(HttpMethod.DELETE, "/api/comments/{id}").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
.antMatchers(HttpMethod.PUT, "/api/comments/{id}").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
위의 4개의 uri가 추가되었다.
Comment
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Builder
@Entity
public class Comment extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "comment_id")
private Long id;
@Column(nullable = false)
@Lob
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Board board;
public Comment(String content, Member member, Board board) {
this.content = content;
this.member = member;
this.board = board;
}
public boolean isOwnComment(Member member) {
return this.member.equals(member);
}
public void edit(CommentEditRequestDto req){
this.content = req.getContent();
}
}
Member와 Board가 다대일 관계이다. 둘 중 하나라도 삭제되면 댓글이 삭제되도록 설정하였다.
기존에 게시판의 작성자 비교를 서비스에서 했는데, 로직을 도메인으로 바꾸었다.
댓글도 그에 맞게 작성자 비교를 도메인에서 처리해주었다.
CommentController
@Api(value = "Comment Controller", tags = "Comment ")
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/comments")
public class CommentController {
private final CommentService commentService;
private final MemberRepository memberRepository;
@ApiOperation(value = "댓글 조회", notes = "댓글을 조회합니다.")
@GetMapping
@ResponseStatus(HttpStatus.OK)
public Response findAllComments(@Valid CommentReadNumber commentReadNumber){
return Response.success(commentService.findAllComments(commentReadNumber));
}
@ApiOperation(value = "댓글 작성", notes = "댓글을 작성 합니다.")
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Response createComment(@Valid @RequestBody CommentCreateRequestDto req) {
commentService.createComment(req, getPrincipal());
return Response.success();
}
@ApiOperation(value = "댓글 수정", notes = "댓글을 수정 합니다.")
@PutMapping("/{id}")
@ResponseStatus(HttpStatus.OK)
public Response editComment(@Valid @RequestBody CommentEditRequestDto req, @PathVariable Long id) {
commentService.editComment(req, getPrincipal(),id);
return Response.success();
}
@ApiOperation(value = "댓글 삭제", notes = "댓글을 삭제 합니다.")
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.OK)
public Response deleteComment(@ApiParam(value = "댓글 id", required = true) @PathVariable Long id) {
commentService.deleteComment(id, getPrincipal());
return Response.success();
}
private Member getPrincipal() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Member member = memberRepository.findByUsername(authentication.getName())
.orElseThrow(MemberNotFoundException::new);
return member;
}
}
댓글 전체조회 같은 경우 경로변수를 받는 것이 아닌, 쿼리파라미터로 해당 게시판 번호를 받아서 찾게 된다.
게시판 작성의 경우에도 경로변수를 받지 않고, request에 게시판 번호가 들어가게 된다.
수정과 삭제의 경우 댓글 아이디를 알아야 하므로 경로변수를 받도록 하였다.
CommentService
@Service
@RequiredArgsConstructor
public class CommentService {
private final CommentRepository commentRepository;
private final BoardRepository boardRepository;
@Transactional(readOnly = true)
public List<CommentResponseDto> findAllComments(CommentReadNumber number) {
List<Comment> comments = commentRepository.findAllByBoardId(number.getBoardId());
return comments.stream()
.map(comment -> new CommentResponseDto().toDto(comment))
.collect(Collectors.toList());
}
@Transactional
public void createComment(CommentCreateRequestDto req, Member member) {
Board board = boardRepository.findById(req.getBoardId()).orElseThrow(BoardNotFoundException::new);
Comment comment = new Comment(req.getContent(), member, board);
commentRepository.save(comment);
}
@Transactional
public void editComment(CommentEditRequestDto req, Member member,Long id) {
Comment comment = commentRepository.findById(id).orElseThrow(CommentNotFoundException::new);
validateOwnComment(comment,member);
comment.edit(req);
}
@Transactional
public void deleteComment(Long id, Member member) {
Comment comment = commentRepository.findById(id).orElseThrow(CommentNotFoundException::new);
validateOwnComment(comment, member);
commentRepository.delete(comment);
}
private void validateOwnComment(Comment comment, Member member) {
if (!comment.isOwnComment(member)) {
throw new MemberNotEqualsException();
}
}
}
삭제와 수정의 경우 게시판 작성자만 가능하므로 validate를 추가해 주었다.
로직자체가 단순하여 쉽게 이해할 수 있을 것이다.
이제 테스트를 만들어보자.
CommentControllerTest
@ExtendWith(MockitoExtension.class)
public class CommentControllerTest {
@InjectMocks
CommentController commentController;
@Mock
MemberRepository memberRepository;
@Mock
CommentService commentService;
MockMvc mockMvc;
ObjectMapper objectMapper = new ObjectMapper();
@BeforeEach
public void beforeEach() {
mockMvc = MockMvcBuilders.standaloneSetup(commentController).build();
}
@Test
public void 댓글작성_테스트() throws Exception {
// given
CommentCreateRequestDto req = new CommentCreateRequestDto(1L, "content");
Member member = createMember();
Authentication authentication = new UsernamePasswordAuthenticationToken(member.getId(), "",
Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
given(memberRepository.findByUsername(authentication.getName())).willReturn(Optional.of(member));
//when then
mockMvc.perform(
post("/api/comments")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isCreated());
verify(commentService).createComment(req, member);
}
@Test
public void 댓글조회_테스트() throws Exception {
// given
CommentReadNumber req = new CommentReadNumber(1L);
// when, then
mockMvc.perform(
get("/api/comments")
.param("boardId", String.valueOf(req.getBoardId()))
.contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isOk());
verify(commentService).findAllComments(req);
}
@Test
public void 댓글수정_테스트() throws Exception {
// given
Long id = 1L;
CommentEditRequestDto req = new CommentEditRequestDto("hi");
Member member = createMember();
Authentication authentication = new UsernamePasswordAuthenticationToken(member.getId(), "",
Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
given(memberRepository.findByUsername(authentication.getName())).willReturn(Optional.of(member));
// when, then
mockMvc.perform(
put("/api/comments/{id}",id)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req))
).andExpect(status().isOk());
verify(commentService).editComment(req,member,id);
}
@Test
@DisplayName("댓글 삭제")
public void deleteTest() throws Exception {
// given
Long id = 1L;
Member member = createMember();
Authentication authentication = new UsernamePasswordAuthenticationToken(member.getId(), "",
Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
given(memberRepository.findByUsername(authentication.getName())).willReturn(Optional.of(member));
// when, then
mockMvc.perform(
delete("/api/comments/{id}", id))
.andExpect(status().isOk());
verify(commentService).deleteComment(id, member);
}
}
CommentServiceTest
@ExtendWith(MockitoExtension.class)
public class CommentServiceTest {
@InjectMocks
CommentService commentService;
@Mock
CommentRepository commentRepository;
@Mock
MemberRepository memberRepository;
@Mock
BoardRepository boardRepository;
@Test
public void 댓글조회_테스트() {
// given
List<Comment> commentList = new ArrayList<>();
commentList.add(createComment(createMember()));
CommentReadNumber req = new CommentReadNumber(anyLong());
given(commentRepository.findAllByBoardId(req.getBoardId())).willReturn(commentList);
// when
List<CommentResponseDto> result = commentService.findAllComments(req);
// then
assertThat(result.size()).isEqualTo(1);
}
@Test
public void 댓글작성_테스트() {
// given
Board board = new Board(1L, "title", "content", createMember(), List.of(createImage()), 0, 0);
CommentCreateRequestDto req = new CommentCreateRequestDto(board.getId(), "content");
given(boardRepository.findById(anyLong())).willReturn(Optional.of(board));
// when
commentService.createComment(req, createMember());
// then
verify(commentRepository).save(any());
}
@Test
public void 댓글수정_테스트() {
// given
Member member = createMember();
Comment comment = new Comment(1L, "content", member, createBoard());
CommentEditRequestDto req = new CommentEditRequestDto("newContent");
given(commentRepository.findById(anyLong())).willReturn(Optional.of(comment));
// when
commentService.editComment(req, member, 1L);
// then
assertThat(comment.getContent()).isEqualTo("newContent");
}
@Test
void 댓글삭제_테스트() {
// given
Member member = createMember();
Comment comment = createComment(member);
given(commentRepository.findById(anyLong())).willReturn(Optional.of(comment));
// when
commentService.deleteComment(anyLong(), member);
// then
verify(commentRepository).delete(any());
}
@Test
public void 댓글수정예외_테스트() {
// given
Member member = createMember();
Member member2 = new Member(2l,"u","1","n",null);
Comment comment = new Comment(1L, "content", member, createBoard());
CommentEditRequestDto req = new CommentEditRequestDto("newContent");
given(commentRepository.findById(anyLong())).willReturn(Optional.of(comment));
// when, then
assertThatThrownBy(()->commentService.editComment(req, member2, 1L))
.isInstanceOf(MemberNotEqualsException.class);
}
@Test
void 댓글삭제예외_테스트() {
// given
Member member = createMember();
Member member2 = new Member(2l,"u","1","n",null);
Comment comment = createComment(member);
given(commentRepository.findById(anyLong())).willReturn(Optional.of(comment));
// when, then
assertThatThrownBy(()->commentService.deleteComment(anyLong(), member2))
.isInstanceOf(MemberNotEqualsException.class);
}
}
작성한 테스트 무사히 통과하게 된다.
이제 포스트맨으로 작동해 보자.
우선 댓글을 달아보자.
조회도 해보자.
이후 댓글 1번을 수정해 보자.
삭제까지 진행해 보자.
마지막으로 다른 사람이 다른 댓글을 조작하려 할 때 예외가 발생하는지 확인해 보자.
작동이 무사히 되는 것을 볼 수 있다.
마지막으로 스웨거에 추가된 모습을 보자.
자세한 코드는 아래 깃허브를 통해 확인할 수 있다.
https://github.com/kimtaesoo99/community
'프로젝트 > 커뮤니티' 카테고리의 다른 글
[프로젝트] 커뮤니티 REST API 서버만들기 #8 어드민 페이지 만들기 (4) | 2023.01.28 |
---|---|
[프로젝트] 커뮤니티 REST API 서버만들기 #7 Report API 만들기 (0) | 2023.01.27 |
[프로젝트] 커뮤니티 REST API 서버만들기 #5 게시판 부가기능 추가 (0) | 2023.01.24 |
[프로젝트] 커뮤니티 REST API 서버만들기 #4 Board API만들기 (0) | 2023.01.23 |
[프로젝트] 커뮤니티 REST API 서버만들기 #3 - Message API 만들기 (6) | 2023.01.21 |