조회수 증가
이번엔 게시글의 조회수를 구현해보도록 하겠습니다. (간단합니다!)
BoardRepository 클래스
public interface BoardRepository extends JpaRepository<Board, Long> {
@Modifying
@Query("update Board p set p.count = p.count + 1 where p.id = :id")
int updateCount(Long id);
}
수정 반영을 위한 @Modifying를 사용합니다.
@Modifying에 대해 자세히 알아보려면 clearAutomatically에 대해서 알아보는게 좋을 듯 합니다.
BoardController 클래스
@RequiredArgsConstructor
@Controller
public class BoardController {
private final BoardService boardService;
...
/**
* 글상세 페이지
*/
@GetMapping("/board/{id}")
public String detail(@PathVariable Long id, Model model) {
model.addAttribute("board", boardService.detail(id));
boardService.updateCount(id);
return "layout/board/board-detail";
}
}
boardService.updateCount(id); 이 코드를 추가합니다.
그러면 끝납니다. 이제 View에 뿌려주기만 하면 되죠.
저는 board-detail.html에
<div class="d-flex" style="position: absolute; left: 20px; top: 150px;"><h2 style="margin-right: 10px;">조회수:</h2><h2 th:text="${board.count}"></h2></div>
이런 식으로 뿌려주었습니다.
페이징
BoardService 클래스
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
...
/**
* 글목록 로직
*/
@Transactional(readOnly = true)
public Page<Board> findAll(Pageable pageable) {
return boardRepository.findAll(pageable);
}
}
findAll() 메소드를 변경합니다.
JPA에서는 Pageable 인터페이스를 사용하면 페이징을 만들 수 있습니다. (이미 페이징은 모듈화해서 쓰는 곳이 많다고 하는데 이것을 따로 공부해도 좋을 것 같습니다.)
이 때 리턴타입은 Page
로 변경합니다.
IndexController 클래스
@RequiredArgsConstructor
@Controller
public class IndexController {
private final BoardService boardService;
@GetMapping("/")
public String index(Model model, @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC) Pageable pageable) {
Page<Board> boards = boardService.findAll(pageable);
int startPage = Math.max(1, boards.getPageable().getPageNumber() - 4);
int endPage = Math.min(boards.getTotalPages(), boards.getPageable().getPageNumber() + 4);
model.addAttribute("startPage", startPage);
model.addAttribute("endPage", endPage);
model.addAttribute("boards", boards);
return "index";
}
}
@PageableDefault를 설정하면 페이지의 size, 정렬순을 정할 수 있습니다. 저는 한 페이지당 5 Size, 최신글을 제일 맨위로 볼 수 있게 해두었습니다.
boards.getPageable().getPageNumber() : 현재 페이지 번호
startPage, endPage는 페이지 목록에서 시작 페이지 번호와 끝 페이지 번호입니다.
index.html
<!doctype html>
<html lang="en" class="h-100" xmlns:th="http://www.thymeleaf.org">
<head th:replace="layout/header :: head ('블로그')"></head>
<body class="d-flex flex-column h-100">
<header th:replace="layout/header :: header"></header>
<div class="d-flex" style="position: absolute; left: 20px; top: 70px;"><h2 style="margin-right: 10px;">총 건수:</h2><h2 th:text="${boards.totalElements}"></h2></div>
<main class="flex-shrink-0">
<div class="container">
<div class="p-2"></div>
<div th:each="board : ${boards}" class="row g-0 border rounded overflow-hidden flex-md-row mb-4 shadow-sm h-md-250 position-relative">
<div class="col p-4 d-flex flex-column position-static">
<a th:href="@{/board/{id}(id=${board.id})}" class="a-title">
<h3 class="mb-0 title" style="padding-bottom: 10px;" th:text="${board.title}"></h3>
</a>
<div class="card-text mb-auto" th:text="${board.content}">
</div>
<div class="mb-1 text-muted" style="padding-top: 15px;" th:text="${#temporals.format(board.createdDate, 'yyyy-MM-dd')}"></div>
</div>
</div>
</div>
</main>
<nav aria-label="Page navigation example">
<ul class="pagination">
<li class="page-item" th:classappend="${1 == boards.pageable.pageNumber + 1} ? 'disabled' : '' ">
<a class="page-link" th:href="@{/(page=${boards.pageable.pageNumber - 1})}">Previous</a>
</li>
<li class="page-item" th:classappend="${i == boards.pageable.pageNumber + 1} ? 'active' : '' " th:each="i : ${#numbers.sequence(startPage, endPage)}">
<a class="page-link" th:href="@{/(page=${i - 1})}" th:text="${i}">1</a>
</li>
<li class="page-item" th:classappend="${boards.totalPages == boards.pageable.pageNumber + 1} ? 'disabled' : '' ">
<a class="page-link" th:href="@{/(page=${boards.pageable.pageNumber + 1})}">Next</a>
</li>
</ul>
</nav>
<footer th:replace="layout/footer :: footer"></footer>
<!-- Option 1: Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</body>
</html>
th:each="i : ${#numbers.sequence(startPage, endPage)}" : 시작 페이지부터 끝 페이지까지 Loop를 돕니다.
th:classappend="${i == boards.pageable.pageNumber + 1} ? 'active' : '' "
JPA에서는 페이지 번호가 0부터 시작하므로 1부터 카운트되게 하기 위해 +1을 해줍니다. boards.pageable.pageNumber + 1
그래서 서로 비교를 해서 같으면 'active' 를 추가합니다.
th:href="@{/(page=${i - 1})}"
thymeleaf에서 쿼리스트링을 사용하려면 () 안에 파라미터=${}
이런식으로 값을 넣어주시면 됩니다. 페이지 번호가 0부터 시작하므로 -1을 해줍니다.
검색
검색을 구현하겠습니다.
index.html에 검색 폼을 가져옵니다.
<!doctype html>
<html lang="en" class="h-100" xmlns:th="http://www.thymeleaf.org">
<head th:replace="layout/header :: head ('블로그')"></head>
<body class="d-flex flex-column h-100">
<header th:replace="layout/header :: header"></header>
<div class="d-flex" style="position: relative; top: 30px;"><h2 style="margin-right: 10px;">총 건수:</h2><h2 th:text="${boards.totalElements}"></h2></div>
<form class="d-flex" style="position: relative; top: 40px;" method="get" th:action="@{/}">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search"
id="search" name="search" th:value="${param.search}">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
<main class="flex-shrink-0">
...
</main>
</body>
</html>
form 태그에 method와 action을 줍니다.
input 태그에 id, name 값을 줍니다.
BoardRepository 인터페이스
public interface BoardRepository extends JpaRepository<Board, Long> {
@Modifying
@Query("update Board p set p.count = p.count + 1 where p.id = :id")
int updateCount(Long id);
Page<Board> findByTitleContainingOrContentContaining(String title, String content, Pageable pageable);
}
Containing이라는 키워드를 사용하면 JPA에서 LIKE문으로 실행해준다고 합니다.
BoardService 클래스
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
/**
* 글목록 로직
*/
@Transactional(readOnly = true)
public Page<Board> findByTitleContainingOrContentContaining(String title, String content, Pageable pageable) {
return boardRepository.findByTitleContainingOrContentContaining(title, content, pageable);
}
}
findAll 메소드를 findByTitleContainingOrContentContaining로 변경을 해줍니다.
IndexController 클래스
@RequiredArgsConstructor
@Controller
public class IndexController {
private final BoardService boardService;
@GetMapping("/")
public String index(Model model,
@PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
@RequestParam(required = false, defaultValue = "") String search) {
Page<Board> boards = boardService.findByTitleContainingOrContentContaining(search, search, pageable);
...
return "index";
}
}
IndexController에서도 findByTitleContainingOrContentContaining 메소드로 변경합니다.
@RequestParam(required = false, defaultValue = "")
search 값이 null이어도 동작할 수 있도록 합니다.
여기까지 잘 동작하는 것 같지만 검색하고나서 페이지 번호를 누르면 초기화가 되서 페이지에도 주소값에 search 쿼리스트링을 같이 날립니다.
index.html
<nav aria-label="Page navigation example">
<ul class="pagination">
<li class="page-item" th:classappend="${1 == boards.pageable.pageNumber + 1} ? 'disabled' : '' ">
<a class="page-link" th:href="@{/(page=${boards.pageable.pageNumber - 1}, search=${param.search})}">Previous</a>
</li>
<li class="page-item" th:classappend="${i == boards.pageable.pageNumber + 1} ? 'active' : '' " th:each="i : ${#numbers.sequence(startPage, endPage)}">
<a class="page-link" th:href="@{/(page=${i - 1}, search=${param.search})}" th:text="${i}">1</a>
</li>
<li class="page-item" th:classappend="${boards.totalPages == boards.pageable.pageNumber + 1} ? 'disabled' : '' ">
<a class="page-link" th:href="@{/(page=${boards.pageable.pageNumber + 1}, search=${param.search})}">Next</a>
</li>
</ul>
</nav>
다음으로
References
댓글