본문 바로가기
공부 및 정리/점프 투 스프링부트

점프 투 스프링부트 추가 기능 구현 - 댓글

by 스파이펭귄 2024. 1. 19.
728x90

https://wikidocs.net/162814의 추가 기능 구현 - 댓글

 

3-14 SBB 추가 기능 구현하기

이 책에서 구현할 SBB의 기능은 아쉽지만 여기까지이다. 함께 더 많은 기능을 추가하고 싶지만 이 책은 SBB의 완성이 아니라 SBB를 성장시키는 경험을 전달하는 데 목표를 두고…

wikidocs.net

이 글은 점프 투 스프링 부트의 3-13까지 구현과 이 글 이전의 구현을 마친 것을 전제로 한다.


Comment 클래스 생성

comment 패키지와 Comment 엔티티부터 생성해보자.

package com.example.main.comment;

import com.example.main.answer.Answer;
import com.example.main.question.Question;
import com.example.main.user.SiteUser;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;

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

    @Column(columnDefinition = "TEXT")
    private String content;

    private LocalDateTime createDate;

    @ManyToOne
    private Question question;

    @ManyToOne
    private Answer answer;

    @ManyToOne
    private SiteUser author;
}

Question과 Answer 모두에 달 수 있는 객체로 Comment를 생성했다. Question에 달린 Comment면 단순히 Answer는 null로 저장하면 된다.

Answer와 Question에도 마찬가지로 추가해주자.

public class Answer {
    (...생략...)
    @OneToMany(mappedBy = "answer", cascade = CascadeType.REMOVE)
    private List<Comment> commentList;
}
public class Question {
    (...생략...)
    @OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
    private List<Comment> commentList;
}

CommentRepository을 생성하자.

package com.example.main.comment;

import com.example.main.question.Question;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface CommentRepository extends JpaRepository<Comment, Integer> {

    List<Comment> findByQuestion(Question question);
}

모든 Comment에는 Question이 있고, 특정 Question의 detail에 들어갈 때 댓글을 조회하기 때문에 Question을 통해 찾을 수 있는 함수를 추가하였다. CommentService도 생성하자.

package com.example.main.comment;

import com.example.main.DataNotFoundException;
import com.example.main.answer.Answer;
import com.example.main.question.Question;
import com.example.main.user.SiteUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Service
public class CommentService {
    private final CommentRepository commentRepository;

    @Autowired
    public CommentService(CommentRepository commentRepository) {
        this.commentRepository = commentRepository;
    }

    public Comment getComment(int id) {
        Optional<Comment> oc = this.commentRepository.findById(id);
        if (oc.isPresent()) {
            return oc.get();
        } else {
            throw new DataNotFoundException("comment not found");
        }
    }

    public Comment create(String content, Question question, Answer answer, 
                          SiteUser siteUser) {
        Comment c = new Comment();
        c.setContent(content);
        c.setQuestion(question);
        c.setAnswer(answer);
        c.setAuthor(siteUser);
        c.setCreateDate(LocalDateTime.now());
        this.commentRepository.save(c);
        return c;
    }

    public List<Comment> getCommentList(Question question) {
        return this.commentRepository.findByQuestion(question);
    }
}

간단히 일단은 Comment 생성과 Question과 id를 통해 Comment를 조회하는 함수만을 생성하였다.

Controller도 생성해주자. 일단은 댓글 작성 관련 함수들만 생성해주자.

package com.example.main.comment;

import com.example.main.answer.Answer;
import com.example.main.answer.AnswerService;
import com.example.main.question.Question;
import com.example.main.question.QuestionService;
import com.example.main.user.SiteUser;
import com.example.main.user.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.security.Principal;

@RequestMapping("/comment")
@Controller
public class CommentController {

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

    @Autowired
    public CommentController(CommentService commentService, QuestionService questionService,
                             AnswerService answerService, UserService userService) {
        this.commentService = commentService;
        this.questionService = questionService;
        this.answerService = answerService;
        this.userService = userService;
    }

