Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Add] Recomment #77

Merged
merged 2 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion src/main/java/nova/mjs/comment/DTO/CommentResponseDto.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package nova.mjs.comment.DTO;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.*;
import nova.mjs.comment.entity.Comment;
import nova.mjs.community.entity.CommunityBoard;
import nova.mjs.member.Member;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.UUID;

@Getter
Expand All @@ -16,6 +19,15 @@ public class CommentResponseDto {
private UUID communityBoardUuid; // 댓글이 작성된 게시글의 uuid
private List<CommentSummaryDto> comments;

@JsonPropertyOrder({
"commentUUID",
"content",
"nickname",
"likeCount",
"createdAt",
"liked",
"replies"
})
@Getter
@Builder
@AllArgsConstructor
Expand All @@ -27,18 +39,66 @@ public static class CommentSummaryDto {
private LocalDateTime createdAt;
private boolean isLiked; // 현재 로그인 한 사용자가 좋아요를 눌렀는가 T/F

private List<CommentSummaryDto> replies;

public static CommentSummaryDto fromEntity(Comment comment, boolean isLiked) {
return CommentSummaryDto.builder()
.commentUUID(comment.getUuid())
.content(comment.getContent())
.nickname(comment.getMember().getNickname())
.likeCount(comment.getLikeCount())
.isLiked(isLiked)
.createdAt(comment.getCreatedAt())
.build();
}

public static CommentSummaryDto fromEntity(Comment comment) {
return CommentSummaryDto.builder()
.commentUUID(comment.getUuid())
.content(comment.getContent())
.nickname(comment.getMember().getNickname())
.likeCount(comment.getLikeCount())
.createdAt(comment.getCreatedAt())
.build();
}
// "부모 댓글"을 DTO로 변환하되, 자식 목록도 함께 변환하는 메서드
public static CommentSummaryDto fromEntityWithReplies(Comment comment, boolean isLiked, Set<UUID> likedSet) {
// 1) 부모 댓글의 기본 정보
CommentSummaryDto.CommentSummaryDtoBuilder builder = CommentSummaryDto.builder()
.commentUUID(comment.getUuid())
.content(comment.getContent())
.nickname(comment.getMember().getNickname())
.likeCount(comment.getLikeCount())
.createdAt(comment.getCreatedAt())
.isLiked(isLiked);

// 2) 자식 댓글(대댓글) 변환
// depth=1만 허용 → 자식의 자식은 처리 안 함
List<CommentSummaryDto> replyDtos = comment.getReplies().stream()
.map(child -> {
boolean childIsLiked = (likedSet != null && likedSet.contains(child.getUuid()));
return fromEntityNoReplies(child, childIsLiked);
})
.toList();

builder.replies(replyDtos);
return builder.build();
}

// "자식 댓글"은 더 이상의 자식 목록을 보지 않는다고 가정(depth=1)
public static CommentSummaryDto fromEntityNoReplies(Comment comment, boolean isLiked) {
return CommentSummaryDto.builder()
.commentUUID(comment.getUuid())
.content(comment.getContent())
.nickname(comment.getMember().getNickname())
.likeCount(comment.getLikeCount())
.createdAt(comment.getCreatedAt())
.isLiked(isLiked)
.replies(List.of()) // 자식은 depth=1까지만
.build();
}
}
}
/*
// Entity 리스트 -> DTO 변환 (게시글의 모든 댓글)
public static CommentResponseDto fromEntities(UUID communityBoardUuid, List<Comment> comments) {
Expand Down Expand Up @@ -71,4 +131,4 @@ public Comment toEntity(CommunityBoard communityBoard, Member member) {
}
*/

}

20 changes: 19 additions & 1 deletion src/main/java/nova/mjs/comment/controller/CommentController.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,30 @@ public ResponseEntity<ApiResponse<CommentResponseDto.CommentSummaryDto>> createC
}

// 3. DELETE 댓글 삭제
@DeleteMapping("/{boardUUID/comments/{commentUUID}")
@DeleteMapping("/{boardUUID}/comments/{commentUUID}")
@PreAuthorize("isAuthenticated() and ((#userPrincipal.email.equals(principal.username)) or hasRole('ADMIN'))")
public ResponseEntity<Void> deleteComment(
@PathVariable UUID commentUUID,
@AuthenticationPrincipal UserPrincipal userPrincipal) {
service.deleteCommentByUuid(commentUUID, userPrincipal.getUsername());
return ResponseEntity.noContent().build();
}

