고전적인 방식의 페이징
- SQL의 limit과 offset 사용하기
스프링 데이터 JPA가 제공하는 Pageable 사용하기
- page와 size
- sort도 지원한다.
- 기본값 설정하는 방법 @PageableDefault
이전에 empty 컬렉션을 Model에 넣을 때 발생했던 버그(?)에 대하여...
- 사실 버그가 아니라 스프링 MVC의 정해진 동작 방식.(이상하긴 하지만..)
- 우회하려면 이름을 반드시 줄 것.
페이징 적용을 확인하기 위해 /study/data라는 요청을 받는 핸들러로 임시로 여러개의 스터디를 삽입 할 수 있게 코드를 생성한다.(생성 후 제거)
package me.weekbelt.studyolle.modules.study;
@RequiredArgsConstructor
@Controller
public class StudyController {
// .........
// 임시로 스터디 데이터를 넣어주기위한 핸들러(나중에 삭제 예정)
@GetMapping("/study/data")
public String generateTestData(@CurrentAccount Account account) {
studyService.generateTestStudies(account);
return "redirect:/";
}
}
package me.weekbelt.studyolle.modules.study;
@RequiredArgsConstructor
@Transactional
@Service
public class StudyService {
// ...........
// 테스트 데이터를 만들기 위한 임시 로직(삭제 예정)
public void generateTestStudies(Account account) {
for (int i = 0; i < 30; i++) {
String randomValue = RandomString.make(5);
Study study = Study.builder()
.title("테스트 스터디 " + randomValue)
.path("test-" + randomValue)
.shortDescription("테스트용 스터디 입니다.")
.fullDescription("test")
.tags(new HashSet<>())
.managers(new HashSet<>())
.build();
study.publish();;
Study newStudy = this.createNewStudy(study, account);
Tag jpa = tagRepository.findByTitle("JPA");
newStudy.getTags().add(jpa);
}
}
}
페이징을 적용하기 위해 코드 리팩토링
package me.weekbelt.studyolle.modules.study;
@Transactional(readOnly = true)
public interface StudyRepositoryExtension {
Page<Study> findByKeyword(String keyword, Pageable pageable);
}
package me.weekbelt.studyolle.modules.study;
public class StudyRepositoryExtensionImpl extends QuerydslRepositorySupport implements StudyRepositoryExtension{
// .........
@Override
public Page<Study> findByKeyword(String keyword, Pageable pageable) {
QStudy study = QStudy.study;
JPQLQuery<Study> query = from(study).where(study.published.isTrue()
.and(study.title.containsIgnoreCase(keyword))
.or(study.tags.any().title.containsIgnoreCase(keyword))
.or(study.zones.any().localNameOfCity.containsIgnoreCase(keyword)))
.leftJoin(study.tags, QTag.tag).fetchJoin()
.leftJoin(study.zones, QZone.zone).fetchJoin()
.leftJoin(study.members, QAccount.account).fetchJoin()
.distinct();
JPQLQuery<Study> pageableQuery = getQuerydsl().applyPagination(pageable, query);
QueryResults<Study> fetchResults = pageableQuery.fetchResults();
return new PageImpl<>(fetchResults.getResults(), pageable, fetchResults.getTotal());
}
}
package me.weekbelt.studyolle.modules.main;
@RequiredArgsConstructor
@Controller
public class MainController {
// .......
@GetMapping("/search/study") // TODO: size, page, sort를 Pageable에 바인딩
public String searchStudy(String keyword, Model model,
@PageableDefault(size = 9, sort = "publishedDateTime", direction = Sort.Direction.DESC)
Pageable pageable) {
Page<Study> studyPage = studyRepository.findByKeyword(keyword, pageable);
model.addAttribute("studyPage", studyPage);
model.addAttribute("keyword", keyword);
return "search";
}
}
뷰 리팩토링
search.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments.html :: head"></head>
<body class="bg-light">
<div th:replace="fragments.html :: main-nav"></div>
<div class="container">
<div class="py-5 text-center">
<p class="lead" th:if="${studyPage.getTotalElements() == 0}">
<strong th:text="${keyword}" id="keyword" class="context"></strong>에 해당하는 스터디가 없습니다.
</p>
<p class="lead" th:if="${studyPage.getTotalElements() > 0}">
<strong th:text="${keyword}" id="keyword" class="context"></strong>에 해당하는 스터디를
<span th:text="${studyPage.getTotalElements()}"></span>개
찾았습니다.
</p>
<div class="dropdown">
<button class="btn btn-light dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
검색 결과 정렬 방식
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" th:classappend="${#strings.equals(sortProperty, 'publishedDateTime')}? active"
th:href="@{'/search/study?sort=publishedDateTime,desc&keyword=' + ${keyword}}">
스터디 공개일
</a>
<a class="dropdown-item" th:classappend="${#strings.equals(sortProperty, 'memberCount')}? active"
th:href="@{'/search/study?sort=memberCount,desc&keyword=' + ${keyword}}">
멤버수
</a>
</div>
</div>
</div>
<div class="row justify-content-center">
<div th:replace="fragments.html :: study-list (studyList=${studyPage.getContent()})"></div>
</div>
<div class="row justify-content-center">
<div class="col-sm-10">
<nav>
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${!studyPage.hasPrevious()}? disabled">
<a th:href="@{'/search/study?keyword=' + ${keyword} + '&sort=' + ${sortProperty} + ',desc&page=' + ${studyPage.getNumber() - 1}}"
class="page-link" tabindex="-1" aria-disabled="true">
Previous
</a>
</li>
<li class="page-item" th:classappend="${i == studyPage.getNumber()}? active"
th:each="i: ${#numbers.sequence(0, studyPage.getTotalPages() - 1)}">
<a th:href="@{'/search/study?keyword=' + ${keyword} + '&sort=' + ${sortProperty} + ',desc&page=' + ${i}}"
class="page-link" href="#" th:text="${i + 1}">1</a>
</li>
<li class="page-item" th:classappend="${!studyPage.hasNext()}? disabled">
<a th:href="@{'/search/study?keyword=' + ${keyword} + '&sort=' + ${sortProperty} + ',desc&page=' + ${studyPage.getNumber() + 1}}"
class="page-link">
Next
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div th:replace="fragments.html :: footer"></div>
<script th:replace="fragments.html :: date-time"></script>
<script src="/node_modules/mark.js/dist/jquery.mark.min.js"></script>
<script type="application/javascript">
$(function(){
var mark = function() {
// Read the keyword
var keyword = $("#keyword").text();
// Determine selected options
var options = {
"each": function(element) {
setTimeout(function() {
$(element).addClass("animate");
}, 150);
}
};
// Mark the keyword inside the context
$(".context").unmark({
done: function() {
$(".context").mark(keyword, options);
}
});
};
mark();
});
</script>
</body>
</html>
fragments.html에 study-list fragment추가
<div th:fragment="study-list (studyList)" class="col-sm-12">
<div class="row">
<div class="col-md-4" th:each="study: ${studyList}">
<div class="card mb-4 shadow-sm">
<img th:src="${study.image}" class="card-img-top" th:alt="${study.title}" >
<div class="card-body">
<a th:href="@{'/study/' + ${study.path}}" class="text-decoration-none">
<h5 class="card-title context" th:text="${study.title}"></h5>
</a>
<p class="card-text" th:text="${study.shortDescription}">Short description</p>
<p class="card-text context">
<span th:each="tag: ${study.tags}" class="font-weight-light text-monospace badge badge-pill badge-info mr-3">
<a th:href="@{'/search/tag/' + ${tag.title}}" class="text-decoration-none text-white">
<i class="fa fa-tag"></i> <span th:text="${tag.title}">Tag</span>
</a>
</span>
<span th:each="zone: ${study.zones}" class="font-weight-light text-monospace badge badge-primary mr-3">
<a th:href="@{'/search/zone/' + ${zone.id}}" class="text-decoration-none text-white">
<i class="fa fa-globe"></i> <span th:text="${zone.localNameOfCity}" class="text-white">City</span>
</a>
</span>
</p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
<i class="fa fa-user-circle"></i>
<span th:text="${study.memberCount}"></span>명
</small>
<small class="text-muted date" th:text="${study.publishedDateTime}">9 mins</small>
</div>
</div>
</div>
</div>
</div>
</div>
참고: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1#
'스프링과 JPA 기반 웹 어플리케이션 개발 > 8부 검색 및 첫 페이지' 카테고리의 다른 글
83. 로그인 한 사용자를 위한 첫 화면 (0) | 2020.05.18 |
---|---|
82. 로그인 하지 않은 사용자를 위한 첫 화면 (0) | 2020.05.18 |
81. 페이징 뷰 개선 (0) | 2020.05.18 |
79. N+1 Select 문제 해결 (0) | 2020.05.17 |
78. 검색 기능 구현 (0) | 2020.05.17 |