    @PreAuthorize("isAuthenticated()")
    @PostMapping(value="/create/question/{id}")
    public String questionCommentCreate(Model model, @PathVariable("id") Integer id,
                                @Valid CommentForm commentForm, 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";
        }
        Comment comment = this.commentService.create(commentForm.getContent(), question, null, siteUser);
        return String.format("redirect:/question/detail/%s", id);
    }

    @PreAuthorize("isAuthenticated()")
    @PostMapping(value="/create/answer/{id}")
    public String answerCommentCreate(Model model, @PathVariable("id") Integer id,
                                        @Valid CommentForm commentForm, BindingResult bindingResult,
                                        Principal principal) {
        Answer answer = this.answerService.getAnswer(id);
        Question question = answer.getQuestion();
        SiteUser siteUser = this.userService.getUser(principal.getName());
        if (bindingResult.hasErrors()) {
            model.addAttribute("question", question);
            return "question_detail";
        }
        Comment comment = this.commentService.create(commentForm.getContent(), question, answer, siteUser);
        return String.format("redirect:/question/detail/%s#answer_%s", question.getId(), id);
    }
}

여기서는 댓글 작성 시 answer 댓글과 question 댓글을 다른 주소로 post 처리하였고, 마지막에는 각각 answer의 id, question의 id를 두게 했다. 그리고 CommentForm이라는 것을 통해 댓글 내용을 전달하게 하였다.

함수 내부 구현은 기존과 거의 동일하므로 생략한다.

CommentForm은 아래와 같다.

package com.example.main.comment;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class CommentForm {

    @NotEmpty(message="내용은 필수 항목입니다.")
    private String content;
}

아래와 같은 테스트로 잘 들어가는지 확인해보자.

@SpringBootTest
class MainApplicationTests {

    @Autowired
    private QuestionService questionService;
    @Autowired
    private AnswerService answerService;
    @Autowired
    private UserService userService;
    @Autowired
    private CommentService commentService;

//    @Transactional
    @Test
    void testJpa() {
        List<Question> questionLst = this.questionService.getList();
        Question question = questionLst.get(questionLst.size() - 1);
//        SiteUser user = this.userService.create("temp", "temp@temp.com", "1234");
        SiteUser user = this.userService.getUser("temp");

        this.commentService.create("테스트 질문 댓글", question, null, user);
        Answer answer = this.answerService.create(question, "테스트 답변", user);
        this.commentService.create("테스트 답변 댓글", question, answer, user);
    }
}

http://localhost:8080/h2-console에 접속해 확인해보면 잘 들어간 것을 확인할 수 있다.

이제 댓글을 출력하고, 작성하는 부분을 구현해주자.

 

댓글 버튼 추가

기존의 추천, 수정, 삭제 버튼을 아래와 같이 수정하고 댓글 버튼을 우측에 추가해주자.

(...생략...)
            <!-- 기능 버튼 -->
            <div class="my-3 d-flex justify-content-between">
                <!-- 왼쪽 버튼들 -->
                <div>
                    <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>

                <!-- 오른쪽 버튼 -->
                <a href="javascript:void(0);" class="comment btn btn-sm btn-outline-secondary"
                   th:id="|question-${question.id}|" th:text="|댓글 ${#lists.size(question.getCommentList())}">
                </a>
            </div>
        </div>
    </div>
        (...생략...)

            <!-- 기능 버튼 -->
            <div class="my-3 d-flex justify-content-between">
                <!-- 왼쪽 버튼들 -->
                <div>
                    <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>

                <!-- 오른쪽 버튼 -->
                <a href="javascript:void(0);" class="comment btn btn-sm btn-outline-secondary"
                   th:id="|ans-${answer.id}|" th:text="|댓글 ${#lists.size(answer.getCommentList())}|">
                </a>
            </div>
        </div>

위 그림처럼 댓글 버튼이 질문과 답변 카드 우측 하단에 존재하게 된다.

 

댓글 테이블 추가

