본문 바로가기
공부 및 정리/점프 투 스프링부트

점프 투 스프링부트 추가 기능 구현 - 답변 페이징 및 정렬

by 스파이펭귄 2024. 1. 19.
728x90

https://wikidocs.net/162814의 추가 기능 구현 - 답변 페이징 및 정렬

 

3-14 SBB 추가 기능 구현하기

이 책에서 구현할 SBB의 기능은 아쉽지만 여기까지이다. 함께 더 많은 기능을 추가하고 싶지만 이 책은 SBB의 완성이 아니라 SBB를 성장시키는 경험을 전달하는 데 목표를 두고…

wikidocs.net

이 글은 점프 투 스프링 부트의 3-13까지 구현과 이 글 이전의 구현을 마친 것을 전제로 한다.


기존에 구현한 질문 페이징을 토대로 답변 페이징을 구현해보도록 하자.

답변 페이징 기능 구현

Question의 페이징은 아래와 같이 Pageable 객체와 Page 객체를 통해 페이징을 수행했다.

public class QuestionService {

    (... 생략 ...)

    public Page<Question> getList(int page) {
        Pageable pageable = PageRequest.of(page, 10);
        return this.questionRepository.findAll(pageable);
    }

    (... 생략 ...)
}

이때 모든 글을 찾아야 하기에 JPA에서 기본 제공하는 findAll 함수를 사용했다. 하지만 우리는 특정 Question 게시물의 Answer만을 뽑아 Paging을 해야 한다.

 

먼저 특정 Question 게시물의 Answer를 뽑는 방법은 매우 간단하다. AnswerRepository에 findByQuestion 함수를 만들어주면 알아서 파싱해서 특정 Question의 Answer들만을 가져오게 된다.

public interface AnswerRepository extends JpaRepository<Answer, Integer> {
    Page<Answer> findByQuestion(Question question, Pageable pageable);
}

 

위 Question처럼 그냥 Pageable 객체를 같이 넣어주면 알아서 파싱에서 준다. (아래 글 Special Parameter Handling 참조.)

Spring Data JPA를 활용한 페이징 처리

 

Spring Data JPA를 활용한 페이징 처리

Spring Data JPA를 이용한 페이징 처리 PagingAndSortingRepository PagingAndSortingRepository는 CrudRepository를 상속하고 있는 인터페이스이다. PagingAndSortingRepository는 페이징 처리를 위한 메소드를 제공하고 있다. p

gunju-ko.github.io

  • 쿼리 메소드 작동 원리
    1. 메소드 이름 파싱:
      • JPA는 인터페이스에 정의된 메소드 이름을 분석합니다. 예를 들어, findByUsername(String username) 메소드의 경우, findBy 다음에 오는 Username이라는 단어를 분석합니다.
    2. 쿼리 생성:
      • JPA는 이 이름을 Entity의 속성과 매핑합니다. 여기서 Username은 Entity의 username 속성과 일치해야 합니다.
      • 그 후, JPA는 이 정보를 바탕으로 SQL 쿼리를 생성합니다. 위의 예에서는 SELECT * FROM entity_table WHERE username = ?와 같은 쿼리가 될 수 있습니다.
    3. 파라미터 바인딩:
      • 메소드 매개변수는 쿼리의 조건 값으로 사용됩니다. 이 경우, findByUsername 메소드의 username 매개변수는 쿼리의 username 조건에 바인딩됩니다.

 

 

 

AnswerService에 Question이 주어지면 Question에 달린 Answer를 리턴하는 getAnswerList 함수를 만들어주자.

@Service
public class AnswerService {
        (...생략...)

        public Page<Answer> getAnswerList(Question question, int page) {
        List<Sort.Order> sorts = new ArrayList<>();
        sorts.add(Sort.Order.desc("createDate"));
        Pageable pageable = PageRequest.of(page, 5, Sort.by(sorts));
        return this.answerRepository.findByQuestion(question, pageable);
    }
}

그 후 QuestionController에서 지금까지 만든 것들을 이용해 detail 함수를 수정해주자.

이제 지금까지 만든것들을 사용해 Answer를 페이징 해보자. Answer의 페이징은 Question을 클릭해 Detail을 볼 때 발생되어야 한다. 그러므로 QuestionController의 detail 함수를 아래와 같이 수정하자.

@Service
public class QuestionController {
        (...생략...)

        private final AnswerService answerService;

        (...생략...)

