Follow Work/SpringbootBoard

[StringBoot] 추천 (24)

ReCode.B 2022. 8. 17. 23:27
728x90

참고 링크 : https://wikidocs.net/book/7601

오라클DB와 intelliJ로 작업하였습니다


이번에는 질문과 답변에 "추천(좋아요)" 기능을 구현해 보자.

 

엔티티 변경

질문, 답변의 추천은 추천한 사람(SiteUser 객체)을 질문, 답변 엔티티에 추가해야 한다.

 

Question

Question과 Answer 엔티티에 추천인(voter) 속성을 추가해 보자.

하나의 질문에 여러사람이 추천할 수 있고 한 사람이 여러 개의 질문을 추천할 수 있다.

이렇듯 질문과 추천인은 부모와 자식의 관계가 아니고 대등한 관계이기 때문에 @ManyToMany를 사용해야 한다.

package com.gosari.repick_project.question;


import com.gosari.repick_project.answer.Answer;
import com.gosari.repick_project.user.SiteUser;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.Set;

@Setter
@Getter
@Entity
public class Question {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Integer id;

    private String subject;

    private String content;

    @Column(insertable = false, updatable = false, columnDefinition = "date default LOCALTIMESTAMP")
    private Date createDate;

    /*수정일시*/
    @Column(insertable = true, updatable = true, columnDefinition = "date default LOCALTIMESTAMP")
    private Date modifyDate;

    @OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
    private List<Answer>answerList; //질문엔티티에 답변엔티티참조 (질문1:답변N)

    @ManyToOne //여러개의 질문이 한명의 사용자에게 작성될수있으므로 @ManyToOne
    private SiteUser author; /*author 속성추가*/

    @ManyToMany //하나의 질문에 여러사람이 추천, 한사람이 여러개의 질문을 추천(대등관계)
    Set<SiteUser> voter; /*추천인은 중복되면 안됨-Set*/
}

Answer

package com.gosari.repick_project.answer;

import com.gosari.repick_project.question.Question;
import com.gosari.repick_project.user.SiteUser;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Set;

@Setter
@Getter
@Entity
public class Answer {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Integer id;

    private String content;

    @Column(insertable = false, updatable = false, columnDefinition = "date default LOCALTIMESTAMP")
    private Date createDate;

    /*수정일시*/
    @Column(insertable = true, updatable = true, columnDefinition = "date default LOCALTIMESTAMP")
    private Date modifyDate;

    @ManyToOne
    private Question question; //답변엔티티에 질문엔티티참조 (질문1:답변N)

    @ManyToOne //여러개의 질문이 한명의 사용자에게 작성될수있으므로 @ManyToOne
    private SiteUser author; /*author 속성추가*/

    @ManyToMany //하나의 질문에 여러사람이 추천, 한사람이 여러개의 질문을 추천(대등관계)
    Set<SiteUser> voter; /*추천인은 중복되면 안됨-Set*/
}

테이블 확인

질문과 답변 엔티티에 voter 속성을 추가한후 확인해 보자

QUESTION_VOTER, ANSWER_VOTER 테이블이 생성된 것을 확인할 수 있다.

이렇게 @ManyToMany 관계로 속성을 생성하면 새로운 테이블을 생성하여 데이터를 관리한다.

테이블에는 서로 연관된 엔티티의 고유번호(id) 2개가 프라이머리 키로 되어 있기 때문에

다대다(N:N) 관계가 성립하는 구조이다.


질문 추천

Question 엔티티에 추천인 속성을 추가 했으니 이제 질문 추천 기능을 만들어 보자.

 

 

질문 추천 버튼

질문을 추천할 수 있는 버튼의 위치는 어디가 좋을까?

그렇다. 질문 상세 화면이다. 질문 상세 템플릿을 다음과 같이 수정하자.

<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>

질문의 추천 버튼을 질문의 수정 버튼 좌측에 추가하자. 버튼에는 추천수도 함께 보이도록 했다.

추천 버튼을 클릭하면 href의 속성이 javascript:void(0)으로 되어 있기 때문에 아무런 동작도 하지 않는다.