댓글을 질문, 답변 카드 안에 나오도록 하게 하자. 이때 댓글들은 테이블 속성을 사용해 틀에 맞춰서 나오도록 하자.

                <!-- 오른쪽 버튼 -->
                <a href="javascript:void(0);" class="comment btn btn-sm btn-outline-secondary"
                   th:id="|question-${question.id}|" th:text="|댓글 ${#lists.size(question.getCommentList())}">
                </a>
            </div>
            <table class="table" th:if="${#lists.size(question.commentList)!=0}" style="display: none"
                   th:id="|comment-question-${question.id}|">
                <thead class="table-dark">
                <tr class="text-center">
                    <th style="width:70%">내용</th>
                    <th>글쓴이</th>
                    <th>작성일시</th>
                </tr>
                </thead>
                <tbody>
                <tr class="text-center" th:each="question_comment, loop : ${comment_list}"
                    th:if="${question_comment.getAnswer() == null}">
                    <td class="text-center" th:text="${question_comment.getContent()}"></td>
                    <td><span th:if="${question_comment.author != null}" th:text="${question_comment.author.username}"</td>
                    <td th:text="${#temporals.format(question_comment.createDate, 'yyyy-MM-dd HH:mm')}"></td>
                </tr>
                </tbody>
            </table>
        </div>
    </div>

    <!-- 답변 갯수 표시 -->
        (...생략...)
                <!-- 오른쪽 버튼 -->
                <a href="javascript:void(0);" class="comment btn btn-sm btn-outline-secondary"
                   th:id="|ans-${answer.id}|" th:text="|댓글 ${#lists.size(answer.getCommentList())}|">
                </a>
            </div>
        </div>
            <table class="table" th:if="${#lists.size(answer.commentList)!=0}" style="display: none"
               th:id="|comment-ans-${answer.id}|">
            <thead class="table-dark">
            <tr class="text-center">
                <th style="width:70%">내용</th>
                <th>글쓴이</th>
                <th>작성일시</th>
            </tr>
            </thead>
            <tbody>
            <tr class="text-center" th:each="answer_comment, loop : ${comment_list}"
                th:if="${answer_comment.getAnswer() != null and answer_comment.getAnswer().getId() == answer.getId()}">
                <td class="text-center" th:text="${answer_comment.getContent()}"></td>
                <td><span th:if="${answer_comment.author != null}" th:text="${answer_comment.author.username}"</td>
                <td th:text="${#temporals.format(answer_comment.createDate, 'yyyy-MM-dd HH:mm')}"></td>
            </tr>
            </tbody>
        </table>
    </div>

        <!-- 답변 페이징 처리 시작 -->
        (...생략...)

기본적인 display가 none이므로 보이지 않는다. 댓글은 내용 - 글쓴이 - 작성일시로 구성하였다. 또 댓글 버튼에 댓글의 갯수를 보여주게 하였다.

이제 자바스크립트를 이용해 댓글 버튼을 누르면 댓글이 나타나게 하자.

(...생략...)
<script>
    (...생략...)
    const comment_elements = document.getElementsByClassName("comment btn");
    Array.from(comment_elements).forEach(function(element) {
        element.addEventListener('click', function() {
            const comment_table = document.getElementById("comment-" + element.id)
            if (comment_table.style.display === 'none') {
                comment_table.style.display = 'table';
                element.style.background = 'gray';
                element.style.color = 'white';
            } else {
                comment_table.style.display = 'none';
                element.style.background = 'white';
                element.style.color = 'gray';
            }
        })
    })
</script>
</html>

댓글 버튼의 클래스를 불러와서 comment-와 해당 댓글의 아이디를 붙여서 테이블의 display를 수정시켰다. 또, 토글시마다 버튼의 색이 바뀌게 하였다.

 

결과적으로 댓글 버튼을 누르면 아래와 같이 보인다.

이제 남은건 댓글 작성과 댓글 삭제다.

 

 

댓글 삭제 추가하기

먼저 댓글 삭제를 추가해보자. 작성일시 옆에 작성자만 볼 수 있는 a 태그와 이미지 태그로 삭제 버튼을 만들어 주도록 하자.


