[Spring Boot] (6) 게시판 - 일반회원 게시글 등록 / 조회 / 페이징 :: 음악학원 홈페이지 프로젝트

2020. 10. 6. 22:42Spring Boot

https://codecrafting.tistory.com/16 에서 이어지는 포스팅입니다.

 

[Spring Boot] (5) 로그인/회원가입 - Oauth2를 이용한 카카오 로그인 :: 음악학원 홈페이지 프로젝트

https://codecrafting.tistory.com/10 에서 이어집니다. [Spring Boot] (4) 로그인/회원가입 - 자사 로그인 :: 음악학원 홈페이지 프로젝트 https://codecrafting.tistory.com/9 에서 이어지는 포스팅입니다. [Sp..

codecrafting.tistory.com

오랜만에 스프링 부트 게시글을 쓰게 되었습니다. 그 동안 오픽도 보고 알고리즘도 공부하느라 바빴네요 ㅎㅎ 

바로 본론으로 넘어가 보겠습니다.

 

앞으로 연재할 게시글 CRUD 포스팅에는 JS 기능에 대한 내용을 싣지 않고 백엔드 기능만 집중해서 정리해보도록 하겠습니다. 하지만 혹시 Ajax나 기타 이벤트 처리가 궁금하신 분들은 댓글로 요청해주시면 올려드리도록 하겠습니다.

 

 

제가 만들 게시판의 기본 틀입니다. 우상단에 글쓰기 버튼이 있고 밑에 게시글들이 보이고, 페이징!이 구현되어 있습니다. 조금 썰렁해보이는건 게시글들이 두개밖에 없어서 그런거라고 생각해주세요... 🙃

 

이제 기본적인 html 코드를 작성해봅시다.

 

0. src/main/java/com/soriel/music/springboot/web/soriel_Inquire_board.html

<html xmlns:th="http://www.thymleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:sec="http://www.w3.org/1999/xhtml" lang="ko">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Inquire board</title>
        <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css" rel="stylesheet">
        <link
            rel="stylesheet"
            href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
            integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
            crossorigin="anonymous"
        />
        <script src="https://code.jquery.com/jquery-3.5.1.slim.js" integrity="sha256-DrT5NfxfbHvMHux31Lkhxg42LY6of8TaYyK50jnxRnM=" crossorigin="anonymous"></script>
        <script defer src="https://code.jquery.com/jquery-3.5.1.js"></script>
        <script defer src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"></script>

        <link href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@500&display=swap" rel="stylesheet" />
        <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@700&display=swap" rel="stylesheet" />
        <link href="https://fonts.googleapis.com/css2?family=Noto+Serif+KR:wght@600&display=swap" rel="stylesheet" />

        <link rel="stylesheet" th:href="@{/static/css/soriel_manageStudent.css}" />
        <link rel="stylesheet" th:href="@{/static/css/soriel_Header.css}" />
    </head>
    <body>
        <div class="container">
            <th:block th:replace="fragment/soriel_Header :: headerFragment"></th:block>

            <div id="inq" class="text-center">
                <p class="h4">문의게시판</p>
                <p class="h2">ㅡ</p>
            </div>
            <!-- Inquire Text 1 -->

            <ul class="nav nav-pills justify-content-end" style="margin-bottom: 5px">
                <li class="nav-item">
                    <a sec:authorize="isAuthenticated()" class="nav-link active" href="../inquire_form">글쓰기</a>
                </li>
            </ul>
            <table class="table">
                <thead>
                <tr style="text-align: center;">
                    <th scope="col">#</th>
                    <th id="title" scope="col">제목</th>
                    <th scope="col">작성자</th>
                    <th scope="col">카테고리</th>
                    <th scope="col">날짜</th>
                    <th scope="col">답변여부</th>
                </tr>
                </thead>
                <tbody>
                <tr style="text-align: center;" th:each="board : ${postList.content}">
                    <th scope="row">
                        <span th:text="${board.id}"></span>
                    </th>
                    <td id="title_rl">
                        <a th:href="@{'/inquire_view/'+${board.id}}">
                            <span th:text="${board.title}"></span>
                        </a>
                    </td>
                    <td>
                        <span th:text="${board.writer}"></span>
                    </td>
                    <td>
                        <span th:text="${board.category}"></span>
                    </td>
                    <td>
                        <span th:text="${#temporals.format(board.createdDate, 'yyyy-MM-dd HH:mm')}"></span>
                    </td>
                    <td>
                        <span th:text="${board.verify_reply}"></span>
                    </td>
                </tr>

                </tbody>
            </table>

            <nav id="pag" style="text-align: center;">
                <ul class="pagination justify-content-center"
                    th:with="start=${T(Math).floor(postList.number/10)*10 + 1},
                    last=(${start + 9 < postList.totalPages ? start + 9 : postList.totalPages})">
                    <li class="page-item">
                        <a class="page-link" th:href="@{/inquire_board(page=1)}" aria-label="First">
                            <span aria-hidden="true">&laquo;</span>
                        </a>
                    </li>

                    <li class="page-item" th:class="${postList.first} ? 'disabled'">
                        <a class="page-link" th:href="${postList.first} ? '#' :@{/inquire_board(page=${postList.number})}" aria-label="Previous">
                            <span aria-hidden="true">&lt;</span>
                        </a>
                    </li>

                    <li class="page-item" th:each="page: ${#numbers.sequence(start, last)}" th:class="${page == postList.number + 1} ? 'active'">
                        <a class="page-link" th:text="${page}" th:href="@{/inquire_board(page=${page})}"></a>
                    </li>

                    <li class="page-item" th:class="${postList.last} ? 'disabled'">
                        <a class="page-link" th:href="${postList.last} ? '#' : @{/inquire_board(page=${postList.number + 2})}" aria-label="Next">
                            <span aria-hidden="true">&gt;</span>
                        </a>
                    </li>

                    <li class="page-item">
                        <a class="page-link" th:href="@{/inquire_board(page=${postList.totalPages})}" aria-label="Last">
                            <span aria-hidden="true">&raquo;</span>
                        </a>
                    </li>
                </ul>
            </nav>

        </div>
        <th:block th:replace="fragment/soriel_Footer :: footerFragment"></th:block>
    </body>
