03K. ์ถ์ฒ
03K. ์ถ์ฒ ๊ด๋ จ
์ด๋ฒ์๋ ์ง๋ฌธ๊ณผ ๋ต๋ณ์ "์ถ์ฒ(์ข์์)" ๊ธฐ๋ฅ์ ๊ตฌํํด ๋ณด์.
์ํฐํฐ ๋ณ๊ฒฝ
์ง๋ฌธ, ๋ต๋ณ์ ์ถ์ฒ์ ์ถ์ฒํ ์ฌ๋(SiteUser ๊ฐ์ฒด)์ ์ง๋ฌธ, ๋ต๋ณ ์ํฐํฐ์ ์ถ๊ฐํด์ผ ํ๋ค.
Question
์ฐ์ Question ์ํฐํฐ์ ์ถ์ฒ์ธ(voter) ์์ฑ์ ์ถ๊ฐํด ๋ณด์.
ํ๋์ ์ง๋ฌธ์ ์ฌ๋ฌ์ฌ๋์ด ์ถ์ฒํ ์ ์๊ณ ํ ์ฌ๋์ด ์ฌ๋ฌ ๊ฐ์ ์ง๋ฌธ์ ์ถ์ฒํ ์ ์๋ค. ์ด๋ ๋ฏ ์ง๋ฌธ๊ณผ ์ถ์ฒ์ธ์ ๋ถ๋ชจ์ ์์์ ๊ด๊ณ๊ฐ ์๋๊ณ ๋๋ฑํ ๊ด๊ณ์ด๊ธฐ ๋๋ฌธ์ @ManyToMany๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค.
๐์ฐธ๊ณ : https://docs.oracle.com/javaee/7/api/jakarta/persistence/ManyToMany.html
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/question/Question.java
// (... ์๋ต ...)
import java.util.Set;
import jakarta.persistence.ManyToMany;
// (... ์๋ต ...)
public class Question {
// (... ์๋ต ...)
@ManyToMany
Set<SiteUser> voter;
}
Set<SiteUser> voter ์ฒ๋ผ ์ถ์ฒ์ธ(voter)์ @ManyToMany ๊ด๊ณ๋ก ์ถ๊ฐํ๋ค. List๊ฐ ์๋ Set์ผ๋ก ํ ์ด์ ๋ ์ถ์ฒ์ธ์ ์ค๋ณต๋๋ฉด ์๋๊ธฐ ๋๋ฌธ์ด๋ค.
Set์ ์ค๋ณต์ ํ์ฉํ์ง ์๋ ์๋ฃํ์ด๋ค.
Answer
Answer ์ํฐํฐ ์ญ์ ๋ง์ฐฌ๊ฐ์ง ๋ฐฉ๋ฒ์ผ๋ก ์ถ์ฒ์ธ(voter) ์์ฑ์ ์ถ๊ฐํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/answer/Answer.java
// (... ์๋ต ...)
import java.util.Set;
import jakarta.persistence.ManyToMany;
// (... ์๋ต ...)
public class Answer {
// (... ์๋ต ...)
@ManyToMany
Set<SiteUser> voter;
}
ํ ์ด๋ธ ํ์ธ