(...생략...)

                <table class="table" th:if="${#lists.size(question.commentList)!=0}" style="display: none"
                   th:id="|comment-question-${question.id}|">
                <thead class="table-dark">
                <tr class="text-center">
                    <th style="width:70%">내용</th>
                    <th>글쓴이</th>
                    <th>작성일시</th>
                </tr>
                </thead>
                <tbody>
                <tr class="text-center" th:each="question_comment, loop : ${comment_list}"
                    th:if="${question_comment.getAnswer() == null}">
                    <td class="text-center" th:text="${question_comment.getContent()}"></td>
                    <td><span th:if="${question_comment.author != null}" th:text="${question_comment.author.username}"</td>
                    <td>
                        <span th:text="${#temporals.format(question_comment.createDate, 'yyyy-MM-dd HH:mm')}"></span>
                        <a th:href="@{|/comment/delete/${question_comment.id}|}" sec:authorize="isAuthenticated()"
                           th:if="${question_comment.author != null and #authentication.getPrincipal().getUsername() == question_comment.author.username}">
                            <img src="/images/erase_button.png"/>
                        </a>
                    </td>
                </tr>
                </tbody>
            </table>
        </div>
    </div>

(...생략...)

            <table class="table" th:if="${#lists.size(answer.commentList)!=0}" style="display: none"
               th:id="|comment-ans-${answer.id}|">
            <thead class="table-dark">
            <tr class="text-center">
                <th style="width:70%">내용</th>
                <th>글쓴이</th>
                <th>작성일시</th>
            </tr>
            </thead>
            <tbody>
            <tr class="text-center" th:each="answer_comment, loop : ${comment_list}"
                th:if="${answer_comment.getAnswer() != null and answer_comment.getAnswer().getId() == answer.getId()}">
                <td class="text-center" th:text="${answer_comment.getContent()}"></td>
                <td><span th:if="${answer_comment.author != null}" th:text="${answer_comment.author.username}"</td>
                <td>
                    <span th:text="${#temporals.format(answer_comment.createDate, 'yyyy-MM-dd HH:mm')}"></span>
                    <a th:href="@{|/comment/delete/${answer_comment.id}|}" sec:authorize="isAuthenticated()"
                       th:if="${answer_comment.author != null and #authentication.getPrincipal().getUsername() == answer_comment.author.username}">
                        <img src="/images/erase_button.png"/>
                    </a>
                </td>
            </tr>
            </tbody>
        </table>
    </div>

    <!-- 답변 페이징 처리 시작 -->
    (...생략...)

댓글에서 날짜 부분을 아래와 같이 고쳐주었다.

                <td>
                    <span th:text="${#temporals.format(answer_comment.createDate, 'yyyy-MM-dd HH:mm')}"></span>
                    <a th:href="@{|/comment/delete/${answer_comment.id}|}" sec:authorize="isAuthenticated()"
                       th:if="${answer_comment.author != null and #authentication.getPrincipal().getUsername() == answer_comment.author.username}">
                        <img src="/images/erase_button.png"/>
                    </a>
                </td>

댓글의 주인이 아닌 경우 보이지 않게 하였고, 이미지를 static/images에 추가해 주었다. (단순히 그림판에서 16x16으로 x를 그린 그림이다.)

이제 이 버튼을 누르면 /comment/delete/코멘트 아이디로 이동하게 되니 이 부분을 controller에서 처리해주자.

 

Service에서 Delete 함수를 만들어주자.

@Service
public class CommentService {
        (...생략...)
    public void delete(Comment comment) {
        this.commentRepository.delete(comment);
    }
}

그냥 단순히 comment를 받으면 CommentRepository의 delete 함수를 호출해 지우는 함수이다.

@RequestMapping("/comment")
@Controller
public class CommentController {
    (...생략...)
    @PreAuthorize("isAuthenticated()")
    @GetMapping(value="/delete/{id}")
    public String commentDelete(Model model, @PathVariable("id") Integer id,
                                Principal principal) {
        Comment comment = this.commentService.getComment(id);
        if (!comment.getAuthor().getUsername().equals(principal.getName())) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정 권한이 없습니다.");
        }
        this.commentService.delete(comment);
        return String.format("redirect:/question/detail/%s", comment.getQuestion().getId());
    }
}

PathVariable id는 댓글의 id로 생성때와 달리 질문과 답변 댓글 삭제를 따로 만들 필요 없다. 기존 answer, question의 delete와 동일하게 principal을 받아서 댓글의 author와 동일한 이름을 가진 경우 지울 수 있도록 하였다.

이제 댓글 등록하는 기능을 추가해보자.

 

댓글 등록 추가하기

