(11) 기능구현 - 네비게이션, 페이징
네비게이션 바와 페이징 기능 구현하기
부트스트랩 Navbar를 사용하여 네비게이션 바를 구현해보자
부트스트랩 네비게이션 바 : https://getbootstrap.com/docs/5.1/components/navbar/
시작하기 전에
개요 : 질문과 답변을 할 수 있는 게시판 서비스를 스프링부트를 통해 만들어 본다.
학습사이트 : https://wikidocs.net/book/7601
예제 코드 : https://github.com/pahkey/sbb
네비게이션 바
- bootstrap.min.js 파일 추가
- 부트스트랩 반응형 웹 기능 사용하기
- 경로 - sbb/main/resuorces/static
- layout.html 수정
- 네이게이션 바는 모든 페이지에서 공통적으로 보여야 하므로 layout.html에 추가하자
- bootstrap.min.js파일 사용하기
<!-- 경로 : sbb/src/main/resources/templates/layout.html -->
<!doctype html>
<html lang="ko">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,
initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}">
<!-- sbb CSS -->
<link rel="stylesheet" type="text/css" th:href="@{/style.css}">
<title>Hello, sbb!</title>
</head>
<body>
<!-- 네비게이션 바 -->
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
<div class="container-fluid">
<a class="navbar-brand" href="/">SBB</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="#">로그인</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- 기본 템플릿 안에 삽입될 내용 Start -->
<th:block layout:fragment="content"></th:block>
<!-- 기본 템플릿 안에 삽입될 내용 End -->
<!-- Bootstrap JS -->
<script th:src="@{/bootstrap.min.js}"></script>
</body>
</html>
변경점1 - 이제 어느 페이지에서든 SBB 네비게이션을 이용하여 SBB Home으로 갈수있다.
변경점2 - 페이지를 줄이면 js파일이 동작하여 토글 바가 생성된다.
로그인 네비게이션 기능은 추후에 구현
네비게이션 바 분리하기
이전 포스팅처럼 네비게이션 바도 공통 템플릿으로 사용해보자.
- navbar.html 작성
<!-- 경로 : /sbb/src/main/resources/templates/navbar.html -->
<nav th:fragment="navbarFragment"
class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
<div class="container-fluid">
<a class="navbar-brand" href="/">SBB</a>
<button class="navbar-toggler" type="button"
data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="#">로그인</a>
</li>
</ul>
</div>
</div>
</nav>
- layout.html 수정
- 이전에 삽입했던 네비게이션 태그 수정
<!-- 경로 : sbb/src/main/resources/templates/layout.html -->
<!doctype html>
<html lang="ko">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,
initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}">
<!-- sbb CSS -->
<link rel="stylesheet" type="text/css" th:href="@{/style.css}">
<title>Hello, sbb!</title>
</head>
<body>
<!-- 네비게이션 바 -->
<nav th:replace="navbar :: navbarFragment"></nav>
<!-- 기본 템플릿 안에 삽입될 내용 Start -->
<th:block layout:fragment="content"></th:block>
<!-- 기본 템플릿 안에 삽입될 내용 End -->
<!-- Bootstrap JS -->
<script th:src="@{/bootstrap.min.js}"></script>
</body>
</html>
navbar.html 파일은 다른 템플릿들에서 중복되어 사용되지는 않지만 독립된 하나의 템플릿으로 관리하는 것이 유지 보수에 유리하므로 분리하였다.
페이징
테스트 케이스 작성
대량의 게시물로 페이징을 테스트 하기 위해, 스프링부트 테스트 프레임워크 사용.
포스팅4에서 사용하였던 파일 수정
- JunitTest순서
- 로컬서버 중지 - Run as - Junit Test - 서버 재시작
- SbbApplicationTests.java 수정
// 경로 sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java
package com.mysite.sbb;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.mysite.sbb.question.QuestionService;
@SpringBootTest
class SbbApplicationTests {
@Autowired
private QuestionService questionService;
@Test
void testJpa() {
for (int i = 1; i<= 300 ; i++) {
String subject = String.format("테스트 데이터 [%03d] ", i);
String content = "내용";
this.questionService.create(subject, content);
}
}
}
정말 페이징 기능이 필요하다고 생각이 든다.
그리고 최신 게시물이 제일 앞에 와야 하는데, 지금은 반대로 되어 있으니 앞으로 수정해보자.
페이징 구현하기
org.springframework.data.domain.Page 패키지 활용 (페이징을 위한 패키지)
- QuestionRepository.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionRepository.jav
package com.mysite.sbb.question;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer>{
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
List<Question> findBySubjectLike(String subject);
}
Pageable 객체를 입력으로 받아 Page<Question>타입 객체를 리턴하는 findAll 메서드를 생성
- QuestionService.java 수정
- getList 메서드 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionService.jav
(... 생략 ...)
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
(... 생략 ...)
public class QuestionService {
(... 생략 ...)
// 페이징 메서드
public Page<Question> getList(int page) {
Pageable pageable = PageRequest.of(page, 10);
return this.questionRepository.findAll(pageable);
}
(... 생략 ...)
}
getList : 정수 타입의 페이지 번호를 입력받아 해당 페이지의 질문 목록을 리턴하는 메서드.
PageRequest.of(page, 10) : page는 조회할 번호, 10은 한 페이지에 보여줄 게시물의 갯수.
- QuestionController.java 수정
QuestionService의 getList 메서드의 입출력 구조가 변경되었으니 컨트롤러도 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionController.java
package com.mysite.sbb.question;
(... 생략 ...)
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.data.domain.Page;
(... 생략 ...)
public class QuestionController {
(... 생략 ...)
// 질문 페이지 매핑
@RequestMapping("/question/list")
public String list(Model model,
@RequestParam(value="page", defaultValue="0") int page) {
Page<Question> paging = this.questionService.getList(page);
model.addAttribute("paging", paging);
return "question_list";
}
(... 생략 ...)
}
URL에 페이지 파라미터가 전달되지 않을 경우 디폴트 값을 0으로 한다.
템플릿에 Page
(참고로 페이지 페키지에는 다음과 같은 속성들이 있다.)
항목 | 설명 |
---|---|
paging.isEmpty | 페이지 존재 여부 (게시물이 있으면 false, 없으면 true) |
paging.totalElements | 전체 게시물 개수 |
paging.totalPages | 전체 페이지 개수 |
paging.size | 페이지당 보여줄 게시물 개수 |
paging.number | 현재 페이지 번호 |
paging.hasPrevious | 이전 페이지 존재 여부 |
paging.hasNext | 다음 페이지 존재 여부 |
- question_list.html 수정
<!-- 경로 : sbb/src/main/resources/templates/question_list.html -->
<!-- layout.html상속 -->
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<table class="table">
(... 생략 ...)
<tbody>
<tr th:each="question, loop : ${paging}">
(... 생략 ...)
</tr>
</tbody>
</table>
<a th:href="@{/question/create}" class="btn btn-primary">질문 등록하기</a>
</div>
</html>
기존에는 ${questionList} 이름으로 전달했었으므로 수정해준다.
- SBB화면
이제 301개의 게시물이 전부 표시되지않고, URL을 통해 페이지가 이동된다.
페이지 이동 기능 구현하기
URL을 통해 이동하는 기능을 수정해보자.
- question_list.html 수정
- 부트스트랩 pagination 사용
- https://getbootstrap.com/docs/5.1/components/pagination/
<!-- 경로 : sbb/src/main/resources/templates/question_list.html -->
<!-- layout.html상속 -->
<html layout:decorate="~{layout}">
(... 생략 ...)
</table>
<!-- 페이징 이동 기능 구현부 -->
<div th:if="${!paging.isEmpty()}">
<ul class="pagination justify-content-center">
<!-- '이전'링크 -->
<!-- 이전 페이지가 없을 경우 '이전'링크 비활성화 -->
<li class="page-item"
th:classappend="${!paging.hasPrevious} ? 'disabled'">
<a class="page-link"
th:href="@{|?page=${paging.number-1}|}">
<span>이전</span>
</a>
</li>
<!-- 페이지 리스트 -->
<li th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}"
th:if="${page >= paging.number-5 and page <= paging.number+5}"
th:classappend="${page == paging.number} ? 'active'"
class="page-item">
<a th:text="${page}"
class="page-link" th:href="@{|?page=${page}|}"></a>
</li>
<!-- '다음'링크 -->
<!-- 다음 페이지가 없을 경우 '다음'링크 비활성화 -->
<li class="page-item" th:classappend="${!paging.hasNext} ? 'disabled'">
<a class="page-link" th:href="@{|?page=${paging.number+1}|}">
<span>다음</span>
</a>
</li>
</ul>
</div>
<!-- 페이징 이동 기능 구현부 끝-->
<a th:href="@{/question/create}" class = "btn btn-primary">질문 등록하기</a>
</div>
</html>
- SBB화면
작성일시 역순으로 조회
위의 화면을 보면, 제일 먼저 작성했던 글이 0페이지다.
사실상 맨 뒤로 가야 맞으므로 작성일시를 역순으로 조회하자
- QuestionService.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionService.java
(... 생략 ...)
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.domain.Sort;
(... 생략 ...)
public class QuestionService {
(... 생략 ...)
// 페이징 메서드
public Page<Question> getList(int page) {
List<Sort.Order> sorts = new ArrayList<>();
sorts.add(Sort.Order.desc("createDate"));
Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
return this.questionRepository.findAll(pageable);
}
(... 생략 ...)
}
Sort 패키지를 사용
- SBB 화면
페이지 리스트를 아무리 옮겨도, 게시물 번호의 숫자가 변하지 않는다. 이를 해결해보자
게시물에 일련번호 추가
게시물 번호 공식
번호 = 전체 게시물 개수 - (현재 페이지 * 페이지당 게시물 개수) - 나열 인덱스
항목 | 설명 |
---|---|
번호 | 최종 표시될 게시물 번호 |
전체 게시물 개수 | 데이터베이스에 저장된 게시물 전체 개수 |
현재 페이지 | 페이징에서 현재 선택한 페이지 (만약 페이지가 1부터 시작한다면 1을 빼주어야 한다. 하지만 스프링부트의 페이징은 0부터 시작하므로 1을 뺄 필요가 없다.) |
페이지당 게시물 개수 | 한 페이지당 보여줄 게시물의 개수 |
나열 인덱스 | for 문 안의 게시물 순서 (나열 인덱스는 현재 페이지에서 표시할 수 있는 게시물의 인덱스이므로 10개를 표시하는 페이지에서는 0~9, 2개를 표시하는 페이지에서는 0~1로 반복된다.) |
템플릿에 적용
게시물 번호 공식을 question_list에 적용
- question_list.html 수정
<!-- 경로 : sbb/src/main/resources/templates/question_list.html -->
<!-- layout.html상속 -->
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<table class="table">
<thead class="table-secondary">
<tr>
<th>번호</th>
<th>제목</th>
<th>작성일시</th>
</tr>
</thead>
<tbody>
<tr th:each="question, loop : ${paging}">
<!-- 게시물 공식 대입 -->
<td th:text="${paging.getTotalElements - (paging.number * paging.size) - loop.index}"></td>
<td>
<a th:href="@{|/question/detail/${question.id}|}"
th:text="${question.subject}"></a>
</td>
<td th:text="${#temporals.format(question.createDate,
'yyyy-MM-dd HH:mm')}"></td>
</tr>
</tbody>
</table>
<!-- 페이징 이동 기능 구현부 -->
(... 생략 ... )
</html>
- question_list에 적용된 공식의 상세 정보
항목 | 설명 |
---|---|
paging.getTotalElements | 전체 게시물 개수 |
paging.number | 현재 페이지 번호 |
paging.size | 페이지당 게시물 개수 |
loop.index | 나열 인덱스(0부터 시작) |
- SBB 화면
성공!
Leave a comment