
첫번째 추가 기능인 답변 페이징 및 정렬을 구현했다. 일단 질문 목록을 페이징하는 과정과 비슷하게 답변 목록을 페이징 처리 해줬다. 간단한 기능이라 생각했는데 생각보다 쩔쩔맸다. 답변을 추천순으로 정렬하는 기능을 만들기 위해서 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에서 꺼내 페이징 처리하는 과정을 추가해야 한다.
특정 질문 글의 답변을 조회해서 Page형식으로 반환하는 메서드를 AnswerRepository에 추가했다.
public interface AnswerRepository extends JpaRepository<Answer, Integer> {
Page<Answer> findAllByQuestion(Question question, Pageable pagealbe);
}
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);
}
}
이제 특정 질문에 해당하는 답변들을 페이지 데이터 타입으로 조회할 수 있게 되었다.
이제 특정 질문 글을 조회했을 때 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";
}
...(생략)...
}
이제 필요한 정보(질문, 답변)들은 모두 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());
}