-
Notifications
You must be signed in to change notification settings - Fork 1
[LNK-39] 피드 댓글 기능 구현 #79
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
The head ref may contain hidden characters: "LNK-39-Leenk-\uD53C\uB4DC-\uB313\uAE00-\uC791\uC131-\uAE30\uB2A5-\uAD6C\uD604"
Changes from all commits
c3f2865
ee27dfa
a81bd2b
486fc18
2a9d879
b1f6933
0c58efd
a70d888
f3b04a3
394ae0b
f25e32c
6c51e48
dd3dd72
b73fcca
7b0096e
72af698
1256131
170095f
301c600
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package leets.leenk.domain.feed.application.dto.request; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
|
|
||
| public record CommentWriteRequest( | ||
| @NotBlank | ||
| String comment | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package leets.leenk.domain.feed.application.dto.response; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonInclude; | ||
| import com.fasterxml.jackson.annotation.JsonUnwrapped; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import leets.leenk.domain.user.application.dto.response.UserProfileResponse; | ||
| import lombok.Builder; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Builder | ||
| @JsonInclude(JsonInclude.Include.NON_NULL) | ||
| public record FeedCommentResponse( | ||
| @Schema(description = "댓글 id", example = "1") | ||
| long commentId, | ||
|
|
||
| @JsonUnwrapped | ||
| @Schema(implementation = UserProfileResponse.class) | ||
| UserProfileResponse user, | ||
|
|
||
| @Schema(description = "댓글", example = "오 좋은데??") | ||
| String comment, | ||
|
|
||
| @Schema(description = "댓글 작성 시간", example = "2025-06-30T00:00:00") | ||
| LocalDateTime createdAt | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package leets.leenk.domain.feed.application.exception; | ||
|
|
||
| import leets.leenk.global.common.exception.BaseException; | ||
|
|
||
| public class CommentDeleteNotAllowedException extends BaseException { | ||
| public CommentDeleteNotAllowedException() { | ||
| super(ErrorCode.COMMENT_DELETE_NOT_ALLOWED); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package leets.leenk.domain.feed.application.exception; | ||
|
|
||
| import leets.leenk.global.common.exception.BaseException; | ||
|
|
||
| public class CommentNotFoundException extends BaseException { | ||
| public CommentNotFoundException() { | ||
| super(ErrorCode.COMMENT_NOT_FOUND); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package leets.leenk.domain.feed.application.mapper; | ||
|
|
||
| import leets.leenk.domain.feed.application.dto.request.CommentWriteRequest; | ||
| import leets.leenk.domain.feed.domain.entity.Comment; | ||
| import leets.leenk.domain.feed.domain.entity.Feed; | ||
| import leets.leenk.domain.user.domain.entity.User; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| public class CommentMapper { | ||
| public Comment toComment(User user, Feed feed, CommentWriteRequest request) { | ||
| return Comment.builder() | ||
| .user(user) | ||
| .feed(feed) | ||
| .comment(request.comment()) | ||
| .build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,16 @@ | ||
| package leets.leenk.domain.feed.application.usecase; | ||
|
|
||
| import leets.leenk.domain.feed.application.dto.request.FeedReportRequest; | ||
| import leets.leenk.domain.feed.application.dto.request.FeedUpdateRequest; | ||
| import leets.leenk.domain.feed.application.dto.request.FeedUploadRequest; | ||
| import leets.leenk.domain.feed.application.dto.request.ReactionRequest; | ||
| import leets.leenk.domain.feed.application.dto.request.*; | ||
| import leets.leenk.domain.feed.application.dto.response.*; | ||
| import leets.leenk.domain.feed.application.exception.CommentDeleteNotAllowedException; | ||
| import leets.leenk.domain.feed.application.exception.FeedDeleteNotAllowedException; | ||
| import leets.leenk.domain.feed.application.exception.FeedUpdateNotAllowedException; | ||
| import leets.leenk.domain.feed.application.exception.SelfReactionNotAllowedException; | ||
| import leets.leenk.domain.feed.application.mapper.CommentMapper; | ||
| import leets.leenk.domain.feed.application.mapper.FeedMapper; | ||
| import leets.leenk.domain.feed.application.mapper.FeedUserMapper; | ||
| import leets.leenk.domain.feed.application.mapper.ReactionMapper; | ||
| import leets.leenk.domain.feed.domain.entity.Comment; | ||
| import leets.leenk.domain.feed.domain.entity.Feed; | ||
| import leets.leenk.domain.feed.domain.entity.LinkedUser; | ||
| import leets.leenk.domain.feed.domain.entity.Reaction; | ||
|
|
@@ -66,12 +66,17 @@ public class FeedUsecase { | |
| private final ReactionGetService reactionGetService; | ||
| private final ReactionSaveService reactionSaveService; | ||
|
|
||
| private final CommentSaveService commentSaveService; | ||
| private final CommentGetService commentGetService; | ||
| private final CommentDeleteService commentDeleteService; | ||
|
|
||
| private final FeedNotificationUsecase feedNotificationUsecase; | ||
|
|
||
| private final FeedMapper feedMapper; | ||
| private final MediaMapper mediaMapper; | ||
| private final FeedUserMapper feedUserMapper; | ||
| private final ReactionMapper reactionMapper; | ||
| private final CommentMapper commentMapper; | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public FeedListResponse getFeeds(long userId, int pageNumber, int pageSize) { | ||
|
|
@@ -95,8 +100,9 @@ public FeedDetailResponse getFeedDetail(Long feedId) { | |
| Feed feed = feedGetService.findById(feedId); | ||
| List<Media> medias = mediaGetService.findAllByFeed(feed); | ||
| List<LinkedUser> linkedUsers = linkedUserGetService.findAll(feed); | ||
| List<Comment> comments = commentGetService.findAllByFeed(feed); | ||
|
|
||
| return feedMapper.toFeedDetailResponse(feed, medias, linkedUsers); | ||
| return feedMapper.toFeedDetailResponse(feed, medias, linkedUsers, comments); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
|
|
@@ -146,12 +152,19 @@ public FeedNavigationResponse getFeedNavigation( | |
| linkedUserMap.put(feed.getId(), linkedUsers); | ||
| } | ||
|
|
||
| Map<Long, List<Comment>> commentsMap = new HashMap<>(); | ||
| for (Feed feed : allFeeds) { | ||
| List<Comment> comments = commentGetService.findAllByFeed(feed); | ||
| commentsMap.put(feed.getId(), comments); | ||
| } | ||
|
Comment on lines
+155
to
+159
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, find the CommentGetService file
fd CommentGetService --type fRepository: Leets-Makers/Leenk-BE Length of output: 140 🏁 Script executed: # Search for batch methods in CommentGetService
rg -A 10 'findAllByFeeds|findByFeedIn|findAllByFeedIn' --type=javaRepository: Leets-Makers/Leenk-BE Length of output: 7861 🏁 Script executed: # Also search for the current method being used (findAllByFeed)
rg -B 5 -A 10 'findAllByFeed' --type=java -g '*CommentGetService*'Repository: Leets-Makers/Leenk-BE Length of output: 1075 🏁 Script executed: # Check the FeedUsecase to understand the full context around lines 155-159
rg -B 10 -A 15 'commentsMap.*HashMap' src/main/java/leets/leenk/domain/feed/application/usecase/FeedUsecase.javaRepository: Leets-Makers/Leenk-BE Length of output: 1067 Implement batch query method in CommentGetService to avoid N+1 query issue The concern is valid. The codebase already demonstrates the correct pattern: 🤖 Prompt for AI Agents |
||
|
|
||
| return feedMapper.toFeedNavigationResponse( | ||
| currentFeed, | ||
| prevFeeds, | ||
| nextFeeds, | ||
| mediaMap, | ||
| linkedUserMap, | ||
| commentsMap, | ||
| hasMorePrev, | ||
| hasMoreNext | ||
| ); | ||
|
|
@@ -219,6 +232,16 @@ public void reactToFeed(long userId, long feedId, ReactionRequest request) { | |
| notifyIfReachedReactionMilestone(previousReactionCount, updatedReactionCount, feed); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void writeComment(long userId, long feedId, CommentWriteRequest request) { | ||
| User user = userGetService.findById(userId); | ||
| Feed feed = feedGetService.findById(feedId); | ||
|
|
||
| Comment comment = commentMapper.toComment(user, feed, request); | ||
|
|
||
| commentSaveService.saveComment(comment); | ||
| } | ||
|
Comment on lines
+235
to
+243
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 댓글 작성 후 알림 기능 추가를 고려하세요. 현재 댓글 작성 기능은 구현되었지만, 피드 작성자에게 새 댓글 알림을 전송하는 로직이 누락되었습니다. 다음과 같이 알림 로직을 추가할 수 있습니다: @Transactional
public void writeComment(long userId, long feedId, CommentWriteRequest request) {
User user = userGetService.findById(userId);
Feed feed = feedGetService.findById(feedId);
Comment comment = commentMapper.toComment(user, feed, request);
commentSaveService.saveComment(comment);
+
+ // 피드 작성자에게 댓글 알림 전송 (자기 자신은 제외)
+ if (!feed.getUser().equals(user)) {
+ feedNotificationUsecase.saveCommentNotification(feed, comment, user);
+ }
}
🤖 Prompt for AI Agents |
||
|
|
||
| private void validateReaction(Feed feed, User user) { | ||
| if (feed.getUser().equals(user)) { | ||
| throw new SelfReactionNotAllowedException(); | ||
|
|
@@ -325,6 +348,18 @@ public void deleteFeed(long userId, long feedId) { | |
| feedDeleteService.delete(feed); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void deleteComment(long userId, long commentId) { | ||
| User user = userGetService.findById(userId); | ||
| Comment comment = commentGetService.findCommentByIdNotDeleted(commentId); | ||
|
|
||
| if (!comment.getUser().equals(user)) { | ||
| throw new CommentDeleteNotAllowedException(); | ||
| } | ||
|
|
||
| commentDeleteService.deleteComment(comment); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public void reportFeed(long userId, long feedId, FeedReportRequest request) { | ||
| User user = userGetService.findById(userId); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| package leets.leenk.domain.feed.domain.entity; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import leets.leenk.domain.user.domain.entity.User; | ||
| import leets.leenk.global.common.entity.BaseEntity; | ||
| import lombok.AccessLevel; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.experimental.SuperBuilder; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Getter | ||
| @Entity | ||
| @SuperBuilder | ||
| @Table(name = "comments") | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class Comment extends BaseEntity { | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| @Column(name = "comment_id") | ||
| private Long commentId; | ||
|
|
||
|
Comment on lines
+17
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Find CommentGetService file
fd -t f "CommentGetService" --extension javaRepository: Leets-Makers/Leenk-BE Length of output: 140 🏁 Script executed: # Also search for findCommentById usage and implementation
rg "findCommentById" -t java -A 5Repository: Leets-Makers/Leenk-BE Length of output: 1388 🏁 Script executed: # Check the Comment entity file to understand the full structure
cat -n src/main/java/leets/leenk/domain/feed/domain/entity/Comment.javaRepository: Leets-Makers/Leenk-BE Length of output: 1487 🏁 Script executed: # Look for BaseEntity to understand soft delete handling pattern
fd -t f "BaseEntity" --extension java | head -5Repository: Leets-Makers/Leenk-BE Length of output: 127 🏁 Script executed: cat -n src/main/java/leets/leenk/global/common/entity/BaseEntity.javaRepository: Leets-Makers/Leenk-BE Length of output: 1040 🏁 Script executed: # Find CommentRepository
fd -t f "CommentRepository" --extension javaRepository: Leets-Makers/Leenk-BE Length of output: 143 🏁 Script executed: # Check the repository interface
cat -n src/main/java/leets/leenk/domain/feed/domain/repository/CommentRepository.javaRepository: Leets-Makers/Leenk-BE Length of output: 544 🏁 Script executed: # Check all methods in CommentGetService
cat -n src/main/java/leets/leenk/domain/feed/domain/service/CommentGetService.javaRepository: Leets-Makers/Leenk-BE Length of output: 1101
삭제된 댓글 조회 시도 시 404 예외를 발생시켜야 한다면, repository에 🤖 Prompt for AI Agents |
||
| @Column(name = "comments") | ||
| private String comment; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY, optional = false) | ||
| @JoinColumn(name = "feed_id", nullable = false, updatable = false) | ||
| private Feed feed; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY, optional = false) | ||
| @JoinColumn(name = "user_id", nullable = false, updatable = false) | ||
| private User user; | ||
|
|
||
| @Column(name = "deleted_at") | ||
| private LocalDateTime deletedAt; | ||
|
|
||
| public void deleteComment() { | ||
| this.deletedAt = LocalDateTime.now(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package leets.leenk.domain.feed.domain.repository; | ||
|
|
||
| import leets.leenk.domain.feed.domain.entity.Comment; | ||
| import leets.leenk.domain.feed.domain.entity.Feed; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| public interface CommentRepository extends JpaRepository<Comment, Long> { | ||
| Optional<Comment> findByCommentIdAndDeletedAtIsNull(long id); | ||
|
|
||
| List<Comment> findAllByFeedAndDeletedAtIsNullOrderByCreateDateDesc(Feed feed); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package leets.leenk.domain.feed.domain.service; | ||
|
|
||
|
|
||
| import leets.leenk.domain.feed.domain.entity.Comment; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class CommentDeleteService { | ||
|
|
||
| public void deleteComment(Comment comment) { | ||
| comment.deleteComment(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package leets.leenk.domain.feed.domain.service; | ||
|
|
||
| import leets.leenk.domain.feed.application.exception.CommentNotFoundException; | ||
| import leets.leenk.domain.feed.domain.entity.Comment; | ||
| import leets.leenk.domain.feed.domain.entity.Feed; | ||
| import leets.leenk.domain.feed.domain.repository.CommentRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class CommentGetService { | ||
| private final CommentRepository commentRepository; | ||
|
|
||
| public Comment findCommentByIdNotDeleted(long commentId) { | ||
| return commentRepository.findByCommentIdAndDeletedAtIsNull(commentId) | ||
| .orElseThrow(CommentNotFoundException::new); | ||
| } | ||
|
|
||
| public List<Comment> findAllByFeed(Feed feed) { | ||
| return commentRepository.findAllByFeedAndDeletedAtIsNullOrderByCreateDateDesc(feed); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package leets.leenk.domain.feed.domain.service; | ||
|
|
||
| import leets.leenk.domain.feed.domain.entity.Comment; | ||
| import leets.leenk.domain.feed.domain.repository.CommentRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class CommentSaveService { | ||
| private final CommentRepository commentRepository; | ||
|
|
||
| public void saveComment(Comment comment) { | ||
| commentRepository.save(comment); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
노션 명세 추가도 부탁드립니당
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추가해두었습니다!!