From ffbdfd9f0abaf9fe1973305e99e8f0b0955ecc37 Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Wed, 28 May 2025 18:53:53 +0900 Subject: [PATCH 01/10] =?UTF-8?q?fix:=20#283=20=EB=8B=B5=EB=B3=80=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=20isLiked=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../answer/repository/AnswerRepository.java | 36 +++++++++++++------ .../answer/service/AnswerServiceImpl.java | 5 ++- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/server/capple/domain/answer/repository/AnswerRepository.java b/src/main/java/com/server/capple/domain/answer/repository/AnswerRepository.java index ec4e968..22f0e55 100644 --- a/src/main/java/com/server/capple/domain/answer/repository/AnswerRepository.java +++ b/src/main/java/com/server/capple/domain/answer/repository/AnswerRepository.java @@ -30,17 +30,33 @@ public interface AnswerRepository extends JpaRepository { boolean existsByQuestionAndMember(Question question, Member member); +// @Query("SELECT a AS answer, " + +// "(r IS NOT NULL) AS isReported, " + +// "a.member.id AS writerId, " + +// "a.member.profileImage AS writerProfileImage, " + +// "a.member.nickname AS writerNickname," + +// "a.member.academyGeneration AS writerAcademyGeneration " + +// "FROM Answer a " + +// "LEFT JOIN " + +// "Report r ON r.answer = a " + +// "WHERE (a.id < :lastIndex OR :lastIndex IS NULL) AND a.question.id = :questionId") +// Slice findByQuestion(@Param("questionId") Long questionId, Long lastIndex, Pageable pageable); + @Query("SELECT a AS answer, " + - "(r IS NOT NULL) AS isReported, " + - "a.member.id AS writerId, " + - "a.member.profileImage AS writerProfileImage, " + - "a.member.nickname AS writerNickname," + - "a.member.academyGeneration AS writerAcademyGeneration " + - "FROM Answer a " + - "LEFT JOIN " + - "Report r ON r.answer = a " + - "WHERE (a.id < :lastIndex OR :lastIndex IS NULL) AND a.question.id = :questionId") - Slice findByQuestion(@Param("questionId") Long questionId, Long lastIndex, Pageable pageable); + "(r IS NOT NULL) AS isReported, " + + "a.member.id AS writerId, " + + "a.member.profileImage AS writerProfileImage, " + + "a.member.nickname AS writerNickname, " + + "a.member.academyGeneration AS writerAcademyGeneration, " + + "CASE WHEN ah.id IS NOT NULL THEN TRUE ELSE FALSE END AS isLiked " + + "FROM Answer a " + + "LEFT JOIN Report r ON r.answer = a " + + "LEFT JOIN AnswerHeart ah ON ah.answer = a AND ah.member = :member AND ah.isLiked = TRUE " + + "WHERE (a.id < :lastIndex OR :lastIndex IS NULL) AND a.question.id = :questionId") + Slice findByQuestion(@Param("questionId") Long questionId, + @Param("lastIndex") Long lastIndex, + @Param("member") Member member, + Pageable pageable); @Query(""" SELECT diff --git a/src/main/java/com/server/capple/domain/answer/service/AnswerServiceImpl.java b/src/main/java/com/server/capple/domain/answer/service/AnswerServiceImpl.java index 8879998..3219b5e 100644 --- a/src/main/java/com/server/capple/domain/answer/service/AnswerServiceImpl.java +++ b/src/main/java/com/server/capple/domain/answer/service/AnswerServiceImpl.java @@ -16,6 +16,7 @@ import com.server.capple.domain.answer.repository.AnswerRepository; import com.server.capple.domain.member.entity.Member; import com.server.capple.domain.member.service.MemberService; +import com.server.capple.domain.member.service.MemberServiceImpl; import com.server.capple.domain.notifiaction.service.NotificationService; import com.server.capple.domain.question.entity.Question; import com.server.capple.domain.question.service.QuestionService; @@ -54,6 +55,7 @@ public class AnswerServiceImpl implements AnswerService { private final AnswerHeartRepository answerHeartRepository; private final AnswerHeartMapper answerHeartMapper; private final AnswerConcurrentService answerConcurrentService; + private final MemberServiceImpl memberServiceImpl; @Transactional @Override @@ -133,7 +135,8 @@ public AnswerLike toggleAnswerHeart(Member loginMember, Long answerId) { @Override public SliceResponse getAnswerList(Long memberId, Long questionId, Long lastIndex, Pageable pageable) { - Slice answerInfoSliceInterface = answerRepository.findByQuestion(questionId, lastIndex, pageable); + Member member = memberServiceImpl.findMember(memberId); + Slice answerInfoSliceInterface = answerRepository.findByQuestion(questionId, lastIndex, member, pageable); lastIndex = getLastIndexFromAnswerInfoInterface(answerInfoSliceInterface); return SliceResponse.toSliceResponse(answerInfoSliceInterface, answerInfoSliceInterface.getContent().stream().map( answerInfoDto -> answerMapper.toAnswerInfo( From a31e1412bd905aff1d682e2a4ca17fa9465e472c Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Thu, 29 May 2025 04:17:44 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20#283=20AnswerCommentHeart=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/AnswerCommentHeart.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/main/java/com/server/capple/domain/answerComment/entity/AnswerCommentHeart.java diff --git a/src/main/java/com/server/capple/domain/answerComment/entity/AnswerCommentHeart.java b/src/main/java/com/server/capple/domain/answerComment/entity/AnswerCommentHeart.java new file mode 100644 index 0000000..732c199 --- /dev/null +++ b/src/main/java/com/server/capple/domain/answerComment/entity/AnswerCommentHeart.java @@ -0,0 +1,35 @@ +package com.server.capple.domain.answerComment.entity; + +import com.server.capple.domain.answer.entity.Answer; +import com.server.capple.domain.member.entity.Member; +import com.server.capple.global.common.BaseEntity; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.SQLRestriction; + +@Getter +@Builder +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@SQLRestriction("deleted_at is null") +public class AnswerCommentHeart extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "answerComment_id", nullable = false) + private AnswerComment answerComment; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + private boolean isLiked; + + public boolean toggleHeart() { + this.isLiked = !this.isLiked; + return isLiked; + } +} From 38360379a8685893018e14dd7ce26b35fdf4261c Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Thu, 29 May 2025 04:18:52 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20#283=20AnswerCommentHeart=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=84=9C=EB=B9=84=EC=8A=A4/=EB=A0=88?= =?UTF-8?q?=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/AnswerCommentHeartMapper.java | 20 +++++++++++ .../AnswerCommentHeartRepository.java | 15 ++++++++ .../service/AnswerCommentCountService.java | 36 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentHeartMapper.java create mode 100644 src/main/java/com/server/capple/domain/answerComment/repository/AnswerCommentHeartRepository.java create mode 100644 src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentCountService.java diff --git a/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentHeartMapper.java b/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentHeartMapper.java new file mode 100644 index 0000000..301b042 --- /dev/null +++ b/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentHeartMapper.java @@ -0,0 +1,20 @@ +package com.server.capple.domain.answerComment.mapper; + +import com.server.capple.domain.answer.entity.Answer; +import com.server.capple.domain.answer.entity.AnswerHeart; +import com.server.capple.domain.answerComment.dto.AnswerCommentResponse; +import com.server.capple.domain.answerComment.entity.AnswerComment; +import com.server.capple.domain.answerComment.entity.AnswerCommentHeart; +import com.server.capple.domain.member.entity.Member; +import org.springframework.stereotype.Component; + +@Component +public class AnswerCommentHeartMapper { + public AnswerCommentHeart toAnswerCommentHeart(AnswerComment answerComment, Member member) { + return AnswerCommentHeart.builder() + .answerComment(answerComment) + .member(member) + .isLiked(false) + .build(); + } +} diff --git a/src/main/java/com/server/capple/domain/answerComment/repository/AnswerCommentHeartRepository.java b/src/main/java/com/server/capple/domain/answerComment/repository/AnswerCommentHeartRepository.java new file mode 100644 index 0000000..c573fdb --- /dev/null +++ b/src/main/java/com/server/capple/domain/answerComment/repository/AnswerCommentHeartRepository.java @@ -0,0 +1,15 @@ +package com.server.capple.domain.answerComment.repository; + +import com.server.capple.domain.answer.entity.Answer; +import com.server.capple.domain.answer.entity.AnswerHeart; +import com.server.capple.domain.answerComment.entity.AnswerComment; +import com.server.capple.domain.answerComment.entity.AnswerCommentHeart; +import com.server.capple.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface AnswerCommentHeartRepository extends JpaRepository { + + Optional findByMemberAndAnswerComment(Member member, AnswerComment answerComment); +} diff --git a/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentCountService.java b/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentCountService.java new file mode 100644 index 0000000..a6ae88c --- /dev/null +++ b/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentCountService.java @@ -0,0 +1,36 @@ +package com.server.capple.domain.answerComment.service; + +import com.server.capple.domain.answer.repository.AnswerRepository; +import com.server.capple.domain.answerComment.repository.AnswerCommentRepository; +import com.server.capple.domain.member.entity.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.util.concurrent.CompletableFuture; + +@Service +@RequiredArgsConstructor +public class AnswerCommentCountService { + private final AnswerCommentRepository answerCommentRepository; + + @Cacheable(value = "answerComment", key = "#answerId", cacheManager = "oneDayExpireCacheManager") + public Integer getAnswerCommentCount(Long answerId) { + return answerCommentRepository.getAnswerCommentCountByAnswerId(answerId); + } + + @Async + @CachePut(value = "answerComment", key = "#answerId", cacheManager = "oneDayExpireCacheManager") + public CompletableFuture updateAnswerCommentCount(Long answerId) { + return CompletableFuture.completedFuture(answerCommentRepository.getAnswerCommentCountByAnswerId(answerId)); + } + + @Cacheable(value = "answerCommentCountByMember", key = "#member.id", cacheManager = "oneDayExpireCacheManager") + public Integer getAnswerCommentCountByMember(Member member) { return answerCommentRepository.getAnswerCommentCountByMember(member);} + + @CacheEvict(value = "answerCommentCountByMember", key = "#member.id", cacheManager = "oneDayExpireCacheManager") + public void expireMembersAnswerCommentedCount(Member member) {} +} From 62fad6085ebfe5fc7c7127a93841f202a16efc00 Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Thu, 29 May 2025 04:21:53 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20#283=20AnswerCommentHeart=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../answer/repository/AnswerRepository.java | 7 +- .../answer/service/AnswerServiceImpl.java | 4 +- .../controller/AnswerCommentController.java | 22 +++-- .../dao/AnswerCommentRDBDao.java | 16 ++++ .../dto/AnswerCommentResponse.java | 8 +- .../mapper/AnswerCommentMapper.java | 26 ++++-- .../repository/AnswerCommentRepository.java | 32 ++++++- .../service/AnswerCommentService.java | 9 +- .../service/AnswerCommentServiceImpl.java | 83 +++++++++++++++---- 9 files changed, 165 insertions(+), 42 deletions(-) create mode 100644 src/main/java/com/server/capple/domain/answerComment/dao/AnswerCommentRDBDao.java diff --git a/src/main/java/com/server/capple/domain/answer/repository/AnswerRepository.java b/src/main/java/com/server/capple/domain/answer/repository/AnswerRepository.java index 22f0e55..954b16d 100644 --- a/src/main/java/com/server/capple/domain/answer/repository/AnswerRepository.java +++ b/src/main/java/com/server/capple/domain/answer/repository/AnswerRepository.java @@ -41,7 +41,7 @@ public interface AnswerRepository extends JpaRepository { // "Report r ON r.answer = a " + // "WHERE (a.id < :lastIndex OR :lastIndex IS NULL) AND a.question.id = :questionId") // Slice findByQuestion(@Param("questionId") Long questionId, Long lastIndex, Pageable pageable); - + @Query("SELECT a AS answer, " + "(r IS NOT NULL) AS isReported, " + "a.member.id AS writerId, " + @@ -53,10 +53,7 @@ public interface AnswerRepository extends JpaRepository { "LEFT JOIN Report r ON r.answer = a " + "LEFT JOIN AnswerHeart ah ON ah.answer = a AND ah.member = :member AND ah.isLiked = TRUE " + "WHERE (a.id < :lastIndex OR :lastIndex IS NULL) AND a.question.id = :questionId") - Slice findByQuestion(@Param("questionId") Long questionId, - @Param("lastIndex") Long lastIndex, - @Param("member") Member member, - Pageable pageable); + Slice findByQuestion(@Param("questionId") Long questionId, @Param("lastIndex") Long lastIndex, @Param("member") Member member, Pageable pageable); @Query(""" SELECT diff --git a/src/main/java/com/server/capple/domain/answer/service/AnswerServiceImpl.java b/src/main/java/com/server/capple/domain/answer/service/AnswerServiceImpl.java index 3219b5e..74dd16e 100644 --- a/src/main/java/com/server/capple/domain/answer/service/AnswerServiceImpl.java +++ b/src/main/java/com/server/capple/domain/answer/service/AnswerServiceImpl.java @@ -107,8 +107,6 @@ public AnswerResponse.AnswerId deleteAnswer(Member loginMember, Long answerId) { return new AnswerResponse.AnswerId(answerId); } - - //답변 좋아요 / 취소 @Override @Transactional public AnswerLike toggleAnswerHeart(Member loginMember, Long answerId) { @@ -135,7 +133,7 @@ public AnswerLike toggleAnswerHeart(Member loginMember, Long answerId) { @Override public SliceResponse getAnswerList(Long memberId, Long questionId, Long lastIndex, Pageable pageable) { - Member member = memberServiceImpl.findMember(memberId); + Member member = memberService.findMember(memberId); Slice answerInfoSliceInterface = answerRepository.findByQuestion(questionId, lastIndex, member, pageable); lastIndex = getLastIndexFromAnswerInfoInterface(answerInfoSliceInterface); return SliceResponse.toSliceResponse(answerInfoSliceInterface, answerInfoSliceInterface.getContent().stream().map( diff --git a/src/main/java/com/server/capple/domain/answerComment/controller/AnswerCommentController.java b/src/main/java/com/server/capple/domain/answerComment/controller/AnswerCommentController.java index 62da47f..cee2c73 100644 --- a/src/main/java/com/server/capple/domain/answerComment/controller/AnswerCommentController.java +++ b/src/main/java/com/server/capple/domain/answerComment/controller/AnswerCommentController.java @@ -6,10 +6,14 @@ import com.server.capple.domain.answerComment.service.AnswerCommentService; import com.server.capple.domain.member.entity.Member; import com.server.capple.global.common.BaseResponse; +import com.server.capple.global.common.SliceResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.web.bind.annotation.*; @Tag(name = "답변 댓글 API", description = "답변 댓글 API입니다.") @@ -40,14 +44,22 @@ public BaseResponse updateAnswerComment(@AuthMember Member memb @Operation(summary = "답변 댓글 좋아요/취소 토글 API", description = " 답변 댓글 좋아요/취소 토글 API 입니다. pathvariable 으로 commentId를 주세요.") @PatchMapping("/heart/{commentId}") - public BaseResponse heartAnswerComment(@AuthMember Member member, @PathVariable(value = "commentId") Long commentId) { - return BaseResponse.onSuccess(answerCommentService.heartAnswerComment(member, commentId)); + public BaseResponse heartAnswerComment(@AuthMember Member member, @PathVariable(value = "commentId") Long commentId) { + return BaseResponse.onSuccess(answerCommentService.toggleAnswerCommentHeart(member, commentId)); } - @Operation(summary = "답변에 대한 댓글 조회 API", description = " 답변에 대한 댓글 조회 API 입니다. pathvariable 으로 answerId를 주세요.") + @Operation(summary = "답변에 대한 댓글 조회 API", description = " 답변에 대한 댓글 조회 API 입니다. pathvariable 으로 answerId를 주세요." + + "pathVariable으로 answerId 주세요.
**첫 번째 조회 시 threshold를 비워 보내고, 이후 조회 시 앞선 조회의 반환값으로 받은 threshold를 보내주세요.**") @GetMapping("/{answerId}") - public BaseResponse getAnswerCommentInfos(@PathVariable(value = "answerId") Long answerId) { - return BaseResponse.onSuccess(answerCommentService.getAnswerCommentInfos(answerId)); + public BaseResponse> getAnswerCommentInfos( + @AuthMember Member member, + @Parameter(description = "답변 식별자") + @PathVariable(value = "answerId") Long answerId, + @Parameter(description = "이전 조회의 마지막 index") + @RequestParam(required = false, name = "threshold") Long lastIndex, + @Parameter(description = "조회할 페이지 크기") + @RequestParam(defaultValue = "1000", required = false) Integer pageSize) { + return BaseResponse.onSuccess(answerCommentService.getAnswerCommentInfos(answerId, member.getId(), lastIndex, PageRequest.of(0, pageSize, Sort.by(Sort.Direction.DESC, "createdAt")))); } } diff --git a/src/main/java/com/server/capple/domain/answerComment/dao/AnswerCommentRDBDao.java b/src/main/java/com/server/capple/domain/answerComment/dao/AnswerCommentRDBDao.java new file mode 100644 index 0000000..93db24e --- /dev/null +++ b/src/main/java/com/server/capple/domain/answerComment/dao/AnswerCommentRDBDao.java @@ -0,0 +1,16 @@ +package com.server.capple.domain.answerComment.dao; + +import com.server.capple.domain.answerComment.entity.AnswerComment; +import com.server.capple.domain.member.entity.Member; + +import java.time.LocalDateTime; + +public class AnswerCommentRDBDao { + public interface AnswerCommentInfoInterface { + public AnswerComment getAnswerComment(); + public Member getWriter(); + public String getContent(); + public LocalDateTime getCreatedAt(); + public Boolean getIsLiked(); + } +} diff --git a/src/main/java/com/server/capple/domain/answerComment/dto/AnswerCommentResponse.java b/src/main/java/com/server/capple/domain/answerComment/dto/AnswerCommentResponse.java index f8dba44..7e05260 100644 --- a/src/main/java/com/server/capple/domain/answerComment/dto/AnswerCommentResponse.java +++ b/src/main/java/com/server/capple/domain/answerComment/dto/AnswerCommentResponse.java @@ -1,5 +1,6 @@ package com.server.capple.domain.answerComment.dto; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -16,20 +17,23 @@ public static class AnswerCommentId { @Getter @AllArgsConstructor - public static class AnswerCommentHeart { + public static class AnswerCommentLike { private Long answerCommentId; private Boolean isLiked; } @Getter + @AllArgsConstructor @Builder public static class AnswerCommentInfo { private Long answerCommentId; private Long writerId; private String content; private Integer heartCount; + private Boolean isLiked; + @JsonProperty("isMine") + private Boolean isMine; private LocalDateTime createdAt; - } @Getter diff --git a/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentMapper.java b/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentMapper.java index ae87a58..841ad90 100644 --- a/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentMapper.java +++ b/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentMapper.java @@ -1,6 +1,7 @@ package com.server.capple.domain.answerComment.mapper; import com.server.capple.domain.answer.entity.Answer; +import com.server.capple.domain.answerComment.dao.AnswerCommentRDBDao; import com.server.capple.domain.answerComment.dto.AnswerCommentResponse.*; import com.server.capple.domain.answerComment.entity.AnswerComment; import com.server.capple.domain.member.entity.Member; @@ -17,14 +18,25 @@ public AnswerComment toAnswerCommentEntity(Member member, Answer answer, String .build(); } - public AnswerCommentInfo toAnswerCommentInfo(AnswerComment comment) { +// public AnswerCommentInfo toAnswerCommentInfo(AnswerComment comment) { +// return AnswerCommentInfo.builder() +// .answerCommentId(comment.getId()) +// .writerId(comment.getMember().getId()) +// .content(comment.getContent()) +// .heartCount(comment.getHeartCount()) +// .createdAt(comment.getCreatedAt()) +// .build(); +// } + + public AnswerCommentInfo toAnswerCommentInfo(AnswerCommentRDBDao.AnswerCommentInfoInterface answerCommentInfoDto, Long memberId) { return AnswerCommentInfo.builder() - .answerCommentId(comment.getId()) - .writerId(comment.getMember().getId()) - .content(comment.getContent()) - .heartCount(comment.getHeartCount()) - .createdAt(comment.getCreatedAt()) + .answerCommentId(answerCommentInfoDto.getAnswerComment().getId()) + .writerId(answerCommentInfoDto.getWriter().getId()) + .content(answerCommentInfoDto.getAnswerComment().getContent()) + .heartCount(answerCommentInfoDto.getAnswerComment().getHeartCount()) + .isMine(answerCommentInfoDto.getWriter().getId().equals(memberId)) + .isLiked(answerCommentInfoDto.getIsLiked() == null ? false : answerCommentInfoDto.getIsLiked()) + .createdAt(answerCommentInfoDto.getAnswerComment().getCreatedAt()) .build(); } - } diff --git a/src/main/java/com/server/capple/domain/answerComment/repository/AnswerCommentRepository.java b/src/main/java/com/server/capple/domain/answerComment/repository/AnswerCommentRepository.java index 303ef63..85d4026 100644 --- a/src/main/java/com/server/capple/domain/answerComment/repository/AnswerCommentRepository.java +++ b/src/main/java/com/server/capple/domain/answerComment/repository/AnswerCommentRepository.java @@ -1,7 +1,13 @@ package com.server.capple.domain.answerComment.repository; +import com.server.capple.domain.answerComment.dao.AnswerCommentRDBDao; import com.server.capple.domain.answerComment.dto.AnswerCommentDBResponse.AnswerCommentAuthorNAnswerNQuestionInfo; +import com.server.capple.domain.answerComment.dto.AnswerCommentResponse; import com.server.capple.domain.answerComment.entity.AnswerComment; +import com.server.capple.domain.member.entity.Member; +import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -9,8 +15,24 @@ import java.util.Optional; public interface AnswerCommentRepository extends JpaRepository { - @Query("SELECT a FROM AnswerComment a WHERE a.answer.id = :answerId ORDER BY a.createdAt") - List findAnswerCommentByAnswerId(Long answerId); +// @Query("SELECT ac FROM AnswerComment ac WHERE ac.answer.id = :answerId ORDER BY ac.createdAt") +// Slice findAnswerCommentByAnswerId(@Param("answerId") Long answerId, @Param("member") Member member, @Param("lastIndex") Long lastIndex, Pageable pageable); +@Query("SELECT ac AS answerComment, " + + "ac.member AS writer, " + + "ac.content AS content, " + + "ac.heartCount AS heartCount, " + + "ac.createdAt AS createdAt, " + + "CASE WHEN h.id IS NOT NULL THEN TRUE ELSE FALSE END AS isLiked " + + "FROM AnswerComment ac " + + "LEFT JOIN AnswerCommentHeart h ON h.answerComment = ac AND h.member = :member AND h.isLiked = TRUE " + + "WHERE ac.answer.id = :answerId AND (:lastIndex IS NULL OR ac.id < :lastIndex) " + + "ORDER BY ac.createdAt DESC") +Slice findAnswerCommentByAnswerId( + @Param("answerId") Long answerId, + @Param("member") Member member, + @Param("lastIndex") Long lastIndex, + Pageable pageable); + @Query(""" SELECT ac answerComment, @@ -29,4 +51,10 @@ public interface AnswerCommentRepository extends JpaRepository findAnswerCommentInfo(AnswerComment answerComment); + + @Query("SELECT COUNT(ac) FROM AnswerComment ac WHERE ac.answer.id = :answerId") + Integer getAnswerCommentCountByAnswerId(Long answerId); + + @Query("SELECT COUNT(ac.id) FROM AnswerComment ac WHERE ac.member = :member") + Integer getAnswerCommentCountByMember(Member member); } diff --git a/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentService.java b/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentService.java index 21e5af9..e2c184e 100644 --- a/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentService.java +++ b/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentService.java @@ -4,12 +4,17 @@ import com.server.capple.domain.answerComment.dto.AnswerCommentResponse.*; import com.server.capple.domain.answerComment.entity.AnswerComment; import com.server.capple.domain.member.entity.Member; +import com.server.capple.global.common.SliceResponse; +import org.springframework.data.domain.Pageable; public interface AnswerCommentService { + AnswerComment findAnswerComment(Long answerCommentId); AnswerCommentId createAnswerComment(Member member, Long answerId, AnswerCommentRequest request); AnswerCommentId deleteAnswerComment(Member member, Long commentId); AnswerCommentId updateAnswerComment(Member member, Long commentId, AnswerCommentRequest request); - AnswerCommentHeart heartAnswerComment(Member member, Long commentId); - AnswerCommentInfos getAnswerCommentInfos(Long answerId); + AnswerCommentLike toggleAnswerCommentHeart(Member member, Long commentId); +// SliceResponse getAnswerCommentInfos(Long answerId); + SliceResponse getAnswerCommentInfos(Long answerId, Long memberId, Long lastIndex, Pageable pageable); + } diff --git a/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceImpl.java b/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceImpl.java index 41605ba..73c1f65 100644 --- a/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceImpl.java +++ b/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceImpl.java @@ -1,25 +1,36 @@ package com.server.capple.domain.answerComment.service; +import com.server.capple.domain.answer.dao.AnswerRDBDao; import com.server.capple.domain.answer.entity.Answer; import com.server.capple.domain.answer.service.AnswerConcurrentService; import com.server.capple.domain.answer.service.AnswerService; +import com.server.capple.domain.answer.service.AnswerServiceImpl; +import com.server.capple.domain.answerComment.dao.AnswerCommentRDBDao; import com.server.capple.domain.answerComment.dto.AnswerCommentRequest; import com.server.capple.domain.answerComment.dto.AnswerCommentResponse.*; import com.server.capple.domain.answerComment.entity.AnswerComment; +import com.server.capple.domain.answerComment.entity.AnswerCommentHeart; +import com.server.capple.domain.answerComment.mapper.AnswerCommentHeartMapper; import com.server.capple.domain.answerComment.mapper.AnswerCommentMapper; import com.server.capple.domain.answerComment.repository.AnswerCommentHeartRedisRepository; +import com.server.capple.domain.answerComment.repository.AnswerCommentHeartRepository; import com.server.capple.domain.answerComment.repository.AnswerCommentRepository; import com.server.capple.domain.answerSubscribeMember.service.AnswerSubscribeMemberService; import com.server.capple.domain.member.entity.Member; import com.server.capple.domain.member.service.MemberService; import com.server.capple.domain.notifiaction.service.NotificationService; +import com.server.capple.global.common.SliceResponse; import com.server.capple.global.exception.RestApiException; import com.server.capple.global.exception.errorCode.CommentErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; - -import java.util.List; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; @Service @@ -27,7 +38,7 @@ public class AnswerCommentServiceImpl implements AnswerCommentService{ private final AnswerCommentRepository answerCommentRepository; - private final AnswerCommentHeartRedisRepository answerCommentHeartRedisRepository; + private final AnswerCommentHeartRepository answerCommentHeartRepository; private final AnswerCommentMapper answerCommentMapper; private final MemberService memberService; private final AnswerService answerService; @@ -35,6 +46,8 @@ public class AnswerCommentServiceImpl implements AnswerCommentService{ private final AnswerSubscribeMemberService answerSubscribeMemberService; private final AnswerCommentConcurrentService answerCommentConcurrentService; private final AnswerConcurrentService answerConcurrentService; + private final AnswerCommentCountService answerCommentCountService; + private final AnswerCommentHeartMapper answerCommentHeartMapper; /* 댓글 작성 */ @Override @@ -76,28 +89,47 @@ public AnswerCommentId updateAnswerComment(Member member, Long commentId, Answer return new AnswerCommentId(commentId); } - /* 댓글 좋아요/취소 */ @Override @Transactional - public AnswerCommentHeart heartAnswerComment(Member member, Long commentId) { + public AnswerCommentLike toggleAnswerCommentHeart(Member loginMember, Long commentId) { + Member member = memberService.findMember(loginMember.getId()); AnswerComment answerComment = findAnswerComment(commentId); - Boolean isLiked = answerCommentHeartRedisRepository.toggleAnswerCommentHeart(commentId, member.getId()); - if(isLiked) - notificationService.sendAnswerCommentHeartNotification(answerComment); - if (!answerCommentConcurrentService.setHeartCount(answerComment, isLiked)) { // 댓글 좋아요 heartCount 감소 + + // 좋아요 여부 확인 (없으면 새로 저장) + AnswerCommentHeart answerCommentHeart = answerCommentHeartRepository.findByMemberAndAnswerComment(member, answerComment) + .orElseGet(() -> { + AnswerCommentHeart newHeart = answerCommentHeartMapper.toAnswerCommentHeart(answerComment, member); + return answerCommentHeartRepository.save(newHeart); + }); + + boolean isLiked = answerCommentHeart.toggleHeart(); + + if (!answerCommentConcurrentService.setHeartCount(answerComment, isLiked)) { throw new RestApiException(CommentErrorCode.COMMENT_HEART_CHANGE_FAILED); } - return new AnswerCommentHeart(commentId, isLiked); + + if (isLiked) { + notificationService.sendAnswerCommentHeartNotification(answerComment); + } + + return new AnswerCommentLike(commentId, isLiked); } - /* 답변에 대한 댓글 조회 */ @Override - public AnswerCommentInfos getAnswerCommentInfos(Long answerId) { - List commentInfos = answerCommentRepository.findAnswerCommentByAnswerId(answerId).stream() - .map(answerCommentMapper::toAnswerCommentInfo) - .toList(); + public SliceResponse getAnswerCommentInfos(Long answerId, Long memberId, Long lastIndex, Pageable pageable) { + + Member member = memberService.findMember(memberId); + Slice answerCommentSliceInterfaces = answerCommentRepository.findAnswerCommentByAnswerId(answerId, member, lastIndex, pageable); + + lastIndex = getLastIndexFromAnswerCommentInfoInterface(answerCommentSliceInterfaces); + + return SliceResponse.toSliceResponse(answerCommentSliceInterfaces, answerCommentSliceInterfaces.getContent().stream().map( + answerCommentInfoDto -> answerCommentMapper.toAnswerCommentInfo( + answerCommentInfoDto, + memberId + ) + ).toList(), lastIndex.toString(), answerCommentCountService.getAnswerCommentCount(answerId)); - return new AnswerCommentInfos(commentInfos); } private void checkPermission(Member member, AnswerComment answerComment) { @@ -113,4 +145,23 @@ public AnswerComment findAnswerComment(Long answerCommentId) { () -> new RestApiException(CommentErrorCode.COMMENT_NOT_FOUND) ); } + + private Long getLastIndexFromAnswerCommentInfoInterface(Slice answerCommentInfoSliceInterface) { + if(answerCommentInfoSliceInterface.hasContent()) + return answerCommentInfoSliceInterface.stream().map(AnswerCommentRDBDao.AnswerCommentInfoInterface::getAnswerComment).map(AnswerComment::getId).min(Long::compareTo).get(); + return -1L; + } + + @Getter + @AllArgsConstructor + static class AnswerCommentCountChangedEvent { + private Long answerId; + private Member member; + } + + @TransactionalEventListener(classes = AnswerCommentServiceImpl.AnswerCommentCountChangedEvent.class, phase = TransactionPhase.AFTER_COMPLETION) + public void handleQuestionCreatedEvent(AnswerCommentServiceImpl.AnswerCommentCountChangedEvent event) { + answerCommentCountService.updateAnswerCommentCount(event.getAnswerId()); + answerCommentCountService.expireMembersAnswerCommentedCount(event.getMember()); + } } From 1a685f46271757a31045bf16c8e74b3d7b23b0c2 Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Thu, 29 May 2025 04:23:24 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20#283=20AnswerCommentHeart=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AnswerCommentControllerTest.java | 50 +++++++++++-------- .../AnswerCommentConcurrentServiceTest.java | 4 +- .../service/AnswerCommentServiceTest.java | 41 +++++++++------ .../capple/support/ControllerTestConfig.java | 35 ++++++++++--- 4 files changed, 83 insertions(+), 47 deletions(-) diff --git a/src/test/java/com/server/capple/domain/answerComment/controller/AnswerCommentControllerTest.java b/src/test/java/com/server/capple/domain/answerComment/controller/AnswerCommentControllerTest.java index 30f6d4f..0a1e528 100644 --- a/src/test/java/com/server/capple/domain/answerComment/controller/AnswerCommentControllerTest.java +++ b/src/test/java/com/server/capple/domain/answerComment/controller/AnswerCommentControllerTest.java @@ -3,15 +3,21 @@ import com.server.capple.domain.answerComment.dto.AnswerCommentRequest; import com.server.capple.domain.answerComment.dto.AnswerCommentResponse; import com.server.capple.domain.member.entity.Member; +import com.server.capple.global.common.BaseResponse; +import com.server.capple.global.common.SliceResponse; import com.server.capple.support.ControllerTestConfig; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.ResultActions; +import static org.mockito.BDDMockito.given; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @@ -103,9 +109,9 @@ public void deleteAnswerCommentTest() throws Exception { public void heartAnswerCommentTest() throws Exception { //given final String url = "/answer-comments/heart/{commentId}"; - AnswerCommentResponse.AnswerCommentHeart response = new AnswerCommentResponse.AnswerCommentHeart(1L, Boolean.TRUE); + AnswerCommentResponse.AnswerCommentLike response = new AnswerCommentResponse.AnswerCommentLike(1L, Boolean.TRUE); - doReturn(response).when(answerCommentService).heartAnswerComment(any(Member.class), any(Long.class)); + doReturn(response).when(answerCommentService).toggleAnswerCommentHeart(any(Member.class), any(Long.class)); //when ResultActions resultActions = this.mockMvc.perform(patch(url, 1L) @@ -125,28 +131,30 @@ public void heartAnswerCommentTest() throws Exception { @Test @DisplayName("답변에 대한 댓글 조회 API 테스트") public void getAnswerCommentInfosTest() throws Exception { - //given + // given final String url = "/answer-comments/{answerId}"; - AnswerCommentResponse.AnswerCommentInfos response = getAnswerCommentInfos(); - - doReturn(response).when(answerCommentService).getAnswerCommentInfos(any(Long.class)); - - //when - ResultActions resultActions = this.mockMvc.perform(get(url, 1L) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .header("Authorization", "Bearer " + jwt)); - - //then - resultActions. - andDo(print()) + SliceResponse response = getSliceAnswerCommentInfos(); + given(answerCommentService.getAnswerCommentInfos(any(Long.class), any(Long.class), isNull(), any(Pageable.class))).willReturn(response); + + // when + ResultActions resultActions = mockMvc.perform( + get(url, 1L) + .accept(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + jwt) + ); + + // then + resultActions + .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value("COMMON200")) .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")) - .andExpect(jsonPath("$.result.answerCommentInfos[0].answerCommentId").value(1L)) - .andExpect(jsonPath("$.result.answerCommentInfos[0].writerId").value(1L)) - .andExpect(jsonPath("$.result.answerCommentInfos[0].content").value("댓글 1")) - .andExpect(jsonPath("$.result.answerCommentInfos[0].heartCount").value(3)) - .andExpect(jsonPath("$.result.answerCommentInfos[0].createdAt").value("2022-11-01T12:02:00")); - + .andExpect(jsonPath("$.result.content[0].answerCommentId").value(1L)) + .andExpect(jsonPath("$.result.content[0].writerId").value(1L)) + .andExpect(jsonPath("$.result.content[0].content").value("댓글 1")) + .andExpect(jsonPath("$.result.content[0].heartCount").value(3)) + .andExpect(jsonPath("$.result.content[0].createdAt").value("2022-11-01T12:02:00")) + .andExpect(jsonPath("$.result.content[0].isLiked").value(true)) + .andExpect(jsonPath("$.result.content[0].isMine").value(true)); } } diff --git a/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentConcurrentServiceTest.java b/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentConcurrentServiceTest.java index 2a4610c..cc24148 100644 --- a/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentConcurrentServiceTest.java +++ b/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentConcurrentServiceTest.java @@ -79,7 +79,7 @@ void tearDown() { void answerCommentSetHeartCountTest() { // given // when - answerCommentService.heartAnswerComment(writer, answerComment.getId()); + answerCommentService.toggleAnswerCommentHeart(writer, answerComment.getId()); // then answerComment = answerCommentRepository.findById(answerComment.getId()).get(); @@ -117,7 +117,7 @@ void answerCommentSetHeartCountConcurrentTest() throws InterruptedException { int finalI = i; executorService.submit(() -> { try { - answerCommentService.heartAnswerComment(members.get(finalI), answerComment.getId()); + answerCommentService.toggleAnswerCommentHeart(members.get(finalI), answerComment.getId()); } catch (RestApiException e) { increaseHeartFailedCnt.incrementAndGet(); } finally { diff --git a/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceTest.java b/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceTest.java index 46cdac0..c20f467 100644 --- a/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceTest.java +++ b/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceTest.java @@ -1,21 +1,22 @@ package com.server.capple.domain.answerComment.service; import com.server.capple.domain.answerComment.dto.AnswerCommentRequest; -import com.server.capple.domain.answerComment.dto.AnswerCommentResponse.AnswerCommentHeart; -import com.server.capple.domain.answerComment.dto.AnswerCommentResponse.AnswerCommentInfos; +import com.server.capple.domain.answerComment.dto.AnswerCommentResponse; import com.server.capple.domain.answerComment.entity.AnswerComment; import com.server.capple.domain.notifiaction.service.NotificationService; +import com.server.capple.global.common.SliceResponse; import com.server.capple.support.ServiceTestConfig; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Pageable; +import java.util.List; import java.util.Optional; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -79,10 +80,10 @@ public void deleteAnswerCommentTest() { @Test @DisplayName("답변 댓글 좋아요/취소 토글 테스트") - public void heartAnswerCommentTest() { + public void toggleAnswerCommentHeartTest() { //1. 좋아요 //given - AnswerCommentHeart liked = answerCommentService.heartAnswerComment(member, answerComment.getId()); + AnswerCommentResponse.AnswerCommentLike liked = answerCommentService.toggleAnswerCommentHeart(member, answerComment.getId()); //then assertEquals(answerComment.getId(), liked.getAnswerCommentId()); @@ -91,7 +92,7 @@ public void heartAnswerCommentTest() { //2. 좋아요 취소 //given - AnswerCommentHeart unLiked = answerCommentService.heartAnswerComment(member, answerComment.getId()); + AnswerCommentResponse.AnswerCommentLike unLiked = answerCommentService.toggleAnswerCommentHeart(member, answerComment.getId()); //then assertEquals(answerComment.getId(), unLiked.getAnswerCommentId()); @@ -103,24 +104,32 @@ public void heartAnswerCommentTest() { @DisplayName("답변 댓글 조회 테스트") public void getAnswerCommentsTest() { //when - AnswerCommentInfos response = answerCommentService.getAnswerCommentInfos(answer.getId()); - - //then - assertEquals(member.getId(), response.getAnswerCommentInfos().get(0).getWriterId()); - assertEquals("답변에 대한 댓글이어유", response.getAnswerCommentInfos().get(0).getContent()); - assertEquals(0, response.getAnswerCommentInfos().get(0).getHeartCount()); + SliceResponse response = + answerCommentService.getAnswerCommentInfos(answer.getId(), member.getId(), null, Pageable.ofSize(10)); + + // then + List content = response.getContent(); + assertEquals(member.getId(), content.get(0).getWriterId()); + assertEquals("답변에 대한 댓글이어유", content.get(0).getContent()); + assertEquals(0, content.get(0).getHeartCount()); + assertFalse(content.get(0).getIsLiked()); + assertTrue(content.get(0).getIsMine()); } @Test @DisplayName("좋아한 답변 댓글 조회 테스트") public void getAnswerCommentsWithHeartTest() { //given - AnswerCommentHeart liked = answerCommentService.heartAnswerComment(member, answerComment.getId()); + AnswerCommentResponse.AnswerCommentLike liked = answerCommentService.toggleAnswerCommentHeart(member, answerComment.getId()); //when - AnswerCommentInfos response = answerCommentService.getAnswerCommentInfos(answer.getId()); + SliceResponse response = + answerCommentService.getAnswerCommentInfos(answer.getId(), member.getId(), null, Pageable.ofSize(10)); + //then - assertEquals(1, response.getAnswerCommentInfos().get(0).getHeartCount()); + assertEquals(1, response.getContent().get(0).getHeartCount()); + assertTrue(response.getContent().get(0).getIsLiked()); + assertTrue(response.getContent().get(0).getIsMine()); } } diff --git a/src/test/java/com/server/capple/support/ControllerTestConfig.java b/src/test/java/com/server/capple/support/ControllerTestConfig.java index ae8435b..dd08192 100644 --- a/src/test/java/com/server/capple/support/ControllerTestConfig.java +++ b/src/test/java/com/server/capple/support/ControllerTestConfig.java @@ -164,15 +164,34 @@ protected AnswerCommentRequest getAnswerCommentRequest() { .build(); } - protected AnswerCommentInfos getAnswerCommentInfos() { +// protected AnswerCommentInfos getAnswerCommentInfos() { +// List answerCommentInfos = List.of(AnswerCommentInfo.builder() +// .answerCommentId(1L) +// .writerId(member.getId()) +// .content("댓글 1") +// .createdAt(LocalDateTime.of(2022, 11, 1, 12, 02)) +// .heartCount(3) +// .build()); +// +// return new AnswerCommentInfos(answerCommentInfos); +// } + + protected SliceResponse getSliceAnswerCommentInfos() { List answerCommentInfos = List.of(AnswerCommentInfo.builder() - .answerCommentId(1L) - .writerId(member.getId()) - .content("댓글 1") - .createdAt(LocalDateTime.of(2022, 11, 1, 12, 02)) - .heartCount(3) - .build()); + .answerCommentId(1L) + .writerId(member.getId()) + .content("댓글 1") + .createdAt(LocalDateTime.of(2022, 11, 1, 12, 2)) + .heartCount(3) + .isLiked(true) + .isMine(true) + .build()); - return new AnswerCommentInfos(answerCommentInfos); + return SliceResponse.builder() + .size(10) + .content(answerCommentInfos) + .numberOfElements(1) + .hasNext(FALSE) + .build(); } } From 8fc83896c33d3cbc740a765ffd616e238fa7ed2c Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Thu, 29 May 2025 05:16:45 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20#283=20AnswerCommentHeart=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20Config=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/com/server/capple/support/ServiceTestConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/com/server/capple/support/ServiceTestConfig.java b/src/test/java/com/server/capple/support/ServiceTestConfig.java index ff261c5..557f197 100644 --- a/src/test/java/com/server/capple/support/ServiceTestConfig.java +++ b/src/test/java/com/server/capple/support/ServiceTestConfig.java @@ -87,6 +87,7 @@ void tearDown() { jdbcTemplate.execute("DELETE FROM notification_log"); jdbcTemplate.execute("DELETE FROM answer_subscribe_member"); jdbcTemplate.execute("DELETE FROM answer_comment"); + jdbcTemplate.execute("DELETE FROM answer_comment_heart"); jdbcTemplate.execute("DELETE FROM answer_heart"); jdbcTemplate.execute("DELETE FROM answer"); jdbcTemplate.execute("DELETE FROM board_subscribe_member"); From 452c6817d2b58a45a3178510df0b42da1eced556 Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Fri, 30 May 2025 18:54:59 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20#283=20AnswerCommentHeart=20Concu?= =?UTF-8?q?rrentTest=20=EA=B4=80=EB=A0=A8=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AnswerCommentConcurrentServiceTest.java | 1 + .../java/com/server/capple/support/ConcurrentTestsConfig.java | 3 +++ src/test/java/com/server/capple/support/ServiceTestConfig.java | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentConcurrentServiceTest.java b/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentConcurrentServiceTest.java index cc24148..224a217 100644 --- a/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentConcurrentServiceTest.java +++ b/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentConcurrentServiceTest.java @@ -66,6 +66,7 @@ void setUp() { @AfterEach void tearDown() { + answercommentHeartRepository.deleteAllInBatch(); answerCommentRepository.deleteAllInBatch(); answerRepository.deleteAllInBatch(); questionRepository.deleteAllInBatch(); diff --git a/src/test/java/com/server/capple/support/ConcurrentTestsConfig.java b/src/test/java/com/server/capple/support/ConcurrentTestsConfig.java index 9d11be0..b81b420 100644 --- a/src/test/java/com/server/capple/support/ConcurrentTestsConfig.java +++ b/src/test/java/com/server/capple/support/ConcurrentTestsConfig.java @@ -3,6 +3,7 @@ import com.server.capple.domain.answer.repository.AnswerRepository; import com.server.capple.domain.answer.service.AnswerConcurrentService; import com.server.capple.domain.answer.service.AnswerService; +import com.server.capple.domain.answerComment.repository.AnswerCommentHeartRepository; import com.server.capple.domain.answerComment.repository.AnswerCommentRepository; import com.server.capple.domain.answerComment.service.AnswerCommentConcurrentService; import com.server.capple.domain.answerComment.service.AnswerCommentService; @@ -54,6 +55,8 @@ public abstract class ConcurrentTestsConfig { @Autowired protected AnswerCommentRepository answerCommentRepository; @Autowired + protected AnswerCommentHeartRepository answercommentHeartRepository; + @Autowired protected AnswerCommentConcurrentService answerCommentConcurrentService; @Autowired protected AnswerCommentService answerCommentService; diff --git a/src/test/java/com/server/capple/support/ServiceTestConfig.java b/src/test/java/com/server/capple/support/ServiceTestConfig.java index 557f197..2781144 100644 --- a/src/test/java/com/server/capple/support/ServiceTestConfig.java +++ b/src/test/java/com/server/capple/support/ServiceTestConfig.java @@ -86,8 +86,8 @@ void tearDown() { jdbcTemplate.execute("DELETE FROM notification"); jdbcTemplate.execute("DELETE FROM notification_log"); jdbcTemplate.execute("DELETE FROM answer_subscribe_member"); - jdbcTemplate.execute("DELETE FROM answer_comment"); jdbcTemplate.execute("DELETE FROM answer_comment_heart"); + jdbcTemplate.execute("DELETE FROM answer_comment"); jdbcTemplate.execute("DELETE FROM answer_heart"); jdbcTemplate.execute("DELETE FROM answer"); jdbcTemplate.execute("DELETE FROM board_subscribe_member"); From a3e404a78c75b783d74e3045d75a60dcd3dc9b1e Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Fri, 30 May 2025 19:02:38 +0900 Subject: [PATCH 08/10] =?UTF-8?q?fix:=20#283=20SliceResponse=EB=A5=BC=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85=ED=95=A8=EC=9C=BC=EB=A1=9C=EC=8D=A8=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/answer/repository/AnswerRepository.java | 12 ------------ .../answerComment/mapper/AnswerCommentMapper.java | 10 ---------- .../repository/AnswerCommentRepository.java | 3 +-- .../answerComment/service/AnswerCommentService.java | 1 - .../service/AnswerCommentConcurrentServiceTest.java | 2 +- .../server/capple/support/ConcurrentTestsConfig.java | 2 +- .../server/capple/support/ControllerTestConfig.java | 12 ------------ 7 files changed, 3 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/server/capple/domain/answer/repository/AnswerRepository.java b/src/main/java/com/server/capple/domain/answer/repository/AnswerRepository.java index 954b16d..8496aca 100644 --- a/src/main/java/com/server/capple/domain/answer/repository/AnswerRepository.java +++ b/src/main/java/com/server/capple/domain/answer/repository/AnswerRepository.java @@ -30,18 +30,6 @@ public interface AnswerRepository extends JpaRepository { boolean existsByQuestionAndMember(Question question, Member member); -// @Query("SELECT a AS answer, " + -// "(r IS NOT NULL) AS isReported, " + -// "a.member.id AS writerId, " + -// "a.member.profileImage AS writerProfileImage, " + -// "a.member.nickname AS writerNickname," + -// "a.member.academyGeneration AS writerAcademyGeneration " + -// "FROM Answer a " + -// "LEFT JOIN " + -// "Report r ON r.answer = a " + -// "WHERE (a.id < :lastIndex OR :lastIndex IS NULL) AND a.question.id = :questionId") -// Slice findByQuestion(@Param("questionId") Long questionId, Long lastIndex, Pageable pageable); - @Query("SELECT a AS answer, " + "(r IS NOT NULL) AS isReported, " + "a.member.id AS writerId, " + diff --git a/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentMapper.java b/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentMapper.java index 841ad90..6a31a9b 100644 --- a/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentMapper.java +++ b/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentMapper.java @@ -18,16 +18,6 @@ public AnswerComment toAnswerCommentEntity(Member member, Answer answer, String .build(); } -// public AnswerCommentInfo toAnswerCommentInfo(AnswerComment comment) { -// return AnswerCommentInfo.builder() -// .answerCommentId(comment.getId()) -// .writerId(comment.getMember().getId()) -// .content(comment.getContent()) -// .heartCount(comment.getHeartCount()) -// .createdAt(comment.getCreatedAt()) -// .build(); -// } - public AnswerCommentInfo toAnswerCommentInfo(AnswerCommentRDBDao.AnswerCommentInfoInterface answerCommentInfoDto, Long memberId) { return AnswerCommentInfo.builder() .answerCommentId(answerCommentInfoDto.getAnswerComment().getId()) diff --git a/src/main/java/com/server/capple/domain/answerComment/repository/AnswerCommentRepository.java b/src/main/java/com/server/capple/domain/answerComment/repository/AnswerCommentRepository.java index 85d4026..d53b310 100644 --- a/src/main/java/com/server/capple/domain/answerComment/repository/AnswerCommentRepository.java +++ b/src/main/java/com/server/capple/domain/answerComment/repository/AnswerCommentRepository.java @@ -15,8 +15,7 @@ import java.util.Optional; public interface AnswerCommentRepository extends JpaRepository { -// @Query("SELECT ac FROM AnswerComment ac WHERE ac.answer.id = :answerId ORDER BY ac.createdAt") -// Slice findAnswerCommentByAnswerId(@Param("answerId") Long answerId, @Param("member") Member member, @Param("lastIndex") Long lastIndex, Pageable pageable); + @Query("SELECT ac AS answerComment, " + "ac.member AS writer, " + "ac.content AS content, " + diff --git a/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentService.java b/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentService.java index e2c184e..d87736c 100644 --- a/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentService.java +++ b/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentService.java @@ -14,7 +14,6 @@ public interface AnswerCommentService { AnswerCommentId deleteAnswerComment(Member member, Long commentId); AnswerCommentId updateAnswerComment(Member member, Long commentId, AnswerCommentRequest request); AnswerCommentLike toggleAnswerCommentHeart(Member member, Long commentId); -// SliceResponse getAnswerCommentInfos(Long answerId); SliceResponse getAnswerCommentInfos(Long answerId, Long memberId, Long lastIndex, Pageable pageable); } diff --git a/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentConcurrentServiceTest.java b/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentConcurrentServiceTest.java index 224a217..6c5d3ad 100644 --- a/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentConcurrentServiceTest.java +++ b/src/test/java/com/server/capple/domain/answerComment/service/AnswerCommentConcurrentServiceTest.java @@ -66,7 +66,7 @@ void setUp() { @AfterEach void tearDown() { - answercommentHeartRepository.deleteAllInBatch(); + answerCommentHeartRepository.deleteAllInBatch(); answerCommentRepository.deleteAllInBatch(); answerRepository.deleteAllInBatch(); questionRepository.deleteAllInBatch(); diff --git a/src/test/java/com/server/capple/support/ConcurrentTestsConfig.java b/src/test/java/com/server/capple/support/ConcurrentTestsConfig.java index b81b420..b4e1feb 100644 --- a/src/test/java/com/server/capple/support/ConcurrentTestsConfig.java +++ b/src/test/java/com/server/capple/support/ConcurrentTestsConfig.java @@ -55,7 +55,7 @@ public abstract class ConcurrentTestsConfig { @Autowired protected AnswerCommentRepository answerCommentRepository; @Autowired - protected AnswerCommentHeartRepository answercommentHeartRepository; + protected AnswerCommentHeartRepository answerCommentHeartRepository; @Autowired protected AnswerCommentConcurrentService answerCommentConcurrentService; @Autowired diff --git a/src/test/java/com/server/capple/support/ControllerTestConfig.java b/src/test/java/com/server/capple/support/ControllerTestConfig.java index dd08192..c6d7924 100644 --- a/src/test/java/com/server/capple/support/ControllerTestConfig.java +++ b/src/test/java/com/server/capple/support/ControllerTestConfig.java @@ -164,18 +164,6 @@ protected AnswerCommentRequest getAnswerCommentRequest() { .build(); } -// protected AnswerCommentInfos getAnswerCommentInfos() { -// List answerCommentInfos = List.of(AnswerCommentInfo.builder() -// .answerCommentId(1L) -// .writerId(member.getId()) -// .content("댓글 1") -// .createdAt(LocalDateTime.of(2022, 11, 1, 12, 02)) -// .heartCount(3) -// .build()); -// -// return new AnswerCommentInfos(answerCommentInfos); -// } - protected SliceResponse getSliceAnswerCommentInfos() { List answerCommentInfos = List.of(AnswerCommentInfo.builder() .answerCommentId(1L) From 8f1b301274695aad4a27f38050020e00915e10e4 Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Mon, 2 Jun 2025 15:54:23 +0900 Subject: [PATCH 09/10] =?UTF-8?q?fix:=20#283=20isMine=20JsonProperty=20?= =?UTF-8?q?=EC=A7=80=EC=A0=95=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../capple/domain/answerComment/dto/AnswerCommentResponse.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/server/capple/domain/answerComment/dto/AnswerCommentResponse.java b/src/main/java/com/server/capple/domain/answerComment/dto/AnswerCommentResponse.java index 7e05260..14add56 100644 --- a/src/main/java/com/server/capple/domain/answerComment/dto/AnswerCommentResponse.java +++ b/src/main/java/com/server/capple/domain/answerComment/dto/AnswerCommentResponse.java @@ -31,7 +31,6 @@ public static class AnswerCommentInfo { private String content; private Integer heartCount; private Boolean isLiked; - @JsonProperty("isMine") private Boolean isMine; private LocalDateTime createdAt; } From 64df0a7f3a00bda2b0e77005101b12c25689135c Mon Sep 17 00:00:00 2001 From: KyungsooLee Date: Tue, 3 Jun 2025 00:25:04 +0900 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20#283=20=EC=BA=90=EC=8B=B1=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=A6=AC=EC=8A=A4=EB=84=88=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../answerComment/service/AnswerCommentCountService.java | 6 ------ .../answerComment/service/AnswerCommentServiceImpl.java | 8 +++++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentCountService.java b/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentCountService.java index a6ae88c..07fcbec 100644 --- a/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentCountService.java +++ b/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentCountService.java @@ -27,10 +27,4 @@ public Integer getAnswerCommentCount(Long answerId) { public CompletableFuture updateAnswerCommentCount(Long answerId) { return CompletableFuture.completedFuture(answerCommentRepository.getAnswerCommentCountByAnswerId(answerId)); } - - @Cacheable(value = "answerCommentCountByMember", key = "#member.id", cacheManager = "oneDayExpireCacheManager") - public Integer getAnswerCommentCountByMember(Member member) { return answerCommentRepository.getAnswerCommentCountByMember(member);} - - @CacheEvict(value = "answerCommentCountByMember", key = "#member.id", cacheManager = "oneDayExpireCacheManager") - public void expireMembersAnswerCommentedCount(Member member) {} } diff --git a/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceImpl.java b/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceImpl.java index 73c1f65..c7cc26b 100644 --- a/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceImpl.java +++ b/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentServiceImpl.java @@ -25,6 +25,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; @@ -48,6 +49,7 @@ public class AnswerCommentServiceImpl implements AnswerCommentService{ private final AnswerConcurrentService answerConcurrentService; private final AnswerCommentCountService answerCommentCountService; private final AnswerCommentHeartMapper answerCommentHeartMapper; + private final ApplicationEventPublisher applicationEventPublisher; /* 댓글 작성 */ @Override @@ -56,6 +58,7 @@ public AnswerCommentId createAnswerComment(Member member, Long answerId, AnswerC Member loginMember = memberService.findMember(member.getId()); Answer answer = answerService.findAnswer(answerId); AnswerComment answerComment = answerCommentRepository.save(answerCommentMapper.toAnswerCommentEntity(loginMember, answer, request.getAnswerComment())); + applicationEventPublisher.publishEvent(new AnswerCommentCountChangedEvent(answerId, loginMember)); notificationService.sendAnswerCommentNotification(answer, answerComment); if (!answerConcurrentService.increaseCommentCount(answer)) { // 답변 commentCount 증가 throw new RestApiException(CommentErrorCode.COMMENT_COUNT_INCREASE_FAILED); @@ -69,7 +72,7 @@ public AnswerCommentId createAnswerComment(Member member, Long answerId, AnswerC public AnswerCommentId deleteAnswerComment(Member member, Long commentId) { AnswerComment answerComment = findAnswerComment(commentId); checkPermission(member, answerComment); // 유저 권한 체크 - + applicationEventPublisher.publishEvent(new AnswerCommentCountChangedEvent(answerComment.getAnswer().getId(), member)); answerSubscribeMemberService.deleteAnswerSubscribeMemberByAnswerId(answerComment.getAnswer().getId()); if (!answerConcurrentService.decreaseCommentCount(answerComment.getAnswer())) { // 답변 commentCount 감소 throw new RestApiException(CommentErrorCode.COMMENT_COUNT_DECREASE_FAILED); @@ -160,8 +163,7 @@ static class AnswerCommentCountChangedEvent { } @TransactionalEventListener(classes = AnswerCommentServiceImpl.AnswerCommentCountChangedEvent.class, phase = TransactionPhase.AFTER_COMPLETION) - public void handleQuestionCreatedEvent(AnswerCommentServiceImpl.AnswerCommentCountChangedEvent event) { + public void handleAnswerCommentCountChangedEvent(AnswerCommentServiceImpl.AnswerCommentCountChangedEvent event) { answerCommentCountService.updateAnswerCommentCount(event.getAnswerId()); - answerCommentCountService.expireMembersAnswerCommentedCount(event.getMember()); } }