</html>

 

프로토타입일 때는 th td 부분에 직접 텍스트를 박아넣어야 나중에 Thymeleaf 구문으로 실제 데이터를 적용할 때 편합니다. nav 부분은 페이징으로 나중에 Pagable 클래스를 이용하여 넘겨지는 파라미터들을 처리합니다.

 

 

이제 컨트롤러를 구현해봅시다.

아래 코드는 게시판 이동 / 작성 페이지 이동 / 글 쓰기 / 글 조회 기능이 들어가 있습니다. 수정, 삭제 답글 등의 기능은 추후 포스팅하면서 설명 할 예정입니다.

 

1. src/main/java/com/soriel/music/springboot/web/PostApiController.java

    private final PostsService postsService;
    private final MemberService memberService;
    private Authentication authentication;

    //게시판 리스트 이동
    @GetMapping("/inquire_board")
    public String postList(Model model, @PageableDefault Pageable pageable) {
        Page<PostsEntity> postList = postsService.getPostList(pageable);
        model.addAttribute("postList", postList);
        return "soriel_Inquire_board";
    }
    
    
    //문의 양식 페이지
    @GetMapping("/inquire_form")
    public String inquire_form() {
        return "soriel_Form_Inquiry";
    }
    
    
    //게시판 글쓰기 기능
    @PostMapping("/post")
    public String write(PostsDto postsDto) {
        authentication = SecurityContextHolder.getContext().getAuthentication();

        Long writerId = memberService.getMemberInfo(authentication.getName());

        if(postsDto.getTitle()==null || postsDto.getContent()==null || writerId==null) {
            return "redirect:/error";
        } else {
            postsDto.setWriter_id(writerId);
            postsDto.setWriter(authentication.getName());
            postsService.savePosts(postsDto);

            return "redirect:/inquire_board";
        }
    }


    //문의 게시물 보기
    @GetMapping("/inquire_view/{id}")
    public String inquire_view(@PathVariable("id") Long id, Model model) {
        authentication = SecurityContextHolder.getContext().getAuthentication();
        Long authenticationId = memberService.getMemberInfo(authentication.getName());
        if (authenticationId == null) return "soriel_Login_page";

        PostsDto postsDto = postsService.getPost(id);
        ReplyDto replyDto;
        try {
            replyDto = postsService.getReply(id);
        } catch (NullPointerException e) {
            replyDto = null;
        }

        model.addAttribute("postDto", postsDto);
        model.addAttribute("replyDto", replyDto);

        return "soriel_Inquiry_view";
    }

 