하지만 class 속성에 "recommend"를 추가하여 자바스크립트를 사용하여 data-uri에 정의된 URL이 호출되도록 할 것이다. 이와 같은 방법을 사용하는 이유는 "추천" 버튼을 눌렀을때 확인창을 통해 사용자의 확인을 구하기 위함이다.

 

추천 버튼 확인 창

이어서 <추천> 버튼을 클릭했을 때 '정말로 추천하시겠습니까?'라는 확인 창이 나타나야 하므로 다음 코드를 추가하자.

const recommend_elements = document.getElementsByClassName("recommend");
Array.from(recommend_elements).forEach(function(element) {
    element.addEventListener('click', function() {
        if(confirm("정말로 추천하시겠습니까?")) {
            location.href = this.dataset.uri;
        };
    });
});

추천 버튼에 class="recommend"가 적용되어 있으므로 추천 버튼을 클릭하면 "정말로 추천하시겠습니까?"라는 질문이 나타나고 "확인"을 선택하면 data-uri 속성에 정의한 URL이 호출될 것이다.

 

 

위 두 수정사항을 반영한 question.detail.html 코드이다.

<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml"
      layout:decorate="~{layout}">


<div layout:fragment="content" class="container my-3">
    <!--질문-->
    <h2 class="border-bottom py-2" th:text="${question.subject}"></h2>
    <div class="card my-3">
        <div class="card-body">
            <div class="card-text" style="white-space: pre-line;" th:text="${question.content}"></div>
            <div class="d-flex justify-content-end">
                <!--질문 수정일시표시-->
                <div th:if="${question.modifyDate != null}" class="badge bg-light text-dark p-2 text-start mx-3">
                    <div class="mb-2">modified at</div>
                    <div th:text="${#dates.format(question.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="${question.author != null}" th:text="${question.author.username}"></span>
                    </div>
                    <div th:text="${#dates.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></div>

                </div>
            </div>

            <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>
    <!--답변의 갯수표시-->
    <h5 class="border-bottom my-3 py-2"
        th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
    <!--#list.size(이터러블객체)-타임리프가 제공하는 유틸리티로 객체의 길이를 반환-->
    <!--답변 반복 시작-->
    <div class="card my-3" th:each="answer : ${question.answerList}">
        <div class="card-body">
            <div class="card-text" style="white-space: pre-line;" th:text="${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">modified at</div>
                    <div th:text="${#dates.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="${#dates.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
                </div>
            </div>

            <div class="my-3">
                <!--답변수정버튼-->
                <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>
    <!--답변 반복 끝-->
    <!--답변 작성-->
    <form th:action="@{|/answer/create/${question.id}|}"
          th:object="${answerForm}" method="post" class="my-3">

        <div th:replace="form_errors :: formErrorsFragment"></div>
        <!--th:replace - 공통템플릿을 템플릿내에 삽입가능-->
        <!--div엘리먼트를 form_errors.html파일의 th:fragment속성명이 formErrorsFragment인 엘리먼트로 교체-->


        <textarea sec:authorize="isAnonymous()" disabled th:field="*{content}"
                  class="form-control" rows="10"></textarea>
        <textarea sec:authorize="isAuthenticated()" th:field="*{content}"
                  class="form-control" rows="10"></textarea>

        <input type="submit" value="답변등록" class="btn btn-primary my-2">
    </form>
</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;
            }
        });
    });
    <!--추천자바스크립트-->
    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>

 

QuestionService

그리고 추천인을 저장하기 위해 다음과 같이 QuestionSerivce를 수정하자.

package com.gosari.repick_project.question;
import com.gosari.repick_project.user.SiteUser;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import com.gosari.repick_project.exception.DataNotFoundException;

@RequiredArgsConstructor //QuestionRepository 생성자 생성해줌
@Service
public class QuestionService {
    private final QuestionRepository questionRepository;

    public List<Question> getList(){
        return this.questionRepository.findAll();
    }

    /*Question 데이터 조사*/
    public Question getQuestion(Integer id){
        Optional<Question> question = this.questionRepository.findById(id);
        if(question.isPresent()){
            return question.get();
        }else{
            throw new DataNotFoundException("question not found");
        }
    }

