쌓고 쌓다

[스프링 부트] Ajax 댓글 리스트 및 페이징 - 12 본문

프로그래밍/spring

[스프링 부트] Ajax 댓글 리스트 및 페이징 - 12

승민아 2023. 7. 18. 18:56

게시글의 댓글 리스트를 출력할때 어떤 방법으로 하는지 여러 웹 페이지를 구경했는데

신기하게 댓글 페이지 이동시 URL이 변하지 않는것이다.

이 방법은 Ajax를 통해 웹 페이지 전체를 다시 로딩하지 않고, 웹 페이지의 일부분만 갱신하는 방법이다.

 

그럼 게시글도 그렇고 모든 부분을 Ajax로 처리하면 무조건 이득이 아닌가 생각이 드는데

웹 사이트를 둘러봐도 다들 그렇지 않더라.. 나중에 알아봐야겠다..

 

 

CommentRepository

public interface SpringDataJpaCommentRepository extends JpaRepository<Comment, Long> {
    Page<Comment> findByPno(Long pno, Pageable pageable);
}

페이징된 댓글을 받아오는데 JPA가 기능을 제공한다.

findBy 메서드를 작성하고 페이징을 위해 두번째 파라미터로 Pageable을 넘겨주자.

 

CommentService

@Service
@Transactional
public class CommentService {
    ...
   
    public Page<Comment> findComments(Long pno, Pageable pageable) {
        Page<Comment> comments = commentRepository.findByPno(pno, pageable);
        return comments;
    }
}

 

CommentController

@Controller
public class CommentController {
    ...
    
    @GetMapping("/api")
    @ResponseBody
    public Page<Comment> test(@PageableDefault(sort="id", value=5, direction = Sort.Direction.ASC) Pageable pageable, @RequestParam(name = "pno") Long pno) {
        Page<Comment> comments = commentService.findComments(pno, pageable);
        return comments;
    }
}

간단하게 방법을 보이기위해 "/api"로 GET 매핑했다.

페이징 처리된 댓글 리스트가 반환한다.

 

@ResponseBody

컨트롤러 메서드가 HTTP 응답을 만들 때, 해당 메서드의 리턴값을 HTTP 응답 본문에 직접 써서

데이터를 클라이언트에게 전송하는 역할을 한다. API 개발에 용이.

 

기본적으로 스프링 컨트롤러는 문자열, JSON, XML 등의 데이터를 반환할 수 있다.

@ResponseBody 어노테이션은 메서드 리턴값을 HTTP 응답 본문에 직접 쓰도록하여 데이터를 클라이언트에게 전송.

@RestController
public class MyController {

    @GetMapping("/data")
    @ResponseBody
    public MyData getMyData() {
        ...
        return myData;
    }
}

MyData를 JSON 형태로 반환한다.

 

예를 들어 "http://localhost:8080/api?page=1&pno=227"로 GET 요청시

아래의 JSON을 받아올 수 있다. 우리는 이 데이터를 다뤄 댓글 목록을 처리할 것이다.

 

Ajax 속성

  • type: 통신 타입 설정(GET, POST)
  • url: 요청할 url
  • data: 서버에 보낼 데이터
  • dateType: 응답 받을 데이터 설정
  • success: 성공시 수행
  • error: 실패시 수행
  • 등등...

 

 

Ajax를 통해 동적으로 웹 일부를 갱신할 수 있다.

<ul id="message">
    
</ul>

<ul id="pagination">
    
</ul>

<ul> 태그의 message와 pagination 부분을 동적으로 갱신할 것이다.

message에는 댓글 정보

pagination에는 페이지네이션을 추가할 예정이다.

 

 

1. Ajax로 JSON 받아 message에 넣기

        $.ajax({
            type: "GET",
            url: `api?pno=${pno}&page=${page}`,
            dataType: "json",
            success: function (data) {
                let commentUl = $("#message");
                commentUl.empty(); // 선택 요소 비우는 작업
                
                //makePagination(pno, data); (2) 페이지네이션
                
                data.content.forEach(function (comment) {
                    commentUl.append(`<li> ${comment.writer} ${comment.content} ${comment.regDate} </li>`);
                })
            },
            error: function () {
                alert("error");
            }
        })

url

게시글 번호 pno의 page번째 페이지 댓글들을 요청한다.

이 요청은 Pageable을 통해 페이징처리되게 Controller에 작성해 놨다.

페이징 처리된 댓글만 가져온다. 게시글 번호 pno와 페이징 처리된 댓글 페이지 page를 통해 요청하자.

 

success

성공시 처리되는 부분이다. data에 가져온 데이터가 담겨져 메서드를 수행한다.

$("#message")는 jQuery 라이브러리를 사용하여 DOM(Domcument Object Model) 요소를 선택하는 jQuery 선택자이다.

id가 message인 태그를 선택하여 append를 통해 가져온 데이터를 넣는 과정을 수행할 것이다.

*append: 선택한 컨텐츠의 끝에 추가

 

forEach

console.log(data)

forEach는 배열 메서드라 data.content를 가져와야 수행할 수 있다.

 