댓글 생성 기능은 CommentService의 create와 CommentController의 answerCommentCreate와 questionCommentCreate를 통해 이미 만들었으므로 뷰 부분만 작성해주면 된다.

question_detail.html에 댓글 부분에 작성 폼을 추가해주자.

(...생략...)

                <!-- 오른쪽 버튼 -->
                <a href="javascript:void(0);" class="comment btn btn-sm btn-outline-secondary"
                   th:id="|question-${question.id}|" th:text="|댓글 ${#lists.size(question.getCommentList())}">
                </a>
            </div>
            <div style="display: none"
                 th:id="|comment-question-${question.id}|">
                <table class="table" th:if="${#lists.size(question.commentList)!=0}">
                    <thead class="table-dark">
                    <tr class="text-center">
                        <th style="width:70%">내용</th>
                        <th>글쓴이</th>
                        <th>작성일시</th>
                    </tr>
                    </thead>
                    <tbody>
                    <tr class="text-center" th:each="question_comment, loop : ${comment_list}"
                        th:if="${question_comment.getAnswer() == null}">
                        <td class="text-center" th:text="${question_comment.getContent()}"></td>
                        <td><span th:if="${question_comment.author != null}" th:text="${question_comment.author.username}"</td>
                        <td>
                            <span th:text="${#temporals.format(question_comment.createDate, 'yyyy-MM-dd HH:mm')}"></span>
                            <a th:href="@{|/comment/delete/${question_comment.id}|}" sec:authorize="isAuthenticated()"
                               th:if="${question_comment.author != null and #authentication.getPrincipal().getUsername() == question_comment.author.username}">
                                <img src="/images/erase_button.png"/>
                            </a>
                        </td>
                    </tr>
                    </tbody>
                </table>

                <!-- 질문 댓글 작성 -->
                <form th:action="@{|/comment/create/question/${question.id}|}" th:object="${commentForm}" method="post" class="my-3">
                    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
                    <div th:replace="~{form_errors :: formErrorsFragment}"></div>
                    <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>
        </div>
    </div>

(...생략...)

                <!-- 오른쪽 버튼 -->
                <a href="javascript:void(0);" class="comment btn btn-sm btn-outline-secondary"
                   th:id="|ans-${answer.id}|" th:text="|댓글 ${#lists.size(answer.getCommentList())}|">
                </a>
            </div>
        </div>
        <div style="display: none"
             th:id="|comment-ans-${answer.id}|">
            <table class="table" th:if="${#lists.size(answer.commentList)!=0}">
                <thead class="table-dark">
                <tr class="text-center">
                    <th style="width:70%">내용</th>
                    <th>글쓴이</th>
                    <th>작성일시</th>
                </tr>
                </thead>
                <tbody>
                <tr class="text-center" th:each="answer_comment, loop : ${comment_list}"
                    th:if="${answer_comment.getAnswer() != null and answer_comment.getAnswer().getId() == answer.getId()}">
                    <td class="text-center" th:text="${answer_comment.getContent()}"></td>
                    <td><span th:if="${answer_comment.author != null}" th:text="${answer_comment.author.username}"</td>
                    <td>
                        <span th:text="${#temporals.format(answer_comment.createDate, 'yyyy-MM-dd HH:mm')}"></span>
                        <a th:href="@{|/comment/delete/${answer_comment.id}|}" sec:authorize="isAuthenticated()"
                           th:if="${answer_comment.author != null and #authentication.getPrincipal().getUsername() == answer_comment.author.username}">
                            <img src="/images/erase_button.png"/>
                        </a>
                    </td>
                </tr>
                </tbody>
            </table>

            <!-- 질문 댓글 작성 -->
            <form th:action="@{|/comment/create/answer/${answer.id}|}" th:object="${commentForm}" method="post" class="my-3">
                <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
                <div th:replace="~{form_errors :: formErrorsFragment}"></div>
                <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>
    </div>

    <!-- 답변 페이징 처리 시작 -->
    (...생략...)
