스트링부트 블로그 만들기 – 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) 부분 리로드 – 사용자가 적은게 많을때

