(17) 기능구현 - 추천
질문과 답변에 추천 기능을 구현하자
시작하기 전에
개요 : 질문과 답변을 할 수 있는 게시판 서비스를 스프링부트를 통해 만들어 본다.
학습사이트 : https://wikidocs.net/book/7601
예제 코드 : https://github.com/pahkey/sbb
엔티티 변경
질문과 답변 엔티티에 추천한 사람에 대한 정보(SiteUser 객체)가 있어야 한다.
질문 엔티티 변경
Question 엔티티에 추천인(voter) 속성을 추가해 보자.
한 사람이 여러개의 게시물을 추천할 수 있고, 한 게시물은 여러 명에게 추천을 받을 수 있으므로
voter속성은 ManyToMany
- Question.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/question/Question.java
package com.mysite.sbb.question;
(... 생략 ...)
import java.util.Set;
import javax.persistence.ManyToMany;
(... 생략 ...)
@Getter
@Setter
@Entity
public class Question {
(... 생략 ...)
// 추천인
@ManyToMany
Set<SiteUser> voter;
}
한 사람이 한개의 게시물에 여러 개의 추천을 주면 안되기 때문에 Set 자료형 사용.
Set 자료형은 중복을 허용하지 않음.
답변 엔티티 변경
Answer 엔티티에도 추천인(voter) 속성을 추가.
- Answer.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/answer/Answer.java
package com.mysite.sbb.answer;
(... 생략 ...)
import java.util.Set;
import javax.persistence.ManyToMany;
(... 생략 ...)
@Getter
@Setter
@Entity
public class Answer {
(... 생략 ...)
// 추천인
@ManyToMany
Set<SiteUser> voter;
}
엔티티 변경 확인
새로운 테이블 ANSWER_VOTER와 QUESTION_VOTER가 생성된다.
근데 왜 테이블이 만들어질까?
@ManyToMany 관계로 속성을 생성하면 새로운 테이블을 생성하여 데이터를 관리한다.
테이블에는 서로 연관된 엔티티의 고유번호(id) 2개가 프라이머리 키로 되어 있기 때문에 다대다(N:N) 관계가 성립하는 구조이다.
질문 추천
Question 엔티티에 추천인 속성을 추가 했으니 이제 질문 추천 기능을 만들어 보자.
질문 추천 버튼
질문 상세 화면에 추천버튼을 만들어주자.
- question_detail.html 수정
<!-- 경로 : sbb/src/main/resources/templates/question_detail.html -->
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
(... 생략 ...)
<!-- 추천, 수정, 삭제 버튼 -->
<div class="my-3">
<!-- 추천 -->
<a href="javascript:void(0);" class="recommend btn btn-sm btn-outline-secondary"
th:data-uri="@{|/question/vote/${question.id}|}">
추천
<span class="badge rounded-pill bg-success" th:text="${#lists.size(question.voter)}"></span>
</a>
<!-- 수정 -->
<a th:href="@{|/question/modify/${question.id}|}" class="btn btn-sm btn-outline-secondary"
sec:authorize="isAuthenticated()"
th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
th:text="수정"></a>
<!-- 삭제 -->
<a href="javascript:void(0);"
th:data-uri="@{|/question/delete/${question.id}|}"
class="delete btn btn-sm btn-outline-secondary"
sec:authorize="isAuthenticated()"
th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
th:text="삭제"></a>
</div>
</div>
</div>
<!-- 답변을 확인할 수 있는 영역 -->
(... 생략 ...)
</html>
수정 버튼 왼쪽에 추천 버튼을 추가하고, 추천 수도 표시되게 하였다.
추천 확인 알람창을 띄워주기 위해 자바스크립트 링크를 추가해주었다. 바로 구현해보자.
- 버튼 확인
추천 확인
추천 버튼을 눌렀을 때 확인 알람창을 띄워보자
- question_detail.html 수정
<!-- 경로 : sbb/src/main/resources/templates/question_detail.html -->
<html layout:decorate="~{layout}">
(... 생략 ...)
<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;
};
});
});
</script>
</html>
삭제 확인 알람 밑에 추가해주었다.
이제 버튼 class가 recommend이면 추천 확인 알람이 뜨고, 확인을 선택하면 data-uri속성에 정의한 URL이 호출될 것이다.
- 추천 알람 확인
추천인 저장
추천인을 저장하여 추천버튼을 카운트시켜보자
- QuestionService.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionService.java
(... 생략 ...)
@RequiredArgsConstructor
@Service
public class QuestionService {
(... 생략 ...)
// 질문 추천 메서드
public void vote(Question question, SiteUser siteUser) {
question.getVoter().add(siteUser);
this.questionRepository.save(question);
}
}
질문 엔티티에 사용자를 추천인으로 저장하는 vote 메서드 추가.
추천 URL 처리
- QuestionController.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionController.java
package com.mysite.sbb.question;
(... 생략 ...)
@RequiredArgsConstructor
@Controller
public class QuestionController {
(... 생략 ...)
// 추천 URL 매핑 (GET)
@PreAuthorize("isAuthenticated()") // 로그인한 사람만 추천 가능
@GetMapping("/question/vote/{id}")
public String questionVote(Principal principal, @PathVariable("id") Integer id) {
Question question = this.questionService.getQuestion(id);
SiteUser siteUser = this.userService.getUser(principal.getName());
this.questionService.vote(question, siteUser);
return String.format("redirect:/question/detail/%s", id);
}
}
추천알림 후에 호출된 URL을 처리하기위한 questionVote메서드 추가.
QuestionService의 vote 메서드를 호출하여 추천인을 저장.
- 추천 기능 확인
코드 수정
이 부분은 학습 사이트와 다른 부분이다.
테스트를 하다보니, 맘에 안드는 점이 있었다.
추천을 이미 했어도 알림 메시지가 뜨고, 또 이미 준 추천을 취소하지도 못한다.
따라서 추천을 다시 누르면 취소가 되고, 알림 메시지를 제거하려고 한다.
알림 메시지 취소
- question_detail.html 수정
<!-- 경로 : sbb/src/main/resources/templates/question_detail.html -->
<html layout:decorate="~{layout}">
(... 생략 ...)
<!-- 질문 - 추천, 수정, 삭제 버튼 -->
<div class="my-3">
<!-- 추천 -->
<a th:href="@{|/question/vote/${question.id}|}" class="btn btn-sm btn-outline-secondary">
추천
<span class="badge rounded-pill bg-success" th:text="${#lists.size(question.voter)}"></span>
</a>
<!-- 수정 -->
<a th:href="@{|/question/modify/${question.id}|}" class="btn btn-sm btn-outline-secondary"
sec:authorize="isAuthenticated()"
th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
th:text="수정"></a>
<!-- 삭제 -->
<a href="javascript:void(0);"
th:data-uri="@{|/question/delete/${question.id}|}"
class="delete btn btn-sm btn-outline-secondary"
sec:authorize="isAuthenticated()"
th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
th:text="삭제"></a>
</div>
</div>
</div>
(... 생략 ...)
<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;
};
});
});
</script>
</html>
추천의 href속성을 지우고 th:href 속성에 기존 URL을 매핑시켜주었다.
추천확인 자바스크립트 코드도 지워주었다.
- QuestionService.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionService.java
package com.mysite.sbb.question;
(... 생략 ...)
@RequiredArgsConstructor
@Service
public class QuestionService {
(... 생략 ...)
// 질문 추천 취소 메서드
public void votedel(Question question, SiteUser siteUser) {
question.getVoter().remove(siteUser);
this.questionRepository.save(question);
}
}
질문 추천을 취소하는 votedel 메서드를 추가.
- QuestionController.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionController.java
package com.mysite.sbb.question;
(... 생략 ...)
@RequiredArgsConstructor
@Controller
public class QuestionController {
(... 생략 ...)
// 추천 URL 매핑 (GET)
@PreAuthorize("isAuthenticated()") // 로그인한 사람만 추천 가능
@GetMapping("/question/vote/{id}")
public String questionVote(Principal principal, @PathVariable("id") Integer id) {
Question question = this.questionService.getQuestion(id);
SiteUser siteUser = this.userService.getUser(principal.getName());
// 추천 중복검사
if (question.getVoter().contains(siteUser) == true) {
this.questionService.votedel(question, siteUser);
}
else {
this.questionService.vote(question, siteUser);
}
return String.format("redirect:/question/detail/%s", id);
}
}
Set 자료구조의 값을 검색하는 contains를 사용하여 추천 여부를 판단한다.
답변 추천
질문 추천 기능과 동일!
답변 추천 버튼
- question_detail.html 수정
<!-- 경로 : sbb/src/main/resources/templates/question_detail.html -->
<html layout:decorate="~{layout}">
(... 생략 ...)
<!-- 답변 반복 시작 -->
<div th:each="answer : ${question.answerList}">
<div class="card my-3">
<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">수정됨</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 th:href="@{|/answer/vote/${answer.id}|}" class="btn btn-sm btn-outline-secondary">
추천
<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>
<!-- 답변 반복 끝 -->
(... 생략 ...)
</html>
답변 추천 버튼도 마찬가지로, 알림을 지우고, 추천취소 기능도 추가하였다.
추천인 저장
- AnswerService.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/answer/AnswerService.java
package com.mysite.sbb.answer;
(... 생략 ...)
@RequiredArgsConstructor
@Service
public class AnswerService {
(... 생략 ...)
// 답변 추천 메서드
public void vote(Answer answer, SiteUser siteUser) {
answer.getVoter().add(siteUser);
this.answerRepository.save(answer);
}
// 답변 추천 취소 메서드
public void votedel(Answer answer, SiteUser siteUser) {
answer.getVoter().remove(siteUser);
this.answerRepository.save(answer);
}
}
Answer엔티티에 사용자를 저장 / 삭제하는 메서드를 추가.
추천 URL 처리
- AnswerController.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/answer/AnswerController.java
package com.mysite.sbb.answer;
(... 생략 ...)
@RequestMapping("/answer") // URL 프리픽스
@RequiredArgsConstructor
@Controller
public class AnswerController {
(... 생략 ...)
// 추천 URL 매핑 (GET)
@PreAuthorize("isAuthenticated()")
@GetMapping("/vote/{id}")
public String answerVote(Principal principal, @PathVariable("id") Integer id) {
Answer answer = this.answerService.getAnswer(id);
SiteUser siteUser = this.userService.getUser(principal.getName());
if(answer.getVoter().contains(siteUser) == true) {
this.answerService.votedel(answer, siteUser);
}
else {
this.answerService.vote(answer, siteUser);
}
return String.format("redirect:/question/detail/%s", answer.getQuestion().getId());
}
}
question과 마찬가지로 똑같은 로직을 주어 처리했다
SBB테스트
다른 아이디를 만들어 추천을 하고 취소를 해보자!
Leave a comment