본문 바로가기
개발 공부/Spring Boot

Spring Boot - 댓글

by 깐테 2023. 6. 13.

Spring Boot - 댓글 기능 구현하기

https://wikidocs.net/162833

 

3-15 SBB 추가 기능

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

wikidocs.net

위키독스 - 점프 투 스프링부트 추가 기능 구현에 대해 설명한다.

 

게시글에 대한 댓글 기본 작성과 수정, 삭제 기능을 추가했다.

 

댓글 도메인

  • com.example.sbb.comment 생성
  • VS Code의 경우에는 sbb 폴더 우클릭 → New Folder → comment 생성

 

댓글 모델

comment

// comment.java
package com.example.sbb.comment;
import java.time.LocalDateTime;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;

import com.example.sbb.answer.Answer;
import com.example.sbb.question.Question;
import com.example.sbb.user.SiteUser;

import lombok.Getter;
import lombok.Setter;

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

    @ManyToOne
    private SiteUser author;

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

    private LocalDateTime createDate;
    
    private LocalDateTime modifyDate;

    @ManyToOne
    private Question question;

    @ManyToOne
    private Answer answer;
    
    //수정 및 삭제 후 상세 페이지로 redirect를 위한 메서드
    public Integer getQuestionId() {
        Integer result = null;
        if (this.question != null) {
            result = this.question.getId();
        } else if (this.answer != null) {
            result = this.answer.getQuestion().getId();
        }
        return result;
    }
    
}
  • 한 명의 유저가 여러 댓글을 달 수 있다(ManyToOne)
  • 한 개의 질문에 여러 개의 댓글을 달 수 있다(ManyToOne)
  • 한 개의 답변에 여러 개의 댓글을 달 수 있다(ManyToOne)

댓글 수정, 삭제 후 질문 상세 페이지로 redirect 하기 위해서는 질문 id를 알아내는 메서드 작성이 필요하다.

getQuestionId() 메서드를 미리 작성해두었다.

 

Question 모델

//question/Question.java
package com.mysite.sbb.question;

(... 생략 ...)
import com.mysite.sbb.comment.Comment;
(... 생략 ...)

@Getter
@Setter
@Entity
public class Question {
    (... 생략 ...)

    @OneToMany(mappedBy = "question")
    private List<Comment> commentList;
}

 

Answer 모델

package com.mysite.sbb.answer;

(... 생략 ...)
import com.mysite.sbb.comment.Comment;
(... 생략 ...)

@Entity
@Getter
@Setter
public class Answer {
    (... 생략 ...)

    @OneToMany(mappedBy = "answer")
    private List<Comment> commentList;
}
  • Question, Answer 엔티티를 연결해준다

 

질문 댓글

질문 댓글 링크

<!--templates/question_detail.html-->

<html 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="my-3" sec:authorize="isAuthenticated()"
                th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}">
                <a th:href="@{|/question/modify/${question.id}|}" class="btn btn-sm btn-outline-secondary">수정</a>
                <a href="javascript:void(0);" class="delete btn btn-sm btn-outline-secondary"
                    th:data-uri="@{|/question/delete/${question.id}|}">삭제</a>
            </div>
            <!-- 질문 댓글 Start -->
            <div class="mt-3" th:if="${not #lists.isEmpty(question.commentList)}">
                <div th:each="comment,index : ${question.commentList}" class="comment py-2 text-muted">
                    <span style="white-space: pre-line;" th:text="${comment.content}"></span>
                    <span th:if="${comment.modifyDate != null}"
                        th:text="| - ${comment.author.username}, ${#temporals.format(comment.createDate, 'yyyy-MM-dd HH:mm')} (수정: ${#temporals.format(comment.modifyDate, 'yyyy-MM-dd HH:mm')})|"></span>
                    <span th:if="${comment.modifyDate == null}"
                        th:text="| - ${comment.author.username}, ${#temporals.format(comment.createDate, 'yyyy-MM-dd HH:mm')}|"></span>
                    <a sec:authorize="isAuthenticated()"
                        th:if="${#authentication.getPrincipal().getUsername() == comment.author.username}"
                        th:href="@{|/comment/modify/${comment.id}|}" class="small">수정</a>,
                    <a sec:authorize="isAuthenticated()"
                        th:if="${#authentication.getPrincipal().getUsername() == comment.author.username}"
                        href="javascript:void(0);" class="small delete" th:data-uri="@{|/comment/delete/${comment.id}|}">삭제</a>
                </div>
            </div>
            <div>
                <a th:href="@{|/comment/create/question/${question.id}|}" class="small"><small>댓글 추가 ..</small></a>
            </div>
            <!-- 질문 댓글 End -->
        </div>
    </div>
    <h5 class="border-bottom my-3 py-2" th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
(... 생략 ...)
// static/style.css
.comment {
	border-top: dotted 1px #ddd;
	font-size:0.7em;
}

질문에 대한 댓글을 나타내기 위해 질문 상세 페이지 -> 질문 하단에 작성한 댓글이 표시되도록 처리했다.

 

댓글 추가도 하단의 버튼을 통해 입력 폼으로 넘어가도록 처리.

 

CommentRepository

package com.mysite.sbb.comment;

import org.springframework.data.jpa.repository.JpaRepository;

