Back-end/GDSC_Web Curriculum

[GDSC] 웹 커리큘럼 5주차 : 네비게이션바 & 페이징 & 게시물 일련번호 · 답변 개수 표시

2023. 10. 12. 19:18
목차
  1. ▼ What ?
  2. ▼ 네비게이션바 (Front-End)
  3. 네비게이션바 생성
  4. ▼ 페이징
  5. 테스트 케이스 생성
  6. 페이징 구현
  7. ▼ 게시물 일련번호 · 답변 개수 표시
  8. 모든 게시물 번호가 1부터 시작하는 문제 해결 방법
  9. 답변 개수 표시

▼ What ?

이번 주차엔 웹사이트의 내비게이션바, 페이징 기능, 게시물의 일련번호와 답변 개수를 표시하도록 구현해보고, 스프링 시큐리티가 무엇이며 어떤 원리로 동작하는지에 대해 알아보는 시간을 가져보았다.


▼ 네비게이션바 (Front-End)

 

네비게이션바 생성

 

  • 메인페이지로 돌아갈 수 있는 기능을 추가하는 것이다.
    ➜ 모든 페이지에서 공통적으로 보여야 하므로 layout.html 템플릿에 추가하자 !
    [ .../main/resources/templates/layout.html ]
...
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
    <div class="container-fluid">
        <a class="navbar-brand" href="/">WEBBOARD</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>

 

  • " WEBBOARD " 를 클릭하면 메인페이지로 이동 가능

 

  • 부트스트랩(Bootstrap)은 창의 크기를 줄이면 아래처럼 네비게이션 바에 있는 링크들은 작은 메뉴 버튼 " ≡ "으로 숨긴다.

🔻 하지만, 해당 메뉴버튼을 클릭해도 아무런 변화가 일어나지 않는다.

➜ 부트스트랩 자바스크립트 파일(bootstrap.min.js)이 layout.html 파일에 포함되지 않았기 때문이다 ! 

 

  • 4주차에 다운받은 'bootstrap-5.2.3-dist.zip' 압축파일에 있는 bootstrap.min.js 파일을 아래의 경로로 복사하자
    [ /webboard/src/main/resources/static ]
    ➜ 복사한 부트스트랩 자바스크립트 파일을 사용하기 위해 layout.html 파일 하단의 '<body/>' 태그 바로 위해 추가 !

🔻 메뉴 버튼 " ≡ " 을 클릭하면 위처럼 로그인 링크가 표시된다.

 

  • 네비게이션바 템플릿도 공통 템플릿으로 분리해주자.
    ➜ navbar.html 템플릿을 다른 템플릿에서 중복되어 사용되는 것은 아니지만 독립된 하나의 템플릿으로 관리하는 것이 유지 보수에 유리하다 !
    ( layout.html 파일에 삽입했던 코드와 동일하다. )

    [ /webboard/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 파일도 다음과 같이 기존에 있던 네비게이션바 관련 코드를 삭제하고 navbar.html 템플릿을 타임리프의 'th:replace' 속성으로 포함시키면 된다 !
...
<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>

 

 


▼ 페이징

 

테스트 케이스 생성

 

  • 페이징을 구현하기 전에 페이징을 테스트할 수 있을 정도로 충분한 데이터를 생성하자.
    ➜ 대량의 테스트 데이터를 만드는 가장 간단한 방법은 스프링부트의 '테스트 프레임워크' 를 사용하는 것이다 !
    [ /webboard/src/test/java/com/gdsc/webboard/WebBoardApplicationTests.java ]
package com.gdsc.webboard;

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);
        }
    }
}

🔻 총 300개의 테스트를 생성하는 테스트 케이스 코드이다.

 

  • 로컬 서버를 중지하고 testJpa 메서드를 실행해준 후에 다시 로컬 서버를 실행하고 브라우저를 확인하면 아래처럼 300개 이상의 데이터가 한 페이지에 모두 보여지고 최근 순이 아닌 등록한 순서로 나타나는 문제를 확인할 수 있다.
    ➜ 페이징을 구현하자 !

 


 

페이징 구현

 

  • 페이징 구현을 위한 패키지들은 초반에 설치한 JPA 라이브러리에 들어있다.
    ( org.springframwor.data.domain.Page, Pageable, PageRequest )
    ➜ 별도의 라이브러리 추가 설치 없이 Pagable 객체를 입력으로 받아 Page<Question> 타입 객체를 리턴하는 findAll 메서드를 QuestionRepository 클래스에 생성해주자 !
    [ .../webboard/question/QuestionRepository.java ]
