[게시판 프로젝트] 게시판 검색 구현
2024. 8. 26. 19:47ㆍCS/프로젝트
SearchType enum 클래스
ArticleController - 검색관련 SearchType 데이터 ModelMap에 추가
Before
@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
){
Page<ArticleResponseV1> articles = articleService.searchArticles(searchType,searchValue, pageable).map(ArticleResponseV1::from);
List<Integer> barNumbers = paginationService.getPaginationBarNumbers(pageable.getPageNumber(),articles.getTotalPages());
map.addAttribute("articles",articles);
map.addAttribute("paginationBarNumbers",barNumbers);
return "articles/index";
}
After
@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
){
Page<ArticleResponseV1> articles = articleService.searchArticles(searchType,searchValue, pageable).map(ArticleResponseV1::from);
List<Integer> barNumbers = paginationService.getPaginationBarNumbers(pageable.getPageNumber(),articles.getTotalPages());
map.addAttribute("articles",articles);
map.addAttribute("paginationBarNumbers",barNumbers);
map.addAttribute("searchTypes",SearchType.values()); // 검색
return "articles/index";
}
뷰 수정 - index.html/ index.th.xm
Before
<div class="row">
<div class="card card-margin search-form">
<div class="card-body p-0">
<form id="search-form">
<div class="row">
<div class="col-12">
<div class="row no-gutters">
<?xml version="1.0"?>
<thlogic>
<attr sel="#header" th:replace="header :: header"/>
<attr sel="#footer" th:replace="footer :: footer"/>
<attr sel="main" th:object="${articles}">
<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' : '') : '')
)}"/>
<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' : '') : '')
)}"/>
<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' : '') : '')
)}"/>
<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' : '') : '')
)}"/>
</attr>
<attr sel="tbody" th:remove="all-but-first"> <!-- tbody의 첫번째만 남기고 전부 지운다 -->
<attr sel="tr[0]" th:each="article : ${articles}"> <!-- tr의 0번째부터 순회하며 아래 요소에 대한 작업 진행-->
<attr sel="td.title/a" th:text="${article.title}" th:href="@{'/articles/' + ${article.id}}" /> <!-- a 부분에 다음과 같은 양식으로 하이퍼링크 작성-->
<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>
<attr sel="#pagination">
<attr sel="li[0]/a"
th:text="'previous'"
th:href="@{/articles(page=${articles.number - 1})}"
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:class="'page-link' + (${pageNumber} == ${articles.number} ? ' disabled' : '')"
/>
</attr>
<attr sel="li[2]/a"
th:text="'next'"
th:href="@{/articles(page=${articles.number + 1})}"
th:class="'page-link' + (${articles.number} >= ${articles.totalPages - 1} ? ' disabled' : '')"
/>
</attr>
</attr>
</thlogic>
After
<div class="row">
<div class="card card-margin search-form">
<div class="card-body p-0">
<form action="/articles" method="get">
<div class="row">
<div class="col-12">
<div class="row no-gutters">
<?xml version="1.0"?>
<thlogic>
<attr sel="#header" th:replace="header :: header"/>
<attr sel="#footer" th:replace="footer :: footer"/>
<attr sel="main" th:object="${articles}">
<!-- search-type-->
<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)}"
/>
<!--param : 현재 GetParameter 에서 searchType 을 꺼내온 것
직전 검색 했던 내용은 url Parameter 에 남아있을 것임
지금 option dropdown 메뉴의 이름과 같은지 확인 후 true false 넣어주겠다는 뜻
즉, 계속 같은 searchType 으로 연속으로 검색하고 싶은 사람을 배려해서 다음 검색 결과에 반영될 수 있도록
직전 검색 항목만 true 가 되어있어서 화면에 표현할 수 있음
paramMap 에서 꺼낸 searchType 은 enum 이 보장되지 않기 때문에, name 으로 호출 불가해서 toString 으로 호출-->
</attr>
<!-- search-value-->
<attr sel="#search-value" th:value="${param.searchValue}" />
<!--직전에 검색했던 검색어를 유지하기 위해서-->
<!-- article-table-->
<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' : '') : ''),
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' : '') : ''),
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' : '') : ''),
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' : '') : ''),
searchType=${param.searchType},
searchValue=${param.searchValue}
)}"/>
</attr>
<attr sel="tbody" th:remove="all-but-first"> <!-- tbody의 첫번째만 남기고 전부 지운다 -->
<attr sel="tr[0]" th:each="article : ${articles}"> <!-- tr의 0번째부터 순회하며 아래 요소에 대한 작업 진행-->
<attr sel="td.title/a" th:text="${article.title}" th:href="@{'/articles/' + ${article.id}}" /> <!-- a 부분에 다음과 같은 양식으로 하이퍼링크 작성-->
<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>
<!-- 페이지네이션-->
<attr sel="#pagination">
<attr sel="li[0]/a"
th:text="'previous'"
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},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},searchType=${param.searchType}, searchValue=${param.searchValue})}"
th:class="'page-link' + (${articles.number} >= ${articles.totalPages - 1} ? ' disabled' : '')"
/>
</attr>
</attr>
</thlogic>
index.th.html 코드 분석
1. 파일의 구조
<?xml version="1.0"?> <thlogic> ... </thlogic>
- <?xml version="1.0"?>: 이 파일이 XML 형식으로 작성되었음을 나타냅니다.
- <thlogic>: 이 태그는 실제로 Thymeleaf에서 사용되지 않지만, 여기서는 코드 블록을 감싸기 위한 용도로 사용된 것으로 보입니다. 실제 Thymeleaf 템플릿 파일에서는 <html> 태그로 시작하게 됩니다.
2. 헤더와 푸터 삽입
<attr sel="#header" th:replace="header :: header"/>
<attr sel="#footer" th:replace="footer :: footer"/>
- <attr sel="#header" 및 <attr sel="#footer": 이 태그들은 헤더와 푸터의 HTML 요소를 선택하고, 해당 위치에 다른 템플릿 파일(예: header.html, footer.html)의 특정 부분을 삽입하는 역할을 합니다.
- th:replace="header :: header": header.html 파일 내에 있는 header라는 이름의 fragment(조각)를 현재 위치에 삽입합니다.
- th:replace="footer :: footer": footer.html 파일 내의 footer라는 이름의 fragment를 현재 위치에 삽입합니다.
3. 주 콘텐츠 영역 설정
<attr sel="main" th:object="${articles}">
- <attr sel="main": <main> 태그를 선택하고, 해당 영역에서 작업을 수행합니다.
- th:object="${articles}": articles라는 객체를 이 영역에 바인딩하여 사용할 수 있게 합니다. 이 객체는 게시글 목록을 포함하고 있을 것입니다.
4. 검색 타입 드롭다운 메뉴
<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>
- th:remove="all-but-first": 선택된 요소(즉, #search-type)의 자식 요소 중 첫 번째 요소를 제외한 모든 요소를 제거합니다.
- th:each="searchType : ${searchTypes}": searchTypes 리스트를 반복(iterate)하면서, 각각의 searchType객체에 대해 <option> 태그를 생성합니다.
- th:value="${searchType.name}": 각 searchType 객체의 name 속성을 <option> 태그의 value로 설정합니다.
- th:text="${searchType.description}": 각 searchType 객체의 description 속성을 <option> 태그의 텍스트로 설정합니다.
- th:selected="${param.searchType != null && (param.searchType.toString == searchType.name)}": 현재 선택된 검색 타입과 일치하는 경우, 해당 <option>을 선택된 상태로 표시합니다.
5. 검색어 입력 필드
<attr sel="#search-value" th:value="${param.searchValue}" />
- th:value="${param.searchValue}": 검색어 입력 필드에 이전에 입력된 검색어(searchValue)를 유지시킵니다.
6. 게시글 테이블
<attr sel="#article-table">
<attr sel="thead/tr">
<attr sel="th.title/a" th:text="'제목'" th:href="@{/articles(...)"/>
<attr sel="th.hashtag/a" th:text="'해시태그'" th:href="@{/articles(...)"/>
<attr sel="th.user-id/a" th:text="'작성자'" th:href="@{/articles(...)"/>
<attr sel="th.created-at/a" th:text="'작성일'" th:href="@{/articles(...)"/>
</attr>
- <attr sel="thead/tr">: 테이블 헤더에서 <tr> 태그를 선택하여 내부의 <th> 태그들에 대한 작업을 수행합니다.
- th:text="'제목'": 해당 <th> 태그의 텍스트를 "제목"으로 설정합니다.
- th:href="@{/articles(...)}": 테이블 헤더의 각 항목(제목, 해시태그, 작성자, 작성일)에 대해 정렬을 수행할 수 있도록 링크를 설정합니다. 각 링크는 현재 페이지와 검색 조건을 유지한 채로 정렬 기준만 변경합니다.
7. 게시글 목록 표시
<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>
- th:remove="all-but-first": <tbody>의 첫 번째 행을 제외한 나머지 행을 제거합니다. 이후 첫 번째 행을 템플릿으로 사용하여 반복 렌더링됩니다.
- th:each="article : ${articles}": articles 리스트를 반복하면서 각 article 객체에 대해 <tr> 태그를 생성합니다.
- th:text="${article.title}" 및 th:href="@{'/articles/' + ${article.id}}": 각 게시글의 제목과 링크를 설정합니다.
- th:text="${article.hashtag}": 각 게시글의 해시태그를 표시합니다.
- th:text="${article.nickname}": 각 게시글의 작성자 닉네임을 표시합니다.
- th:datetime="${article.createdAt}" 및 th:text="${#temporals.format(article.createdAt, 'yyyy-MM-dd')}": 각 게시글의 작성일을 형식화하여 표시합니다.
8. 페이지네이션
<attr sel="#pagination">
<attr sel="li[0]/a"
th:text="'previous'"
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},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},searchType=${param.searchType}, searchValue=${param.searchValue})}"
th:class="'page-link' + (${articles.number} >= ${articles.totalPages - 1} ? ' disabled' : '')"
/>
</attr>
- <attr sel="li[0]/a": 이전 페이지 링크를 설정합니다. 현재 페이지가 첫 페이지인 경우 링크를 비활성화(disabled)합니다.
- <attr sel="li[1]" th:each="pageNumber : ${paginationBarNumbers}": 페이지 번호를 반복하여 각 페이지 번호에 대한 링크를 생성합니다. 현재 페이지 번호는 비활성화(disabled)합니다.
- <attr sel="li[2]/a": 다음 페이지 링크를 설정합니다. 현재 페이지가 마지막 페이지인 경우 링크를 비활성화(disabled)합니다.