<script>
    (...생략...)
    const comment_elements = document.getElementsByClassName("comment btn");
    Array.from(comment_elements).forEach(function(element) {
        element.addEventListener('click', function() {
            const comment_table = document.getElementById("comment-" + element.id)
            if (comment_table.style.display === 'none') {
                comment_table.style.display = 'block';
                element.style.background = 'gray';
                element.style.color = 'white';
            } else {
                comment_table.style.display = 'none';
                element.style.background = 'white';
                element.style.color = 'gray';
            }
        })
    })
</script>
</html>

기존에는 style과 아이디를 table 자체에 두었지만, 이제는 댓글 폼까지 한번에 보여주어야 하므로 div로 겉을 한번 감싼 후 이 div에 두었다.

댓글 작성 자체는 답변 작성과 action 주소와 th:object만 제외하고는 거의 동일한 것을 알 수 있다.

그리고 display style을 바꾸는 부분이 div로 옮겼기 때문에 display 스타일을 table이 아닌 block으로 변경해야한다.

댓글 작성 전
댓글 작성 후

잘 작성되는 것을 확인할 수 있다.

 

아쉽게도 아직 할 일이 남았다. 프로필 화면에 내가 쓴 댓글을 보여주어야 한다.

프로필 화면에 댓글 내역 추가하기

대부분의 기능은 완료되었으나, 아직 다 끝난건 아니다. 마지막으로 이전에 만들었던 프로필 화면에서 내가 쓴 댓글들을 보여주어야 한다.

프로필 화면에서 보여주게 하기 위해서는 UserController의 profile 함수에서 댓글 내역을 model에 attribute로 추가해주어야 한다.

@Controller
@RequestMapping("/user")
public class UserController {
        (...생략...)
        private final CommentService commentService;

    @Autowired
    public UserController(UserService userService, QuestionService questionService,
                          AnswerService answerService, JavaMailSender mailSender,
                          CommentService commentService) {
        this.userService = userService;
        this.questionService = questionService;
        this.answerService = answerService;
        this.mailSender = mailSender;
        this.commentService = commentService;
    }
    
    (...생략...)
    
    @PreAuthorize("isAuthenticated()")
    @GetMapping("/profile")
    public String profile(UserUpdateForm userUpdateForm, Model model, Principal principal,
                          @RequestParam(value="question-page", defaultValue="0") int questionPage,
                          @RequestParam(value="ans-page", defaultValue="0") int ansPage,
                          @RequestParam(value="question-vote-page", defaultValue="0") int questionVoterPage,
                          @RequestParam(value="ans-vote-page", defaultValue="0") int ansVoterPage,
                          @RequestParam(value="comment-page", defaultValue="0") int commentPage) {
        SiteUser siteUser = this.userService.getUser(principal.getName());
        Page<Question> wroteQuestions = this.questionService.getListByAuthor(questionPage, siteUser);
        Page<Answer> wroteAnswers = this.answerService.getListByAuthor(ansPage, siteUser);
        Page<Question> votedQuestions = this.questionService.getListByVoter(questionVoterPage, siteUser);
        Page<Answer> votedAnswers = this.answerService.getListByVoter(ansVoterPage, siteUser);
        Page<Comment> wroteComments = this.commentService.getListByAuthor(commentPage, siteUser);

        model.addAttribute("wrote_question_paging", wroteQuestions);
        model.addAttribute("wrote_answer_paging", wroteAnswers);
        model.addAttribute("voted_question_paging", votedQuestions);
        model.addAttribute("voted_answer_paging", votedAnswers);
        model.addAttribute("username", siteUser.getUsername());
        model.addAttribute("userEmail", siteUser.getEmail());
        model.addAttribute("wrote_comment_paging", wroteComments);
        return "profile";
    }
    (...생략...)

이제 CommentService에 가서 작성자를 통해 댓글을 찾을 수 있는 getListByAuthor 함수를 만들어주자.

@Service
public class CommentService {
        (...생략...)
    public Page<Comment> getListByAuthor(int page, SiteUser user) {
        List<Sort.Order> sorts = new ArrayList<>();
        sorts.add(Sort.Order.desc("createDate"));
        Pageable pageable = PageRequest.of(page, 5, Sort.by(sorts));
        return this.commentRepository.findByAuthor(user, pageable);
    }
}

CommentRepository에도 Author를 통해 찾아 Paging하는 함수를 추가해주자.

public interface CommentRepository extends JpaRepository<Comment, Integer> {