2. 페이지네이션

function makePagination(pno, page) {

        let pagination = $("#pagination");
        pagination.empty();

        let cur = page.number; // 0부터 센다.
        let endPage = Math.ceil((cur + 1) / 10.0) * 10; // 1~10
        let startPage = endPage - 9; // 1~10
        if (endPage > page.totalPages - 1) // totalPage는 1부터 센다 그래서 1을 빼줌
            endPage = page.totalPages;

        if (cur > 0) // 이전 버튼
            pagination.append(`<li><a onclick="loadComments(${pno}, ${cur - 1})">이전</a></li>`);

        for (let i = startPage; i <= endPage; i++) { // 페이지네이션
            pagination.append(`<li><a onclick="loadComments(${pno}, ${i - 1})">${i}</a></li>`);
        }
        if (cur + 1 < page.totalPages) // 다음 버튼
            pagination.append(`<li><a onclick="loadComments(${pno}, ${cur + 1})">다음</a></li>`);
    }

API 요청으로 반환받은 Page<Comment>에는 페이징을 쉽게할 수 있는 속성들이 많이 있다.

받아온 JSON 객체의 속성에 페이징처리했을시 나오는 총 페이지 수 totalPages, 현재 페이지 number를 이용해

페이지네이션을 구현한다.

속성들을 이용한 페이지네이션 구현은 이전에 다뤘었다.

https://non-stop.tistory.com/478

 

 

<a> 태그 클릭시 자바스크립트 함수 실행

<a onclick="loadComments(게시글번호, 볼 페이지)">ㅎㅎ</a>

onClick 속성으로 수행할 함수를 지정할 수 있다.

 

 

실무에서 개발자들이 페이지네이션에 어떻게 <a> 태그를 작성했는지 구경해봤다.

Musinsa 후기 페이지네이션

href 속성을 사용하고 끝에 return false;를 붙였다. 이건 뭘까?

void(0): 링크가 정상적으로 수행하지 않게 undefined를 반환해주는 함수이다.

return false: 기본 설정을 무시하고 상단으로 페이지 이동이 이뤄지지 않게 해준다.

 

 

 

 

 

+ 게시글 첫 방문시 댓글 목록과 페이지네이션은 어떻게 불러올까?

@GetMapping("/poster/read")
    public String read(Model model,@PageableDefault(sort="id", value=5, direction = Sort.Direction.ASC) Pageable pageable, @RequestParam(name = "id") Long id) {
        Poster poster = posterService.findByOne(id).get();
        model.addAttribute("poster", poster);
        
        return "posters/posterView";
    }

현재 Poster Controller에서 댓글을 가져오는 로직은 없다.

posterView.html안에 API 요청으로 게시글 번호를 통해 댓글을 가져오는 로직뿐이다.

어떻게 해결해야하나...

 

ready를 사용하여 DOM 로드시 로직을 수행하게 할 수 있다.

ready와 onload가 유사한 기능을 한다.

타임리프 변수를 자바스크립트에서 사용하려면 [[${}]] 형식으로 변수를 사용할 수 있다.

$(document).ready(function () {
        let pno = [[${poster.id}]];
        loadComments(pno, 0);
        }
    );

*<script th:inline="javascript"> </script>라고 해줘야한다는데 안해도 돌아간다..

 

 

test.html - 전체 코드

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>



<ul id="message">
</ul>

<ul id="pagination">
</ul>

<script>
    function loadComments(pno, page) {
        $.ajax({
            type: "GET",
            url: `api?pno${pno}0&page=${page}`,
            dataType: "json",
             success: function (data) {
                 let commentUl = $("#message");
                 commentUl.empty(); // 선택 요소 비우는 작업

                 makePagination(pno, data);

                 data.content.forEach(function (comment) {
                     commentUl.append(`<li> ${comment.writer} ${comment.content} ${comment.regDate} </li>`);
                 })
             },
            error: function () {
                alert("error");
            }
        })
    }

    function makePagination(pno, page) {

        let pagination = $("#pagination");
        pagination.empty();

        let cur = page.number; // 0부터 센다.
        let endPage = Math.ceil((cur+1)/10.0)*10; // 1~10
        let startPage = endPage-9; // 1~10
        if(endPage>page.totalPages-1) // totalPage는 1부터 센다 그래서 1을 빼줌
            endPage=page.totalPages;

        if(cur>0) // 이전 버튼
            pagination.append(`<li><a onclick="loadComments(${pno}, ${cur-1})">이전</a></li>`);

        for(let i=startPage; i<=endPage; i++) { // 페이지네이션
            pagination.append(`<li><a onclick="loadComments(${pno}, ${i-1})">${i}</a></li>`);
        }
        if(cur+1<page.totalPages) // 다음 버튼
            pagination.append(`<li><a onclick="loadComments(${pno}, ${cur + 1})">다음</a></li>`);
    }

    $(document).ready(function () {
        let pno = [[${poster.id}]];
        loadComments(pno, 0);
        }
    );
</script>
</body>
</html>

 

구현 결과

 

 

Comments