점프투스프링부트 따라하기
3-15 (1) 답변 페이징과 정렬
- 질문 글에 답변이 100개가 달린다면 이것도 페이징 처리를 해야 된다.
- 또한 답변 정렬을 추천 및 최신 순으로 정렬한다.
AnswerRepository
findAllByQuestion
- 매개변수
question
객체를 조건으로 연관되는 Answer
엔티티를 페이징하여 반환한다.
public interface AnswerRepository extends JpaRepository<Answer, Long> {
Page<Answer> findAllByQuestion(Question question, Pageable pageable);
}
AnswerService
Pageable
객체에 page
번호, 페이지당 게시물 개수, 정렬 기준을 담는다.
- 컨트롤러부터 넘겨받은
Question
객체와 서비스에서 만든 Pageable
객체를 통해 Page<Answer>
객체를 반환받는다.
- 최종적으로 컨트롤러에
Page<Answer>
객체를 반환한다.
public Page<Answer> getAnswerList(int page, Question question) {
List<Sort.Order> sorts = new ArrayList<>();
sorts.add(Sort.Order.desc("voter"));
sorts.add(Sort.Order.desc("createDate"));
Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
return answerRepository.findAllByQuestion(question, pageable);
}
QuestionController
- 쿼리스트링으로 받는
page
의 기본값을 0
으로 설정
answerService
로 부터 받은 Page<Answer>
객체를 model
을 통해 뷰에 넘겨준다.
public String detail(@PathVariable Long id, Model model, AnswerForm answerForm,
@RequestParam(value = "page", defaultValue = "0") int page) {
Question question = questionService.getQuestion(id);
Page<Answer> paging = answerService.getAnswerList(page, question);
model.addAttribute("paging", paging);
model.addAttribute("question", question);
return "/question/question_detail";
}
question_detail
//이전
<div class="card my-3" th:each="answer : ${question.answerList}">
//수정
<div class="card my-3" th:each="answer : ${paging}">
//추가
<div th:if="${!paging.isEmpty()}">
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${!paging.hasPrevious} ? 'disabled'">
<a class="page-link"
th:href="@{|?page=${paging.number - 1}|}">
<span>이전</span>
</a>
</li>
<li th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}"
th:if="${page >= paging.number-5 and page <= paging.number+5}"
th:classappend="${page == paging.number} ? 'active'"
class="page-item">
<a th:text="${page}" class="page-link" th:href="@{|?page=${page}|}">"></a>
</li>
<li class="page-item" th:classappend="${!paging.hasNext} ? 'disabled'">
<a class="page-link" th:href="@{|?page=${paging.number+1}|}">
<span>다음</span>
</a>
</li>
</ul>
</div>
3-15 (2) 질문에 댓글 기능
- 질문에 댓글도 달 수 있도록 기능을 추가해보자.
ManyToOne
은 기본이 즉시로딩이므로, 직접 지연로딩으로 세팅해야 한다.
- 댓글과 작성자는
ManyToOne
관계
- 댓글과 질문글은
ManyToOne
관계
- 댓글과 답변은
ManyToOne
관계 (아직 개발 진행 중)
@Entity
@Getter
@Setter
public class Comment {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
private LocalDateTime createDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private SiteUser author;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "question_id")
private Question question;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "answer_id")
private Answer answer;
public Long getQuestionId() {
Long result = null;
if (this.question.getId() != null) {
return this.question.getId();
}
if (this.answer.getId() != null) {
return this.answer.getId();
}
return result;
}
}
Question - 추가
Comment
객체의 question
속성이 관계의 주인이다.
@OneToMany(mappedBy = "question")
private List<Comment> comments = new ArrayList<>();
Answer - 추가
@OneToMany(mappedBy = "answer")
private List<Comment> comments = new ArrayList<>();
public interface CommentRepository extends JpaRepository<Comment, Long> {
}
- 컨트롤러로부터
Question
객체, 댓글 내용, 작성자를 매개변수로 받아온다.
Comment
객체를 만들어 데이터를 넣어준다.
commentRepository
에 저장하고 comment
객체를 반환한다.
@Service
@RequiredArgsConstructor
public class CommentService {
private final CommentRepository commentRepository;
public Comment create(Question question, String content, SiteUser author) {
Comment comment = new Comment();
comment.setContent(content);
comment.setQuestion(question);
comment.setCreateDate(LocalDateTime.now());
comment.setAuthor(author);
commentRepository.save(comment);
return comment;
}
}
- 컨트롤러와 뷰 사이에서 데이터를 전달할 폼 객체 생성
@Getter
@Setter
public class CommentForm {
@NotEmpty(message = "내용은 필수입니다!")
private String content;
}
- 댓글을 작성하는
post
요청 메서드 실행
- 댓글 폼을
question_detail
에 만들어 따로 get
요청 메서드는 만들지 않았음.
- 경로변수
id
는 Question
객체의 id
이다.
commentService
의 create
메서드에 매개변수를 넣어 실행한다.
@Controller
@RequestMapping("/comment")
@RequiredArgsConstructor
public class CommentController {
private final QuestionService questionService;
private final CommentService commentService;
private final UserService userService;
@PostMapping("/create/{id}")
public String create(@PathVariable Long id, @ModelAttribute @Validated CommentForm commentForm, BindingResult bindingResult, Model model, Principal principal) {
Question question = questionService.getQuestion(id);
SiteUser user = userService.getUser(principal.getName());
if (bindingResult.hasErrors()) {
model.addAttribute("question", question);
return "question/question_detail";
}
commentService.create(question, commentForm.getContent(), user);
return "redirect:/question/detail/" + question.getId();
}
}
QuestionController - 추가
- 매개변수로
CommentForm
객체를 추가하여 동적 데이터를 입력 받을 수 있도록 하였다.
@GetMapping("/detail/{id}")
public String detail(@PathVariable Long id, Model model, AnswerForm answerForm,
CommentForm commentForm,
@RequestParam(value = "page", defaultValue = "0") int page) {
Question question = questionService.getQuestion(id);
Page<Answer> paging = answerService.getAnswerList(page, question);
model.addAttribute("paging", paging);
model.addAttribute("question", question);
return "question/question_detail";
}
question_detail - 추가
<div>
<span class="border mb-4 p-1">
<span class="comment py-2 text-muted">댓글</span>
<span class="badge text-bg-primary" th:text="${#lists.size(question.comments)}"></span>
</span>
</div>
<div class="comment py-2 text-muted" th:if="${not #lists.isEmpty(question.comments)}">
<div th:each="comment,index : ${question.comments}" class="comment-content py-2 text-muted">
<span class="border-bottom py-1" style="white-space: pre-line;" th:text="${comment.content}"></span>
<span class="text-end text-success" th:text="${comment.author.username}"></span>
</div>
</div>
<div>
<h5 class="border-bottom my-3 py-2">댓글 등록</h5>
<form class="my-3" th:object="${commentForm}" th:action="@{|/comment/create/${question.id}|}" method="post">
<div class="alert alert-danger" role="alert" th:if="${#fields.hasAnyErrors()}">
<div th:each="err : ${#fields.allErrors()}" th:text="${err}" />
</div>
<textarea name="content" sec:authorize="isAnonymous()" disabled th:field="*{content}" rows="2" class="form-control"></textarea>
<textarea name="content" sec:authorize="isAuthenticated()" th:field="*{content}" rows="2" class="form-control"></textarea>
<input type="submit" value="댓글등록" class="btn btn-primary my-2">
</form>
</div>