#1 : 내비게이션바
01. 내비게이션바
- 공통적인 부분이므로 layout.html에 추가
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<!--required meta tags-->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.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-->
</body>
</html>

* SBB 로고를 가장 왼쪽에 배치, 로그인 링크 추가(추후 구현)
* SBB 로고를 누르면 화면의 어느 페이지에서나 홈으로 이동 가능
* 브라우저의 크기를 줄일 경우 아래처럼 버튼이 나타난다.

원래하면 저 버튼을 누르면 숨겨진 로그인 버튼이 나타나는데, 아직 js 파일을 불러오지 않아 변화가 없다.
이전에 bootstrap.min.css를 붙여넣은 것처럼 같은 static 폴더 안에 bootstrap.min.js 파일을 붙여넣고 파일을 불러온다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<!--required meta tags-->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}">
<link rel="stylesheet" type="text/css" th:href="@{style.css}">
<title>Hello, sbb!</title>
</head>
<body>
...생략...
<!--기본 템플릿 안에 삽입될 내용 start-->
<th:block layout:fragment="content"></th:block>
<!--기본 템플릿 안에 삽입될 내용 end-->
<script th:src="@{/bootstrap.min.js}"></script>
</body>
</html>

02. 내비게이션바 분리하기
- 오류메세지처럼 내비게이션바도 공통템플릿으로 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>
* fragment 추가
- layout.html 수정
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<!--required meta tags-->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}">
<link rel="stylesheet" type="text/css" th:href="@{/style.css}">
<title>Hello, sbb!</title>
</head>
<body>
<!--내비게이션바-->
<nav th:replace="~{common/navbar :: navbarFragment}"></nav>
<!--기본 템플릿 안에 삽입될 내용 start-->
<th:block layout:fragment="content"></th:block>
<!--기본 템플릿 안에 삽입될 내용 end-->
<!--부트스트랩 JS-->
<script th:src="@{/bootstrap.min.js}"></script>
</body>
</html>
* replace로 포함시킴
#2 : 페이징
01. 대량 테스트 데이터 만들기
- 페이징을 테스트 할 수 있도록 충분한 데이터 생성, 테스트파일 수정 후 실행
package com.mysite.sbb;
import com.mysite.sbb.service.QuestionService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public 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);
}
}
}
실행 후 다시 로컬 서버 실행 시 테스트 데이터가 잘 들어간 걸 확인할 수 있다.

약 300개가 넘는 항목이 펼쳐져서 보이기 때문에 페이징이 필요하다.
02. 페이징 구현하기
- 페이징을 위한 라이브러리는 JPA 관련 라이브러리에 이미 포함되어 있다.
* org.springframework.data.domain.Page
* org.springframework.data.domain.PageRequest
* org.springframework.data.domain.Pageable
- QuestionRepository.java에 findAll() 메서드 추가
package com.mysite.sbb.repository;
import com.mysite.sbb.entity.*;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
List<Question> findBySubjectLike(String subject);
Page<Question> findAll(Pageable pageable);
}
* Pageable 객체를 입력받아 Page<Question> 객체를 반환하는 메서드 생성
- QuestionService.java의 목록을 받아오는 getList() 메서드 수정
* 수정 전
public List<Question> getList(){
return this.questionRepository.findAll();
}
* 수정 후
public Page<Question> getList(int page){
Pageable pageable = PageRequest.of(page, 10);
return this.questionRepository.findAll(pageable);
}
* 정수 타입의 페이지 번호를 입력받아 해당 페이지의 목록을 반환
* Pageable 객체 생성 시 PageRequest.of(page, 10)를 사용하는데, page는 조회할 페이지의 번호이고 10은 한 페이지에 보여줄 항목의 갯수이다.
- QuestionController.java 수정
@GetMapping("/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";
}
* GET 방식으로 요청된 URL에서 page값을 가져오기 위해 @RequestParam을 이용한다. 만약 page가 전달되지 않을 경우 기본값은 0이 되도록 설정한다.(첫 페이지 번호가 0)
* 템플릿에 Page<Question> 객체 전달
- Page 객체는 다음의 속성들을 가지고 있다.
* paging.isEmpty : 페이지 존재여부(있으면 false, 없으면 true)
* paging.totalElements : 전체 게시물 개수
* paging.totalPages : 전체 페이지 개수
* paging.size : 페이지당 보여줄 게시물 개수
* paging.number : 현재 페이지 번호
* paging.hasPrevious : 이전 페이지 존재 여부
* paging.hasNext : 다음 페이지 존재 여부
- question_list.html 수정
qList 이름의 리스트 객체를 페이지 객체로 변경했으므로 해당 부분을 paging으로 바꾼다.
<tr th:each="question, loop : ${paging}">