...
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
...
public interface QuestionRepository extends JpaRepository<Question, Integer> {

    ...
    
    Page<Question> findAll(Pageable pageable); 
}

 

  • QuestionRepository 클래스를 수정해주었으니 QuestionService 클래스도 아래와 같이 수정 !
    ➜ 질문 목록을 조회하는 getList 메서드를 int 타입의 페이지 번호를 입력받아 해당 페이지의 질문 목록을 리턴하는 메서드로 변경해주자.
...
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);
    }
}

 

  • QuestionService 클래스의 getList 메서드의 입출력 구조가 변경되었으므로 QuestionController 클래스도 아래와 같이 수정해주자 !
    ➜ 'http://localhost:8080/question/list?page=0' 처럼 GET 방식으로 호출된 URL에서 page값을 가져오기 위해 '@RequestParam(value='page', defaultValue='0') int page' 를 매개변수로 받도록 list 메서드를 수정해준다.
    [ .../webboard/question/QuestionController.java ]
    ...
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.data.domain.Page;

public class QuestionController {

    ...
    
    @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";
    }
}

🔻 'defaultValue' 를 "0" 으로 설정해줌으로 URL에 페이지 파라미터인 'page' 가 전달되지 않은 경우 첫 페이지를 불러오도록 하였다.

( 스프링부트의 페이징은 첫 페이지 번호는 1이 아닌 0이다. )

🔻'model.addAttribute("paging", paging);' 을 통해 "paging" 이름으로 템플릿에 객체 'paging' 을 넘겨주었다 !

 

  • Page 객체의 속성 ➜ 템플릿에서 페이징을 처리할 때 사용 !

 

  • 템플릿에 Page<Question> 객체인 'paging' 을 "questionList" 대신 "paging" 이라는 이름으로 넘겨줬으니,   question_list.html 템플릿도 그에 맞게 수정해주자.
    [ .../templates/question_list.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>

 

  • 'http://localhost:8080/question/list?page=0' URL을 요청해보면, 다음과 같이 첫 페이지에 해당하는 게시물 10개만 조회되는 것을 확인 가능하다 !

 

  • 템플릿에 페이지 이동 기능 구현
    ➜ "이전", "다음" 과 같은 페이지를 이동할 수 있는 링크 추가 !
    [ .../templates/question_list.html ]
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
    <table class="table">
        ...
    </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: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>

🔻 이전 페이지가 없는 경우에는 "이전" 링크가 비활성화(disabled)되도록 하였다. ( 다음페이지의 경우도 마찬가지 )

🔻 페이지 리스트를 루프 돌면서 해당 페이지로 이동할 수 있는 링크를 생성한다.

➜ 이때 루프 도중의 페이지가 현재 페이지와 같을 경우에는 active 클래스를 적용하여 현재 페이지를 알 수 있도록 강조 표시를 해주었다.

🔻 'pagination', 'page-item', 'page-link' 등은 페이지 리스트를 보기 좋게 표시하기 위해 사용한 부트스트랩 pagination 컴포넌트 클래스이다 !

 

  • 위 템플릿에 사용된 주요 페이징 기능

 

  • 페이지 표시 제한 기능
    ➜ 아래처럼, 이동할 수 있는 페이지가 모두 표시되지 않고, 현재 페이지 기준으로 좌우 5개씩만 보이도록 question_list.html 템플릿을 수정해주자 !
    [ .../templates/question_list.html ]

 

    ...
    <!-- 페이징처리 시작 -->
    <div th:if="${!paging.isEmpty()}">
        ...
            <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>
        ...
    </div>
    <!-- 페이징처리 끝 -->
    <a th:href="@{/question/create}" class="btn btn-primary">질문 등록하기</a>
</div>
</html>

 

  • 작성일시 역순으로 조회
    ➜  등록한 순서가 아닌 최근 게시물이 먼저 보이도록 QuestionService 클래스를 수정해주자 !
    [ .../gdsc/webboard/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);
    }
}

🔻 게시물을 역순으로 조회하기 위해서는 위와 같이 PageRequest.of 메서드의 세번째 파라미터로 Sort 객체를 전달해야 한다.

➜ Sort.Order 객체로 구성된 sorts 리스트에 Sort.Order 객체를 추가하고 'Sort.by(sorts)' 로 Sort 객체를 생성할 수 있다.

➜ 작성일시(createDate)를 역순(Desc)으로 조회하려면, sorts 리스트에 'Sort.Order.desc("createDate")' 를 추가해주면 된다.

