점프투스프링부트 개선 | 답변 페이징과 정렬

Park JeaHyun·2023년 1월 30일

첫번째 추가 기능인 답변 페이징 및 정렬을 구현했다. 일단 질문 목록을 페이징하는 과정과 비슷하게 답변 목록을 페이징 처리 해줬다. 간단한 기능이라 생각했는데 생각보다 쩔쩔맸다. 답변을 추천순으로 정렬하는 기능을 만들기 위해서 Answer 엔티티에 연관관계로 설정된 @ManyToMany set<SiteUser> voter를 PageRequest.of(page, 10, sort)의 sort에 어떻게 넣어야 할지 생각이 나지 않았다.

1시간 정도 구글링과 고민하다가 https://pybo.kr/pybo/question/detail/1369/ 질문 글을 보고 허무하게 해결했다. (서비스 계층에sorts.add(Sort.Order.desc("voter")); 만 추가하면 된다)

배움의 과정이라 생각하자 ^^;

특정 질문 글의 답변 조회

기존의 QuestionController 에서는 따로 답변 데이터들을 가져오지 않았다. Question 엔티티만을 조회하고 question.answerList 를 조회하는 형식을 사용했다. 하지만 이제 답변 데이터들을 페이징 처리해서 템플릿에 넘겨줘야 한다. 그렇기에 특정 질문 글을 조회했을 때 해당 질문에 딸린 답변을 DB에서 꺼내 페이징 처리하는 과정을 추가해야 한다.

AnswerRepository

특정 질문 글의 답변을 조회해서 Page형식으로 반환하는 메서드를 AnswerRepository에 추가했다.

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

AnswerService

1순위 정렬 조건으로 추천순을 적용해야 하기 때문에 voter를 첫번째로 넣는다. 과거 데이터부터 조회하고 싶기 때문에 2순위 정렬 조건으로 createDate를 넣는다.

@Service
public class AnswerService {

    private final AnswerRepository answerRepository;
	
    ...(생략)...
    
    public Page<Answer> getList(Question question, int page) {
		List<Sort.Order> sorts = new ArrayList<>();
		sorts.add(Sort.Order.desc("voter")); 
        sorts.add(Sort.Order.asc("createDate"));
    	Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
    	return this.answerRepository.findAllByQuestion(question, pageable);
   }
}

이제 특정 질문에 해당하는 답변들을 페이지 데이터 타입으로 조회할 수 있게 되었다.

QuestionController

이제 특정 질문 글을 조회했을 때 Question 엔티티 뿐만 아니라 Page<Answer> 데이터도 조회해보도록 하자.

먼저 조회하고자 하는 페이지 넘버가 필요하기 때문에 @RequestParam(value = "answerPage", defaultValue = "0") int answerPage으로 페이지 넘버를 받아온다. 이후 먼저 만들어 놓은 서비스 메서드를 통해 템플릿에 필요한 Question question, Page<Answer> answerPaging 데이터를 받아와 템플릿에 넘겨준다.

@Controller
public class QuestionController {
	
    private final QuestionService questionService;
	private final UserService userService;
	private final AnswerService answerService;

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

템플릿

question_detail.html

이제 필요한 정보(질문, 답변)들은 모두 DB에서 가져왔다. 템플릿에서 적절히 데이터를 보여준다.

담변 페이지를 넘길 때 (이전, 다음 또는 숫자 페이지 링크를 클릭할 때) 답변의 페이지 넘버를 컨트롤러에 넘겨주기 위해서 @PathVariable을 사용할 수도 있지만 @RequestParam을 사용해서 페이지 넘버를 넘겨주도록 했다. (딱히 이유는 없지만 이게 더 깔끔하다고 생각했다) 그러기 위해서 (맨 하단) form 데이터에 hidden을 추가하고 page-link를 클릭했을 때 해당 page-link의 값 (data-page) 을 form의 hidden 값에 넣어서 컨트롤러에 넘겨주는 방식을 적용했다.

<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
	
  	...(생략)...
  
    <!-- 답변의 갯수 표시 -->
    <h5 class="border-bottom my-3 py-2" 
        th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
    <!-- 답변 반복 시작 -->
    <div class="card my-3" th:each="answer : ${answerPaging}">
		
      	...(생략)...
      
    </div>
    <!-- 답변 반복 끝  -->
    <!-- 답변 페이징처리 시작 -->
    <div th:if="${!answerPaging.isEmpty()}">
        <ul class="pagination justify-content-center">
            <li class="page-item" th:classappend="${!answerPaging.hasPrevious} ? 'disabled'">
                <a class="page-link" href="javascript:void(0)" th:data-page="${answerPaging.number - 1}">
                    <span>이전</span>
                </a>
            </li>
            <li th:each="page: ${#numbers.sequence(0, answerPaging.totalPages-1)}"
            th:if="${page >= answerPaging.number-5 and page <= answerPaging.number+5}"
                th:classappend="${page == answerPaging.number} ? 'active'" 
                class="page-item">
                <a th:text="${page}" class="page-link" href="javascript:void(0)" th:data-page="${page}"></a>
            </li>
            <li class="page-item" th:classappend="${!answerPaging.hasNext} ? 'disabled'">
                <a class="page-link" href="javascript:void(0)" th:data-page="${answerPaging.number+1}">
                    <span>다음</span>
                </a>
            </li>
        </ul>
    </div>
    <!-- 답변 페이징처리 끝 -->
	
  	...(생략)...
  
    <!-- 답변 페이징 검색 폼 -->
    <form th:action="@{|/question/detail/${question.id}|}" method="get" id="answerPageForm">
        <input type="hidden" id="answerPage" name="answerPage" th:value="${answerPaging.number}">
    </form>
</div>

<script layout:fragment="script" type='text/javascript'>
  
...(생략)...

const page_elements = document.getElementsByClassName("page-link");
Array.from(page_elements).forEach(function(element) {
    element.addEventListener('click', function() {
        document.getElementById('answerPage').value = this.dataset.page;
        document.getElementById('answerPageForm').submit();
    });
});
</script>
</html>

추가

기존에 답변을 생성하게 되면 아래와 같이 질문 상세페이지로 리다이렉트 된다. 그 후 앵커 기능을 통해 작성한 댓글로 바로 화면 이동하게 된다. 하지만 이제 답변들이 페이징 처리가 되어 있으므로 기존 앵커 기능을 수정해야 한다.

return String.format("redirect:/question/detail/%s#answer_%s", 
                answer.getQuestion().getId(), answer.getId());

수정 방법

답변 페이징은 10개씩 처리된다. (한 페이지에 10개의 답변) 따라서 질문에 해당하는 총 답변 갯수에서 10을 나눈 몫이 페이지 넘버가 된다. 그리고 이 값을 아래의 파라미터(answerPage)로 넘겨주자.

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

파리미터를 넘겨주기 위해 기존의 @PostMapping("/create/{id}") 메서드를 아래와 같이 수정하자.

    @PreAuthorize("isAuthenticated()")
    @PostMapping("/create/{id}")
    public String createAnswer(Model model, @PathVariable("id") Integer id, 
            @Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal, RedirectAttributes re) {
        Question question = this.questionService.getQuestion(id);
        
		...(생략)...
        
        Answer answer = this.answerService.create(question, 
                answerForm.getContent(), siteUser);
        int page = question.getAnswerList().size() / 10;
        re.addAttribute("answerPage", page);
        return String.format("redirect:/question/detail/%s#answer_%s", 
                answer.getQuestion().getId(), answer.getId());
    }

0개의 댓글