본문 바로가기
공부 기록

[스프링 부트로 게시판 만들기] 10. 조회수, 페이징과 검색

by 매트(Mat) 2021. 7. 23.



조회수 증가

  • 이번엔 게시글의 조회수를 구현해보도록 하겠습니다. (간단합니다!)

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> 이런 식으로 뿌려주었습니다.



페이징

  • 이번에 구현해볼 것은 페이징입니다.
  • 사실.. 이 페이징이 간단하게 하면 쉬울 수 있지만 검색이랑 이전 페이지의 정보를 들고 가는 것 등등... 신경쓰면 결코 쉽지 않다고 생각듭니다.
  • 저는 https://www.youtube.com/watch?v=hmSPJHtZyp4&t=388s 이 영상을 참고해서 구현하였습니다. 설명을 참 잘해주셔서 꼭 보시면 좋을 것 같습니다!

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>






다음으로

  • 여기까지 페이징과 검색을 마치겠습니다. 저도 아직 너무 미숙해서 반복해서 해봐야 할 것 같습니다..ㅠ
  • 솔직히 말씀드리면 저는 https://www.youtube.com/watch?v=hmSPJHtZyp4&t=388s 이 영상을 보고 똑같이 따라했습니니다.ㅎㅎ; 그러니 이 영상은 꼭 꼭 봐주시면 좋겠습니다.
  • 다음에는 댓글 구현을 하도록 하겠습니다.






References

댓글