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
๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ ์ถ์ฒ์ธ์ ์ ์ฅํ๋ค. ์ค๋ฅ๊ฐ ์๋ค๋ฉด ์ง๋ฌธ ์์ธํ๋ฉด์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ํ๋ค.