이제 주석에 맞춰 정리해보겠습니다.

 

게시판 페이지 이동

헤더에서 "게시판" 을 누르면 바로 게시판 페이지로 이동할 수 있게 postList() 메소드에 매핑을 해주었습니다. 그냥 게시판으로 이동만 하는 것이라면 바로 Stirng 을 리턴해주면 되지만, 페이징을 구현해야 하기 때문에 Pagable과 이를 통해 가공된 객체를 담을 Model을 파라미터로 설정해줍시다. Pagable에 대한 설명은 곧 나옵니다 🧐

 

문의 양식 페이지

글 쓰는 페이지로 이동합니다. 해당 html 파일 제목을 그대로 리턴해주면 gradle 에서 implement 한 Thymeleaf dependency에 의해 자동으로 해당 html 을 호출합니다.

 

게시판 글쓰기 기능

게시글을 쓰게 되면 데이터베이스에는 글 ID와 제목, 내용, 카테고리, 글쓴이 ID 정보가 들어가게 됩니다. 이를 위해 Entity 테이블과 Request, Response 시 데이터를 담을 DTO를 만들어 줘야겠죠?

 

글을 다 작성하고 저장버튼을 누르면 이 컨트롤러 처리부분에서 몇가지 유효성 검사를 거친 후 PostsDTO에 담겨 Service를 거친 뒤 Entity화 시킨 뒤 JPA에 save를 요청하게 됩니다.

 

아래는 PostsEntity와 PostsDTO, 그리고 PostsService 글 저장 구현 코드입니다.

1-1. PostsEntity

package com.soriel.music.springboot.domain.posts;

import com.soriel.music.springboot.domain.BaseTimeEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.ColumnDefault;
import org.springframework.data.annotation.CreatedDate;

import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Getter
@NoArgsConstructor
@Entity(name="posts")
public class PostsEntity extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 100, nullable = false)
    private String title;

    @Column
    private String category;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;

    @Column
    private String writer;

    @Column
    private String verify_reply;

    @Column
    private Long writer_id;

    @Builder
    public PostsEntity(Long id, String title, String writer, String content, String category, String verify_reply, Long writer_id) {
        this.id = id;
        this.title = title;
        this.writer = writer;
        this.content = content;
        this.category = category;
        this.verify_reply = verify_reply;
        this.writer_id = writer_id;

    }

    public void update(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public void doneReply() {
        this.verify_reply = "답변 완료";
    }

    public void undoReply() {
        this.verify_reply = "";
    }
}

 

1-2. PostsDTO

package com.soriel.music.springboot.web.dto.posts;

import com.soriel.music.springboot.domain.posts.PostsEntity;
import lombok.*;

import java.time.LocalDateTime;

@Getter
@Setter
@ToString
@NoArgsConstructor
public class PostsDto {

    private Long id;
    private String title;
    private String category;
    private String content;
    private String writer;
    private String verify_reply;
    private Long writer_id;
    private LocalDateTime createdDate;
    private LocalDateTime modifiedDate;

    public PostsEntity toEntity() {
        return PostsEntity.builder()
                .id(id)
                .title(title)
                .writer(writer)
                .content(content)
                .category(category)
                .writer_id(writer_id)
                .verify_reply(verify_reply)
                .build();
    }

    @Builder
    public PostsDto(Long id, String title, String category, String content, String writer,
                    String verify_reply, Long writer_id, LocalDateTime createdDate, LocalDateTime modifiedDate) {
        this.id = id;
        this.title = title;
        this.category = category;
        this.content = content;
        this.writer = writer;
        this.writer_id = writer_id;
        this.verify_reply = verify_reply;
        this.createdDate = createdDate;
        this.modifiedDate = modifiedDate;
    }

}

 

1-3 PostsService (게시글 저장)

@RequiredArgsConstructor
@Service
public class PostsService {

    private final PostsRepository postsRepository;
    private final ReplyRepository replyRepository;

