From 2b297f3d97d58fbebab6c7310a411f21d9aaf682 Mon Sep 17 00:00:00 2001 From: JaeHyeon Lee Date: Fri, 17 Nov 2023 18:32:15 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EC=BB=A4=EB=AE=A4=EB=8B=88=ED=8B=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20API=20=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD?= =?UTF-8?q?=20=EB=B0=98=EC=98=81=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat(#54): resolved rebase conflicts Feat(#54): 커뮤니티 API 기초 구현 Feat(#54): 게시글 상세 조회 시 태그 포함 Feat(#54): todo 작성 Feat(#54): 게시글 생성 시 이미지 업로드 bug fix Feat(#54): 게시글 상세 조회 시 좋아요 여부와 작성자 프로필 사진 url 반환 Feat(#54): 게시글 상세 조회 시 좋아요 여부와 작성자 프로필 사진 url 반환 Feat(#54): 게시글 리스트 조회 시, 좋아요 누른 여부를 포함 Feat(#54): todo 주석 제거 및 더미데이터 제거 Feat(#54): 썸네일 이미지 관련 요구사항 적용 - 첫 이미지를 썸네일 이미지로 지정 Feat(#54): 게시물 카테고리 수정 - 일상, 교통, 치안, 기타 Hotfix: npe fix Feat(#54): rename field for resolve rebase conflict Docs(#54): update todo Fix(#54): 좋아요 버그 수정 Fix(#54): 스웨거 jwt 버그 수정 Feat(#54): 게시글/댓글 좋아요 api 구현 Feat(#54): 알림 저장 시 닉네임도 저장 Feat(#54): 좋아요 관련 예외 로직 Feat(#54): 푸시 알림 태그 구분 Feat(#54): 푸시 알림 발송 시 db 에 저장 Feat(#54): 게시글 좋아요 api (wip) Feat(#54): 게시글 좋아요 api (wip) Feat(#54): improvement community apis - 게시글 작성 시, 작성자의 주소 로깅 - 게시글에 댓글 등록 시 푸시 알림 전송 Feat(#54): improvement community apis - 게시글 작성 시, 작성자의 주소 로깅 - 게시글에 댓글 등록 시 푸시 알림 전송 Feat(#54): catch exception Docs(#54): todo 작성 Feat(#54): 게시글 수정하기 api 구현 및 게시글 삭제 http method 를 patch 로 변경 Feat(#54): 대댓글 계층형 조회 api 구현 Feat(#54): 대댓글 작성 api 구현 Feat(#54): 게시글에 댓글 작성하기 api 구현 Feat(#54): 게시글 조회 no offset paging api 구현 Feat(#54): removed test api for fcm Feat(#54): removed test api for fcm Feat(#54): 게시글 상세 조회 시, 제목 및 내용 포함 Feat(#54): 게시글 상세 조회 및 삭제 API 구현 Docs(#54): 게시글 작성 API Feat(#54): removed Qclass Feat(#54): 게시글 활성화 상태 필드 추가 Feat(#54): 게시글 작성 API 구현 Feat(#54): rename CommunityParticipant - to ArticleParticipant Feat(#54): 게시글, 댓글, 커뮤니티 참여자 레포지토리 구현 Feat(#54): relocate s3Provider Docs(#54): 프로필 사진 업로드 api - swagger api summary, description Feat(#54): 커뮤니티 엔티티 설계 - Article: 동네생활 게시글 - ArticleTag: 게시글 태그 - CommentEntity: 댓글 - CommunityParticipant: 게시글 참여자 Feat(#54): 회원 프로필 사진 업로드 API 구현 Feat(#54): 회원 엔티티 필드 추가 - 프로필 사진, 닉네임, 실명, fcm 토큰 Feat(#54): implement SecurityContextProvider - for parsing principal from jwt token Feat(#54): member extends baseTimeEntity * Feat(#54): fix invalid uri prefix * Feat(#54): fix invalid uri prefix * Feat(#54): 댓글 리스트 조회 시, 좋아요 여부 반환하도록 변경 * Feat(#54): 댓글 삭제 api 구현 * Feat(#54): 댓글 삭제 api docs 작성 * Feat(#54): comment count 관련 bug fix * Feat(#54): removed unused field * Feat(#54): 베스트 게시글 작성자에게 푸시알람 전송 * Feat(#54): 베스트 게시글 작성자에게 푸시알람 전송 --- .../article/controller/ArticleController.java | 8 ++-- .../dto/request/UploadArticleRequest.java | 1 - .../response/GetArticleDetailResponse.java | 32 +++++++++++---- .../dto/response/GetArticleListResponse.java | 17 ++++++-- .../dto/response/ModifyArticleResponse.java | 4 +- .../dto/response/UploadArticleResponse.java | 2 +- .../domain/article/entity/Article.java | 7 +--- .../domain/article/entity/ArticleTag.java | 4 +- .../article/service/ArticleService.java | 39 ++++++++++++++----- .../comment/controller/CommentController.java | 18 ++++++++- .../dto/response/DeleteCommentResponse.java | 12 ++++++ .../comment/dto/response/GetCommentDto.java | 4 +- .../domain/comment/entity/CommentEntity.java | 2 +- .../custom/CommentRepositoryCustom.java | 3 +- .../custom/CommentRepositoryCustomImpl.java | 11 ++++++ .../comment/service/CommentService.java | 23 ++++++++++- .../domain/like/service/LikeService.java | 16 +++++++- .../member/controller/MemberController.java | 2 +- .../backend/domain/member/entity/Member.java | 1 + .../domain/member/service/MemberService.java | 2 +- .../fcm/service/FcmMessageProvider.java | 1 + .../notification/NotificationMessage.java | 4 +- 22 files changed, 165 insertions(+), 48 deletions(-) create mode 100644 src/main/java/com/numberone/backend/domain/comment/dto/response/DeleteCommentResponse.java diff --git a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java index 9bcb9151..d4bea002 100644 --- a/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java +++ b/src/main/java/com/numberone/backend/domain/article/controller/ArticleController.java @@ -34,16 +34,16 @@ public class ArticleController { 1. title 은 글 제목 입니다 (not null) 2. content 는 글 내용 입니다 (not null) - 3. articleTag 는 게시글 태그 입니다. LIFE(일상), FRAUD(사기), SAFETY(안전), REPORT(제보) + 3. articleTag 는 게시글 태그 입니다. LIFE(일상), TRAFFIC(교통), SAFETY(치안), NONE(기타) -> 영어로 보내주세요 4. imageList 는 이미지 (MultiPart) 리스트 입니다. - 5. thumbNailImageIdx 는 썸네일 이미지의 인덱스 입니다. (0,1,2, ... + 5. imageList 의 첫 원소를 썸네일로 지정합니다. imageList 에 이미지를 담아서 보내는 경우, idx 에 따라서 썸네일 이미지를 결정합니다. """) @PostMapping - public ResponseEntity uploadArticle(@RequestBody @Valid UploadArticleRequest request) { + public ResponseEntity uploadArticle(@ModelAttribute @Valid UploadArticleRequest request) { return ResponseEntity.created(URI.create("/api/articles")) .body(articleService.uploadArticle(request)); } @@ -87,7 +87,7 @@ public ResponseEntity getArticleDetails(@PathVariable( @GetMapping public ResponseEntity> getArticlePages( Pageable pageable, - @ModelAttribute ArticleSearchParameter param) { // todo: 해당 유저가 좋아요를 눌렀는지 여부까지 표시되도록 수정 + @ModelAttribute ArticleSearchParameter param) { return ResponseEntity.ok(articleService.getArticleListPaging(param, pageable)); } diff --git a/src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java b/src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java index 04b76643..9bcea7c0 100644 --- a/src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java +++ b/src/main/java/com/numberone/backend/domain/article/dto/request/UploadArticleRequest.java @@ -30,7 +30,6 @@ public class UploadArticleRequest { // 이미지 관련 private List imageList; // 이미지 리스트 - private Long thumbNailImageIdx; // 썸네일 이미지의 순서 (0,1,2,...) private Double longitude; private Double latitude; diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java index cc356667..a695eb61 100644 --- a/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleDetailResponse.java @@ -1,6 +1,8 @@ package com.numberone.backend.domain.article.dto.response; import com.numberone.backend.domain.article.entity.Article; +import com.numberone.backend.domain.article.entity.ArticleStatus; +import com.numberone.backend.domain.article.entity.ArticleTag; import com.numberone.backend.domain.member.entity.Member; import lombok.*; @@ -22,18 +24,28 @@ public class GetArticleDetailResponse { private LocalDateTime modifiedAt; private String title; private String content; + private boolean isLiked; + private ArticleTag articleTag; + private Long commentCount; // 작성자 관련 - private String memberName; - private String memberNickName; - private String address; // todo: 더미 데이터 + private String ownerName; + private String ownerNickName; + private String address; private Long ownerMemberId; + private String ownerProfileImageUrl; // 이미지 관련 private List imageUrls; private String thumbNailImageUrl; - public static GetArticleDetailResponse of(Article article, List imageUrls, String thumbNailImageUrl, Member member){ + public static GetArticleDetailResponse of( + Article article, + List imageUrls, + String thumbNailImageUrl, + Member owner, + List memberLikedArticleList, + Long commentCount ) { return GetArticleDetailResponse.builder() .articleId(article.getId()) .title(article.getTitle()) @@ -45,12 +57,16 @@ public static GetArticleDetailResponse of(Article article, List imageUrl ) .createdAt(article.getCreatedAt()) .modifiedAt(article.getModifiedAt()) - .ownerMemberId(member.getId()) - .memberName(member.getRealName()) - .memberNickName(member.getNickName()) + .ownerMemberId(owner.getId()) + .ownerName(owner.getRealName()) + .ownerNickName(owner.getNickName()) .imageUrls(imageUrls) .thumbNailImageUrl(thumbNailImageUrl) - .address("서울시 광진구 자양동") // 교체 + .address(article.getAddress()) + .ownerProfileImageUrl(owner.getProfileImageUrl()) + .isLiked(memberLikedArticleList.contains(article.getId())) + .articleTag(article.getArticleTag()) + .commentCount(commentCount) .build(); } diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleListResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleListResponse.java index 6b88af6d..cf86ba21 100644 --- a/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleListResponse.java +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/GetArticleListResponse.java @@ -9,6 +9,7 @@ import lombok.*; import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; @ToString @@ -31,7 +32,8 @@ public class GetArticleListResponse { private Long thumbNailImageId; private Integer articleLikeCount; - private Integer commentCount; + private Long commentCount; + private Boolean isLiked; @QueryProjection @@ -46,7 +48,6 @@ public GetArticleListResponse(Article article, Long ownerId, Long thumbNailImage this.articleStatus = article.getArticleStatus(); this.thumbNailImageId = thumbNailImageId; this.articleLikeCount = article.getLikeCount(); - this.commentCount = article.getCommentCount(); } public void setOwnerNickName(String nickName){ @@ -57,7 +58,14 @@ public void setThumbNailImageUrl(String thumbNailImageUrl){ this.thumbNailImageUrl = thumbNailImageUrl; } - public void updateInfo(Optional owner, Optional articleImage){ + public void setCommentCount(Long commentCount){ + this.commentCount = commentCount; + } + + public void updateInfo(Optional owner, + Optional articleImage, + List memberLikedArticleIdList, + Long commentCount ){ owner.ifPresentOrElse( o -> setOwnerNickName(o.getNickName()), () -> setOwnerNickName("알 수 없는 사용자") @@ -66,7 +74,8 @@ public void updateInfo(Optional owner, Optional articleIma image -> setThumbNailImageUrl(image.getImageUrl()), () -> setThumbNailImageUrl("") ); + this.isLiked = memberLikedArticleIdList.contains(id); + this.commentCount = commentCount; } - } diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/ModifyArticleResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/ModifyArticleResponse.java index 88a57536..62ce0b63 100644 --- a/src/main/java/com/numberone/backend/domain/article/dto/response/ModifyArticleResponse.java +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/ModifyArticleResponse.java @@ -25,7 +25,7 @@ public class ModifyArticleResponse { private String thumbNailImageUrl; // 작성자 주소 - private String address; // todo: 더미 데이터 + private String address; public static ModifyArticleResponse of(Article article, List imageUrls, String thumbNailImageUrl){ return ModifyArticleResponse.builder() @@ -34,7 +34,7 @@ public static ModifyArticleResponse of(Article article, List imageUrls, .modifiedAt(article.getModifiedAt()) .imageUrls(imageUrls) .thumbNailImageUrl(thumbNailImageUrl) - .address("서울시 광진구 자양동") + .address(article.getAddress()) .build(); } diff --git a/src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java b/src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java index fef61aa5..c18a13c7 100644 --- a/src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java +++ b/src/main/java/com/numberone/backend/domain/article/dto/response/UploadArticleResponse.java @@ -21,7 +21,7 @@ public class UploadArticleResponse { private String thumbNailImageUrl; // 작성자 주소 - private String address; // todo: 더미 데이터 + private String address; public static UploadArticleResponse of(Article article, List imageUrls, String thumbNailImageUrl){ return UploadArticleResponse.builder() diff --git a/src/main/java/com/numberone/backend/domain/article/entity/Article.java b/src/main/java/com/numberone/backend/domain/article/entity/Article.java index c0bea662..92f0785f 100644 --- a/src/main/java/com/numberone/backend/domain/article/entity/Article.java +++ b/src/main/java/com/numberone/backend/domain/article/entity/Article.java @@ -56,11 +56,7 @@ public class Article extends BaseTimeEntity { @ColumnDefault("0") @Comment("게시글 좋아요 개수") - private Integer likeCount; // todo: 동시성 처리 - - @ColumnDefault("0") - @Comment("게시글에 달린 댓글 개수") - private Integer commentCount; + private Integer likeCount; @Comment("작성자 ID") private Long articleOwnerId; @@ -71,7 +67,6 @@ public Article(String title, String content, Long articleOwnerId, ArticleTag tag this.articleOwnerId = articleOwnerId; this.articleTag = tag; this.articleStatus = ArticleStatus.ACTIVATED; - this.commentCount = 0; this.likeCount = 0; } diff --git a/src/main/java/com/numberone/backend/domain/article/entity/ArticleTag.java b/src/main/java/com/numberone/backend/domain/article/entity/ArticleTag.java index 1ebf82a4..f5215017 100644 --- a/src/main/java/com/numberone/backend/domain/article/entity/ArticleTag.java +++ b/src/main/java/com/numberone/backend/domain/article/entity/ArticleTag.java @@ -5,8 +5,8 @@ @RequiredArgsConstructor public enum ArticleTag { LIFE, // 일상 - FRAUD, // 사기 + TRAFFIC, // 교통 SAFETY, // 치안 - REPORT; // 제보 + NONE; // 기타 private String value; } diff --git a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java index c2c5d312..52de3c0d 100644 --- a/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java +++ b/src/main/java/com/numberone/backend/domain/article/service/ArticleService.java @@ -14,6 +14,8 @@ import com.numberone.backend.domain.comment.dto.response.CreateCommentResponse; import com.numberone.backend.domain.comment.entity.CommentEntity; import com.numberone.backend.domain.comment.repository.CommentRepository; +import com.numberone.backend.domain.like.entity.ArticleLike; +import com.numberone.backend.domain.like.repository.ArticleLikeRepository; import com.numberone.backend.domain.member.entity.Member; import com.numberone.backend.domain.member.repository.MemberRepository; import com.numberone.backend.domain.notification.entity.NotificationTag; @@ -50,6 +52,7 @@ public class ArticleService { private final ArticleParticipantRepository articleParticipantRepository; private final ArticleImageRepository articleImageRepository; private final CommentRepository commentRepository; + private final ArticleLikeRepository articleLikeRepository; private final S3Provider s3Provider; private final LocationProvider locationProvider; private final FcmMessageProvider fcmMessageProvider; @@ -68,6 +71,7 @@ public UploadArticleResponse uploadArticle(UploadArticleRequest request) { owner.getId(), request.getArticleTag()) ); + articleParticipantRepository.save( new ArticleParticipant(article, owner) ); @@ -89,7 +93,7 @@ public UploadArticleResponse uploadArticle(UploadArticleRequest request) { new ArticleImage(article, imageUrl) ); articleImages.add(savedArticleImage); - if (Objects.equals(i, request.getThumbNailImageIdx())) { + if (i == 0) { thumbNailImageUrl = imageUrl; thumbNailImageId = savedArticleImage.getId(); } @@ -104,6 +108,7 @@ public UploadArticleResponse uploadArticle(UploadArticleRequest request) { Double latitude = request.getLatitude(); Double longitude = request.getLongitude(); if (latitude != null && longitude != null) { + // 주소가 null 이 아닌 경우에만 api 요청하여 update String address = locationProvider.pos2address(request.getLatitude(), request.getLongitude()); article.updateAddress(address); } @@ -122,10 +127,12 @@ public DeleteArticleResponse deleteArticle(Long articleId) { public GetArticleDetailResponse getArticleDetail(Long articleId) { String principal = SecurityContextProvider.getAuthenticatedUserEmail(); - Member owner = memberRepository.findByEmail(principal) + Member member = memberRepository.findByEmail(principal) // 회원 .orElseThrow(NotFoundMemberException::new); Article article = articleRepository.findById(articleId) .orElseThrow(NotFoundArticleException::new); + Member owner = memberRepository.findById(article.getArticleOwnerId()) // 작성자 + .orElseThrow(NotFoundMemberException::new); List imageUrls = articleImageRepository.findByArticle(article) .stream() @@ -134,32 +141,46 @@ public GetArticleDetailResponse getArticleDetail(Long articleId) { Optional thumbNailImage = articleImageRepository.findById(article.getThumbNailImageUrlId()); + Long commentCount = commentRepository.countAllByArticle(articleId); String thumbNailImageUrl = ""; if (thumbNailImage.isPresent()) { thumbNailImageUrl = thumbNailImage.get().getImageUrl(); } - return GetArticleDetailResponse.of(article, imageUrls, thumbNailImageUrl, owner); + // 내가 좋아요 한 게시글의 ID 리스트 + List memberLikedArticleIdList = articleLikeRepository.findByMember(member) + .stream().map(ArticleLike::getArticleId) + .toList(); + + return GetArticleDetailResponse.of(article, imageUrls, thumbNailImageUrl, owner, memberLikedArticleIdList, commentCount); } public Slice getArticleListPaging(ArticleSearchParameter param, Pageable pageable) { + String principal = SecurityContextProvider.getAuthenticatedUserEmail(); + Member member = memberRepository.findByEmail(principal) + .orElseThrow(NotFoundMemberException::new); + List memberLikedArticleIdList = articleLikeRepository.findByMember(member) + .stream().map(ArticleLike::getArticleId) + .toList(); return new SliceImpl<>( articleRepository.getArticlesNoOffSetPaging(param, pageable) .stream() - .peek(this::updateArticleInfo) - .toList() - ); + .peek(article -> { + updateArticleInfo(article, memberLikedArticleIdList); + }) + .toList()); } - public void updateArticleInfo(GetArticleListResponse articleInfo) { + public void updateArticleInfo(GetArticleListResponse articleInfo, List memberLikedArticleIdList) { Long ownerId = articleInfo.getOwnerId(); Long thumbNailImageUrlId = articleInfo.getThumbNailImageId(); Optional owner = memberRepository.findById(ownerId); Optional articleImage = articleImageRepository.findById(thumbNailImageUrlId); + Long commentCount = commentRepository.countAllByArticle(articleInfo.getId()); - articleInfo.updateInfo(owner, articleImage); + articleInfo.updateInfo(owner, articleImage, memberLikedArticleIdList, commentCount); } @Transactional @@ -174,8 +195,8 @@ public CreateCommentResponse createComment(Long articleId, CreateCommentRequest ); articleParticipantRepository.save(new ArticleParticipant(article, member)); + // 게시글 작성자에게 알림을 보낸다. fcmMessageProvider.sendFcm(member, ARTICLE_COMMENT_FCM_ALARM, NotificationTag.COMMUNITY); - return CreateCommentResponse.of(savedComment); } diff --git a/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java b/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java index 6ccf4158..be571887 100644 --- a/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java +++ b/src/main/java/com/numberone/backend/domain/comment/controller/CommentController.java @@ -2,12 +2,14 @@ import com.numberone.backend.domain.comment.dto.request.CreateChildCommentRequest; import com.numberone.backend.domain.comment.dto.response.CreateChildCommentResponse; +import com.numberone.backend.domain.comment.dto.response.DeleteCommentResponse; import com.numberone.backend.domain.comment.dto.response.GetCommentDto; import com.numberone.backend.domain.comment.service.CommentService; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.hibernate.sql.Delete; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -56,10 +58,22 @@ public ResponseEntity createChildComment( """) @GetMapping("{article-id}") public ResponseEntity> getCommentsByArticle(@PathVariable("article-id") Long articleId){ - List response = commentService.getCommentsByArticle(articleId); // todo: 해당 유저가 좋아요를 눌렀는지 여부까지 표시되도록 수정 + List response = commentService.getCommentsByArticle(articleId); return ResponseEntity.ok(response); } - // todo: 댓글 삭제, 가장 많은 좋아요 상단 고정, 대댓글 달리면 푸시 알람 전송, 상단 고정된 작성자에게 푸시알람 전송, 댓글 신고 기능 + @Operation(summary = "댓글 삭제 API 입니다", description = """ + 삭제할 댓글의 id 를 path variable 으로 보내주세요. + + 대댓글이 존재하는 댓글을 삭제 요청하는 경우에는, 대댓글까지 모두 삭제됩니다. + + 대댓글이 없는 댓글을 삭제 요청하는 경우에는 해당 댓글만 삭제됩니다. + """) + @DeleteMapping("{comment-id}") + public ResponseEntity deleteComment(@PathVariable("comment-id") Long commentId){ + return ResponseEntity.ok(commentService.deleteComment(commentId)); + } + + // todo: 가장 많은 좋아요 상단 고정, 대댓글 달리면 푸시 알람 전송, 상단 고정된 작성자에게 푸시알람 전송, 댓글 신고 기능 } diff --git a/src/main/java/com/numberone/backend/domain/comment/dto/response/DeleteCommentResponse.java b/src/main/java/com/numberone/backend/domain/comment/dto/response/DeleteCommentResponse.java new file mode 100644 index 00000000..7c06b632 --- /dev/null +++ b/src/main/java/com/numberone/backend/domain/comment/dto/response/DeleteCommentResponse.java @@ -0,0 +1,12 @@ +package com.numberone.backend.domain.comment.dto.response; + +import lombok.*; + +@ToString +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class DeleteCommentResponse { + private Long commentId; +} diff --git a/src/main/java/com/numberone/backend/domain/comment/dto/response/GetCommentDto.java b/src/main/java/com/numberone/backend/domain/comment/dto/response/GetCommentDto.java index 4fe99af3..efedf0c3 100644 --- a/src/main/java/com/numberone/backend/domain/comment/dto/response/GetCommentDto.java +++ b/src/main/java/com/numberone/backend/domain/comment/dto/response/GetCommentDto.java @@ -32,6 +32,7 @@ public class GetCommentDto { private Long authorId; private String authorNickName; private String authorProfileImageUrl; + private boolean isLiked; @QueryProjection @@ -47,7 +48,7 @@ public GetCommentDto(CommentEntity comment){ this.likeCount = comment.getLikeCount(); } - public void updateCommentInfo(Optional author){ + public void updateCommentInfo(Optional author, List likedCommentIdList){ author.ifPresentOrElse( a -> { this.authorNickName = a.getNickName(); @@ -57,6 +58,7 @@ public void updateCommentInfo(Optional author){ this.authorNickName = "알 수 없는 사용자"; } ); + this.isLiked = likedCommentIdList.contains(commentId); } } diff --git a/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java b/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java index 91fb1299..24978a86 100644 --- a/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java +++ b/src/main/java/com/numberone/backend/domain/comment/entity/CommentEntity.java @@ -31,7 +31,7 @@ public class CommentEntity extends BaseTimeEntity { private Integer depth; @Comment("댓글 좋아요 개수") - private Integer likeCount; // todo: 동시성 처리 + private Integer likeCount; @Comment("댓글 내용") private String content; diff --git a/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustom.java b/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustom.java index fe326c39..66adfcbf 100644 --- a/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustom.java +++ b/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustom.java @@ -5,6 +5,7 @@ import java.util.List; public interface CommentRepositoryCustom { - public List findAllByArticle(Long articleId); + List findAllByArticle(Long articleId); + Long countAllByArticle(Long articleId); } diff --git a/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustomImpl.java b/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustomImpl.java index 69331895..ce15479f 100644 --- a/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustomImpl.java +++ b/src/main/java/com/numberone/backend/domain/comment/repository/custom/CommentRepositoryCustomImpl.java @@ -31,4 +31,15 @@ public List findAllByArticle(Long articleId) { ) .fetch(); } + + + @Override + public Long countAllByArticle(Long articleId) { + return queryFactory.select(commentEntity.count()) + .from(commentEntity) + .innerJoin(article, article) + .where(article.id.eq(articleId)) + .fetchOne(); + + } } diff --git a/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java b/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java index ed8b83d7..825ab152 100644 --- a/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java +++ b/src/main/java/com/numberone/backend/domain/comment/service/CommentService.java @@ -6,9 +6,12 @@ import com.numberone.backend.domain.articleparticipant.repository.ArticleParticipantRepository; import com.numberone.backend.domain.comment.dto.request.CreateChildCommentRequest; import com.numberone.backend.domain.comment.dto.response.CreateChildCommentResponse; +import com.numberone.backend.domain.comment.dto.response.DeleteCommentResponse; import com.numberone.backend.domain.comment.dto.response.GetCommentDto; import com.numberone.backend.domain.comment.entity.CommentEntity; import com.numberone.backend.domain.comment.repository.CommentRepository; +import com.numberone.backend.domain.like.entity.CommentLike; +import com.numberone.backend.domain.like.repository.CommentLikeRepository; import com.numberone.backend.domain.member.entity.Member; import com.numberone.backend.domain.member.repository.MemberRepository; import com.numberone.backend.domain.token.util.SecurityContextProvider; @@ -32,6 +35,7 @@ public class CommentService { private final ArticleRepository articleRepository; private final ArticleParticipantRepository articleParticipantRepository; private final MemberRepository memberRepository; + private final CommentLikeRepository commentLikeRepository; @Transactional public CreateChildCommentResponse createChildComment( @@ -56,10 +60,15 @@ public CreateChildCommentResponse createChildComment( } public List getCommentsByArticle(Long articleId) { + String principal = SecurityContextProvider.getAuthenticatedUserEmail(); + Member member = memberRepository.findByEmail(principal) + .orElseThrow(NotFoundMemberException::new); Article article = articleRepository.findById(articleId) .orElseThrow(NotFoundArticleException::new); List comments = commentRepository.findAllByArticle(article.getId()); - + List likedCommentIdList = commentLikeRepository.findByMember(member) + .stream().map(CommentLike::getCommentId) + .toList(); // 계층 구조로 변환 (추후 리팩토링 필요) List result = new ArrayList<>(); Map map = new HashMap<>(); @@ -68,7 +77,7 @@ public List getCommentsByArticle(Long articleId) { CommentEntity commentEntity = commentRepository.findById(comment.getCommentId()) .orElseThrow(NotFoundCommentException::new); Optional author = memberRepository.findById(commentEntity.getAuthorId()); - comment.updateCommentInfo(author); + comment.updateCommentInfo(author, likedCommentIdList); map.put(comment.getCommentId(), comment); @@ -85,4 +94,14 @@ public List getCommentsByArticle(Long articleId) { return result; } + @Transactional + public DeleteCommentResponse deleteComment(Long commentId){ + CommentEntity commentEntity = commentRepository.findById(commentId) + .orElseThrow(NotFoundCommentException::new); + commentRepository.delete(commentEntity); + return DeleteCommentResponse.builder() + .commentId(commentId) + .build(); + } + } diff --git a/src/main/java/com/numberone/backend/domain/like/service/LikeService.java b/src/main/java/com/numberone/backend/domain/like/service/LikeService.java index c43c90d7..f215355e 100644 --- a/src/main/java/com/numberone/backend/domain/like/service/LikeService.java +++ b/src/main/java/com/numberone/backend/domain/like/service/LikeService.java @@ -10,19 +10,24 @@ import com.numberone.backend.domain.like.repository.CommentLikeRepository; import com.numberone.backend.domain.member.entity.Member; import com.numberone.backend.domain.member.repository.MemberRepository; +import com.numberone.backend.domain.notification.entity.NotificationTag; import com.numberone.backend.domain.token.util.SecurityContextProvider; import com.numberone.backend.exception.conflict.AlreadyLikedException; import com.numberone.backend.exception.conflict.AlreadyUnLikedException; import com.numberone.backend.exception.notfound.NotFoundApiException; import com.numberone.backend.exception.notfound.NotFoundCommentException; import com.numberone.backend.exception.notfound.NotFoundMemberException; +import com.numberone.backend.support.fcm.service.FcmMessageProvider; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.hibernate.id.IntegralDataTypeHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import static com.numberone.backend.support.notification.NotificationMessage.BEST_ARTICLE_FCM_ALARM; + @Slf4j @Service @RequiredArgsConstructor @@ -34,7 +39,9 @@ public class LikeService { private final CommentLikeRepository commentLikeRepository; private final CommentRepository commentRepository; private final MemberRepository memberRepository; + private final FcmMessageProvider fcmMessageProvider; + private final Integer BEST_ARTICLE_LIKE_COUNT = 20; @Transactional public Integer increaseArticleLike(Long articleId) { @@ -50,6 +57,13 @@ public Integer increaseArticleLike(Long articleId) { article.increaseLikeCount(); articleLikeRepository.save(new ArticleLike(member, article)); + if (article.getLikeCount() >= BEST_ARTICLE_LIKE_COUNT) { + Long ownerId = article.getArticleOwnerId(); + Member owner = memberRepository.findById(ownerId) + .orElseThrow(NotFoundMemberException::new); + fcmMessageProvider.sendFcm(owner, BEST_ARTICLE_FCM_ALARM, NotificationTag.COMMUNITY); + } + return article.getLikeCount(); } @@ -97,7 +111,7 @@ public Integer decreaseCommentLike(Long commentId) { .orElseThrow(NotFoundMemberException::new); CommentEntity commentEntity = commentRepository.findById(commentId) .orElseThrow(NotFoundCommentException::new); - if (!isAlreadyLikedComment(member, commentId)){ + if (!isAlreadyLikedComment(member, commentId)) { // 좋아요를 누르지 않은 댓글이라 좋아요를 취소할 수 없습니다. throw new AlreadyUnLikedException(); } diff --git a/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java b/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java index 6acfc70b..2344e859 100644 --- a/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java +++ b/src/main/java/com/numberone/backend/domain/member/controller/MemberController.java @@ -28,8 +28,8 @@ @Slf4j @Tag(name = "members", description = "사용자 관련 API") @RestController -@RequiredArgsConstructor @RequestMapping("/api/members") +@RequiredArgsConstructor public class MemberController { private final MemberService memberService; diff --git a/src/main/java/com/numberone/backend/domain/member/entity/Member.java b/src/main/java/com/numberone/backend/domain/member/entity/Member.java index cea2b8e5..53d9ee4a 100644 --- a/src/main/java/com/numberone/backend/domain/member/entity/Member.java +++ b/src/main/java/com/numberone/backend/domain/member/entity/Member.java @@ -16,6 +16,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.Comment; +import org.springframework.web.bind.annotation.RequestMapping; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/numberone/backend/domain/member/service/MemberService.java b/src/main/java/com/numberone/backend/domain/member/service/MemberService.java index cab4b378..7107ebc6 100644 --- a/src/main/java/com/numberone/backend/domain/member/service/MemberService.java +++ b/src/main/java/com/numberone/backend/domain/member/service/MemberService.java @@ -46,8 +46,8 @@ public void create(String email, String realName) { public void initMemberData(String email, OnboardingRequest onboardingRequest) { Member member = memberRepository.findByEmail(email) .orElseThrow(NotFoundMemberException::new); - member.setOnboardingData(onboardingRequest.getNickname(), onboardingRequest.getFcmToken()); notificationDisasterRepository.deleteAllByMemberId(member.getId()); + member.setOnboardingData(onboardingRequest.getNickname(), onboardingRequest.getFcmToken()); notificationRegionRepository.deleteAllByMemberId(member.getId()); for (OnboardingAddress address : onboardingRequest.getAddresses()) { notificationRegionRepository.save(NotificationRegion.of( diff --git a/src/main/java/com/numberone/backend/support/fcm/service/FcmMessageProvider.java b/src/main/java/com/numberone/backend/support/fcm/service/FcmMessageProvider.java index 5bfff921..62131f2e 100644 --- a/src/main/java/com/numberone/backend/support/fcm/service/FcmMessageProvider.java +++ b/src/main/java/com/numberone/backend/support/fcm/service/FcmMessageProvider.java @@ -27,6 +27,7 @@ public void sendFcm(Member member, NotificationMessage notificationMessage, Noti String token = member.getFcmToken(); if (Objects.isNull(token)){ log.error("해당 회원의 fcm 토큰이 존재하지 않아, 푸시알람을 전송할 수 없습니다."); + // todo : 예외 핸들링 return; } diff --git a/src/main/java/com/numberone/backend/support/notification/NotificationMessage.java b/src/main/java/com/numberone/backend/support/notification/NotificationMessage.java index 8fdf8316..aa0a387c 100644 --- a/src/main/java/com/numberone/backend/support/notification/NotificationMessage.java +++ b/src/main/java/com/numberone/backend/support/notification/NotificationMessage.java @@ -7,7 +7,9 @@ @RequiredArgsConstructor public enum NotificationMessage implements NotificationMessageSpec { - ARTICLE_COMMENT_FCM_ALARM("[대피로 알림]", "게시글에 댓글이 달렸어요!", null); + ARTICLE_COMMENT_FCM_ALARM("[대피로 알림]", "게시글에 댓글이 달렸어요!", null), + BEST_ARTICLE_FCM_ALARM("[대피로 알림]", "축하드립니다! 베스트 게시글로 선정되었습니다. 🎉", null); + ; private final String title; private final String body;