( 만약 작성일시 외에 추가로 정렬조건이 필요할 경우에는 sorts 리스트에 추가하면 된다. )

 


▼ 게시물 일련번호 · 답변 개수 표시

 

모든 게시물 번호가 1부터 시작하는 문제 해결 방법

 

 

  • 게시물 번호 공식 만들기
번호 = 전체 게시물 개수 - (현재 페이지 * 페이지당 게시물 개수) - 나열 인덱스

 

 

  • 게시물 번호 공식을 question_list.html 템플릿에 적용
    ➜ 아래 코드의 1번째 td 엘리먼트에 이 공식을 그대로 적용시킨다.
    [ .../templates/question_list.html ]

<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
    <table class="table">
        <thead class="table-dark">
            ...
        </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>
    ...
</div>
</html>

 

  • 'http://localhost:8080/question/list?page=1' URL을 요청해보면 아래처럼 게시물 번호가 의도대로 출력되는 것을 확인할 수 있다.

 


 

답변 개수 표시

 

  • "해당 질문에 달린 답변 개수" 를 표시해주기 위해 question_list.html 템플릿을 수정 !
    ➜ 답변 개수는 게시물 제목 바로 오른쪽에 표시하도록 하자.
    [ .../templates/question_list.html ]
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
    <table class="table">
        ...
        <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>
                    <!-- 추가한 코드 시작 -->
                    <span class="text-danger small ms-2"
                        th:if="${#lists.size(question.answerList) > 0}" 
                        th:text="${#lists.size(question.answerList)}">
                    </span>
                    <!-- 추가한 코드 끝 -->
                </td>
                <td th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></d>
            </tr>
        </tbody>
    </table>
...

🔻 th:if="${#lists.size(question.answerList) > 0}" 로 답변이 있는지 체크

➜ th:text="${#lists.size(question.answerList)}" 로 답변 개수를 표시

( #list.size(이터러블 객체)는 "iterable(반복할 수 있는) 객체" 의 사이즈를 반환하는 타임리프의 유틸리티이다. )

🔻 답변이 있는 질문은 다음처럼 제목 오른쪽에 빨간색('text-danger') 숫자로, 작게('small'), 왼쪽 여백('ms-2')이 추가되어 표시된다.

 

 

  1. ▼ What ?
  2. ▼ 네비게이션바 (Front-End)
  3. 네비게이션바 생성
  4. ▼ 페이징
  5. 테스트 케이스 생성
  6. 페이징 구현
  7. ▼ 게시물 일련번호 · 답변 개수 표시
  8. 모든 게시물 번호가 1부터 시작하는 문제 해결 방법
  9. 답변 개수 표시
'Back-end/GDSC_Web Curriculum' 카테고리의 다른 글
  • [GDSC] 웹 커리큘럼 6주차 : 회원가입 & 로그인/로그아웃
  • [GDSC] 웹 커리큘럼 5주차 : 스프링 시큐리티 (Spring Security)
  • [GDSC] 웹 커리큘럼 4주차 : Form & 공통 템플릿
  • [GDSC] 웹 커리큘럼 4주차 : HTML & Bootstrap & Template 상속
Uykm
Uykm
포트폴리오
Uykm
Uykm_Note
Uykm
전체
오늘
어제
  • 분류 전체보기 (213)
    • Algorithm (87)
    • Back-end (56)
      • Study <객체지향의 사실과 오해> (8)
      • Study <자바의 정석> (15)
      • Tools Growing - Java Study (1)
      • Spring & JPA (15)
      • GDSC_Web Curriculum (13)
    • CS (19)
      • Data Structure (2)
      • DB (8)
      • Network (2)
      • OS (1)
      • Lecture Note (6)
    • Git (3)
    • Language (8)
      • C++ (1)
      • Java (6)
      • Python (1)
    • Project (34)
      • P.S-Bot_Discord (1)
      • Toos Experts - 노졸중 (1)
      • Tools Experts - Oneshot FP.. (15)
      • Tools Experts - Dongram (11)
      • 목표 달성 백준 장학금 (5)
      • GDSC_Hackathon (1)
      • GDSC_Google Solution Challe.. (0)
    • Software Design Pattern (4)
    • Seminar (2)

블로그 메뉴

  • 홈
  • 태그
  • 방명록
  • 글쓰기
  • 관리자

공지사항

인기 글

태그

  • 오블완
  • 티스토리챌린지

최근 댓글

최근 글

hELLO · Designed By 정상우.
Uykm
[GDSC] 웹 커리큘럼 5주차 : 네비게이션바 & 페이징 & 게시물 일련번호 · 답변 개수 표시
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.