    public Long savePosts(PostsDto postsDto) {
        return postsRepository.save(postsDto.toEntity()).getId();
    }

 

postsRepository는 JpaRepository를 extends 하기 때문에 Spring Data JPA 의 기능을 정말 편리하게 사용할 수 있습니다. Spring Data JAP에 대한 설명도 나중에 포스팅하면 좋을 것 같네용 :)

 

문의 게시글 보기

게시글 조회 구현입니다. 클릭한 게시글의 PK를 파라미터로 들고 DTO를 요청하면 해당 게시글의 데이터가 띄워지는 방식인데요, 필자는 로그인하지 않은 사용자(visitor)가 글을 볼 수 없게 했기 때문에 로그인 창으로 redirect 하도록 했습니다. 조회시에는 게시글의 데이터 뿐만 아니라 답글 데이터도 같이 요청을 하게 되는데요, 이때 답글은 있을 수도, 없을 수도 있는 상태기 때문에 try-catch 구문을 이용하여 null 체크를 해주었습니다. 

 

게시글의 데이터를 불러오는 Service 구현 부분입니다.

 

1-4 PostsService (게시글 load)

    public PostsDto getPost(Long id) {
        Optional<PostsEntity> postsEntityOptional = postsRepository.findById(id);
        PostsEntity postsEntity = postsEntityOptional.get();

        PostsDto postsDto = PostsDto.builder()
                            .id(postsEntity.getId())
                            .title(postsEntity.getTitle())
                            .writer(postsEntity.getWriter())
                            .content(postsEntity.getContent())
                            .category(postsEntity.getCategory())
                            .writer_id(postsEntity.getWriter_id())
                            .createdDate(postsEntity.getCreatedDate())
                            .modifiedDate(postsEntity.getModifiedDate())
                            .build();

        return postsDto;
    }

 

1-5 PostsService (답글 load)

    public ReplyDto getReply(Long id) {
        ReplyEntity replyEntity = replyRepository.findByPostId(id).orElseThrow(NullPointerException::new);

        return ReplyDto.builder()
                .id(replyEntity.getId())
                .reply_content(replyEntity.getReply_content())
                .reply_writer(replyEntity.getReply_writer())
                .post_id(replyEntity.getPost_id())
                .createdDate(replyEntity.getCreatedDate())
                .modifiedDate(replyEntity.getModifiedDate())
                .build();
    }

 

데이터를 불러오는 기능 또한 Spring Data JPA의 기능을 extends 하고 있으므로 findBy 구문을 사용하면 곧바로 Entity에 받아 사용할 수 있습니다. 

 

항상 주의해야 할 점은, Entity에 담긴 데이터를 그대로 사용하기보다는 DTO에 담아서 build(혹은 set) 해야 한다는 점입니다. 데이터 영속성을 해칠 수 있는 부분이기에 반드시 이러한 부분을 구현해주셔야 합니다!

 

 

마지막으로 페이징을 위한 Pagable에 대해 설명하고 포스팅을 마무리하도록 하겠습니다.

 

페이징과 Pagable

기존에 페이징을 구현하기 위해서는 여러가지 귀찮은 작업들을 해야했습니다. 먼저 총 글수를 가지고 와서 한 페이지에 최대 몇 개의 글을 띄울 것인지 계산하고.. 하지만 Pagable은 이러한 수작업 페이징을 정말 편하게 사용할 수 있게 해주는 추상화 인터페이스로 다양한 기능을 제공하고 있습니다.

 

 

위에서 컨트롤러에서 게시판을 불러올 때 Pagable 을 파라메터로 담아 전달해 주었습니다.

    //게시판 리스트 이동
    @GetMapping("/inquire_board")
    public String postList(Model model, @PageableDefault Pageable pageable) {
        Page<PostsEntity> postList = postsService.getPostList(pageable);
        model.addAttribute("postList", postList);
        return "soriel_Inquire_board";
    }

이 Pagable을 디버그 모드로 확인해보면 pagable 객체에 page, size, sort 프로퍼티가 있는 것을 확인할 수 있습니다. 이것을 통해 페이징 기능이 구현될 것입니다.

 

