Skip to content

Commit 0677abe

Browse files
authored
feat: 뉴스 댓글/답글 좋아요 & 조회 시 좋아요 수 반영 (#107)
* feat: 좋아요 누르기 기능 구현 * feat: 뉴스 댓글 조회 시 좋아요 수 반영 * fix: 좋아요 여부 조회 기능 추가 * fix: 주석 제거 * fix: 답글 작성 시 좋아요 수 초기화 * fix: 다른 사람 댓글에 답글 달 수 있도록 수정
1 parent 65e64fa commit 0677abe

11 files changed

Lines changed: 257 additions & 18 deletions

src/main/java/com/tave/alarmissue/news/controller/NewsCommentController.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
import com.tave.alarmissue.auth.dto.request.PrincipalUserDetails;
44
import com.tave.alarmissue.news.domain.News;
55
import com.tave.alarmissue.news.dto.request.NewsCommentRequestDto;
6+
import com.tave.alarmissue.news.dto.response.NewsCommentLikeResponse;
67
import com.tave.alarmissue.news.dto.request.NewsCommentUpdateRequest;
78
import com.tave.alarmissue.news.dto.request.NewsReplyRequest;
9+
import com.tave.alarmissue.news.dto.response.NewsCommentLikeStatusResponse;
810
import com.tave.alarmissue.news.dto.response.NewsCommentListResponseDto;
911
import com.tave.alarmissue.news.dto.response.NewsCommentResponseDto;
12+
import com.tave.alarmissue.news.service.NewsCommentLikeService;
1013
import com.tave.alarmissue.news.service.NewsCommentService;
1114
import io.swagger.v3.oas.annotations.Operation;
1215
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -25,6 +28,7 @@
2528
public class NewsCommentController {
2629

2730
private final NewsCommentService newsCommentService;
31+
private final NewsCommentLikeService newsCommentLikeService;
2832

2933
@PostMapping
3034
@Operation(summary = "댓글 작성", description = "특정 뉴스에 댓글 작성합니다.")
@@ -82,4 +86,25 @@ public ResponseEntity<NewsCommentResponseDto> createReply(@RequestBody NewsReply
8286
return ResponseEntity.ok(response);
8387
}
8488

89+
@PostMapping("/like/{commentId}")
90+
@Operation(summary = "댓글/답글 좋아요", description = "댓글/답글에 좋아요를 추가하거나 제거합니다.")
91+
public ResponseEntity<NewsCommentLikeResponse> toggleLike(@PathVariable Long commentId, @AuthenticationPrincipal PrincipalUserDetails principal){
92+
Long userId= principal.getUserId();
93+
NewsCommentLikeResponse response=newsCommentLikeService.commentLike(commentId,userId);
94+
95+
return ResponseEntity.ok(response);
96+
}
97+
98+
@GetMapping("/like/{commentId}/status")
99+
@Operation(summary = "댓글 좋아요 상태 조회", description = "특정 사용자의 댓글 좋아요 상태를 조회합니다.")
100+
public ResponseEntity<NewsCommentLikeStatusResponse> getLikeStatus(
101+
@PathVariable Long commentId,
102+
@AuthenticationPrincipal PrincipalUserDetails principal) {
103+
104+
Long userId = principal.getUserId();
105+
NewsCommentLikeStatusResponse response = newsCommentLikeService.getLikeStatus(commentId, userId);
106+
107+
return ResponseEntity.ok(response);
108+
}
109+
85110
}

src/main/java/com/tave/alarmissue/news/converter/NewsCommentConverter.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ public static NewsCommentResponseDto toCommentResponseDto(NewsComment newsCommen
2525
return NewsCommentResponseDto.builder()
2626
.commentId(newsComment.getId())
2727
.comment(newsComment.getComment())
28+
.likeCount(newsComment.getLikeCount())
2829
.nickName(newsComment.getUser().getNickName())
2930
.createdAt(newsComment.getCreatedAt())
3031
.timeAgo(TimeAgoUtil.getTimeAgo(newsComment.getCreatedAt())) //현재 시간과 계산한 값
3132
.parentId(newsComment.getParentComment()!=null ? newsComment.getParentComment().getId():null)
32-
// .voteType(newsComment.getVoteType() != null ? newsComment.getVoteType() : null)
3333
.voteType(currentUserVoteType)
3434
.build();
3535
}
@@ -39,14 +39,14 @@ public NewsComment toComment(NewsCommentRequestDto dto, UserEntity user, News ne
3939
.comment(dto.getComment())
4040
.user(user)
4141
.news(news)
42+
.likeCount(0L)
4243
.voteType(voteType)
4344
.build();
4445

4546
}
4647

4748
public static NewsCommentListResponseDto toCommentListResponseDto(Long newsId, Long totalCount, List<NewsComment> comments,NewsVoteType currentUserVoteType) {
4849
List<NewsCommentResponseDto> commentResponseDtos = comments.stream()
49-
// .map(NewsCommentConverter::toCommentWithRepliesDto)
5050
.map(comment->toCommentWithRepliesDto(comment,currentUserVoteType))
5151
.collect(Collectors.toList());
5252

@@ -62,7 +62,6 @@ public static NewsCommentResponseDto toCommentWithRepliesDto(NewsComment newsCom
6262
// 답글들을 DTO로 변환
6363
List<NewsCommentResponseDto> replyDtos = newsComment.getReplies().stream()
6464
.sorted((r1, r2) -> r1.getCreatedAt().compareTo(r2.getCreatedAt())) // 답글은 오래된 순
65-
// .map(NewsCommentConverter::toCommentResponseDto)
6665
.map(reply->toCommentResponseDto(reply,currentUserVoteType))
6766
.collect(Collectors.toList());
6867

@@ -72,7 +71,8 @@ public static NewsCommentResponseDto toCommentWithRepliesDto(NewsComment newsCom
7271
.nickName(newsComment.getUser().getNickName())
7372
.createdAt(newsComment.getCreatedAt())
7473
.timeAgo(TimeAgoUtil.getTimeAgo(newsComment.getCreatedAt()))
75-
// .voteType(newsComment.getVoteType())
74+
.voteType(newsComment.getVoteType())
75+
.likeCount(newsComment.getLikeCount())
7676
.voteType(currentUserVoteType)
7777
.parentId(null) // 원댓글이므로 null
7878
.replies(replyDtos) // 답글들 포함

src/main/java/com/tave/alarmissue/news/domain/NewsComment.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.tave.alarmissue.global.domain.BaseTimeEntity;
44
import com.tave.alarmissue.news.domain.enums.NewsVoteType;
55
import com.tave.alarmissue.report.domain.Report;
6+
import com.tave.alarmissue.post.domain.PostLike;
67
import com.tave.alarmissue.user.domain.UserEntity;
78
import jakarta.persistence.*;
89
import lombok.AccessLevel;
@@ -54,8 +55,26 @@ public void updateContent(String newComment) {
5455
this.comment=newComment;
5556
}
5657

58+
@OneToMany(mappedBy = "comment", cascade = CascadeType.ALL, orphanRemoval = true)
59+
private List<NewsCommentLike> likes = new ArrayList<>();
60+
61+
// 좋아요 개수 필드 추가 (성능 최적화용)
62+
@Column(name = "like_count", nullable = false)
63+
private Long likeCount = 0L;
64+
65+
// 좋아요 관련 메서드
66+
public void incrementLikeCount() {
67+
this.likeCount++;
68+
}
69+
5770
public void updateVoteType(NewsVoteType voteType){
5871
this.voteType=voteType;
5972
}
6073

74+
public void decrementLikeCount() {
75+
if (this.likeCount > 0) {
76+
this.likeCount--;
77+
}
78+
}
79+
6180
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.tave.alarmissue.news.domain;
2+
3+
import com.tave.alarmissue.user.domain.UserEntity;
4+
import jakarta.persistence.*;
5+
import lombok.*;
6+
7+
@Getter
8+
@AllArgsConstructor
9+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
10+
@Entity
11+
@Builder
12+
@Table(name="news_comment_like")
13+
public class NewsCommentLike {
14+
@Id
15+
@GeneratedValue(strategy = GenerationType.IDENTITY)
16+
private Long id;
17+
18+
@ManyToOne(fetch = FetchType.LAZY)
19+
@JoinColumn(name="comment_id", nullable = false)
20+
private NewsComment comment;
21+
22+
@ManyToOne(fetch = FetchType.LAZY)
23+
@JoinColumn(name="user_id", nullable = false)
24+
private UserEntity user;
25+
26+
public NewsCommentLike(NewsComment comment, UserEntity user) {
27+
this.comment = comment;
28+
this.user = user;
29+
}
30+
31+
32+
33+
34+
35+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.tave.alarmissue.news.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
8+
@Getter
9+
@AllArgsConstructor
10+
@Builder
11+
@Schema(description = "댓글/답글 좋아요 토글 응답")
12+
public class NewsCommentLikeResponse {
13+
@Schema(description = "댓글 ID")
14+
private Long commentId;
15+
16+
@Schema(description = "좋아요 상태(true: 좋아요, false: 좋아요 취소")
17+
private Boolean isLiked;
18+
19+
@Schema(description = "총 좋아요 개수")
20+
private Long likeCount;
21+
22+
23+
24+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.tave.alarmissue.news.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
8+
@Getter
9+
@AllArgsConstructor
10+
@Builder
11+
@Schema(description = "좋아요 상태 조회용 응답")
12+
public class NewsCommentLikeStatusResponse {
13+
@Schema(description = "댓글 ID")
14+
private Long commentId;
15+
16+
@Schema(description = "사용자 ID")
17+
private Long userId;
18+
19+
@Schema(description = "좋아요 상태(true: 좋아요, false: 좋아요 안함)")
20+
private Boolean isLiked;
21+
22+
@Schema(description = "총 좋아요 개수")
23+
private Long likeCount;
24+
25+
}

src/main/java/com/tave/alarmissue/news/dto/response/NewsCommentResponseDto.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@ public class NewsCommentResponseDto {
2626
@Schema(description = "부모 댓글 ID (답글인 경우만)")
2727
private Long parentId; //부모댓글 id, null이면 원댓글, 값이 있으면 답글
2828

29-
@Schema(description = "답글 목록")
30-
@JsonInclude(JsonInclude.Include.NON_NULL) // null인 경우 JSON에서 제외
31-
private List<NewsCommentResponseDto> replies; //답글 리스트
32-
3329
@Schema(description = "답글 개수")
3430
@JsonInclude(JsonInclude.Include.NON_NULL) // null인 경우 JSON에서 제외
3531
private Integer replyCount;
32+
33+
@Schema(description = "좋아요 개수")
34+
private Long likeCount;
35+
36+
@Schema(description = "답글 목록")
37+
@JsonInclude(JsonInclude.Include.NON_NULL) // null인 경우 JSON에서 제외
38+
private List<NewsCommentResponseDto> replies; //답글 리스트
3639
}

src/main/java/com/tave/alarmissue/news/exceptions/NewsErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public enum NewsErrorCode implements ErrorCode {
1515
NEWS_ID_NOT_FOUND(HttpStatus.BAD_REQUEST, "해당 뉴스가 존재하지 않습니다."),
1616
COMMENT_ID_NOT_FOUND(HttpStatus.BAD_REQUEST,"해당 댓글이 존재하지 않습니다."),
1717
VOTE_NOT_FOUND(HttpStatus.BAD_REQUEST,"투표가 존재하지 않습니다."),
18-
UNAUTHORIZED_DELETE(HttpStatus.BAD_REQUEST,"댓글 삭제 권한이 없습니다."),
18+
COMMENT_ACCESS_DENIED(HttpStatus.FORBIDDEN,"해당 댓글에 권한이 없습니다."),
19+
1920
INVALID_REQUEST(HttpStatus.BAD_REQUEST,"대댓글에는 답글을 달 수 없습니다.");
2021

2122
private final HttpStatus httpStatus;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.tave.alarmissue.news.repository;
2+
3+
import com.tave.alarmissue.news.domain.NewsCommentLike;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.stereotype.Repository;
6+
7+
import java.util.Optional;
8+
9+
@Repository
10+
public interface NewsCommentLikeRepository extends JpaRepository<NewsCommentLike, Long> {
11+
12+
//특정 사용자가 특정 댓글에 좋아요를 눌렀는지 확인
13+
Optional<NewsCommentLike> findByCommentIdAndUserId(Long commentId, Long userId);
14+
15+
//특정 댓글의 좋아요 개수
16+
long countByCommentId(Long commentId);
17+
18+
boolean existsByCommentIdAndUserId(Long commentId, Long userId);
19+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.tave.alarmissue.news.service;
2+
3+
import com.tave.alarmissue.news.domain.NewsComment;
4+
import com.tave.alarmissue.news.domain.NewsCommentLike;
5+
import com.tave.alarmissue.news.dto.response.NewsCommentLikeResponse;
6+
import com.tave.alarmissue.news.dto.response.NewsCommentLikeStatusResponse;
7+
import com.tave.alarmissue.news.exceptions.NewsException;
8+
import com.tave.alarmissue.news.repository.NewsCommentLikeRepository;
9+
import com.tave.alarmissue.news.repository.NewsCommentRepository;
10+
import com.tave.alarmissue.user.domain.UserEntity;
11+
import com.tave.alarmissue.user.repository.UserRepository;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.stereotype.Service;
14+
import org.springframework.transaction.annotation.Transactional;
15+
16+
import javax.swing.text.html.Option;
17+
18+
import java.util.Optional;
19+
20+
import static com.tave.alarmissue.news.exceptions.NewsErrorCode.*;
21+
22+
@Service
23+
@RequiredArgsConstructor
24+
@Transactional(readOnly = true)
25+
public class NewsCommentLikeService {
26+
27+
private final NewsCommentLikeRepository newsCommentLikeRepository;
28+
private final NewsCommentRepository newsCommentRepository;
29+
private final UserRepository userRepository;
30+
31+
//(좋아요/좋아요 취소)
32+
@Transactional
33+
public NewsCommentLikeResponse commentLike(Long commentId, Long userId){
34+
//댓글 존재 확인
35+
NewsComment comment = newsCommentRepository.findById(commentId).orElseThrow(() -> new NewsException(COMMENT_ID_NOT_FOUND, " 댓글을 찾을 수 없습니다."));
36+
37+
//사용자 존재 확인
38+
UserEntity user = userRepository.findById(userId).orElseThrow(() -> new NewsException(USER_ID_NOT_FOUND, "사용자를 찾을 수 없습니다."));
39+
40+
//기존 좋아요 확인
41+
Optional<NewsCommentLike> existingLike = newsCommentLikeRepository.findByCommentIdAndUserId(commentId, userId);
42+
43+
boolean isLiked = false;
44+
if(existingLike.isPresent()){
45+
//이미 좋아요가 있으면 삭제(좋아요 취소)
46+
newsCommentLikeRepository.delete(existingLike.get());
47+
comment.decrementLikeCount();
48+
isLiked=false;
49+
}
50+
else{
51+
NewsCommentLike newLike=NewsCommentLike.builder()
52+
.comment(comment)
53+
.user(user)
54+
.build();
55+
newsCommentLikeRepository.save(newLike);
56+
comment.incrementLikeCount();
57+
isLiked = true;
58+
}
59+
60+
return new NewsCommentLikeResponse(commentId, isLiked, comment.getLikeCount());
61+
}
62+
63+
64+
@Transactional(readOnly = true)
65+
public NewsCommentLikeStatusResponse getLikeStatus(Long commentId, Long userId) {
66+
// 댓글 존재 확인
67+
NewsComment comment = newsCommentRepository.findById(commentId)
68+
.orElseThrow(() -> new NewsException(COMMENT_ID_NOT_FOUND, "댓글을 찾을 수 없습니다."));
69+
70+
// 좋아요 상태 확인
71+
boolean isLiked = newsCommentLikeRepository.existsByCommentIdAndUserId(commentId, userId);
72+
73+
return new NewsCommentLikeStatusResponse(
74+
commentId,
75+
userId,
76+
isLiked,
77+
comment.getLikeCount()
78+
);
79+
}
80+
81+
82+
}

0 commit comments

Comments
 (0)