Skip to content

Commit 55f8e0e

Browse files
authored
Merge pull request #77 from NOVA-MJU/add-recomment
[Add] Recomment
2 parents 839787e + 159d60a commit 55f8e0e

File tree

8 files changed

+193
-41
lines changed

8 files changed

+193
-41
lines changed

src/main/java/nova/mjs/comment/DTO/CommentResponseDto.java

+61-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package nova.mjs.comment.DTO;
2+
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
23
import lombok.*;
34
import nova.mjs.comment.entity.Comment;
45
import nova.mjs.community.entity.CommunityBoard;
56
import nova.mjs.member.Member;
67

78
import java.time.LocalDateTime;
89
import java.util.List;
10+
import java.util.Set;
11+
import java.util.HashSet;
912
import java.util.UUID;
1013

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

22+
@JsonPropertyOrder({
23+
"commentUUID",
24+
"content",
25+
"nickname",
26+
"likeCount",
27+
"createdAt",
28+
"liked",
29+
"replies"
30+
})
1931
@Getter
2032
@Builder
2133
@AllArgsConstructor
@@ -27,18 +39,66 @@ public static class CommentSummaryDto {
2739
private LocalDateTime createdAt;
2840
private boolean isLiked; // 현재 로그인 한 사용자가 좋아요를 눌렀는가 T/F
2941

42+
private List<CommentSummaryDto> replies;
3043

3144
public static CommentSummaryDto fromEntity(Comment comment, boolean isLiked) {
45+
return CommentSummaryDto.builder()
46+
.commentUUID(comment.getUuid())
47+
.content(comment.getContent())
48+
.nickname(comment.getMember().getNickname())
49+
.likeCount(comment.getLikeCount())
50+
.isLiked(isLiked)
51+
.createdAt(comment.getCreatedAt())
52+
.build();
53+
}
54+
55+
public static CommentSummaryDto fromEntity(Comment comment) {
56+
return CommentSummaryDto.builder()
57+
.commentUUID(comment.getUuid())
58+
.content(comment.getContent())
59+
.nickname(comment.getMember().getNickname())
60+
.likeCount(comment.getLikeCount())
61+
.createdAt(comment.getCreatedAt())
62+
.build();
63+
}
64+
// "부모 댓글"을 DTO로 변환하되, 자식 목록도 함께 변환하는 메서드
65+
public static CommentSummaryDto fromEntityWithReplies(Comment comment, boolean isLiked, Set<UUID> likedSet) {
66+
// 1) 부모 댓글의 기본 정보
67+
CommentSummaryDto.CommentSummaryDtoBuilder builder = CommentSummaryDto.builder()
68+
.commentUUID(comment.getUuid())
69+
.content(comment.getContent())
70+
.nickname(comment.getMember().getNickname())
71+
.likeCount(comment.getLikeCount())
72+
.createdAt(comment.getCreatedAt())
73+
.isLiked(isLiked);
74+
75+
// 2) 자식 댓글(대댓글) 변환
76+
// depth=1만 허용 → 자식의 자식은 처리 안 함
77+
List<CommentSummaryDto> replyDtos = comment.getReplies().stream()
78+
.map(child -> {
79+
boolean childIsLiked = (likedSet != null && likedSet.contains(child.getUuid()));
80+
return fromEntityNoReplies(child, childIsLiked);
81+
})
82+
.toList();
83+
84+
builder.replies(replyDtos);
85+
return builder.build();
86+
}
87+
88+
// "자식 댓글"은 더 이상의 자식 목록을 보지 않는다고 가정(depth=1)
89+
public static CommentSummaryDto fromEntityNoReplies(Comment comment, boolean isLiked) {
3290
return CommentSummaryDto.builder()
3391
.commentUUID(comment.getUuid())
3492
.content(comment.getContent())
3593
.nickname(comment.getMember().getNickname())
3694
.likeCount(comment.getLikeCount())
3795
.createdAt(comment.getCreatedAt())
3896
.isLiked(isLiked)
97+
.replies(List.of()) // 자식은 depth=1까지만
3998
.build();
4099
}
41100
}
101+
}
42102
/*
43103
// Entity 리스트 -> DTO 변환 (게시글의 모든 댓글)
44104
public static CommentResponseDto fromEntities(UUID communityBoardUuid, List<Comment> comments) {
@@ -71,4 +131,4 @@ public Comment toEntity(CommunityBoard communityBoard, Member member) {
71131
}
72132
*/
73133

74-
}
134+

src/main/java/nova/mjs/comment/controller/CommentController.java