    /*페이징*/
    public Page<Question> getList(int page){
        List<Sort.Order> sorts = new ArrayList<>();
        sorts.add(Sort.Order.desc("createDate"));
        Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
        return this.questionRepository.findAll(pageable);
    }

    /*질문데이터를 저장하는 create메서드*/
    public void create(String subject, String content, SiteUser user){
        Question q = new Question();
        q.setSubject(subject);
        q.setContent(content);
        q.setCreateDate(new Date());
        q.setAuthor(user);
        this.questionRepository.save(q);
    }

    /*질문데이터 수정*/
    public void modify(Question question, String subject, String content) {
        question.setSubject(subject);
        question.setContent(content);
        question.setModifyDate(new Date());

        this.questionRepository.save(question);
    }

    /*질문데이터 삭제*/
    public void delete(Question question){
        this.questionRepository.delete(question);
    }

    /*추천인 저장*/
    public void vote(Question question, SiteUser siteUser) {
        question.getVoter().add(siteUser);
        this.questionRepository.save(question);
    }
}

Question 엔티티에 사용자를 추천인으로 저장하는 vote 메서드를 추가했다.

 

QuestionController

이제 추천 버튼을 눌렀을때 호출되는 URL을 처리하기 위해 다음과 같이 QuestionController를 수정하자.

package com.gosari.repick_project.question;

import com.gosari.repick_project.answer.AnswerForm;
import com.gosari.repick_project.user.SiteUser;
import com.gosari.repick_project.user.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;


import javax.validation.Valid;
import java.security.Principal;
import java.util.List;


@RequestMapping("/question") //URL프리픽스(prefix)
@RequiredArgsConstructor
@Controller
public class QuestionController {

    private final QuestionService questionService;
    private final UserService userService;

    /*질문리스트*/
    @RequestMapping("/list")
    public String list(Model model,
                       @RequestParam(value="page", defaultValue="0") int page){
        Page<Question> paging = this.questionService.getList(page);
        model.addAttribute("paging", paging); /*페이징*/
        return "question_list";
    }

    /*질문상세*/
    @RequestMapping(value = "/detail/{id}")
    public String detail(Model model, @PathVariable("id") Integer id,
                         AnswerForm answerForm) { /*AnswerForm추가*/
        Question question = this.questionService.getQuestion(id);
        model.addAttribute("question", question);
        return "question_detail";
    }

    /*질문등록하기*/
    @PreAuthorize("isAuthenticated()") //추가
    @GetMapping("/create")
    public String questionCreate(QuestionForm questionForm) {
        return "question_form";
    }

    /*질문등록하기*/
    @PreAuthorize("isAuthenticated()") //추가
    @PostMapping("/create")
    public String questionCreate(@Valid QuestionForm questionForm,
                                 BindingResult bindingResult,
                                 Principal principal) {

        if (bindingResult.hasErrors()) {
            return "question_form";
        }

        SiteUser siteUser = this.userService.getUser(principal.getName());

        this.questionService.create(questionForm.getSubject(), questionForm.getContent(), siteUser);
        return "redirect:/question/list";
    }

    /*질문 수정하기 GET*/
    @PreAuthorize("isAuthenticated()")
    @GetMapping("/modify/{id}")
    public String questionModify(QuestionForm questionForm,
                                 @PathVariable("id") Integer id, Principal principal) {

        Question question = this.questionService.getQuestion(id);

        /*로그인한 사용자와 질문의 작성자가 동일하지않은 경우*/
        if(!question.getAuthor().getUsername().equals(principal.getName())) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정권한이 없습니다.");
        }
        /*수정할 질문의 제목과 내용을 보여줌*/
        questionForm.setSubject(question.getSubject());
        questionForm.setContent(question.getContent());
        return "question_form";
    }

    /*질문 수정하기 POST*/
    @PreAuthorize("isAuthenticated()")
    @PostMapping("/modify/{id}")
    public String questionModify(@Valid QuestionForm questionForm, BindingResult bindingResult,
                                 Principal principal, @PathVariable("id") Integer id) {
        if (bindingResult.hasErrors()) {
            return "question_form";
        }
        Question question = this.questionService.getQuestion(id);
        if (!question.getAuthor().getUsername().equals(principal.getName())) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정권한이 없습니다.");
        }
        this.questionService.modify(question, questionForm.getSubject(), questionForm.getContent());
        return String.format("redirect:/question/detail/%s", id);
    }

    /*질문 삭제하기*/
    @PreAuthorize("isAuthenticated()")
    @GetMapping("/delete/{id}")
    public String questionModify(Principal principal,
                                 @PathVariable("id") Integer id) {

        Question question = this.questionService.getQuestion(id);

        /*로그인한 사용자와 질문의 작성자가 동일하지않은 경우*/
        if(!question.getAuthor().getUsername().equals(principal.getName())) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "삭제권한이 없습니다.");
        }

        this.questionService.delete(question);
        return "redirect:/";
    }
    /*질문추천*/
    @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 메서드를 호출하여 추천인을 저장했다.