이제 서비스에 pagable을 담아 호출해봅시다.

    public Page<PostsEntity> getPostList(Pageable pageable) {
        int page = (pageable.getPageNumber() == 0) ? 0 : (pageable.getPageNumber()-1);
        pageable = PageRequest.of(page, 5, new Sort(Sort.Direction.DESC, "id"));

        return postsRepository.findAll(pageable);
    }

boardRepository.findAll 메서드의 파라미터로 Pageable 객체를 사용할 수 있습니다. Page 객체를 리턴하며 Pageable과 마찬가지로 페이징 기능을 위해 추상화 시킨 인터페이스입니다.

 

한줄 씩 뜯어보도록 하겠습니다.

int page = (pageable.getPageNumber() == 0) ? 0 : (pageable.getPageNumber()-1);

Pageable의 page는 index 처럼 0 부터 시작입니다. 하지만 주로 게시판에서는 1 부터 시작하기 때문에 사용자가 보려는 페이지에서 -1 처리를 해준 것입니다.

 

pageable = PageRequest.of(page, 5, new Sort(Sort.Direction.DESC, "id"));

Pageable 인터페이스를 확인해보면 알겠지만 getter는 있지만 setter는 없습니다. 그래서 PageRequest.of 메서드를 사용하여 새로운 pageable 객체를 생성해 줍니다. 이때 of() 의 파라미터에서 몇 가지 옵션을 사용할 수 있는데, 결론적으로 필자가 사용한 of는

 

	public static PageRequest of(int page, int size, Sort sort) {
		return new PageRequest(page, size, sort);
	}

	/**
	 * Creates a new {@link PageRequest} with sort direction and properties applied.
	 *
	 * @param page zero-based page index.
	 * @param size the size of the page to be returned.
	 * @param direction must not be {@literal null}.
	 * @param properties must not be {@literal null}.
	 * @since 2.0
	 */

으로, 설명을 보면 알겠지만, page와 size를 입력하고, Sort 객체를 넘겨주는데 이때 정렬기준은 "id"로 하면서 Decending 하게 됩니다. 이렇게 되면 글이 누적되었을 때 내림차순으로 글이 정렬되어 화면에 띄워지게 됩니다.

 

서비스에서는 마지막으로 JPA에 설정한 pagable을 인자로 담아 넘겨줍니다.

return postsRepository.findAll(pageable);

 

리턴 타입은 Post<Entity>로 컨트롤러에서 바로 attribute에 받아서 뷰단으로 넘겨주기만 하면 Spring에서 처리할 페이징 구현은 끝입니다.

 

다시 뷰단으로 넘어가서 pagable 데이터를 어떻게 처리하는지 보겠습니다.

아래는 페이징 부분만 떼어낸 코드블럭입니다.

            <nav id="pag" style="text-align: center;">
                <ul class="pagination justify-content-center"
                    th:with="start=${T(Math).floor(postList.number/10)*10 + 1},
                    last=(${start + 9 < postList.totalPages ? start + 9 : postList.totalPages})">
                    <li class="page-item">
                        <a class="page-link" th:href="@{/inquire_board(page=1)}" aria-label="First">
                            <span aria-hidden="true">&laquo;</span>
                        </a>
                    </li>

                    <li class="page-item" th:class="${postList.first} ? 'disabled'">
                        <a class="page-link" th:href="${postList.first} ? '#' :@{/inquire_board(page=${postList.number})}" aria-label="Previous">
                            <span aria-hidden="true">&lt;</span>
                        </a>
                    </li>

                    <li class="page-item" th:each="page: ${#numbers.sequence(start, last)}" th:class="${page == postList.number + 1} ? 'active'">
                        <a class="page-link" th:text="${page}" th:href="@{/inquire_board(page=${page})}"></a>
                    </li>

                    <li class="page-item" th:class="${postList.last} ? 'disabled'">
                        <a class="page-link" th:href="${postList.last} ? '#' : @{/inquire_board(page=${postList.number + 2})}" aria-label="Next">
                            <span aria-hidden="true">&gt;</span>
                        </a>
                    </li>

                    <li class="page-item">
                        <a class="page-link" th:href="@{/inquire_board(page=${postList.totalPages})}" aria-label="Last">
                            <span aria-hidden="true">&raquo;</span>
                        </a>
                    </li>
                </ul>
            </nav>

 

복잡해 보이지만 하나씩 뜯어내보면 크게 어렵지 않습니다.

 

