diff --git a/board/src/main/java/com/example/board/controller/ArticleCommentController.java b/board/src/main/java/com/example/board/controller/ArticleCommentController.java new file mode 100644 index 00000000..00fa9e05 --- /dev/null +++ b/board/src/main/java/com/example/board/controller/ArticleCommentController.java @@ -0,0 +1,44 @@ +package com.example.board.controller; + +import com.example.board.dto.UserAccountDto; +import com.example.board.dto.request.ArticleCommentRequest; +import com.example.board.service.ArticleCommentService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@RequiredArgsConstructor +@RequestMapping("/comments") +@Controller +public class ArticleCommentController { + + private final ArticleCommentService articleCommentService; + + @PostMapping("/new") + public String postNewArticleComment(ArticleCommentRequest articleCommentRequest) { + + // TODO: 인증 정보 필요 + articleCommentService.saveArticleComment(articleCommentRequest.toDto( + UserAccountDto.of( + "bobby", + "1234", + "bobby@email.com", + null, + null + ) + )); + + return "redirect:/articles/" + articleCommentRequest.articleId(); + } + + @PostMapping("/{commentId}/delete") + public String deleteArticleComment(@PathVariable Long commentId, + Long articleId) { + + articleCommentService.deleteArticleComment(commentId); + + return "redirect:/articles/" + articleId; + } +} diff --git a/board/src/main/java/com/example/board/dto/ArticleCommentDto.java b/board/src/main/java/com/example/board/dto/ArticleCommentDto.java index 8881a31c..87ac2117 100644 --- a/board/src/main/java/com/example/board/dto/ArticleCommentDto.java +++ b/board/src/main/java/com/example/board/dto/ArticleCommentDto.java @@ -2,6 +2,7 @@ package com.example.board.dto; import com.example.board.domain.Article; import com.example.board.domain.ArticleComment; +import com.example.board.domain.UserAccount; import java.time.LocalDateTime; @@ -15,6 +16,11 @@ public record ArticleCommentDto( LocalDateTime modifiedAt, String modifiedBy ) { + + public static ArticleCommentDto of(Long articleId, UserAccountDto userAccountDto, String content) { + return new ArticleCommentDto(null, articleId, userAccountDto, content, null, null, null, null); + } + public static ArticleCommentDto of(Long id, Long articleId, UserAccountDto userAccountDto, String content, LocalDateTime createdAt, String createdBy, LocalDateTime modifiedAt, String modifiedBy) { return new ArticleCommentDto(id, articleId, userAccountDto, content, createdAt, createdBy, modifiedAt, modifiedBy); } @@ -32,10 +38,10 @@ public record ArticleCommentDto( ); } - public ArticleComment toEntity(Article entity) { + public ArticleComment toEntity(Article article, UserAccount userAccount) { return ArticleComment.of( - entity, - userAccountDto.toEntity(), + article, + userAccount, content ); } diff --git a/board/src/main/java/com/example/board/dto/request/ArticleCommentRequest.java b/board/src/main/java/com/example/board/dto/request/ArticleCommentRequest.java new file mode 100644 index 00000000..ddea998b --- /dev/null +++ b/board/src/main/java/com/example/board/dto/request/ArticleCommentRequest.java @@ -0,0 +1,19 @@ +package com.example.board.dto.request; + +import com.example.board.dto.ArticleCommentDto; +import com.example.board.dto.UserAccountDto; + +public record ArticleCommentRequest(Long articleId, String content) { + + public static ArticleCommentRequest of(Long articleId, String content) { + return new ArticleCommentRequest(articleId, content); + } + + public ArticleCommentDto toDto(UserAccountDto userAccountDto) { + return ArticleCommentDto.of( + articleId, + userAccountDto, + content + ); + } +} diff --git a/board/src/main/java/com/example/board/service/ArticleCommentService.java b/board/src/main/java/com/example/board/service/ArticleCommentService.java index 52e6067f..27b90719 100644 --- a/board/src/main/java/com/example/board/service/ArticleCommentService.java +++ b/board/src/main/java/com/example/board/service/ArticleCommentService.java @@ -1,9 +1,12 @@ package com.example.board.service; +import com.example.board.domain.Article; import com.example.board.domain.ArticleComment; +import com.example.board.domain.UserAccount; import com.example.board.dto.ArticleCommentDto; import com.example.board.repository.ArticleCommentRepository; import com.example.board.repository.ArticleRepository; +import com.example.board.repository.UserAccountRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -20,6 +23,7 @@ public class ArticleCommentService { private final ArticleRepository articleRepository; private final ArticleCommentRepository articleCommentRepository; + private final UserAccountRepository userAccountRepository; @Transactional(readOnly = true) public List searchArticleComments(Long articleId) { @@ -31,9 +35,11 @@ public class ArticleCommentService { public void saveArticleComment(ArticleCommentDto dto) { try { - articleCommentRepository.save(dto.toEntity(articleRepository.getReferenceById(dto.articleId()))); + Article article = articleRepository.getReferenceById(dto.articleId()); + UserAccount userAccount = userAccountRepository.getReferenceById(dto.userAccountDto().userId()); + articleCommentRepository.save(dto.toEntity(article,userAccount)); } catch (EntityNotFoundException e) { - log.warn("댓글 저장 실패. 댓글의 게시글을 찾을 수 없습니다 - dto: {}", dto); + log.warn("댓글 저장 실패. 댓글 작성에 필요한 정보를 찾을 수 없습니다 - dto: {}", e.getLocalizedMessage()); } } diff --git a/board/src/test/java/com/example/board/controller/ArticleCommentControllerTest.java b/board/src/test/java/com/example/board/controller/ArticleCommentControllerTest.java new file mode 100644 index 00000000..8f09ad56 --- /dev/null +++ b/board/src/test/java/com/example/board/controller/ArticleCommentControllerTest.java @@ -0,0 +1,89 @@ +package com.example.board.controller; + +import com.example.board.config.SecurityConfig; +import com.example.board.dto.ArticleCommentDto; +import com.example.board.dto.request.ArticleCommentRequest; +import com.example.board.service.ArticleCommentService; +import com.example.board.util.FormDataEncoder; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@DisplayName("View 컨트롤러 - 댓글") +@Import({SecurityConfig.class, FormDataEncoder.class}) +@WebMvcTest(ArticleCommentController.class) +class ArticleCommentControllerTest { + + private final MockMvc mockMvc; + private final FormDataEncoder formDataEncoder; + + @MockBean + private ArticleCommentService articleCommentService; + + public ArticleCommentControllerTest( + @Autowired MockMvc mockMvc, + @Autowired FormDataEncoder formDataEncoder + ) { + this.mockMvc = mockMvc; + this.formDataEncoder = formDataEncoder; + } + + @DisplayName("[view][POST] 댓글 등록 - 정상 호출") + @Test + void givenArticleCommentInfo_whenRequesting_thenSavesNewArticleComment() throws Exception { + // Given + long articleId = 1L; + ArticleCommentRequest request = ArticleCommentRequest.of(articleId, "test comment"); + willDoNothing().given(articleCommentService).saveArticleComment(any(ArticleCommentDto.class)); + + // When & Then + mockMvc.perform( + post("/comments/new") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .content(formDataEncoder.encode(request)) + .with(csrf()) + ) + .andExpect(status().is3xxRedirection()) + .andExpect(view().name("redirect:/articles/" + articleId)) + .andExpect(redirectedUrl("/articles/" + articleId)); + + then(articleCommentService).should().saveArticleComment(any(ArticleCommentDto.class)); + } + + @DisplayName("[view][POST] 댓글 삭제 - 정상 호출") + @Test + void givenArticleCommentIdToDelete_whenRequesting_thenDeletesArticleComment() throws Exception { + // Given + long articleId = 1L; + long articleCommentId = 1L; + String userId = "unoTest"; + willDoNothing().given(articleCommentService).deleteArticleComment(articleCommentId); + + // When & Then + mockMvc.perform( + post("/comments/" + articleCommentId + "/delete") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .content(formDataEncoder.encode(Map.of("articleId", articleId))) + .with(csrf()) + ) + .andExpect(status().is3xxRedirection()) + .andExpect(view().name("redirect:/articles/" + articleId)) + .andExpect(redirectedUrl("/articles/" + articleId)); + + then(articleCommentService).should().deleteArticleComment(articleCommentId); + } +} \ No newline at end of file diff --git a/board/src/test/java/com/example/board/service/ArticleCommentServiceTest.java b/board/src/test/java/com/example/board/service/ArticleCommentServiceTest.java index a502c913..ee7238c4 100644 --- a/board/src/test/java/com/example/board/service/ArticleCommentServiceTest.java +++ b/board/src/test/java/com/example/board/service/ArticleCommentServiceTest.java @@ -7,6 +7,7 @@ import com.example.board.dto.ArticleCommentDto; import com.example.board.dto.UserAccountDto; import com.example.board.repository.ArticleCommentRepository; import com.example.board.repository.ArticleRepository; +import com.example.board.repository.UserAccountRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -34,6 +35,9 @@ class ArticleCommentServiceTest { @Mock private ArticleCommentRepository articleCommentRepository; + @Mock + private UserAccountRepository userAccountRepository; + @DisplayName("게시글 ID로 조회하면 해당하는 댓글 리스트를 반환한다.") @Test void givenArticleId_whenSearchingArticleComments_thenReturnsArticleComments() { @@ -60,12 +64,15 @@ class ArticleCommentServiceTest { // Given ArticleCommentDto dto = createArticleCommentDto("댓글"); given(articleRepository.getReferenceById(dto.articleId())).willReturn(createArticle()); + given(userAccountRepository.getReferenceById(dto.userAccountDto().userId())).willReturn(createUserAccount()); + given(articleCommentRepository.save(any(ArticleComment.class))).willReturn(null); // When sut.saveArticleComment(dto); // Then then(articleRepository).should().getReferenceById(dto.articleId()); + then(userAccountRepository).should().getReferenceById(dto.userAccountDto().userId()); then(articleCommentRepository).should().save(any(ArticleComment.class)); } @@ -81,6 +88,7 @@ class ArticleCommentServiceTest { // Then then(articleRepository).should().getReferenceById(dto.articleId()); + then(userAccountRepository).shouldHaveNoInteractions(); then(articleCommentRepository).shouldHaveNoInteractions(); }