Comment 엔티티
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Comment extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="post_id")
private Posts posts;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="parent_id")
@JsonIgnore
private Comment parent;
@Column(columnDefinition = "TEXT", nullable = false)
private String content;
@Column(columnDefinition = "TINYINT", length = 1)
private int secret;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Comment> commentList = new ArrayList<>();
@Builder
public Comment(Member member, String content, int secret, Posts posts, Comment parent) {
this.member = member;
this.content = content;
this.secret = secret;
this.posts = posts;
this.parent = parent;
}
public void update(String content, int secret){
this.content = content;
this.secret = secret;
}
}
Posts 엔티티
@OneToMany(mappedBy = "posts", cascade = CascadeType.ALL, orphanRemoval = true)
@Where(clause = "parent_id is null")
private List<Comment> commentList = new ArrayList<>();
게시물과 @ManyToOne으로 양방향 관계를,
사용자와 @ManyToOne으로 단방향 관계를 맺어줬다.
양방향 연관관계 일 때는 각 개체가 서로를 참조해야 하기 때문에, 객체를 테이블에 매핑할 때 두 객체 중 어떤 객체로 외래키를 관리해야 하는지 정해야한다. 여기서 외래키를 관리하게 되는 객체를 연관관계의 주인이라고 한다.
연관관계의 주인은, 테이블의 외래키가 있는 곳으로 정해야한다.
항상 다대일, 일대다 관계에서는 다쪽이 외래키를 가진다.
mappedBy는 연관관계의 주인이 아님을 설정해준다.
연관관계의 주인은 mappedBy 속성을 사용하지 않는다.
여기서 mappedBy="posts"는 posts가 연관관계 주인인 Comment의 posts 필드에 해당한다는 뜻이다.
=> 정리하자면 데이터베이스의 다대일, 일대다 관계에서는 항상 다 쪽이 외래키를 가진다.
항상 다쪽이 연관관계의 주인이 되므로 @ManyToOne에는 mappedBy 속성이 없다.
orphanRemoval = true를 통해 게시글이 삭제되면 그 게시글에 있는 댓글들 모두 삭제가 되도록 했다.
CascadeType.ALL + orphanRemovel=true
이 두개를 같이 사용하게 되면 부모 엔티티가 자식의 생명주기를 모두 관리할 수 있게 된다.
대댓글을 구현하기 위해 부모 Comment인 parent과 자식 Comment인 commentList를 넣어 계층형 테이블로 구성한다.
CommentDto
public class CommentDto {
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class Request{
private String content;
private Long parentId;
private int secret;
public Comment toEntity(Comment parent, Posts post){
Comment comment = Comment.builder()
.posts(post)
.content(content)
.secret(secret)
.parent(parent)
.build();
return comment;
}
}
@RequiredArgsConstructor
@Getter
public static class Response{
private Long id;
private String content;
private int secret;
private String writer_nickname;
private String writer_id;
private List<CommentDto.Response> commentList;
private String createdDate;
private String modifiedDate;
private Boolean isWriter;
public Response(Comment comment, Long currentMemberId, Long postWriterId){
this.id = comment.getId();
this.secret = comment.getSecret();
this.writer_nickname = comment.getMember().getNickname();
this.writer_id = comment.getMember().getStudentId();
if(comment.getCommentList()!= null){
this.commentList = comment.getCommentList().stream().map(c-> new CommentDto.Response(c, currentMemberId, postWriterId)).collect(Collectors.toList());
}
this.createdDate = comment.getCreatedDate();
this.modifiedDate = comment.getModifiedDate();
if(Objects.equals(currentMemberId, comment.getMember().getId())) this.isWriter = true; // 현재 사용자가 댓글 작성자면
else this.isWriter = false;
if(comment.getSecret()==1){ // 비밀 댓글이면
if(Objects.equals(currentMemberId, postWriterId) || Objects.equals(currentMemberId, comment.getMember().getId())){
// 현재 사용자가 글 작성자거나 해당 댓글 작성자면 비밀댓글이여도 보이게
this.content = comment.getContent();
}
else this.content = "비밀 댓글입니다.";
}else{ // 비밀 댓글 아니면 모두가 다 볼 수 있음
this.content = comment.getContent();
}
}
}
}
해당 댓글이 비밀댓글이라면 현재 사용자가 글 작성자거나 해당 댓글 작성자인지를 확인해
둘 중 하나라도 해당되면 댓글 내용이 보이게 했다. 아닐 경우 "비밀 댓글입니다" 라고 표시된다.
이 때 Response 클래스 안에서
if(comment.getCommentList()!= null){
this.commentList = comment.getCommentList().stream().map(c-> new CommentDto.Response(c, currentMemberId, postWriterId)).collect(Collectors.toList());
}
이렇게 null처리를 안하면 무한 참조 관련 에러가 뜬다.
대댓글이 없는 경우에는 접근하면 안되기 때문에 대댓글이 null이 아닐때만 대댓글을 가져오도록 했다.
CommentApiController
@RequiredArgsConstructor
@RequestMapping("/comment")
@RestController
public class CommentApiController {
private final CommentService commentService;
@ApiOperation(value="댓글 등록")
@PostMapping("/{postId}")
public ResponseEntity saveComment(@PathVariable Long postId, @RequestBody CommentDto.Request requestDto, @AuthenticationPrincipal Member member) throws Exception {
return ResponseEntity.ok( commentService.saveComment(postId, requestDto, member.getId()));
}
@ApiOperation(value="댓글 수정")
@PutMapping("/{commentId}")
public ResponseEntity updateComment(@PathVariable Long commentId, @RequestBody CommentDto.Request requestDto, @AuthenticationPrincipal Member member) throws Exception {
commentService.updateComment(commentId, requestDto, member.getId());
return ResponseEntity.ok(commentId);
}
@ApiOperation(value="댓글 삭제")
@DeleteMapping("/{commentId}")
public ResponseEntity delete(@PathVariable Long commentId, @AuthenticationPrincipal Member member) throws Exception {
commentService.deleteComment(commentId, member.getId());
return ResponseEntity.ok(commentId);
}
}
'BACK > SPRING' 카테고리의 다른 글
스프링MVC - 웹 애플리케이션의 이해 (0) | 2023.02.12 |
---|---|
Spring Boot Project(7) - 서비스 인증과 권한 부여 (0) | 2023.01.31 |
Spring Boot Project(5) - 게시글 페이징, 필터링하기 (0) | 2023.01.24 |
Spring Boot Project(4) - JPA Auditing으로 생성/수정 시간 자동화 (0) | 2023.01.24 |
Spring Boot Project(3) - 게시글 CRUD (0) | 2023.01.24 |