public interface CommentRepository extends JpaRepository<Comment, Integer> {
}

 

CommentService

// comment/CommentService.java
package com.example.sbb.comment;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.sbb.DataNotFoundException;
import com.example.sbb.question.Question;
import com.example.sbb.user.SiteUser;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class CommentService {
    
    private final CommentRepository commentRepository;

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

    public Comment getComment(Integer id) {
        Optional<Comment> comment = this.commentRepository.findById(id);
        if (comment.isPresent()) {
            return comment.get();
        } else {
            throw new DataNotFoundException("코멘트를 찾을 수 없습니다.");
        }
        // return this.commentRepository.findById(id);
    }

    public Comment modify(Comment cmt, String content) {
        cmt.setContent(content);
        cmt.setModifyDate(LocalDateTime.now());
        cmt = this.commentRepository.save(cmt);
        return cmt;
    }

    public void delete(Comment c) {
        this.commentRepository.delete(c);
    }
}
  • Private 변수로 선언한 기존 변수를 final(상수)로 변경
  • 생성, 조회, 수정, 삭제 서비스를 여기서 작성한다.

 

CommentForm

package com.mysite.sbb.comment;

import jakarta.validation.constraints.NotEmpty;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class CommentForm {
    @NotEmpty(message = "내용은 필수항목입니다.")
    private String content;
}

CommentController

package com.example.sbb.comment;

import java.security.Principal;
import java.util.Optional;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.ui.Model;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;

import com.example.sbb.question.Question;
import com.example.sbb.question.QuestionService;
import com.example.sbb.user.SiteUser;
import com.example.sbb.user.UserService;

@Controller
@RequestMapping("/comment")
@RequiredArgsConstructor
public class CommentController {
    
    private final CommentService commentService;
    private final QuestionService questionService;
    private final UserService userService;

    @PreAuthorize("isAuthenticated()")
    @GetMapping(value = "/create/question/{id}")
    public String createQuestionComment(CommentForm commentForm) {
        return "comment_form";
    }

    // 질문에 대한 코멘트
    @PreAuthorize("isAuthenticated()")
    @PostMapping(value = "/create/question/{id}")
    public String createQuestionComment(Model model, @PathVariable("id") Integer id, @Valid CommentForm commentForm,
            BindingResult bindingResult, Principal principal) {
        Question question = this.questionService.getQuestion(id);
        SiteUser user = this.userService.getUser(principal.getName());
        if (bindingResult.hasErrors()) {
            model.addAttribute("question", question);
            return "question_detail";
        }

        Comment comment = this.commentService.create(question, commentForm.getContent(), user);
        return String.format("redirect:/question/detail/%s#comment_%s", comment.getQuestion().getId(), comment.getId());
    }

    @PreAuthorize("isAuthenticated()")
    @GetMapping("/modify/{id}")
    public String modifyComment(CommentForm commentForm, @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, "수정 권한이 없습니다.");
        }
        commentForm.setContent(comment.getContent());
        return "comment_form";
    }

    @PreAuthorize("isAuthenticated()")
    @PostMapping("/modify/{id}")
    public String modifyComment(@Valid CommentForm commentForm, BindingResult bindingResult, Principal principal,
            @PathVariable("id") Integer id) {
        if (bindingResult.hasErrors()) {
            return "comment_form";
        }
        Comment comment = this.commentService.getComment(id);
        if (!comment.getAuthor().getUsername().equals(principal.getName())) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정 권한이 없습니다.");

        }
        this.commentService.modify(comment, commentForm.getContent());
        return String.format("redirect:/question/detail/%s", comment.getQuestion().getId(), comment.getId());
    }

    @PreAuthorize("isAuthenticated()")
    @GetMapping("/delete/{id}")
    public String deleteComment(Principal principal, @PathVariable("id") Integer id) {
        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());
    }

}
  • 기존 AnswerController와 같은 처리 방식 사용.
  • Optional 처리되어있던 부분은 서비스에 처리 혹은 상수로 변경.

 

comment_form.html

<!--templates/comment_form.html-->
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container">
    <h5 class="my-3 border-bottom pb-2">댓글 등록</h5>
    <form th:object="${commentForm}" method="post">
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
        <nav th:replace="~{form_errors :: formErrorsFragment}"></nav>
        <div class="mb-3">
            <label for="content" class="form-label">내용</label>
            <textarea th:field="*{content}" class="form-control" rows="10"></textarea>
        </div>
        <input type="submit" value="저장하기" class="btn btn-primary my-2">
    </form>
</div>
  • 기존 코드 사용 시 th:replace= 부분이 콘솔에서 오류를 출력하는 경우가 있음. 예제로 사용되었던 코드가 구버전의 코드이므로, 위와 같이 처리

 

결과

댓글 추가 버튼을 클릭하면 등록 폼으로 이동
댓글 등록 후 수정, 삭제 버튼이 표시.

 

삭제 버튼 클릭시 alert 메시지 추가, 확인 버튼 클릭 시 삭제 후 페이지 redirect.

반응형

'개발 공부 > Spring Boot' 카테고리의 다른 글

Spring Boot - 답변 페이징  (0) 2023.05.30
Spring Boot - VS Code로 개발환경 설정하기  (1) 2023.05.05