QUESTION_VOTER, ANSWER_VOTER ํ
์ด๋ธ์ด ์์ฑ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค. ์ด๋ ๊ฒ @ManyToMany ๊ด๊ณ๋ก ์์ฑ์ ์์ฑํ๋ฉด ์๋ก์ด ํ
์ด๋ธ์ ์์ฑํ์ฌ ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌํ๋ค. ํ
์ด๋ธ์๋ ์๋ก ์ฐ๊ด๋ ์ํฐํฐ์ ๊ณ ์ ๋ฒํธ(id) 2๊ฐ๊ฐ ํ๋ผ์ด๋จธ๋ฆฌ ํค๋ก ๋์ด ์๊ธฐ ๋๋ฌธ์ ๋ค๋๋ค(N:N) ๊ด๊ณ๊ฐ ์ฑ๋ฆฝํ๋ ๊ตฌ์กฐ์ด๋ค.
์ง๋ฌธ ์ถ์ฒ
Question ์ํฐํฐ์ ์ถ์ฒ์ธ ์์ฑ์ ์ถ๊ฐ ํ์ผ๋ ์ด์ ์ง๋ฌธ ์ถ์ฒ ๊ธฐ๋ฅ์ ๋ง๋ค์ด ๋ณด์.
์ง๋ฌธ ์ถ์ฒ ๋ฒํผ
์ง๋ฌธ์ ์ถ์ฒํ ์ ์๋ ๋ฒํผ์ ์์น๋ ์ด๋๊ฐ ์ข์๊น? ๊ทธ๋ ๋ค. ์ง๋ฌธ ์์ธ ํ๋ฉด์ด๋ค. ์ง๋ฌธ ์์ธ ํ ํ๋ฆฟ์ ๋ค์๊ณผ ๊ฐ์ด ์์ ํ์.
ํ์ผ๋ช :
/sbb/src/main/resources/templates/question_detail.html
<!-- (... ์๋ต ...) -->
<!-- ์ง๋ฌธ -->
<h2 class="border-bottom py-2" th:text="${question.subject}"></h2>
<div class="card my-3">
<div class="card-body">
<!-- (... ์๋ต ...) -->
<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>
<!-- (... ์๋ต ...) -->
์ง๋ฌธ์ ์ถ์ฒ ๋ฒํผ์ ์ง๋ฌธ์ ์์ ๋ฒํผ ์ข์ธก์ ์ถ๊ฐํ๋ค. ๊ทธ๋ฆฌ๊ณ ๋ฒํผ์๋ ์ถ์ฒ์๋ ํจ๊ป ๋ณด์ด๋๋ก ํ๋ค. ์ถ์ฒ ๋ฒํผ์ ํด๋ฆญํ๋ฉด href์ ์์ฑ์ด javascript:void(0)์ผ๋ก ๋์ด ์๊ธฐ ๋๋ฌธ์ ์๋ฌด๋ฐ ๋์๋ ํ์ง ์๋๋ค. ํ์ง๋ง class ์์ฑ์ "recommend"๋ฅผ ์ถ๊ฐํ์ฌ ์๋ฐ์คํฌ๋ฆฝํธ๋ฅผ ์ฌ์ฉํ์ฌ data-uri์ ์ ์๋ URL์ด ํธ์ถ๋๋๋ก ํ ๊ฒ์ด๋ค. ์ด์ ๊ฐ์ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๋ ์ด์ ๋ "์ถ์ฒ" ๋ฒํผ์ ๋๋ ์๋ ํ์ธ์ฐฝ์ ํตํด ์ฌ์ฉ์์ ํ์ธ์ ๊ตฌํ๊ธฐ ์ํจ์ด๋ค.
์ถ์ฒ ๋ฒํผ ํ์ธ ์ฐฝ
์ด์ด์ [<์ถ์ฒ>] ๋ฒํผ์ ํด๋ฆญํ์ ๋ '์ ๋ง๋ก ์ถ์ฒํ์๊ฒ ์ต๋๊น?'๋ผ๋ ํ์ธ ์ฐฝ์ด ๋ํ๋์ผ ํ๋ฏ๋ก ๋ค์ ์ฝ๋๋ฅผ ์ถ๊ฐํ์.
ํ์ผ๋ช :
/sbb/src/main/resources/templates/question_detail.html
<!-- (... ์๋ต ...) -->
<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
๊ทธ๋ฆฌ๊ณ ์ถ์ฒ์ธ์ ์ ์ฅํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ด QuestionSerivce๋ฅผ ์์ ํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/question/QuestionService.java
// (... ์๋ต ...)
public class QuestionService {
// (... ์๋ต ...)
public void vote(Question question, SiteUser siteUser) {
question.getVoter().add(siteUser);
this.questionRepository.save(question);
}
}
Question ์ํฐํฐ์ ์ฌ์ฉ์๋ฅผ ์ถ์ฒ์ธ์ผ๋ก ์ ์ฅํ๋ vote ๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค.
QuestionController
์ด์ ์ถ์ฒ ๋ฒํผ์ ๋๋ ์๋ ํธ์ถ๋๋ URL์ ์ฒ๋ฆฌํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ด QuestionController๋ฅผ ์์ ํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/question/QuestionController.java
// (... ์๋ต ...)
public class QuestionController {
// (... ์๋ต ...)
@PreAuthorize("isAuthenticated()")
@GetMapping("/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);
}
}
์์ ๊ฐ์ด questionVote ๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค. ์ถ์ฒ์ ๋ก๊ทธ์ธํ ์ฌ๋๋ง ๊ฐ๋ฅํด์ผ ํ๋ฏ๋ก @PreAuthorize("isAuthenticated()") ์ ๋ํ
์ด์
์ด ์ ์ฉ๋์๋ค. ๊ทธ๋ฆฌ๊ณ ์์์ ์์ฑํ QuestionService์ vote ๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ ์ถ์ฒ์ธ์ ์ ์ฅํ๋ค. ์ค๋ฅ๊ฐ ์๋ค๋ฉด ์ง๋ฌธ ์์ธํ๋ฉด์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ํ๋ค.
์ง๋ฌธ ์ถ์ฒ ํ์ธ

