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 ec4e9681..8496aca2 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 @@ -31,16 +31,17 @@ 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); + "(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 88799982..74dd16ec 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 @@ -105,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) { @@ -133,7 +133,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 = memberService.findMember(memberId); + Slice answerInfoSliceInterface = answerRepository.findByQuestion(questionId, lastIndex, member, pageable); lastIndex = getLastIndexFromAnswerInfoInterface(answerInfoSliceInterface); return SliceResponse.toSliceResponse(answerInfoSliceInterface, answerInfoSliceInterface.getContent().stream().map( answerInfoDto -> answerMapper.toAnswerInfo( 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 62da47fc..cee2c73d 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 00000000..93db24e4 --- /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 f8dba44b..14add56c 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,22 @@ 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; + private Boolean isMine; private LocalDateTime createdAt; - } @Getter 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 00000000..732c1995 --- /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; + } +} 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 00000000..301b0424 --- /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/mapper/AnswerCommentMapper.java b/src/main/java/com/server/capple/domain/answerComment/mapper/AnswerCommentMapper.java index ae87a58b..6a31a9b2 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,15 @@ public AnswerComment toAnswerCommentEntity(Member member, Answer answer, String .build(); } - public AnswerCommentInfo toAnswerCommentInfo(AnswerComment comment) { + 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/AnswerCommentHeartRepository.java b/src/main/java/com/server/capple/domain/answerComment/repository/AnswerCommentHeartRepository.java new file mode 100644 index 00000000..c573fdb7 --- /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/repository/AnswerCommentRepository.java b/src/main/java/com/server/capple/domain/answerComment/repository/AnswerCommentRepository.java index 303ef634..d53b3105 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,23 @@ 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 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 +50,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/AnswerCommentCountService.java b/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentCountService.java new file mode 100644 index 00000000..07fcbecd --- /dev/null +++ b/src/main/java/com/server/capple/domain/answerComment/service/AnswerCommentCountService.java @@ -0,0 +1,30 @@ +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)); + } +} 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 21e5af98..d87736cd 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,16 @@ 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, 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 41605ba6..c7cc26b4 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,37 @@ 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.context.ApplicationEventPublisher; +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 +39,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 +47,9 @@ 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; + private final ApplicationEventPublisher applicationEventPublisher; /* 댓글 작성 */ @Override @@ -43,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); @@ -56,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); @@ -76,28 +92,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 +148,22 @@ 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 handleAnswerCommentCountChangedEvent(AnswerCommentServiceImpl.AnswerCommentCountChangedEvent event) { + answerCommentCountService.updateAnswerCommentCount(event.getAnswerId()); + } } 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 30f6d4f7..0a1e528e 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 2a4610c9..6c5d3ad1 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(); @@ -79,7 +80,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 +118,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 46cdac0f..c20f467c 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/ConcurrentTestsConfig.java b/src/test/java/com/server/capple/support/ConcurrentTestsConfig.java index 9d11be04..b4e1feb8 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/ControllerTestConfig.java b/src/test/java/com/server/capple/support/ControllerTestConfig.java index ae8435b7..c6d7924f 100644 --- a/src/test/java/com/server/capple/support/ControllerTestConfig.java +++ b/src/test/java/com/server/capple/support/ControllerTestConfig.java @@ -164,15 +164,22 @@ protected AnswerCommentRequest getAnswerCommentRequest() { .build(); } - protected AnswerCommentInfos getAnswerCommentInfos() { + 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(); } } diff --git a/src/test/java/com/server/capple/support/ServiceTestConfig.java b/src/test/java/com/server/capple/support/ServiceTestConfig.java index ff261c55..2781144b 100644 --- a/src/test/java/com/server/capple/support/ServiceTestConfig.java +++ b/src/test/java/com/server/capple/support/ServiceTestConfig.java @@ -86,6 +86,7 @@ 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_heart"); jdbcTemplate.execute("DELETE FROM answer_comment"); jdbcTemplate.execute("DELETE FROM answer_heart"); jdbcTemplate.execute("DELETE FROM answer");