오류가 없다면 질문 상세화면으로 리다이렉트 한다.

 

질문 추천 확인

질문 상세 화면의 본문 상단을 보면 <추천> 버튼이 생겼을 것이다. 버튼이 잘 작동하는지 확인하자.


답변 추천

답변 추천 기능은 질문 추천 기능과 동일하므로 빠르게 작성해 보자.

 

답변 추천 버튼

답변의 추천수를 표시하고, 답변을 추천할 수있는 버튼을 질문 상세 템플릿에 다음과 같이 추가하자.

<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml"
      layout:decorate="~{layout}">


<div layout:fragment="content" class="container my-3">
    <!--질문-->
    <h2 class="border-bottom py-2" th:text="${question.subject}"></h2>
    <div class="card my-3">
        <div class="card-body">
            <div class="card-text" style="white-space: pre-line;" th:text="${question.content}"></div>
            <div class="d-flex justify-content-end">
                <!--질문 수정일시표시-->
                <div th:if="${question.modifyDate != null}" class="badge bg-light text-dark p-2 text-start mx-3">
                    <div class="mb-2">modified at</div>
                    <div th:text="${#dates.format(question.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="${question.author != null}" th:text="${question.author.username}"></span>
                    </div>
                    <div th:text="${#dates.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></div>

                </div>
            </div>

            <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>
    <!--답변의 갯수표시-->
    <h5 class="border-bottom my-3 py-2"
        th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
    <!--#list.size(이터러블객체)-타임리프가 제공하는 유틸리티로 객체의 길이를 반환-->
    <!--답변 반복 시작-->
    <div class="card my-3" th:each="answer : ${question.answerList}">
        <div class="card-body">
            <div class="card-text" style="white-space: pre-line;" th:text="${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">modified at</div>
                    <div th:text="${#dates.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="${#dates.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
                </div>
            </div>

            <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>
    <!--답변 반복 끝-->
    <!--답변 작성-->
    <form th:action="@{|/answer/create/${question.id}|}"
          th:object="${answerForm}" method="post" class="my-3">

        <div th:replace="form_errors :: formErrorsFragment"></div>
        <!--th:replace - 공통템플릿을 템플릿내에 삽입가능-->
        <!--div엘리먼트를 form_errors.html파일의 th:fragment속성명이 formErrorsFragment인 엘리먼트로 교체-->


        <textarea sec:authorize="isAnonymous()" disabled th:field="*{content}"
                  class="form-control" rows="10"></textarea>
        <textarea sec:authorize="isAuthenticated()" th:field="*{content}"
                  class="form-control" rows="10"></textarea>

        <input type="submit" value="답변등록" class="btn btn-primary my-2">
    </form>
</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;
            }
        });
    });
    <!--추천자바스크립트-->
    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이 호출될 것이다.

 

AnswerService

그리고 답변에 추천인을 저장하기 위해 다음과 같이 AnswerService를 수정하자.

package com.gosari.repick_project.answer;

import com.gosari.repick_project.exception.DataNotFoundException;
import com.gosari.repick_project.question.Question;
import com.gosari.repick_project.user.SiteUser;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Optional;

@RequiredArgsConstructor
@Service
public class AnswerService {

    private final AnswerRepository answerRepository;