        @GetMapping(value = "/detail/{id}")
    public String detail(Model model, @PathVariable("id") Integer id, AnswerForm answerForm,
                         @RequestParam(value="ans-page", defaultValue="0") int answerPage) {
        this.questionService.viewUp(id);
        Question question = this.questionService.getQuestion(id);
        Page<Answer> answerPaging = this.answerService.getAnswerList(question, answerPage);
        model.addAttribute("question", question);
        model.addAttribute("ans_paging", answerPaging);
        return "question_detail";
    }

        (...생략...)
}

특정 question에 달린 answer들을 paging하는 answerPaging을 getAnswerList를 통해 생성하고, “ans_paging”이라는 파라미터로 model에 넘겨줘 프론트 단에서 사용할 수 있게 만들었다.

 

이제 테스트를 통하여 가장 최신 글에 300개의 테스트 답변을 달자.

@SpringBootTest
class MainApplicationTests {

            @Autowired
            private QuestionService questionService;
            @Autowired
            private AnswerService answerService;
            @Autowired
            private UserService userService;

//        @Transactional
            @Test
            void testJpa() {
                List<Question> questionLst = this.questionService.getList();
                Question question = questionLst.get(questionLst.size() - 1);
                SiteUser user = userService.create("temp", "temp@temp.com", "1234");
                for (int i = 0; i < 300; ++i) {
                    this.answerService.create(question, String.format("테스트 답변 %s!!", i), user);
                }
        }
}

실수로 테스트 댓글이라 달아버렸다

무지하게 많이 달렸다. 이제 HTML을 수정하여 paging 기능을 통해 답변을 5개씩 보여주도록 하자.

question_detail.html에 아래 코드를 추가해주자.

        (...생략...)        
        <!-- 답변 갯수 표시 -->
    <h5 class="border-bottom my-3 py-2"
        th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>

        <!-- 답변 페이징 시작 -->
    <div class="card my-3" th:each="answer, loop : ${ans_paging}">
        <a th:id="|answer_${answer.id}|"></a>
        <div class="card-body">
            <div class="card-text" th:utext="${@commonUtil.markdown(answer.content)}"></div>
            <div class="d-flex justify-content-end">
                <div th:if="${answer.modifyDate != null}" class="badge bg-light text-dark p-2 text-start mx-3">
                    <div class="mb-2">modified at</div>
                    <div th:text="${#temporals.format(answer.modifyDate, 'yyyy-MM-dd HH:mm')}"></div>
                </div>
                <div class="badge bg-light text-dark p-2 text-start">
                    <div class="mb-2">
                        <span th:if="${answer.author != null}" th:text="${answer.author.username}"></span>
                    </div>
                    <div th:text="${#temporals.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
                </div>
            </div>
            <div class="my-3">
                <a href="javascript:void(0);" class="recommend btn btn-sm btn-outline-secondary"
                   th:data-uri="@{|/answer/vote/${answer.id}|}">
                    추천
                    <span class="badge rounded-pill bg-success" th:text="${#lists.size(answer.voter)}"></span>
                </a>
                <a th:href="@{|/answer/modify/${answer.id}|}" class="btn btn-sm btn-outline-secondary"
                   sec:authorize="isAuthenticated()"
                   th:if="${answer.author != null and #authentication.getPrincipal().getUsername() == answer.author.username}"
                   th:text="수정"></a>
                <a href="javascript:void(0);" th:data-uri="@{|/answer/delete/${answer.id}|}"
                   class="delete btn btn-sm btn-outline-secondary" sec:authorize="isAuthenticated()"
                   th:if="${answer.author != null and #authentication.getPrincipal().getUsername() == answer.author.username}"
                   th:text="삭제"></a>
            </div>
        </div>
    </div>

    <!-- 답변 페이징 처리 시작 -->
    <div th:if="${!ans_paging.isEmpty()}">
        <ul class="pagination justify-content-center">
            <li class="page-item" th:classappend="${!ans_paging.hasPrevious} ? 'disabled'">
                <a class="page-link" th:href="@{|?ans-page=${ans_paging.number-1}|}" th:data-page="${ans_paging.number-1}">
                    <span>이전</span>
                </a>
            </li>
            <li th:each="page: ${#numbers.sequence(0, ans_paging.totalPages-1)}"
                th:if="${page >= ans_paging.number - 5 and page <= ans_paging.number+5}"
                th:classappend="${page == ans_paging.number} ? 'active'"
                class="page-item">
                <a th:text="${page}" class="page-link" th:href="@{|?ans-page=${page}|}" th:data-page="${page}"></a>
            </li>
            <li class="page-item" th:classappend="${!ans_paging.hasNext()} ? 'disabled'">
                <a class="page-link" th:href="@{|?ans-page=${ans_paging.number+1}|}" th:data-page="${ans_paging.number+1}">
                    <span>다음</span>
                </a>
            </li>
        </ul>
    </div>

        <!-- 답변 작성 -->
    <form th:action="@{|/answer/create/${question.id}|}" th:object="${answerForm}" method="post" class="my-3">
        <div th:replace="~{form_errors :: formErrorsFragment}"></div>
        <textarea sec:authorize="isAnonymous()" disabled th:field="*{content}" class="form-control" rows="10"></textarea>
        <textarea sec:authorize="isAuthenticated()" th:field="*{content}" class="form-control" rows="10"></textarea>
        <input type="submit" value="답변 등록" class="btn btn-primary my-2">
    </form>
        (...생략...)