+18
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,22 @@ public ResponseEntity<Void> deleteComment(
5656
service.deleteCommentByUuid(commentUUID, userPrincipal.getUsername());
5757
return ResponseEntity.noContent().build();
5858
}
59+
60+
// 4. POST 대댓글 작성
61+
@PostMapping("/{boardUUID}/comments/{parentCommentUUID}/reply")
62+
public ResponseEntity<ApiResponse<CommentResponseDto.CommentSummaryDto>> createReply(
63+
@PathVariable UUID parentCommentUUID,
64+
@RequestBody CommentRequestDto request,
65+
@AuthenticationPrincipal UserPrincipal userPrincipal
66+
) {
67+
// 로그인 사용자 email
68+
String email = (userPrincipal != null) ? userPrincipal.getUsername() : null;
69+
70+
// 실제 서비스 호출
71+
CommentResponseDto.CommentSummaryDto replyDto =
72+
service.createReply(parentCommentUUID, request.getContent(), email);
73+
74+
return ResponseEntity.status(HttpStatus.CREATED)
75+
.body(ApiResponse.success(replyDto));
76+
}
5977
}

src/main/java/nova/mjs/comment/entity/Comment.java

+28
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ public class Comment extends BaseEntity {
4040
@Column
4141
private int likeCount; // 좋아요 수
4242

43+
@ManyToOne(fetch = FetchType.LAZY)
44+
@JoinColumn(name = "parent_id") // 부모 댓글 ID 저장
45+
private Comment parent;
46+
47+
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
48+
private List<Comment> replies = new ArrayList<>();
49+
50+
4351
@OneToMany(mappedBy = "comment", cascade = CascadeType.ALL, orphanRemoval = true)
4452
private List<CommentLike> commentLike = new ArrayList<>();
4553

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

66+
// 부모가 있는 댓글(대댓글) 생성
67+
public static Comment createReply(Comment parent, Member member, String content) {
68+
// 부모 댓글이 속한 CommunityBoard를 그대로 사용 (부모와 같은 게시글)
69+
CommunityBoard board = parent.getCommunityBoard();
70+
71+
Comment reply = Comment.builder()
72+
.uuid(UUID.randomUUID())
73+
.communityBoard(board)
74+
.member(member)
75+
.content(content)
76+
.likeCount(0)
77+
.parent(parent) // 부모 설정
78+
.build();
79+
80+
// 부모의 replies에도 연결
81+
parent.getReplies().add(reply);
82+
83+
return reply;
84+
}
85+
5886
// 게시물 좋아요
5987
public void increaseLikeCommentCount() {
6088
this.likeCount++;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package nova.mjs.comment.exception;
2+
3+
import lombok.extern.log4j.Log4j2;
4+
import nova.mjs.util.exception.BusinessBaseException;
5+
import nova.mjs.util.exception.ErrorCode;
6+
7+
@Log4j2
8+
public class CommentReplyDepthException extends BusinessBaseException {
9+
10+
public CommentReplyDepthException() {
11+
super(ErrorCode.INVALID_REQUEST);
12+
}
13+
14+
public CommentReplyDepthException(String message) {
15+
super(message, ErrorCode.INVALID_REQUEST);
16+
}
17+
}

src/main/java/nova/mjs/comment/likes/CommentLikeService.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public boolean toggleLike(UUID boardUUID, UUID commentsUUID,String emailId) {
4747
log.debug("좋아요 삭제 완료: member_emailId={}, boardUUID={} , commentUUID={}", emailId, boardUUID, commentsUUID);
4848
return false; // 좋아요 취소됨
4949
} else {
50-
CommentLike commentLike = new CommentLike(member, comment);
50+
CommentLike commentLike = CommentLike.create(member, comment);
5151
commentLikeRepository.save(commentLike);
5252
comment.increaseLikeCommentCount(); // 좋아요 증가 메서드
5353
log.debug("좋아요 추가 완료: member_emailId={}, boardUUID={}, commentsUUID={}",emailId, boardUUID, commentsUUID);

src/main/java/nova/mjs/comment/likes/entity/CommentLike.java

+8-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
@Getter
99
@NoArgsConstructor(access = AccessLevel.PROTECTED)
1010
@Entity
11+
@AllArgsConstructor
12+
@Builder
1113
@Table(name = "like_comment")
1214
public class CommentLike {
1315
@Id
@@ -23,8 +25,11 @@ public class CommentLike {
2325
@JoinColumn(name = "comments_id", nullable = false)
2426
private Comment comment;
2527

26-
public CommentLike(Member member, Comment comment) {
27-
this.member = member;
28-
this.comment = comment;
28+
public static CommentLike create(Member member, Comment comment) {
29+
return CommentLike.builder()
30+
.member(member)
31+
.comment(comment)
32+
.build();
33+
2934
}
3035
}

src/main/java/nova/mjs/comment/service/CommentService.java

+59-35
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import nova.mjs.comment.DTO.CommentResponseDto;
66
import nova.mjs.comment.entity.Comment;
77
import nova.mjs.comment.exception.CommentNotFoundException;
8+
import nova.mjs.comment.exception.CommentReplyDepthException;
89
import nova.mjs.comment.likes.repository.CommentLikeRepository;
910
import nova.mjs.comment.repository.CommentRepository;
1011
import nova.mjs.community.entity.CommunityBoard;
@@ -16,6 +17,7 @@
1617
import nova.mjs.community.exception.CommunityNotFoundException;
1718
import nova.mjs.member.exception.MemberNotFoundException;
1819

20+
import java.util.HashSet;
1921
import java.util.List;
2022
import java.util.Set;
2123
import java.util.UUID;
@@ -33,52 +35,42 @@ public class CommentService {
3335
private final CommentLikeRepository commentLikeRepository;
3436

3537

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

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

49-
// 3) 비로그인 사용자면 -> isLiked = false로
50-
if (email == null) {
51-
return comments.stream()
52-
.map(c -> CommentResponseDto.CommentSummaryDto.fromEntity(c, false))
53-
.toList();
54-
}
55-
56-
// 4) 로그인된 사용자 조회
57-
Member member = memberRepository.findByEmail(email)
58-
.orElse(null);
59-
// 만약 email이 있는데 회원 정보가 없으면 -> isLiked = false
60-
if (member == null) {
61-
return comments.stream()
62-
.map(c -> CommentResponseDto.CommentSummaryDto.fromEntity(c, false))
63-
.toList();
64-
}
65-
66-
// 5) 댓글 UUID 목록 추출
67-
List<UUID> commentUuids = comments.stream()
68-
.map(Comment::getUuid)
49+
// 3) 최상위 댓글(부모가 null)만 필터링
50+
List<Comment> topLevelComments = allComments.stream()
51+
.filter(c -> c.getParent() == null)
6952
.toList();
7053

71-
// 6) 사용자가 좋아요한 댓글의 UUID들을 가져오기
72-
List<UUID> likedUuids = commentLikeRepository.findCommentUuidsLikedByMember(member, commentUuids);
73-
74-
// 7) 조회된 UUID를 Set으로 변환(contains()용)
75-
Set<UUID> likedSet = new java.util.HashSet<>(likedUuids);
54+
// 4) 비로그인 사용자면 -> isLiked = false (likedSet=null)
55+
Set<UUID> likedSet = null;
56+
if (email != null) {
57+
Member member = memberRepository.findByEmail(email).orElse(null);
58+
if (member != null) {
59+
List<UUID> allUuids = allComments.stream()
60+
.map(Comment::getUuid)
61+
.toList();
62+
List<UUID> likedUuids = commentLikeRepository.findCommentUuidsLikedByMember(member, allUuids);
63+
likedSet = new HashSet<>(likedUuids);
64+
}
65+
}
66+
// ★ 여기가 핵심
67+
final Set<UUID> finalLikedSet = likedSet;
7668

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

9890
log.debug("댓글 작성 성공. UUID = {}, 작성자 : {}", savedComment.getUuid(), email);
99-
return CommentResponseDto.CommentSummaryDto.fromEntity(savedComment, false);
91+
return CommentResponseDto.CommentSummaryDto.fromEntity(savedComment);
10092
}
10193

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

119+
// 6. 대댓글 작성
120+
@Transactional
121+
public CommentResponseDto.CommentSummaryDto createReply(UUID parentCommentUuid, String content, String email) {
122+
// 1) 부모 댓글 조회
123+
Comment parentComment = commentRepository.findByUuid(parentCommentUuid)
124+
.orElseThrow(CommentNotFoundException::new);
125+
126+
// 2) parentComment가 이미 "자식 댓글"(= 대댓글)인지 확인
127+
if (parentComment.getParent() != null) {
128+
// 로그 남기기
129+
log.error("[MJS] 대댓글 생성 실패: 이미 대댓글인 댓글에는 다시 대댓글을 달 수 없습니다. parentCommentUuid={}", parentCommentUuid);
130+
131+
// 커스텀 예외 던지기
132+
throw new CommentReplyDepthException();
133+
}
134+
135+
136+
// 3) 작성자 조회
137+
Member member = memberRepository.findByEmail(email)
138+
.orElseThrow(MemberNotFoundException::new);
139+
140+
// 4) 대댓글 생성
141+
Comment reply = Comment.createReply(parentComment, member, content);
142+
143+
// 5) DB 저장
144+
Comment savedReply = commentRepository.save(reply);
145+
146+
// 6) DTO 변환 (isLiked=false 초기값)
147+
return CommentResponseDto.CommentSummaryDto.fromEntity(savedReply);
148+
}
149+
150+
127151
private Comment getExistingCommentByUuid(UUID commentUuid) {
128152
return commentRepository.findByUuid(commentUuid)
129153
.orElseThrow(() -> {

src/main/java/nova/mjs/util/jwt/JwtUtil.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
@Slf4j
2020
public class JwtUtil {
2121
private final Key secretKey;
22-
private final long accessTokenExpiration = 60 * 1000L;
22+
private final long accessTokenExpiration = 60 * 1000L * 30;
2323
private final long refreshTokenExpiration;
2424

2525
public JwtUtil(

0 commit comments

Comments
 (0)