// 4. POST 대댓글 작성
@PostMapping("/{boardUUID}/comments/{parentCommentUUID}/reply")
public ResponseEntity<ApiResponse<CommentResponseDto.CommentSummaryDto>> createReply(
@PathVariable UUID parentCommentUUID,
@RequestBody CommentRequestDto request,
@AuthenticationPrincipal UserPrincipal userPrincipal
) {
// 로그인 사용자 email
String email = (userPrincipal != null) ? userPrincipal.getUsername() : null;

// 실제 서비스 호출
CommentResponseDto.CommentSummaryDto replyDto =
service.createReply(parentCommentUUID, request.getContent(), email);

return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(replyDto));
}
}
28 changes: 28 additions & 0 deletions src/main/java/nova/mjs/comment/entity/Comment.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ public class Comment extends BaseEntity {
@Column
private int likeCount; // 좋아요 수

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id") // 부모 댓글 ID 저장
private Comment parent;

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> replies = new ArrayList<>();


@OneToMany(mappedBy = "comment", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CommentLike> commentLike = new ArrayList<>();

Expand All @@ -55,6 +63,26 @@ public static Comment create(CommunityBoard communityBoard, Member member, Strin
.build();
}

// 부모가 있는 댓글(대댓글) 생성
public static Comment createReply(Comment parent, Member member, String content) {
// 부모 댓글이 속한 CommunityBoard를 그대로 사용 (부모와 같은 게시글)
CommunityBoard board = parent.getCommunityBoard();

Comment reply = Comment.builder()
.uuid(UUID.randomUUID())
.communityBoard(board)
.member(member)
.content(content)
.likeCount(0)
.parent(parent) // 부모 설정
.build();

// 부모의 replies에도 연결
parent.getReplies().add(reply);

return reply;
}

// 게시물 좋아요
public void increaseLikeCommentCount() {
this.likeCount++;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package nova.mjs.comment.exception;

import lombok.extern.log4j.Log4j2;
import nova.mjs.util.exception.BusinessBaseException;
import nova.mjs.util.exception.ErrorCode;

@Log4j2
public class CommentReplyDepthException extends BusinessBaseException {

public CommentReplyDepthException() {
super(ErrorCode.INVALID_REQUEST);
}

public CommentReplyDepthException(String message) {
super(message, ErrorCode.INVALID_REQUEST);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public boolean toggleLike(UUID boardUUID, UUID commentsUUID,String emailId) {
log.debug("좋아요 삭제 완료: member_emailId={}, boardUUID={} , commentUUID={}", emailId, boardUUID, commentsUUID);
return false; // 좋아요 취소됨
} else {
CommentLike commentLike = new CommentLike(member, comment);
CommentLike commentLike = CommentLike.create(member, comment);
commentLikeRepository.save(commentLike);
comment.increaseLikeCommentCount(); // 좋아요 증가 메서드
log.debug("좋아요 추가 완료: member_emailId={}, boardUUID={}, commentsUUID={}",emailId, boardUUID, commentsUUID);
Expand Down
11 changes: 8 additions & 3 deletions src/main/java/nova/mjs/comment/likes/entity/CommentLike.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@AllArgsConstructor
@Builder
@Table(name = "like_comment")
public class CommentLike {
@Id
Expand All @@ -23,8 +25,11 @@ public class CommentLike {
@JoinColumn(name = "comments_id", nullable = false)
private Comment comment;

public CommentLike(Member member, Comment comment) {
this.member = member;
this.comment = comment;
public static CommentLike create(Member member, Comment comment) {
return CommentLike.builder()
.member(member)
.comment(comment)
.build();

}
}
94 changes: 59 additions & 35 deletions src/main/java/nova/mjs/comment/service/CommentService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import nova.mjs.comment.DTO.CommentResponseDto;
import nova.mjs.comment.entity.Comment;
import nova.mjs.comment.exception.CommentNotFoundException;
import nova.mjs.comment.exception.CommentReplyDepthException;
import nova.mjs.comment.likes.repository.CommentLikeRepository;
import nova.mjs.comment.repository.CommentRepository;
import nova.mjs.community.entity.CommunityBoard;
Expand All @@ -16,6 +17,7 @@
import nova.mjs.community.exception.CommunityNotFoundException;
import nova.mjs.member.exception.MemberNotFoundException;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
Expand All @@ -33,52 +35,42 @@ public class CommentService {
private final CommentLikeRepository commentLikeRepository;



// 1. GEt 댓글 목록 (게시글 ID 기반, 페이지네이션 제거)
public List<CommentResponseDto.CommentSummaryDto> getCommentsByBoard(UUID communityBoardUuid, String email) {
// 1) 게시글 존재 여부 확인
CommunityBoard board = getExistingBoard(communityBoardUuid);
// 2) 댓글 목록 조회
List<Comment> comments = commentRepository.findByCommunityBoard(board);

// 댓글이 없다면 바로 빈 리스트 리턴
if (comments.isEmpty()) {
// 2) 전체 댓글 목록 조회
List<Comment> allComments = commentRepository.findByCommunityBoard(board);
if (allComments.isEmpty()) {
return List.of();
}

// 3) 비로그인 사용자면 -> isLiked = false로
if (email == null) {
return comments.stream()
.map(c -> CommentResponseDto.CommentSummaryDto.fromEntity(c, false))
.toList();
}

// 4) 로그인된 사용자 조회
Member member = memberRepository.findByEmail(email)
.orElse(null);
// 만약 email이 있는데 회원 정보가 없으면 -> isLiked = false
if (member == null) {
return comments.stream()
.map(c -> CommentResponseDto.CommentSummaryDto.fromEntity(c, false))
.toList();
}

// 5) 댓글 UUID 목록 추출
List<UUID> commentUuids = comments.stream()
.map(Comment::getUuid)
// 3) 최상위 댓글(부모가 null)만 필터링
List<Comment> topLevelComments = allComments.stream()
.filter(c -> c.getParent() == null)
.toList();

// 6) 사용자가 좋아요한 댓글의 UUID들을 가져오기
List<UUID> likedUuids = commentLikeRepository.findCommentUuidsLikedByMember(member, commentUuids);

// 7) 조회된 UUID를 Set으로 변환(contains()용)
Set<UUID> likedSet = new java.util.HashSet<>(likedUuids);
// 4) 비로그인 사용자면 -> isLiked = false (likedSet=null)
Set<UUID> likedSet = null;
if (email != null) {
Member member = memberRepository.findByEmail(email).orElse(null);
if (member != null) {
List<UUID> allUuids = allComments.stream()
.map(Comment::getUuid)
.toList();
List<UUID> likedUuids = commentLikeRepository.findCommentUuidsLikedByMember(member, allUuids);
likedSet = new HashSet<>(likedUuids);
}
}
// ★ 여기가 핵심
final Set<UUID> finalLikedSet = likedSet;

// 8) 각 댓글마다 isLiked 여부 매핑
return comments.stream()
// 5) 부모 + 자식(대댓글)까지 트리 구조로 DTO 변환
return topLevelComments.stream()
.map(comment -> {
boolean isLiked = likedSet.contains(comment.getUuid());
return CommentResponseDto.CommentSummaryDto.fromEntity(comment, isLiked);
boolean isLiked = (finalLikedSet != null && finalLikedSet.contains(comment.getUuid()));
return CommentResponseDto.CommentSummaryDto.fromEntityWithReplies(comment, isLiked, finalLikedSet);
})
.toList();
}
Expand All @@ -96,7 +88,7 @@ public CommentResponseDto.CommentSummaryDto createComment(UUID communityBoardUui
Comment savedComment = commentRepository.save(comment);

log.debug("댓글 작성 성공. UUID = {}, 작성자 : {}", savedComment.getUuid(), email);
return CommentResponseDto.CommentSummaryDto.fromEntity(savedComment, false);
return CommentResponseDto.CommentSummaryDto.fromEntity(savedComment);
}

// 3. DELETE 댓글 삭제, 로그인 연동 추가
Expand Down Expand Up @@ -124,6 +116,38 @@ private Member getExistingMember(UUID uuid) {
.orElseThrow(MemberNotFoundException::new);
}

// 6. 대댓글 작성
@Transactional
public CommentResponseDto.CommentSummaryDto createReply(UUID parentCommentUuid, String content, String email) {
// 1) 부모 댓글 조회
Comment parentComment = commentRepository.findByUuid(parentCommentUuid)
.orElseThrow(CommentNotFoundException::new);

// 2) parentComment가 이미 "자식 댓글"(= 대댓글)인지 확인
if (parentComment.getParent() != null) {
// 로그 남기기
log.error("[MJS] 대댓글 생성 실패: 이미 대댓글인 댓글에는 다시 대댓글을 달 수 없습니다. parentCommentUuid={}", parentCommentUuid);

// 커스텀 예외 던지기
throw new CommentReplyDepthException();
}


// 3) 작성자 조회
Member member = memberRepository.findByEmail(email)
.orElseThrow(MemberNotFoundException::new);

// 4) 대댓글 생성
Comment reply = Comment.createReply(parentComment, member, content);

// 5) DB 저장
Comment savedReply = commentRepository.save(reply);

// 6) DTO 변환 (isLiked=false 초기값)
return CommentResponseDto.CommentSummaryDto.fromEntity(savedReply);
}


private Comment getExistingCommentByUuid(UUID commentUuid) {
return commentRepository.findByUuid(commentUuid)
.orElseThrow(() -> {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/nova/mjs/util/jwt/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
@Slf4j
public class JwtUtil {
private final Key secretKey;
private final long accessTokenExpiration = 60 * 1000L;
private final long accessTokenExpiration = 60 * 1000L * 30;
private final long refreshTokenExpiration;

public JwtUtil(
Expand Down