10개씩 출력되는 항목을 확인할 수 있다.
03. 템플릿에 페이지 이동 기능 구현하기
- question_list.html 수정
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<table class="table">
<thead class="table-dark">
<tr>
<th>번호</th>
<th>제목</th>
<th>작성일시</th>
</tr>
</thead>
<tbody>
<tr th:each="question, loop : ${paging}">
<td th:text="${loop.count}"></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>
<!--paging start-->
<!--페이지가 비어있지 않으면 조건문 수행-->
<div th:if="${!paging.isEmpty}">
<ul class="pagination justify-content-center">
<!--이전 페이지가 없으면 class 에 disable 추가-->
<li class="page-item" th:classappend="${!paging.hasPrevious} ? 'disable'">
<!--이전을 누르면 한 페이지 전으로 이동-->
<a class="page-link"
th:href="@{|?page=${paging.number-1}|}">
<span>이전</span>
</a>
</li>
<!--page 변수에 0부터 전체 페이지-1 만큼 1씩 증가해서 저장-->
<!--page 변수가 현재 페이지 번호와 같으면 active 클래스 추가-->
<li th:each="page : ${#numbers.sequence(0, paging.totalPages-1)}"
th:classappend="${page == paging.number} ? 'active'"
class="page-item">
<!--현재 페이지 값으로 링크-->
<a th:text="${page}" class="page-link" th:href="@{|?page=${page}|}"></a>
</li>
<!--다음 페이지가 없으면 class 에 disable 추가-->
<li class="page-item" th:classappend="${!paging.hasNext} ? 'disable'">
<!--다음을 누르면 한 페이지 앞으로 이동-->
<a class="page-link" th:href="@{|?page=${paging.number+1}|}">
<span>다음</span>
</a>
</li>
</ul>
</div>
<!--paging end-->
<a th:href="@{/question/create}" class="btn btn-primary">질문 등록하기</a>
</div>
</html>
* th:classappend="조건식 ? 클래스값" : 결과가 참이면 클래스 값을 class 속성에 추가
* #numbers.sequence(시작, 끝) : 시작부터 끝까지 루프를 만들어내는 타임리프의 유틸리티
* th:classappend="${!paging.hasPrevious} ? 'disabled'" : 이전 페이지가 없으면 비활성화
* th:classappend="${!paging.hasNext} ? 'disabled'" : 다음 페이지가 없으면 비활성화
* th:href="@{|?page=${paging.number-1}|}" : 이전 페이지 링크
* th:href="@{|?page=${paging.number+1}|}" : 다음 페이지 링크
* th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}" : 페이지 리스트 루프
* th:classappend="${page == paging.number} ? 'active'" : 현재 페이지와 같으면 active 적용
04. 페이지 리스트
- 여기까지 진행 후 페이지를 실행하면 페이지 번호가 모두 표시되는 것을 확인할 수 있다.

이를 해결하기 위해 question_list.html의 페이지 넘버 부분 수정
<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>
* th:if="${page >= paging.number-5 and page <= paging.number+5}" : 현재 페이지 기준 좌우로 5개씩 표시한다. 즉, paging.number 보다 5만큼 작거나 큰 경우에만 표시되도록 한 것이다.

