#16 board : article list page - search(with paging, sort)
This commit is contained in:
@@ -38,6 +38,7 @@ public class ArticleController {
|
||||
|
||||
map.addAttribute("articles", articles);
|
||||
map.addAttribute("paginationBarNumbers", barNumbers);
|
||||
map.addAttribute("searchTypes", SearchType.values());
|
||||
|
||||
return "articles/index";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
package com.example.board.domain.type;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public enum SearchType {
|
||||
TITLE, CONTENT, ID, NICKNAME, HASHTAG
|
||||
TITLE("제목"),
|
||||
CONTENT("본문"),
|
||||
ID("유저 ID"),
|
||||
NICKNAME("닉네임"),
|
||||
HASHTAG("해시태그")
|
||||
;
|
||||
|
||||
@Getter
|
||||
private final String description;
|
||||
|
||||
SearchType(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
<div class="row">
|
||||
<div class="card card-margin search-form">
|
||||
<div class="card-body p-0">
|
||||
<form id="card search-form">
|
||||
<form action="/articles" method="get">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-lg-3 col-md-3 col-sm-12 p-0">
|
||||
<label for="search-type" hidden>검색 유형</label>
|
||||
<select class="form-control" id="search-type">
|
||||
<select class="form-control" id="search-type" name="searchType">
|
||||
<option>제목</option>
|
||||
<option>본문</option>
|
||||
<option>id</option>
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
<div class="col-lg-8 col-md-6 col-sm-12 p-0">
|
||||
<label for="search-value" hidden>검색어</label>
|
||||
<input type="text" placeholder="검색어..." class="form-control" id="search-value" name="search-value">
|
||||
<input type="text" placeholder="검색어..." class="form-control" id="search-value" name="searchValue">
|
||||
</div>
|
||||
<div class="col-lg-1 col-md-3 col-sm-12 p-0">
|
||||
<button type="submit" class="btn btn-base">
|
||||
|
||||
@@ -4,23 +4,42 @@
|
||||
<attr sel="#footer" th:replace="footer :: footer" />
|
||||
|
||||
<attr sel="main" th:object="${articles}">
|
||||
|
||||
<attr sel="#search-type" th:remove="all-but-first">
|
||||
<attr sel="option[0]"
|
||||
th:each="searchType : ${searchTypes}"
|
||||
th:value="${searchType.name}"
|
||||
th:text="${searchType.description}"
|
||||
th:selected="${param.searchType != null && (param.searchType.toString == searchType.name)}"
|
||||
/>
|
||||
</attr>
|
||||
<attr sel="#search-value" th:value="${param.searchValue}" />
|
||||
|
||||
<attr sel="#article-table">
|
||||
<attr sel="thead/tr">
|
||||
<attr sel="th.title/a" th:text="'제목'" th:href="@{/articles(
|
||||
page=${articles.number},
|
||||
sort='title' + (*{sort.getOrderFor('title')} != null ? (*{sort.getOrderFor('title').direction.name} != 'DESC' ? ',desc' : '') : '')
|
||||
sort='title' + (*{sort.getOrderFor('title')} != null ? (*{sort.getOrderFor('title').direction.name} != 'DESC' ? ',desc' : '') : ''),
|
||||
searchType=${param.searchType},
|
||||
searchValue=${param.searchValue}
|
||||
)}"/>
|
||||
<attr sel="th.hashtag/a" th:text="'해시태그'" th:href="@{/articles(
|
||||
page=${articles.number},
|
||||
sort='hashtag' + (*{sort.getOrderFor('hashtag')} != null ? (*{sort.getOrderFor('hashtag').direction.name} != 'DESC' ? ',desc' : '') : '')
|
||||
sort='hashtag' + (*{sort.getOrderFor('hashtag')} != null ? (*{sort.getOrderFor('hashtag').direction.name} != 'DESC' ? ',desc' : '') : ''),
|
||||
searchType=${param.searchType},
|
||||
searchValue=${param.searchValue}
|
||||
)}"/>
|
||||
<attr sel="th.user-id/a" th:text="'작성자'" th:href="@{/articles(
|
||||
page=${articles.number},
|
||||
sort='userAccount.userId' + (*{sort.getOrderFor('userAccount.userId')} != null ? (*{sort.getOrderFor('userAccount.userId').direction.name} != 'DESC' ? ',desc' : '') : '')
|
||||
sort='userAccount.userId' + (*{sort.getOrderFor('userAccount.userId')} != null ? (*{sort.getOrderFor('userAccount.userId').direction.name} != 'DESC' ? ',desc' : '') : ''),
|
||||
searchType=${param.searchType},
|
||||
searchValue=${param.searchValue}
|
||||
)}"/>
|
||||
<attr sel="th.created-at/a" th:text="'작성일'" th:href="@{/articles(
|
||||
page=${articles.number},
|
||||
sort='createdAt' + (*{sort.getOrderFor('createdAt')} != null ? (*{sort.getOrderFor('createdAt').direction.name} != 'DESC' ? ',desc' : '') : '')
|
||||
sort='createdAt' + (*{sort.getOrderFor('createdAt')} != null ? (*{sort.getOrderFor('createdAt').direction.name} != 'DESC' ? ',desc' : '') : ''),
|
||||
searchType=${param.searchType},
|
||||
searchValue=${param.searchValue}
|
||||
)}"/>
|
||||
</attr>
|
||||
|
||||
@@ -37,19 +56,19 @@
|
||||
<attr sel="#pagination">
|
||||
<attr sel="li[0]/a"
|
||||
th:text="'previous'"
|
||||
th:href="@{/articles(page=${articles.number - 1})}"
|
||||
th:href="@{/articles(page=${articles.number - 1}, searchType=${param.searchType}, searchValue=${param.searchValue})}"
|
||||
th:class="'page-link' + (${articles.number} <= 0 ? ' disabled' : '')"
|
||||
/>
|
||||
<attr sel="li[1]" th:class="page-item" th:each="pageNumber : ${paginationBarNumbers}">
|
||||
<attr sel="a"
|
||||
th:text="${pageNumber + 1}"
|
||||
th:href="@{/articles(page=${pageNumber})}"
|
||||
th:href="@{/articles(page=${pageNumber}, searchType=${param.searchType}, searchValue=${param.searchValue})}"
|
||||
th:class="'page-link' + (${pageNumber} == ${articles.number} ? ' disabled' : '')"
|
||||
/>
|
||||
</attr>
|
||||
<attr sel="li[2]/a"
|
||||
th:text="'next'"
|
||||
th:href="@{/articles(page=${articles.number + 1})}"
|
||||
th:href="@{/articles(page=${articles.number + 1}, searchType=${param.searchType}, searchValue=${param.searchValue})}"
|
||||
th:class="'page-link' + (${articles.number} >= ${articles.totalPages - 1} ? ' disabled' : '')"
|
||||
/>
|
||||
</attr>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.example.board.controller;
|
||||
|
||||
import com.example.board.config.SecurityConfig;
|
||||
import com.example.board.domain.type.SearchType;
|
||||
import com.example.board.dto.ArticleWithCommentsDto;
|
||||
import com.example.board.dto.UserAccountDto;
|
||||
import com.example.board.service.ArticleService;
|
||||
@@ -65,9 +66,35 @@ class ArticleControllerTest {
|
||||
then(paginationService).should().getPaginationBarNumbers(anyInt(), anyInt());
|
||||
}
|
||||
|
||||
@DisplayName("[view][GET] 게시글 리스트(게시판) 페이지 - 검색어와 함께 호출")
|
||||
@Test
|
||||
public void givenSearchKeyword_whenSearchingArticlesView_thenReturnsArticlesView() throws Exception {
|
||||
// given
|
||||
SearchType searchType = SearchType.TITLE;
|
||||
String searchValue = "title";
|
||||
given(articleService.searchArticles(eq(searchType), eq(searchValue), any(Pageable.class))).willReturn(Page.empty());
|
||||
given(paginationService.getPaginationBarNumbers(anyInt(), anyInt())).willReturn(List.of(0, 1, 2, 3 ,4));
|
||||
|
||||
// when & then
|
||||
mockMvc.perform(
|
||||
get("/articles")
|
||||
.queryParam("searchType", searchType.name())
|
||||
.queryParam("searchValue", searchValue)
|
||||
)
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
|
||||
.andExpect(view().name("articles/index"))
|
||||
.andExpect(model().attributeExists("articles"))
|
||||
.andExpect(model().attributeExists("searchTypes"))
|
||||
;
|
||||
|
||||
then(articleService).should().searchArticles(eq(searchType), eq(searchValue), any(Pageable.class));
|
||||
then(paginationService).should().getPaginationBarNumbers(anyInt(), anyInt());
|
||||
}
|
||||
|
||||
@DisplayName("[view][GET] 게시글 리스트 (게시판) 페이지 - 페이징, 정렬 기능")
|
||||
@Test
|
||||
void givenPagingAndSortingParams_whenSearchingArticlesPage_thenReturnsArticlesPage() throws Exception {
|
||||
void givenPagingAndSortingParams_whenSearchingArticlesPage_thenReturnsArticlesView() throws Exception {
|
||||
// Given
|
||||
String sortName = "title";
|
||||
String direction = "desc";
|
||||
|
||||
Reference in New Issue
Block a user