변수설정

 <ul class="pagination justify-content-center"
                    th:with="start=${T(Math).floor(postList.number/10)*10 + 1},
                    last=(${start + 9 < postList.totalPages ? start + 9 : postList.totalPages})">

제일 복잡한 부분이 여기인데, th:with는 타임리프에서 해당 변수를 전역적으로 사용할 수 있게 해주는 구문입니다. 여기서는 start와 last를 계산하여 변수로 활용하겠습니다.

  • start=${T(Math).floor(boardList.number/10)*10 + 1} : 현제 페이지를 통해 현재 페이지 그룹의 시작 페이지를 구하는 로직입니다.
  • last=(${start + 9 < boardList.totalPages ? start + 9 : boardList.totalPages}) : 전체 페이지 수와 현재 페이지 그룹의 시작 페이지를 통해 현재 페이지 그룹의 마지막 페이지를 구하는 로직입니다.

 

해당 계산식은 타임리프 신택스가 아니라 SpEL(Spring Expression language)을 이용한 구문으로

아래와 같은 예제를 이용한 방식입니다.

ExpressionParser parser = new SpelExpressionParser();
Double two = parser.parseExpression("T(Math).floor(2.3)").getValue(Double.class);
System.out.println(two);

 

첫 페이지로 가기 버튼

<li class="page-item">
  <a class="page-link" th:href="@{/inquire_board(page=1)}" aria-label="First">
  	<span aria-hidden="true">&laquo;</span>
  </a>
</li>

href 속성을 위와 같이 th:href로 줄 수 있습니다.
href를 사용하지 않고 굳이 th:href를 사용한 이유는 c:url과 같이 컨텍스트명이 추가되더라도 리소스 path에 컨텍스트명을 붙여 리소스를 가져오기 때문에 컨텍스트명 유무에 상관없이 항상 정상적으로 리소스를 가져올 수 있게 해주는 속성을 이용하는 것입니다. 그리고 파라미터는 (key=value) 형식으로 표기합니다.

 

이전 페이지로 이동

<li class="page-item" th:class="${postList.first} ? 'disabled'">
  <a class="page-link" th:href="${postList.first} ? '#' :@{/inquire_board(page=${postList.number})}" aria-label="Previous">
  	<span aria-hidden="true">&lt;</span>
  </a>
</li>

th:class는 위에 보이는 것처럼 조건을 통해 class를 지정할 수 있습니다. 위에서는 현재 페이지가 첫번째 페이지면 disabled를 클래스에 걸어놓습니다. th:href 역시 조건을 통해 href를 지정할 수 있는데 첫 페이지라면 href에 #을 지정하고 아니라면 현재 페이지의 page number를 지정합니다. (현재 페이지의 page number를 지정하는 이유는 page number는 index 처럼 0에서 시작하기 때문에 현재 페이지 - 1이 자동으로 적용됩니다.)

 

현재 페이지 그룹의 페이지 나열

<li class="page-item" th:each="page: ${#numbers.sequence(start, last)}" th:class="${page == postList.number + 1} ? 'active'">
	<a class="page-link" th:text="${page}" th:href="@{/inquire_board(page=${page})}"></a>
</li>

th:each를 사용해서 현재 페이지 그룹의 페이지를 나열합니다. 이때 th:class 속성에서 현재페이지일 경우 ‘active’ class를 추가하는 로직이 있습니다.

 

다음 페이지로 이동

<li class="page-item" th:class="${postList.last} ? 'disabled'">
  <a class="page-link" th:href="${postList.last} ? '#' : @{/inquire_board(page=${postList.number + 2})}" aria-label="Next">
  	<span aria-hidden="true">&gt;</span>
  </a>	
</li>

이전 페이지로 이동과 동일

 

마지막 페이지로 이동

<li class="page-item">
  <a class="page-link" th:href="@{/inquire_board(page=${postList.totalPages})}" aria-label="Last">
  	<span aria-hidden="true">&raquo;</span>
  </a>
</li>

첫 페이지로 이동과 동일

 

 

이렇게 해서 게시판 기능 중 조회와 생성, 페이징에 대해 알아보았습니다.

다음 포스팅에서는 수정과 삭제, 그리고 답글에 대해 다뤄보겠습니다.