    List<Comment> findByQuestion(Question question);

    Page<Comment> findByAuthor(SiteUser siteUser, Pageable pageable);
}

이제 profile.html에서 댓글 부분이 보이도록 수정해주자.

        (...생략...)

        <!-- 답변 페이징 처리 시작 -->
        <div th:if="${!wrote_answer_paging.isEmpty()}">
            <ul class="pagination justify-content-center">
                <li class="page-item" th:classappend="${!wrote_answer_paging.hasPrevious} ? 'disabled'">
                    <a class="page-link" th:href="@{|?ans-page=${wrote_answer_paging.number-1}|}" th:data-page="${wrote_answer_paging.number-1}">
                        <span>이전</span>
                    </a>
                </li>
                <li th:each="page: ${#numbers.sequence(0, wrote_answer_paging.totalPages-1)}"
                    th:if="${page >= wrote_answer_paging.number - 5 and page <= wrote_answer_paging.number+5}"
                    th:classappend="${page == wrote_answer_paging.number} ? 'active'"
                    class="page-item">
                    <a th:text="${page}" class="page-link" th:href="@{|?ans-voter-page=${page}|}" th:data-page="${page}"></a>
                </li>
                <li class="page-item" th:classappend="${!wrote_answer_paging.hasNext()} ? 'disabled'">
                    <a class="page-link" th:href="@{|?ans-page=${wrote_answer_paging.number+1}|}" th:data-page="${wrote_answer_paging.number+1}">
                        <span>다음</span>
                    </a>
                </li>
            </ul>
        </div>
    </div>

<h2 class="border-bottom py-2">내 댓글</h2>
    <div class="card my-3">
        <!-- 댓글 시작 -->
        <table class="table">
            <thead class="table-dark">
            <tr class="text-center">
                <th style="width:70%">내용</th>
                <th>글쓴이</th>
                <th>작성일시</th>
            </tr>
            </thead>
            <tbody><tr class="text-center" th:each="comment, loop : ${wrote_comment_paging}">
                <td class="text-center">
                    <a th:href="@{|/question/detail/${comment.question.id}|}" th:text="${comment.content}"></a>
                </td>
                <td><span th:if="${comment.author != null}" th:text="${comment.author.username}"></span></td>
                <td th:text="${#temporals.format(comment.createDate, 'yyyy-MM-dd HH:mm')}"></td>
            </tr>
            </tbody>
        </table>

        <!-- 페이징 처리 시작 -->
        <div th:if="${!wrote_comment_paging.isEmpty()}">
            <ul class="pagination justify-content-center">
                <li class="page-item" th:classappend="${!wrote_comment_paging.hasPrevious} ? 'disabled'">
                    <a class="page-link" th:href="@{|?comment-page=${wrote_comment_paging.number-1}|}" th:data-page="${wrote_comment_paging.number-1}">
                        <span>이전</span>
                    </a>
                </li>
                <li th:each="page: ${#numbers.sequence(0, wrote_comment_paging.totalPages-1)}"
                    th:if="${page >= wrote_comment_paging.number-5 and page <= wrote_comment_paging.number+5}"
                    th:classappend="${page == wrote_comment_paging.number} ? 'active'"
                    class="page-item">
                    <a th:text="${page}" class="page-link" href="javascript:void(0)" th:data-page="${page}"></a>
                </li>
                <li class="page-item" th:classappend="${!wrote_comment_paging.hasNext} ? 'disabled'">
                    <a class="page-link" th:href="@{|?comment-page=${wrote_comment_paging.number+1}|}" th:data-page="${wrote_comment_paging.number+1}">
                        <span>다음</span>
                    </a>
                </li>
            </ul>
        </div>
    </div>

        <h2 class="border-bottom py-2">추천한 질문</h2>
        (...생략...)

Question의 페이징과 거의 유사하게 table을 통해 페이징을 하였으며, 댓글 내역을 클릭시 댓글을 달은 질문글로 이동할 수 있도록 하였다.

잘 나오는 것을 확인할 수 있다.

 

그냥 지금까지 배운것들의 복습 느낌이었다. 

전체 구현은 깃허브 참조.

728x90