공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.
지난 시간까지 서비스 로직을 먼저 구현했다.
이번에는 그것에 맞춰 컨트롤러를 구현하는 작업을 진행하고, 구체적인 데이터를 다루는 컨트롤러와 뷰 템플릿을 수정하여 실제 데이터를 전달하고 렌더링하는 작업을 진행한다.
게시판 컨트롤러 구현
기존 코드
package org.example.projectboard.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@RequestMapping("/articles")
@Controller
public class ArticleController {
@GetMapping
public String articles(ModelMap map) {
map.addAttribute("articles", List.of());
return "articles/index";
}
@GetMapping("/{articleId}")
public String article(@PathVariable("articleId") Long articleId, ModelMap map) {
map.addAttribute("article", "article"); //TODO: 구현할 때 여기를 실제 데이터로 넣어줘야 함
map.addAttribute("articleComments", List.of());
return "articles/detail";
}
}
- 이 코드에서는 기본적인 페이지를 렌더링하는 컨트롤러가 구현되어 있다.
- 하지만 아직 실제 데이터를 바인딩하거나 검색 및 페이지네이션 기능이 구현되어 있지 않다.
- articles 메서드는 기본적인 게시글 리스트 페이지를, article 메서드는 특정 게시글의 상세 페이지를 반환한다.
수정된 코드
이 코드에서는 ArticleService를 통해 데이터를 받아와 페이지에 전달하는 부분이 추가되었다.
package org.example.projectboard.controller;
import lombok.RequiredArgsConstructor;
import org.example.projectboard.domain.type.SearchType;
import org.example.projectboard.dto.response.ArticleResponse;
import org.example.projectboard.dto.response.ArticleWithCommentsResponse;
import org.example.projectboard.service.ArticleService;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@RequiredArgsConstructor
@RequestMapping("/articles")
@Controller
public class ArticleController {
private final ArticleService articleService;
@GetMapping
public String articles(
@RequestParam(required = false) SearchType searchType,
@RequestParam(required = false) String searchValue,
@PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
ModelMap map
) {
map.addAttribute("articles", articleService.searchArticles(searchType, searchValue, pageable).map(ArticleResponse::from));
return "articles/index";
}
@GetMapping("/{articleId}")
public String article(@PathVariable("articleId") Long articleId, ModelMap map) {
ArticleWithCommentsResponse article = ArticleWithCommentsResponse.from(articleService.getArticle(articleId));
map.addAttribute("article", article);
map.addAttribute("articleComments", List.of(article.articleCommentsResponse()));
return "articles/detail";
}
}
- articles 메서드는 기본 게시글 리스트 페이지를 렌더링한다.
- searchType과 searchValue를 통해 검색 기능을 지원하며, Pageable을 통해 페이지네이션을 처리한다.
- 검색 결과로 반환된 Page<ArticleDto>를 ArticleResponse로 변환하여 뷰 템플릿에 전달한다.
- article 메서드는 특정 게시글의 상세 페이지를 렌더링한다.
- ArticleWithCommentsResponse 객체를 통해 게시글과 댓글을 포함한 상세 데이터를 전달한다.
- articleCommentsResponse를 통해 댓글 목록도 함께 전달된다.
컨트롤러 테스트 코드
package org.example.projectboard.controller;
import org.example.projectboard.config.SecurityConfig;
import org.example.projectboard.dto.ArticleWithCommentsDto;
import org.example.projectboard.dto.UserAccountDto;
import org.example.projectboard.service.ArticleService;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers;
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.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.time.LocalDateTime;
import java.util.Set;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@DisplayName("View 컨트롤러 - 게시글")
@Import(SecurityConfig.class)
@WebMvcTest(ArticleController.class)
class ArticleControllerTest {
private final MockMvc mvc;
@MockBean private ArticleService articleService;
ArticleControllerTest(@Autowired MockMvc mvc) {
this.mvc = mvc;
}
@DisplayName("[view][GET] 게시글 리스트 (게시판) 페이지 - 정상 호출")
@Test
public void givenNothing_whenRequestingArticlesView_thenReturnsArticlesView() throws Exception {
// given
given(articleService.searchArticles(eq(null), ArgumentMatchers.eq(null), any(Pageable.class))).willReturn(Page.empty());
// when & then
mvc.perform(get("/articles"))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
.andExpect(view().name("articles/index"))
.andExpect(model().attributeExists("articles"));
then(articleService).should().searchArticles(eq(null), ArgumentMatchers.eq(null), any(Pageable.class));
}
@DisplayName("[view][GET] 게시글 상세 페이지 - 정상 호출")
@Test
public void givenNothing_whenRequestingArticleView_thenReturnsArticleView() throws Exception {
// given
Long articleId = 1L;
given(articleService.getArticle(articleId)).willReturn(createArticleWithCommentsDto());
// when & then
mvc.perform(get("/articles/" + articleId))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
.andExpect(view().name("articles/detail"))
.andExpect(model().attributeExists("article"))
.andExpect(model().attributeExists("articleComments"));
then(articleService).should().getArticle(articleId);
}
...
private ArticleWithCommentsDto createArticleWithCommentsDto() {
return ArticleWithCommentsDto.of(
1L,
createUserAccountDto(),
Set.of(),
"title",
"content",
"#java",
LocalDateTime.now(),
"eunchan",
LocalDateTime.now(),
"eunchan"
);
}
private UserAccountDto createUserAccountDto() {
return UserAccountDto.of(
1L,
"eunchan",
"pw",
"eunchan@mail.com",
"eunchan",
"memo",
LocalDateTime.now(),
"eunchan",
LocalDateTime.now(),
"eunchan"
);
}
}
- 게시글 리스트 페이지 테스트 (givenNothing_whenRequestingArticlesView_thenReturnsArticlesView):
- given 구문에서 articleService.searchArticles()의 반환값을 빈 페이지로 모킹하여, 서비스 호출 시 특정 데이터를 반환하도록 설정하였다.
- 이후 MockMvc를 사용해 요청을 보내고, 정상적으로 뷰와 모델이 반환되는지 확인한다.
- 게시글 상세 페이지 테스트 (givenNothing_whenRequestingArticleView_thenReturnsArticleView):
- given 구문에서 articleService.getArticle()을 호출할 때, ArticleWithCommentsDto를 반환하도록 설정하였다.
- MockMvc를 통해 /articles/1 요청을 보내고, 상세 페이지가 올바르게 렌더링되며, 필요한 데이터가 모델에 포함되어 있는지 검증한다.
뷰 템플릿 구현
기존코드
이 코드에서는 게시글 데이터가 하드코딩되어 있으며, 동적으로 데이터가 출력되지 않는다.
<tbody>
<tr>
<td>첫 글</td>
<td>#java</td>
<td>Eunchan</td>
<td>2024-08-06</td>
</tr>
...
</tbody>
수정된 코드
수정된 뷰 템플릿에서는 동적으로 데이터를 받아와 테이블에 출력한다.
<?xml version="1.0"?>
<thlogic>
<attr sel = "#header" th:replace="header :: header"/>
<attr sel = "#footer" th:replace="footer :: footer"/>
<attr sel="#article-table">
<attr sel="tbody" th:remove="all-but-first">
<attr sel="tr[0]" th:each="article : ${articles}">
<attr sel="td.title/a" th:text="${article.title}" th:href="@{'/articles/' + ${article.id}}" />
<attr sel="td.hashtag" th:text="${article.hashtag}" />
<attr sel="td.user-id" th:text="${article.nickname}" />
<attr sel="td.created-at/time" th:datetime="${article.createdAt}" th:text="${#temporals.format(article.createdAt, 'yyyy-MM-dd')}" />
</attr>
</attr>
</attr>
</thlogic>
- th:each를 사용하여 컨트롤러에서 전달된 articles 데이터를 순회하며 각 게시글의 제목, 해시태그, 작성자, 작성일을 출력한다.
- th:href를 사용해 각 게시글의 제목에 링크를 걸어, 클릭 시 해당 게시글의 상세 페이지로 이동할 수 있게 한다.
- #temporals.format을 사용하여 작성일을 포맷팅하여 표시한다.
지금까지 코드가 반영된 화면은 다음과 같다.
이번 포스팅에서는 게시판의 컨트롤러와 뷰 템플릿을 구현하며, 서비스 로직과 데이터 바인딩에 대해 자세히 알아보았다.
컨트롤러에서의 데이터 처리와 뷰로의 전달 과정을 통해 실제 애플리케이션이 어떻게 동작하는지 이해할 수 있었다.
'BackEnd > Project' 카테고리의 다른 글
[Board] Ch03. 게시판 페이징 구현 (0) | 2024.08.10 |
---|---|
[Board] Ch03. 게시글 페이지 기능 구현 (0) | 2024.08.10 |
[Board] Ch03. 게시판 페이지 기능 구현(1) (0) | 2024.08.09 |
[Board] Ch03. 게시글 페이지 기능 테스트 정의(2) (0) | 2024.08.09 |
[Board] Ch03. 게시글 페이지 기능 테스트 정의(1) (0) | 2024.08.09 |