[์ถ์ฒ] ๋ฒํผ์ด ์๊ฒผ์ ๊ฒ์ด๋ค. ๋ฒํผ์ด ์ ์๋ํ๋์ง ํ์ธํ์.๋ต๋ณ ์ถ์ฒ
๋ต๋ณ ์ถ์ฒ ๊ธฐ๋ฅ์ ์ง๋ฌธ ์ถ์ฒ ๊ธฐ๋ฅ๊ณผ ๋์ผํ๋ฏ๋ก ๋น ๋ฅด๊ฒ ์์ฑํด ๋ณด์.
๋ต๋ณ ์ถ์ฒ ๋ฒํผ
๋ต๋ณ์ ์ถ์ฒ์๋ฅผ ํ์ํ๊ณ , ๋ต๋ณ์ ์ถ์ฒํ ์์๋ ๋ฒํผ์ ์ง๋ฌธ ์์ธ ํ ํ๋ฆฟ์ ๋ค์๊ณผ ๊ฐ์ด ์ถ๊ฐํ์.
ํ์ผ๋ช :
/sbb/src/main/resources/templates/question_detail.html
<!-- (... ์๋ต ...) -->
<!-- ๋ต๋ณ ๋ฐ๋ณต ์์ -->
<div class="card my-3" th:each="answer : ${question.answerList}">
<div class="card-body">
<!-- (... ์๋ต ...) -->
<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>
<!-- ๋ต๋ณ ๋ฐ๋ณต ๋ -->
<!-- (... ์๋ต ...) -->
์ง๋ฌธ๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก ๋ต๋ณ ์์ญ์ ์๋จ์ ๋ต๋ณ์ ์ถ์ฒํ ์ ์๋ ๋ฒํผ์ ์์ฑํ๋ค. ์ด ์ญ์ ์ถ์ฒ ๋ฒํผ์ class="recommend"๊ฐ ์ ์ฉ๋์ด ์์ผ๋ฏ๋ก ์ถ์ฒ ๋ฒํผ์ ํด๋ฆญํ๋ฉด "์ ๋ง๋ก ์ถ์ฒํ์๊ฒ ์ต๋๊น?"๋ผ๋ ์ง๋ฌธ์ด ๋ํ๋๊ณ ["ํ์ธ"]์ ์ ํํ๋ฉด data-uri ์์ฑ์ ์ ์ํ URL์ด ํธ์ถ๋ ๊ฒ์ด๋ค.
AnswerService
๊ทธ๋ฆฌ๊ณ ๋ต๋ณ์ ์ถ์ฒ์ธ์ ์ ์ฅํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ด AnswerService๋ฅผ ์์ ํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/answer/AnswerService.java
// (... ์๋ต ...)
public class AnswerService {
// (... ์๋ต ...)
public void vote(Answer answer, SiteUser siteUser) {
answer.getVoter().add(siteUser);
this.answerRepository.save(answer);
}
}
Answer ์ํฐํฐ์ ์ฌ์ฉ์๋ฅผ ์ถ์ฒ์ธ์ผ๋ก ์ ์ฅํ๋ vote ๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค.
AnswerController
์ด์ ๋ต๋ณ ์ถ์ฒ ๋ฒํผ์ ๋๋ ์๋ ํธ์ถ๋๋ URL์ ์ฒ๋ฆฌํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ด AnswerController๋ฅผ ์์ ํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/answer/AnswerController.java
// (... ์๋ต ...)
public class AnswerController {
// (... ์๋ต ...)
@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());
this.answerService.vote(answer, siteUser);
return String.format("redirect:/question/detail/%s", answer.getQuestion().getId());
}
}
์์ ๊ฐ์ด answerVote ๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค. ์ถ์ฒ์ ๋ก๊ทธ์ธํ ์ฌ๋๋ง ๊ฐ๋ฅํด์ผ ํ๋ฏ๋ก @PreAuthorize("isAuthenticated()") ์ ๋ํ
์ด์
์ด ์ ์ฉ๋์๋ค. ๊ทธ๋ฆฌ๊ณ ์์์ ์์ฑํ AnswerService์ vote ๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ ์ถ์ฒ์ธ์ ์ ์ฅํ๋ค. ์ค๋ฅ๊ฐ ์๋ค๋ฉด ์ง๋ฌธ ์์ธํ๋ฉด์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ํ๋ค.
๋ต๋ณ ์ถ์ฒ ํ์ธ