model에 attribute로 넘겨준 ans_paging과 question을 사용해서 페이징을 수행하도록 하였고, 페이징 처리는 기존 게시글 페이징 처리를 참고하였다.

Answer의 페이징이 잘 되는 것을 확인할 수 있다.

이번엔 정렬 기능을 추가해보자.

 

답변 정렬 기능 추가

먼저 question_detail.html에 최신순으로 할지 추천순으로 할지 정렬 버튼을 만들어주자.

        (...생략...)        
        <!-- 답변 갯수 표시 -->
    <h5 class="border-bottom my-3 py-2"
    th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
    <span>
        답변 정렬:
    </span>
    <a href="javascript:void(0);" class="ordering btn btn-sm btn-outline-secondary"
       th:data-uri="@{|/question/detail/${question.id}?ans-ordering=recommend|}" th:text="추천순"></a>
    <a href="javascript:void(0);" class="ordering btn btn-sm btn-outline-secondary"
       th:data-uri="@{|/question/detail/${question.id}?ans-ordering=time|}" th:text="시간순"></a>

        (...생략...)

이제 이 링크들을 눌렀을 때 정해준 uri로 이동하도록 해주는 자바스크립트 코드를 짜주자.

(...생략...)
<script layout:fragment="script" type="text/javascript">
    const delete_elements = document.getElementsByClassName("delete")
    Array.from(delete_elements).forEach(function(element) {
        element.addEventListener('click', function() {
            if(confirm("정말로 삭제하시겠습니까?")) {
                location.href = this.dataset.uri;
            }
        });
    });
    const recommend_elements = document.getElementsByClassName("recommend");
    Array.from(recommend_elements).forEach(function(element) {
        element.addEventListener('click', function() {
            if(confirm("정말로 추천하시겠습니까?")) {
                location.href = this.dataset.uri;
            }
        });
    });
    const order_elements = document.getElementsByClassName('ordering');
    Array.from(order_elements).forEach(function(element) {
        element.addEventListener('click', function() {
            location.href = this.dataset.uri;
        })
    })
</script>
</html>

위 그림처럼 두 개의 버튼이 생성되는 것을 확인할 수 있으며 이 두 버튼을 지금 눌러도 별 반응이 없다. (링크에 get 방식의 파라미터만 추가해줬고, 이 파라미터를 아직 사용하지 않기 때문)

이제 get 파라미터로 넘겨준 ans-ordering을 사용하도록 QuestionController의 detail함수를 변경해야 한다.

    (...생략...)    
        @GetMapping(value = "/detail/{id}")
    public String detail(Model model, @PathVariable("id") Integer id, AnswerForm answerForm,
                         @RequestParam(value="ans-page", defaultValue="0") int answerPage,
                         @RequestParam(value="ans-ordering", defaultValue="time") String answerOrderMethod) {
        this.questionService.viewUp(id);
        Question question = this.questionService.getQuestion(id);
        Page<Answer> answerPaging = this.answerService.getAnswerList(question, 
                                                                                                                    answerPage, answerOrderMethod);
        model.addAttribute("question", question);
        model.addAttribute("ans_paging", answerPaging);
        return "question_detail";
    }
        (...생략...)

getAnswerList에서 Paging 정렬 방식을 고치기 위해 answerOrderMethod를 넘겨주었다.

        (...생략...)
        public Page<Answer> getAnswerList(Question question, int page, 
                                                                            String answerOrderMethod) {
        List<Sort.Order> sorts = new ArrayList<>();
        if (answerOrderMethod.startsWith("recommend")) {
            sorts.add(Sort.Order.desc("voter"));
        }
        else {
            sorts.add(Sort.Order.desc("createDate"));
        }
        Pageable pageable = PageRequest.of(page, 5, Sort.by(sorts));
        return this.answerRepository.findByQuestion(question, pageable);
    }
}

 

서버를 재 실행하여 버튼을 눌러보면 추천순으로 재 정렬되는것을 확인할 수 있었고 다시 시간순으로도 변경되는 것을 확인할 수 있었다.

 

전체 구현은 깃허브 참조.

728x90