04. 작성일시 역순으로 조회하기
- 가장 최근에 등록한 게시물이 상단에 표시되도록 QuestionService.java의 getList 메서드 수정
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);
}
* 정렬 순서를 바꾸기 위해서는 위와 같이 PageRequest.of 메서드에 세번째 매개변수로 Sort 객체를 전달한다.
* Sort.Order 객체로 구성된 리스트에 Sort.Order 객체를 추가한 후 정렬의 기준이 될 칼럼을 지정한다.
* 세번째 매개변수로 Sort.by 메서드를 사용해서 기준을 지정한 리스트를 넘겨준다.
* 추가 정렬 기준이 필요할 경우 sorts에 추가해준다.

페이징이 완료되었다.
❗ 추가로 구현한 부분
보통의 페이지 번호는 1부터 시작하기 때문에 0부터 시작하는 페이지 번호를 바꾸어주기로 했다.
- QuestionController.java 수정
@GetMapping("/list")
public String list(Model model,
@RequestParam(value="page", defaultValue = "0") int page) {
Page<Question> paging = this.questionService.getList(page);
//1부터 시작하도록
int nowPage = paging.getPageable().getPageNumber()+1;
int startPage = Math.max(nowPage-4,1);
int endPage = Math.min(nowPage+5,paging.getTotalPages());
model.addAttribute("paging",paging);
model.addAttribute("nowPage",nowPage);
model.addAttribute("startPage",startPage);
model.addAttribute("endPage",endPage);
return "question_list";
}
- 시작 숫자가 달라졌으므로 question_list.html의 페이지 넘버 부분 수정
<div th:if="${!paging.isEmpty}">
<ul class="pagination justify-content-center">
<!--이전 페이지가 없으면 class 에 disable 추가-->
<li class="page-item" th:classappend="${paging.number == startPage-1} ? 'disable'">
<!--이전을 누르면 한 페이지 전으로 이동-->
<a class="page-link"
th:href="@{|?page=${paging.number-1}|}">
<span>이전</span>
</a>
</li>
<!--page 변수에 0부터 전체 페이지-1 만큼 1씩 증가해서 저장-->
<!--page 변수가 현재 페이지 번호와 같으면 active 클래스 추가-->
<li th:each="page : ${#numbers.sequence(startPage, endPage)}"
th:if="${page >= paging.number-5 and page <= paging.number+5}"
th:classappend="${page == nowPage} ? 'active'"
class="page-item">
<!--현재 페이지 값으로 링크-->
<a th:text="${page}" class="page-link" th:href="@{|?page=${page-1}|}"></a>
</li>
<!--다음 페이지가 없으면 class 에 disable 추가-->
<li class="page-item" th:classappend="${paging.number == endPage} ? 'disable'">
<!--다음을 누르면 한 페이지 앞으로 이동-->
<a class="page-link" th:href="@{|?page=${paging.number+1}|}">
<span>다음</span>
</a>
</li>
</ul>
</div>

1부터 시작되어 나오는 것을 확인할 수 있다.
이전과 다음 버튼이 여전히 표시되는 것은 좀 더 생각해보아야 할 것 같다!
'T-I-L > [책] 요약&정리' 카테고리의 다른 글
| [점프 투 스프링부트] 3장 SBB 서비스 개발(스프링 시큐리티) - 2023. 08. 24. (0) | 2023.08.25 |
|---|---|
| [점프 투 스프링부트] 3장 SBB 서비스 개발(일련번호, 답변개수) - 2023. 08. 23. (0) | 2023.08.23 |
| [점프 투 스프링부트] 2장 스프링부트의 기본 요소(템플릿상속, 질문등록과폼, 공통템플릿) - 2023. 08. 21. (0) | 2023.08.21 |
| [점프 투 스프링부트] 2장 스프링부트의 기본 요소(답변등록,부트스트랩) - 2023. 08. 18 (0) | 2023.08.18 |
| [점프 투 스프링부트] 2장 스프링부트의 기본 요소(질문상세) - 2023. 08. 17 (0) | 2023.08.17 |