    /*답변생성*/
    public Answer create(Question question, String content, SiteUser author) {
        Answer answer = new Answer();

        answer.setContent(content);
        answer.setCreateDate(new Date());
        answer.setQuestion(question);
        answer.setAuthor(author);

        this.answerRepository.save(answer);

        return answer;
    }

    /*답변조회*/
    public Answer getAnswer(Integer id) {
        Optional<Answer> answer = this.answerRepository.findById(id);
        if (answer.isPresent()) {
            return answer.get();
        } else {
            throw new DataNotFoundException("answer not found");
        }
    }

    /*답변수정*/
    public void modify(Answer answer, String content) {
        answer.setContent(content);
        answer.setModifyDate(new Date());
        this.answerRepository.save(answer);
    }

    /*답변삭제*/
    public void delete(Answer answer){
        this.answerRepository.delete(answer);
    }

    /*추천인저장*/
    public void vote(Answer answer, SiteUser siteUser){
        answer.getVoter().add(siteUser);
        this.answerRepository.save(answer);
    }
}

Answer 엔티티에 사용자를 추천인으로 저장하는 vote 메서드를 추가했다.

 

AnswerController

이제 답변 추천 버튼을 눌렀을때 호출되는 URL을 처리하기 위해 다음과 같이 AnswerController를 수정하자.

package com.gosari.repick_project.answer;

import com.gosari.repick_project.question.Question;
import com.gosari.repick_project.question.QuestionService;
import com.gosari.repick_project.user.SiteUser;
import com.gosari.repick_project.user.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import javax.validation.Valid;
import java.security.Principal;

@RequestMapping("/answer")
@RequiredArgsConstructor
@Controller
public class AnswerController {

    private final QuestionService questionService;
    private final AnswerService answerService;
    private final UserService userService;

    /*답변생성*/
    @PreAuthorize("isAuthenticated()")
    @PostMapping("/create/{id}")
    public String createAnswer(Model model, @PathVariable("id") Integer id,
                               @Valid AnswerForm answerForm, BindingResult bindingResult,
                               Principal principal) {

        Question question = this.questionService.getQuestion(id);
        SiteUser siteUser = this.userService.getUser(principal.getName());

        if(bindingResult.hasErrors()){
            model.addAttribute("question", question);
            return "question_detail";
        }
        this.answerService.create(question, answerForm.getContent(), siteUser);
        return String.format("redirect:/question/detail/%s", id);
    }

    /*답변수정 GET*/
    @PreAuthorize("isAuthenticated()")
    @GetMapping("/modify/{id}")
    public String answerModify(AnswerForm answerForm, @PathVariable("id") Integer id, Principal principal) {
        Answer answer = this.answerService.getAnswer(id);
        if (!answer.getAuthor().getUsername().equals(principal.getName())) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정권한이 없습니다.");
        }
        answerForm.setContent(answer.getContent());
        return "answer_form";
    }

    /*답변수정 POST*/
    @PreAuthorize("isAuthenticated()")
    @PostMapping("/modify/{id}")
    public String answerModify(@Valid AnswerForm answerForm, BindingResult bindingResult,
                               @PathVariable("id") Integer id, Principal principal) {
        if (bindingResult.hasErrors()) {
            return "answer_form";
        }
        Answer answer = this.answerService.getAnswer(id);
        if (!answer.getAuthor().getUsername().equals(principal.getName())) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정권한이 없습니다.");
        }
        this.answerService.modify(answer, answerForm.getContent());
        return String.format("redirect:/question/detail/%s", answer.getQuestion().getId());
    }

    /*답변삭제*/
    @PreAuthorize("isAuthenticated()")
    @GetMapping("/delete/{id}")
    public String answerDelete(Principal principal, @PathVariable("id") Integer id) {
    Answer answer = this.answerService.getAnswer(id);
    if (!answer.getAuthor().getUsername().equals(principal.getName())){
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "삭제권한이 없습니다.");
    }
    this.answerService.delete(answer);
    return String.format("redirect:/question/detail/%s", answer.getQuestion().getId());
    }

    /*답변추천*/
    @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 메서드를 호출하여 추천인을 저장한다. 오류가 없다면 질문 상세화면으로 리다이렉트 한다.

 

답변 추천 확인

이와 같이 수정 후 답변 추천 기능도 확인해 보자.

728x90