스트링부트 블로그 만들기 – 9강 댓글
댓글 쓰기
데이터베이스 모델 설계하는 규칙하나의 컬럼으로 설명할 수 있는가(작성자, 언제, 어느 게시글에) -> 불가능
보드 테이블에 코멘트 관련 칼럼 추가할 수 있나 -> 가능
한 사람 이상의 코멘트를 한 칼럼으로 처리할 수 있나 -> 불가능
결론 : 코멘트 테이블을 따로 만들어야 함...
1.commant 패키지를 새로 만들어 댓글 모델과 레파지토리를 만들어준다.
package com.cos.blogapp.domain.comment; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; import com.cos.blogapp.domain.board.Board; import com.cos.blogapp.domain.user.User; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Table(name = "comment") @AllArgsConstructor @NoArgsConstructor @Data @Entity public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @Column(nullable = false) private String content; @JoinColumn(name = "userId") @ManyToOne private User user; @JoinColumn(name = "boardId") @ManyToOne private Board board; }
package com.cos.blogapp.domain.comment; import org.springframework.data.jpa.repository.JpaRepository; public interface CommentRepository extends JpaRepository<Comment, Integer> { }
2.detail.jsp 파일에서 댓글 등록 버튼 클릭했을 때 넣을 액션을 추가한다.
<div class="card"> <!-- 댓글 쓰기 시작 --> <form action="/board/${boardEntity.id}/comment" method="post"> <div class="card-body"> <textarea name="content" class="form-control" rows="1"></textarea> </div> <div class="card-footer"> <button type="submit" id="btn-reply-save" class="btn btn-primary">등록</button> </div> </form> <!-- 댓글 쓰기 끝 --> </div>
3.CommentSaveReqDto.java 파일을 만들어 댓글 등록에 필요한 데이터를 담아준다.
package com.cos.blogapp.web.dto; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class CommentSaveReqDto { @Size(min = 1, max = 255) @NotBlank private String content; }
4.BoradController.java 파일에서 댓글 등록 메서드를 추가한다.
@PostMapping("/board/{boardId}/comment") public String commentSave(@PathVariable int boardId, CommentSaveReqDto dto) { Comment comment = new Comment(); User principal = (User) session.getAttribute("principal"); Board boardEntity = boardRepository.findById(boardId) .orElseThrow(()-> new MyNotFoundException("해당 게시글을 찾을 수 없습니다.")); comment.setContent(dto.getContent()); comment.setUser(principal); comment.setBoard(boardEntity); commentRepository.save(comment); return "redirect:/board/"+boardId; }
댓글에서 사용할 데이터 중에 User 정보는 세션값을 사용하고 Board 정보는 글번호를 사용합니다.
즉시로딩(Eager)과 지연로딩(Lazy)즉시로딩(Eager)과 지연로딩(Lazy)
예제로 이해하기
데이터베이스의 사용자 테이블(User)
id
...
5.Board.java에서 Comment.java와 양방향 매핑을 할 수 있게 만들어준다.
@JsonIgnoreProperties({"board"}) @OneToMany(mappedBy = "board", fetch = FetchType.LAZY) private List<Comment> comments;
6.Lazy 전략을 사용하기위해 yml 파일에서 open-in-view 설정을 true로 만들어준다.
jpa: open-in-view: true hibernate: ddl-auto: none naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl show-sql: true
7.datil.jsp 파일에서 댓글을 볼 수 있게 반복문으로 출력한다.
<!-- 댓글 시작 --> <c:forEach var="comment" items="${boardEntity.comments}"> <li id="reply-${comment.id }" class="list-group-item ds-flex justify-content-between"> <!-- LAZY loading 일어남 : 사용직전 --> <div>${comment.content}</div> <div class="d-flex"> <div class="font-italic">작성자 : ${commment.user.username} </div> <button class="badge">삭제</button> </div> </li> </c:forEach> <!-- 댓글 끝 -->
댓글 서비스 만들기
컨트롤러에서 댓글등록의 서비스 코드를 댓글 서비스 파일로 따로 빼준다.
package com.cos.blogapp.service; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.cos.blogapp.domain.board.Board; import com.cos.blogapp.domain.board.BoardRepository; import com.cos.blogapp.domain.comment.Comment; import com.cos.blogapp.domain.comment.CommentRepository; import com.cos.blogapp.domain.user.User; import com.cos.blogapp.handler.ex.MyNotFoundException; import com.cos.blogapp.web.dto.CommentSaveReqDto; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class CommentService { private final CommentRepository commentRepository; private final BoardRepository boardRepository; @Transactional(rollbackFor = MyNotFoundException.class) public void 댓글등록(int boardId, CommentSaveReqDto dto, User principal) { Board boardEntity = boardRepository.findById(boardId) .orElseThrow(() -> new MyNotFoundException("해당 게시글을 찾을 수 없습니다.")); Comment comment = new Comment(); comment.setContent(dto.getContent()); comment.setUser(principal); comment.setBoard(boardEntity); commentRepository.save(comment); } // 트랜잭션 종료 }
private final CommentService commenetservice; @PostMapping("/board/{boardId}/comment") public String commentSave(@PathVariable int boardId, CommentSaveReqDto dto) { User principal = (User) session.getAttribute("principal"); commentService.댓글등록(boardId, dto, principal); // -> 디비와 관련된 트랜젝션을 서비스로 이동 return "redirect:/board/"+boardId; }
댓글 쓰기 컨트롤러를 BoardController에 넣는 이유는 댓글 쓰기 요청이 게시글 페이지를 거쳐서 등록되기 때문이다.
권한 체크
로그인한 사람만 댓글을 쓸 수 있도록 권한 체크를 한다.
private final CommentService commenetservice; @PostMapping("/board/{boardId}/comment") public String commentSave(@PathVariable int boardId, CommentSaveReqDto dto) { User principal = (User) session.getAttribute("principal"); if (principal == null) { throw new MyNotFoundException("인증이 되지 않았습니다."); } commentService.댓글등록(boardId, dto, principal); // -> 디비와 관련된 트랜젝션을 서비스로 이동 return "redirect:/board/"+boardId; }
Comment 객체 내부 필드 제외
Board.java에서 @JsonIgnoreProperties 어노테이션을 추가한다.
package com.cos.blogapp.domain.board; import java.util.List; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.Lob; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; import javax.persistence.OrderBy; import javax.persistence.Table; import com.cos.blogapp.domain.comment.Comment; import com.cos.blogapp.domain.user.User; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Table(name = "board") @AllArgsConstructor @NoArgsConstructor @Data @Entity public class Board { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; //PK (자동증가 번호) @Column(nullable = false, length = 70) private String title; // 아이디 @Lob private String content; @JoinColumn(name = "userId") @ManyToOne(fetch = FetchType.EAGER) private User user; @JsonIgnoreProperties({"board"}) @OneToMany(mappedBy = "board", fetch = FetchType.LAZY) @OrderBy("id desc") private List<Comment> comments; }
댓글 삭제
1.서비스 컨트롤러에 댓글삭제 서비스를 만든다.
package com.cos.blogapp.service; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.cos.blogapp.domain.board.Board; import com.cos.blogapp.domain.board.BoardRepository; import com.cos.blogapp.domain.comment.Comment; import com.cos.blogapp.domain.comment.CommentRepository; import com.cos.blogapp.domain.user.User; import com.cos.blogapp.handler.ex.MyAsyncNotFoundException; import com.cos.blogapp.handler.ex.MyNotFoundException; import com.cos.blogapp.web.dto.CommentSaveReqDto; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class CommentService { private final CommentRepository commentRepository; private final BoardRepository boardRepository; @Transactional(rollbackFor = MyAsyncNotFoundException.class) public void 댓글삭제(int id, User principal) { Comment commentEntity = commentRepository.findById(id) .orElseThrow(()-> new MyAsyncNotFoundException("없는 댓글 번호입니다.")); if(principal.getId() != commentEntity.getUser().getId()) { throw new MyAsyncNotFoundException("해당 게시글을 삭제할 수 없는 유저입니다."); } commentRepository.deleteById(id); } @Transactional(rollbackFor = MyNotFoundException.class) public void 댓글등록(int boardId, CommentSaveReqDto dto, User principal) { Board boardEntity = boardRepository.findById(boardId) .orElseThrow(() -> new MyNotFoundException("해당 게시글을 찾을 수 없습니다.")); Comment comment = new Comment(); comment.setContent(dto.getContent()); comment.setUser(principal); comment.setBoard(boardEntity); commentRepository.save(comment); } }
2.CommentController.java 파일을 만들어 댓글 삭제 컨트롤러를 만든다.
package com.cos.blogapp.web; import javax.servlet.http.HttpSession; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.ResponseBody; import com.cos.blogapp.domain.user.User; import com.cos.blogapp.handler.ex.MyAsyncNotFoundException; import com.cos.blogapp.service.CommentService; import com.cos.blogapp.web.dto.CMRespDto; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Controller public class CommentController { private final CommentService commentService; private final HttpSession session; @DeleteMapping("/comment/{id}") public @ResponseBody CMRespDto<?> deleteById(@PathVariable int id){ User principal = (User) session.getAttribute("principal"); if(principal == null) { throw new MyAsyncNotFoundException("인증되지 않은 사용자입니다"); } commentService.댓글삭제(id, principal); return new CMRespDto<>(1, "성공", null); } }
3.deital.jsp 파일에서 ajax 요청을 한다.
<div class="card"> <div class="card-header"> <b>댓글 리스트</b> </div> <ul id="reply-box" class="list-group"> <!-- 댓글 시작 --> <c:forEach var="comment" items="${boardEntity.comments}"> <li id="reply-${comment.id }" class="list-group-item ds-flex justify-content-between"> <!-- LAZY loading 일어남 : 사용직전 --> <div>${comment.content}</div> <div class="d-flex"> <div class="font-italic">작성자 : ${comment.user.username} </div> <button class="badge" id="reply" onClick="deleteById(${comment.id})">삭제</button> </div> </li> </c:forEach> <!-- 댓글 끝 --> </ul> <script> async function deleteById(commentId){ let response = await fetch("http://localhost:8000/comment/"+commentId, { method:"delete" }); let parseResponse = await response.json(); if(parseResponse.code == 1){ alert("댓글 삭제 성공"); //location.reload(); $("#reply-"+commentId).remove(); }else{ alert("댓글 삭제에 실패하였습니다. "+parseResponse.msg); } } </script> </div>
1. ajax 요청 – fetch 사용하기
2. 모든 dom제어는 jquery 사용하기
3. 이벤트 등록 – 전부다 id만들어서 이벤트 리스너 사용
4. for문안에 dom제어만 dom자체에 onClick() 걸기
5. 전체 리로드와 부분 리로드 사용 구분
(1) 전체 리로드 (기본)
(2) 부분 리로드 – 사용자가 적은게 많을때