diff --git a/src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java b/src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java index 806de6c8..f6176df9 100644 --- a/src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java +++ b/src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java @@ -31,7 +31,7 @@ public class AnswerService { private final CreditHistoryService creditHistoryService; private static void validateIfQuestioner(Member member, QuestionPost questionPost) { - if (!questionPost.isQuestioner(member)) { + if (!questionPost.isQuestioner(member.getId())) { throw new ValidationException(QuestionPostErrorCode.NOT_AUTHORIZED); } } @@ -43,7 +43,8 @@ public AnswerDetailResponse registerAnswer( Member member ) { QuestionPost questionPost = findQuestionPostById(questionPostId); - Answer answer = AnswerMapper.toAnswer(questionPostId, questionPost.isQuestioner(member), request, member); + Answer answer = AnswerMapper.toAnswer(questionPostId, questionPost.isQuestioner(member.getId()), request, + member); return AnswerMapper.toAnswerDetailResponse(answerRepository.save(answer)); } diff --git a/src/main/java/com/dnd/gongmuin/member/controller/MemberController.java b/src/main/java/com/dnd/gongmuin/member/controller/MemberController.java index 21ba4c43..969eef3b 100644 --- a/src/main/java/com/dnd/gongmuin/member/controller/MemberController.java +++ b/src/main/java/com/dnd/gongmuin/member/controller/MemberController.java @@ -1,5 +1,6 @@ package com.dnd.gongmuin.member.controller; +import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; @@ -8,9 +9,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.dnd.gongmuin.common.dto.PageResponse; import com.dnd.gongmuin.member.domain.Member; import com.dnd.gongmuin.member.dto.request.UpdateMemberProfileRequest; +import com.dnd.gongmuin.member.dto.response.AnsweredQuestionPostsByMemberResponse; import com.dnd.gongmuin.member.dto.response.MemberProfileResponse; +import com.dnd.gongmuin.member.dto.response.QuestionPostsByMemberResponse; import com.dnd.gongmuin.member.service.MemberService; import io.swagger.v3.oas.annotations.Operation; @@ -31,6 +35,7 @@ public class MemberController { @GetMapping("/profile") public ResponseEntity getMemberProfile(@AuthenticationPrincipal Member member) { MemberProfileResponse response = memberService.getMemberProfile(member); + return ResponseEntity.ok(response); } @@ -41,6 +46,31 @@ public ResponseEntity updateMemberProfile( @RequestBody UpdateMemberProfileRequest request, @AuthenticationPrincipal Member member) { MemberProfileResponse response = memberService.updateMemberProfile(request, member); + + return ResponseEntity.ok(response); + } + + @Operation(summary = "작성한 질문 전체 조회 API", description = "작성한 질문을 전체 조회한다.") + @ApiResponse(useReturnTypeSchema = true) + @GetMapping("/question-posts") + public ResponseEntity> getQuestionPostsByMember( + @AuthenticationPrincipal Member member, + Pageable pageable) { + PageResponse response = + memberService.getQuestionPostsByMember(member, pageable); + + return ResponseEntity.ok(response); + } + + @Operation(summary = "댓글 단 질문 전체 조회 API", description = "댓글 단 질문을 전체 조회한다.") + @ApiResponse(useReturnTypeSchema = true) + @GetMapping("/answer-posts") + public ResponseEntity> getAnsweredQuestionPostsByMember( + @AuthenticationPrincipal Member member, + Pageable pageable) { + PageResponse response = + memberService.getAnsweredQuestionPostsByMember(member, pageable); + return ResponseEntity.ok(response); } diff --git a/src/main/java/com/dnd/gongmuin/member/dto/response/AnsweredQuestionPostsByMemberResponse.java b/src/main/java/com/dnd/gongmuin/member/dto/response/AnsweredQuestionPostsByMemberResponse.java new file mode 100644 index 00000000..49669d31 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/member/dto/response/AnsweredQuestionPostsByMemberResponse.java @@ -0,0 +1,48 @@ +package com.dnd.gongmuin.member.dto.response; + +import com.dnd.gongmuin.answer.domain.Answer; +import com.dnd.gongmuin.post_interaction.domain.InteractionCount; +import com.dnd.gongmuin.question_post.domain.QuestionPost; +import com.querydsl.core.annotations.QueryProjection; + +public record AnsweredQuestionPostsByMemberResponse( + Long questionPostId, + String title, + String content, + String jobGroup, + int reward, + String questionPostUpdatedAt, + boolean isChosen, + int savedTotalCount, + int recommendTotalCount, + Long answerId, + String answerContent, + String answerUpdatedAt +) { + + @QueryProjection + public AnsweredQuestionPostsByMemberResponse( + QuestionPost questionPost, + InteractionCount savedCount, + InteractionCount recommendCount, + Answer answer) { + this( + questionPost.getId(), + questionPost.getTitle(), + questionPost.getContent(), + questionPost.getJobGroup().getLabel(), + questionPost.getReward(), + questionPost.getUpdatedAt().toString(), + questionPost.getIsChosen(), + extractTotalCount(savedCount), + extractTotalCount(recommendCount), + answer.getId(), + answer.getContent(), + answer.getUpdatedAt().toString() + ); + } + + private static int extractTotalCount(InteractionCount interactionCount) { + return interactionCount != null ? interactionCount.getCount() : 0; + } +} diff --git a/src/main/java/com/dnd/gongmuin/member/dto/response/QuestionPostsByMemberResponse.java b/src/main/java/com/dnd/gongmuin/member/dto/response/QuestionPostsByMemberResponse.java new file mode 100644 index 00000000..ea4a0ee1 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/member/dto/response/QuestionPostsByMemberResponse.java @@ -0,0 +1,42 @@ +package com.dnd.gongmuin.member.dto.response; + +import com.dnd.gongmuin.post_interaction.domain.InteractionCount; +import com.dnd.gongmuin.question_post.domain.QuestionPost; +import com.querydsl.core.annotations.QueryProjection; + +public record QuestionPostsByMemberResponse( + Long questionPostId, + String title, + String content, + String jobGroup, + int reward, + String updatedAt, + boolean isChosen, + int savedTotalCount, + int recommendTotalCount +) { + + @QueryProjection + public QuestionPostsByMemberResponse( + QuestionPost questionPost, + InteractionCount savedCount, + InteractionCount recommendCount + ) { + this( + questionPost.getId(), + questionPost.getTitle(), + questionPost.getContent(), + questionPost.getJobGroup().getLabel(), + questionPost.getReward(), + questionPost.getUpdatedAt().toString(), + questionPost.getIsChosen(), + extractTotalCount(savedCount), + extractTotalCount(recommendCount) + ); + } + + private static int extractTotalCount(InteractionCount interactionCount) { + return interactionCount != null ? interactionCount.getCount() : 0; + } + +} diff --git a/src/main/java/com/dnd/gongmuin/member/exception/MemberErrorCode.java b/src/main/java/com/dnd/gongmuin/member/exception/MemberErrorCode.java index 8c5fa7fd..fa1133df 100644 --- a/src/main/java/com/dnd/gongmuin/member/exception/MemberErrorCode.java +++ b/src/main/java/com/dnd/gongmuin/member/exception/MemberErrorCode.java @@ -13,7 +13,9 @@ public enum MemberErrorCode implements ErrorCode { NOT_FOUND_NEW_MEMBER("신규 회원이 아닙니다.", "MEMBER_002"), LOGOUT_FAILED("로그아웃을 실패했습니다.", "MEMBER_003"), NOT_ENOUGH_CREDIT("보유한 크레딧이 부족합니다.", "MEMBER_004"), - UPDATE_PROFILE_FAILED("프로필 수정에 실패했습니다.", "MEMBER_005"); + UPDATE_PROFILE_FAILED("프로필 수정에 실패했습니다.", "MEMBER_005"), + QUESTION_POSTS_BY_MEMBER_FAILED("작성한 게시글 목록을 찾는 도중 실패했습니다.", "MEMBER_006"), + ANSWERED_QUESTION_POSTS_BY_MEMBER_FAILED("댓글 단 게시글 목록을 찾는 도중 실패했습니다.", "MEMBER_007"); private final String message; private final String code; diff --git a/src/main/java/com/dnd/gongmuin/member/repository/MemberCustom.java b/src/main/java/com/dnd/gongmuin/member/repository/MemberCustom.java new file mode 100644 index 00000000..70a47e30 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/member/repository/MemberCustom.java @@ -0,0 +1,14 @@ +package com.dnd.gongmuin.member.repository; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.member.dto.response.AnsweredQuestionPostsByMemberResponse; +import com.dnd.gongmuin.member.dto.response.QuestionPostsByMemberResponse; + +public interface MemberCustom { + Slice getQuestionPostsByMember(Member member, Pageable pageable); + + Slice getAnsweredQuestionPostsByMember(Member member, Pageable pageable); +} diff --git a/src/main/java/com/dnd/gongmuin/member/repository/MemberCustomImpl.java b/src/main/java/com/dnd/gongmuin/member/repository/MemberCustomImpl.java new file mode 100644 index 00000000..020ece4b --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/member/repository/MemberCustomImpl.java @@ -0,0 +1,101 @@ +package com.dnd.gongmuin.member.repository; + +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; + +import com.dnd.gongmuin.answer.domain.QAnswer; +import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.member.dto.response.AnsweredQuestionPostsByMemberResponse; +import com.dnd.gongmuin.member.dto.response.QAnsweredQuestionPostsByMemberResponse; +import com.dnd.gongmuin.member.dto.response.QQuestionPostsByMemberResponse; +import com.dnd.gongmuin.member.dto.response.QuestionPostsByMemberResponse; +import com.dnd.gongmuin.post_interaction.domain.InteractionType; +import com.dnd.gongmuin.post_interaction.domain.QInteractionCount; +import com.dnd.gongmuin.question_post.domain.QQuestionPost; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class MemberCustomImpl implements MemberCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Slice getQuestionPostsByMember(Member member, Pageable pageable) { + QQuestionPost qp = QQuestionPost.questionPost; + QInteractionCount saved = new QInteractionCount("SAVED"); + QInteractionCount recommend = new QInteractionCount("RECOMMEND"); + + List content = queryFactory + .select(new QQuestionPostsByMemberResponse(qp, saved, recommend)) + .from(qp) + .leftJoin(saved) + .on(qp.id.eq(saved.questionPostId).and(saved.type.eq(InteractionType.SAVED))) + .leftJoin(recommend) + .on(qp.id.eq(recommend.questionPostId).and(recommend.type.eq(InteractionType.RECOMMEND))) + .where(qp.member.eq(member)) + .orderBy(qp.updatedAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1L) + .fetch(); + + boolean hasNext = hasNext(pageable.getPageSize(), content); + + return new SliceImpl<>(content, pageable, hasNext); + } + + @Override + public Slice getAnsweredQuestionPostsByMember( + Member member, Pageable pageable) { + QQuestionPost qp = QQuestionPost.questionPost; + QInteractionCount saved = new QInteractionCount("SAVED"); + QInteractionCount recommend = new QInteractionCount("RECOMMEND"); + QAnswer aw1 = new QAnswer("answer1"); + QAnswer aw2 = new QAnswer("answer2"); + + List content = + queryFactory + .select(new QAnsweredQuestionPostsByMemberResponse(qp, saved, recommend, aw1)) + .from(qp) + .join(aw1) + .on(aw1.id.eq( + JPAExpressions + .select(aw2.id) + .from(aw2) + .where(aw2.questionPostId.eq(qp.id) + .and(aw2.member.eq(member)) + .and(aw2.updatedAt.eq( + JPAExpressions + .select(aw2.updatedAt.max()) + .from(aw2) + .where(aw2.questionPostId.eq(qp.id) + .and(aw2.member.eq(member))) + ))) + )) + .leftJoin(saved) + .on(qp.id.eq(saved.questionPostId).and(saved.type.eq(InteractionType.SAVED))) + .leftJoin(recommend) + .on(qp.id.eq(recommend.questionPostId).and(recommend.type.eq(InteractionType.RECOMMEND))) + .orderBy(qp.updatedAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1L) + .fetch(); + + boolean hasNext = hasNext(pageable.getPageSize(), content); + + return new SliceImpl<>(content, pageable, hasNext); + } + + private boolean hasNext(int pageSize, List content) { + if (content.size() <= pageSize) { + return false; + } + content.remove(pageSize); + return true; + } +} diff --git a/src/main/java/com/dnd/gongmuin/member/repository/MemberRepository.java b/src/main/java/com/dnd/gongmuin/member/repository/MemberRepository.java index 52d72a7d..2179dd47 100644 --- a/src/main/java/com/dnd/gongmuin/member/repository/MemberRepository.java +++ b/src/main/java/com/dnd/gongmuin/member/repository/MemberRepository.java @@ -6,7 +6,7 @@ import com.dnd.gongmuin.member.domain.Member; -public interface MemberRepository extends JpaRepository { +public interface MemberRepository extends JpaRepository, MemberCustom { Optional findBySocialEmail(String socialEmail); boolean existsByNickname(String nickname); diff --git a/src/main/java/com/dnd/gongmuin/member/service/MemberService.java b/src/main/java/com/dnd/gongmuin/member/service/MemberService.java index 504da573..350cff0f 100644 --- a/src/main/java/com/dnd/gongmuin/member/service/MemberService.java +++ b/src/main/java/com/dnd/gongmuin/member/service/MemberService.java @@ -4,12 +4,16 @@ import java.util.Date; import java.util.Objects; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.dnd.gongmuin.auth.domain.Provider; import com.dnd.gongmuin.auth.exception.AuthErrorCode; +import com.dnd.gongmuin.common.dto.PageMapper; +import com.dnd.gongmuin.common.dto.PageResponse; import com.dnd.gongmuin.common.exception.runtime.NotFoundException; import com.dnd.gongmuin.common.exception.runtime.ValidationException; import com.dnd.gongmuin.member.domain.JobCategory; @@ -21,8 +25,10 @@ import com.dnd.gongmuin.member.dto.request.ReissueRequest; import com.dnd.gongmuin.member.dto.request.UpdateMemberProfileRequest; import com.dnd.gongmuin.member.dto.request.ValidateNickNameRequest; +import com.dnd.gongmuin.member.dto.response.AnsweredQuestionPostsByMemberResponse; import com.dnd.gongmuin.member.dto.response.LogoutResponse; import com.dnd.gongmuin.member.dto.response.MemberProfileResponse; +import com.dnd.gongmuin.member.dto.response.QuestionPostsByMemberResponse; import com.dnd.gongmuin.member.dto.response.ReissueResponse; import com.dnd.gongmuin.member.dto.response.SignUpResponse; import com.dnd.gongmuin.member.dto.response.ValidateNickNameResponse; @@ -189,4 +195,28 @@ public MemberProfileResponse updateMemberProfile(UpdateMemberProfileRequest requ throw new ValidationException(MemberErrorCode.UPDATE_PROFILE_FAILED); } } + + public PageResponse getQuestionPostsByMember( + Member member, Pageable pageable) { + try { + Slice responsePage = + memberRepository.getQuestionPostsByMember(member, pageable); + + return PageMapper.toPageResponse(responsePage); + } catch (Exception e) { + throw new NotFoundException(MemberErrorCode.QUESTION_POSTS_BY_MEMBER_FAILED); + } + } + + public PageResponse getAnsweredQuestionPostsByMember( + Member member, Pageable pageable) { + try { + Slice responsePage = + memberRepository.getAnsweredQuestionPostsByMember(member, pageable); + + return PageMapper.toPageResponse(responsePage); + } catch (Exception e) { + throw new NotFoundException(MemberErrorCode.ANSWERED_QUESTION_POSTS_BY_MEMBER_FAILED); + } + } } \ No newline at end of file diff --git a/src/main/java/com/dnd/gongmuin/post_interaction/controller/InteractionController.java b/src/main/java/com/dnd/gongmuin/post_interaction/controller/InteractionController.java new file mode 100644 index 00000000..0eed1dad --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/post_interaction/controller/InteractionController.java @@ -0,0 +1,52 @@ +package com.dnd.gongmuin.post_interaction.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.post_interaction.domain.InteractionType; +import com.dnd.gongmuin.post_interaction.dto.InteractionResponse; +import com.dnd.gongmuin.post_interaction.service.InteractionService; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/question-posts") +public class InteractionController { + + private final InteractionService interactionService; + + @PostMapping("/{questionPostId}/activated") + public ResponseEntity activateInteraction( + @PathVariable Long questionPostId, + @RequestParam String type, + @AuthenticationPrincipal Member member + ) { + InteractionResponse response = interactionService.activateInteraction( + questionPostId, + member.getId(), + InteractionType.from(type) + ); + return ResponseEntity.ok(response); + } + + @PostMapping("/{questionPostId}/inactivated") + public ResponseEntity inactivateInteraction( + @PathVariable("questionPostId") Long questionPostId, + @RequestParam("type") String type, + @AuthenticationPrincipal Member member + ) { + InteractionResponse response = interactionService.inactivateInteraction( + questionPostId, + member.getId(), + InteractionType.from(type) + ); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/dnd/gongmuin/post_interaction/domain/Interaction.java b/src/main/java/com/dnd/gongmuin/post_interaction/domain/Interaction.java new file mode 100644 index 00000000..2a8cda6e --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/post_interaction/domain/Interaction.java @@ -0,0 +1,65 @@ +package com.dnd.gongmuin.post_interaction.domain; + +import com.dnd.gongmuin.common.entity.TimeBaseEntity; +import com.dnd.gongmuin.common.exception.runtime.ValidationException; +import com.dnd.gongmuin.post_interaction.exception.InteractionErrorCode; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Interaction extends TimeBaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "interaction_id", nullable = false) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(name = "type", nullable = false) + private InteractionType type; + + @Column(name = "question_post_id") + private Long questionPostId; + + @Column(name = "is_interacted", nullable = false) + private Boolean isInteracted; + + @Column(name = "member_id") + private Long memberId; + + private Interaction(InteractionType type, Long memberId, Long questionPostId) { + this.isInteracted = true; + this.type = type; + this.memberId = memberId; + this.questionPostId = questionPostId; + } + + public static Interaction of(InteractionType type, Long memberId, Long questionPostId) { + return new Interaction(type, memberId, questionPostId); + } + + public void updateIsInteractedTrue() { + if (Boolean.TRUE.equals(isInteracted)) { + throw new ValidationException(InteractionErrorCode.ALREADY_INTERACTED); + } + isInteracted = true; + } + + public void updateIsInteractedFalse() { + if (Boolean.FALSE.equals(isInteracted)) { + throw new ValidationException(InteractionErrorCode.ALREADY_UNINTERACTED); + } + isInteracted = false; + } +} diff --git a/src/main/java/com/dnd/gongmuin/post_interaction/domain/PostInteractionCount.java b/src/main/java/com/dnd/gongmuin/post_interaction/domain/InteractionCount.java similarity index 62% rename from src/main/java/com/dnd/gongmuin/post_interaction/domain/PostInteractionCount.java rename to src/main/java/com/dnd/gongmuin/post_interaction/domain/InteractionCount.java index fc3be5ab..241a979d 100644 --- a/src/main/java/com/dnd/gongmuin/post_interaction/domain/PostInteractionCount.java +++ b/src/main/java/com/dnd/gongmuin/post_interaction/domain/InteractionCount.java @@ -16,33 +16,38 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class PostInteractionCount extends TimeBaseEntity { +public class InteractionCount extends TimeBaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "post_interaction_count_id", nullable = false) + @Column(name = "interaction_count_id", nullable = false) private Long id; - @Column(name = "total_count", nullable = false) - private int totalCount; + @Column(name = "question_post_id") + private Long questionPostId; + + @Column(name = "count", nullable = false) + private int count; @Enumerated(EnumType.STRING) @Column(name = "type") private InteractionType type; - @Column(name = "question_post_id") - private Long questionPostId; - - private PostInteractionCount(InteractionType type, Long questionPostId) { + private InteractionCount(InteractionType type, Long questionPostId) { + this.count = 1; this.type = type; this.questionPostId = questionPostId; } - public static PostInteractionCount of(InteractionType type, Long questionPostId) { - return new PostInteractionCount(type, questionPostId); + public static InteractionCount of(InteractionType type, Long questionPostId) { + return new InteractionCount(type, questionPostId); + } + + public int increaseCount() { + return ++count; } - private void increaseTotalCount() { - totalCount++; + public int decreaseCount() { + return --count; } } diff --git a/src/main/java/com/dnd/gongmuin/post_interaction/domain/InteractionType.java b/src/main/java/com/dnd/gongmuin/post_interaction/domain/InteractionType.java index f24cf10e..1c9fb509 100644 --- a/src/main/java/com/dnd/gongmuin/post_interaction/domain/InteractionType.java +++ b/src/main/java/com/dnd/gongmuin/post_interaction/domain/InteractionType.java @@ -9,12 +9,12 @@ @RequiredArgsConstructor public enum InteractionType { - SAVED("저장"), + SAVED("북마크"), RECOMMEND("추천"); private final String label; - public static InteractionType of(String input) { + public static InteractionType from(String input) { return Arrays.stream(values()) .filter(type -> type.isEqual(input)) .findAny() diff --git a/src/main/java/com/dnd/gongmuin/post_interaction/domain/PostInteraction.java b/src/main/java/com/dnd/gongmuin/post_interaction/domain/PostInteraction.java deleted file mode 100644 index ac2323f1..00000000 --- a/src/main/java/com/dnd/gongmuin/post_interaction/domain/PostInteraction.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.dnd.gongmuin.post_interaction.domain; - -import static jakarta.persistence.ConstraintMode.*; -import static jakarta.persistence.FetchType.*; - -import com.dnd.gongmuin.common.entity.TimeBaseEntity; -import com.dnd.gongmuin.member.domain.Member; -import com.dnd.gongmuin.question_post.domain.QuestionPost; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.ForeignKey; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class PostInteraction extends TimeBaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "post_interaction_id", nullable = false) - private Long id; - - @Enumerated(EnumType.STRING) - @Column(name = "type", nullable = false) - private InteractionType type; - - @Column(name = "is_interacted", nullable = false) - private Boolean isInteracted; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "member_id", - nullable = false, - foreignKey = @ForeignKey(NO_CONSTRAINT)) - private Member member; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "question_post_id", - nullable = false, - foreignKey = @ForeignKey(NO_CONSTRAINT)) - private QuestionPost questionPost; - - @Builder - public PostInteraction(InteractionType type, Member member, QuestionPost questionPost) { - this.isInteracted = true; - this.type = type; - this.member = member; - this.questionPost = questionPost; - } -} diff --git a/src/main/java/com/dnd/gongmuin/post_interaction/dto/InteractionMapper.java b/src/main/java/com/dnd/gongmuin/post_interaction/dto/InteractionMapper.java new file mode 100644 index 00000000..c38f2e30 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/post_interaction/dto/InteractionMapper.java @@ -0,0 +1,33 @@ +package com.dnd.gongmuin.post_interaction.dto; + +import com.dnd.gongmuin.post_interaction.domain.Interaction; +import com.dnd.gongmuin.post_interaction.domain.InteractionCount; +import com.dnd.gongmuin.post_interaction.domain.InteractionType; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class InteractionMapper { + + public static Interaction toPostInteraction(Long questionPostId, Long memberId, InteractionType type) { + return Interaction.of( + type, + memberId, + questionPostId + ); + } + + public static InteractionCount toPostInteractionCount(Long questionPostId, InteractionType type) { + return InteractionCount.of( + type, + questionPostId + ); + } + + public static InteractionResponse toPostInteractionResponse(int count, InteractionType type) { + return new InteractionResponse( + count, type.getLabel() + ); + } +} diff --git a/src/main/java/com/dnd/gongmuin/post_interaction/dto/InteractionResponse.java b/src/main/java/com/dnd/gongmuin/post_interaction/dto/InteractionResponse.java new file mode 100644 index 00000000..3d5f9f8e --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/post_interaction/dto/InteractionResponse.java @@ -0,0 +1,7 @@ +package com.dnd.gongmuin.post_interaction.dto; + +public record InteractionResponse( + int count, + String interactionType +) { +} \ No newline at end of file diff --git a/src/main/java/com/dnd/gongmuin/post_interaction/exception/InteractionErrorCode.java b/src/main/java/com/dnd/gongmuin/post_interaction/exception/InteractionErrorCode.java new file mode 100644 index 00000000..0248f200 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/post_interaction/exception/InteractionErrorCode.java @@ -0,0 +1,19 @@ +package com.dnd.gongmuin.post_interaction.exception; + +import com.dnd.gongmuin.common.exception.ErrorCode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum InteractionErrorCode implements ErrorCode { + + NOT_FOUND_POST_INTERACTION("상호작용 이력이 존재하지 않습니다.", "PI_001"), + ALREADY_INTERACTED("이미 상호작용한 게시글입니다.", "PI_002"), + ALREADY_UNINTERACTED("이미 상호작용 취소한 게시글입니다.", "PI_003"), + INTERACTION_NOT_ALLOWED("본인 게시물은 상호작용할 수 없습니다", "PI_004"); + + private final String message; + private final String code; +} diff --git a/src/main/java/com/dnd/gongmuin/post_interaction/repository/InteractionCountRepository.java b/src/main/java/com/dnd/gongmuin/post_interaction/repository/InteractionCountRepository.java new file mode 100644 index 00000000..7da929f2 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/post_interaction/repository/InteractionCountRepository.java @@ -0,0 +1,15 @@ +package com.dnd.gongmuin.post_interaction.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.dnd.gongmuin.post_interaction.domain.InteractionCount; +import com.dnd.gongmuin.post_interaction.domain.InteractionType; + +public interface InteractionCountRepository extends JpaRepository { + Optional findByQuestionPostIdAndType( + Long questionPostId, + InteractionType type + ); +} diff --git a/src/main/java/com/dnd/gongmuin/post_interaction/repository/InteractionRepository.java b/src/main/java/com/dnd/gongmuin/post_interaction/repository/InteractionRepository.java new file mode 100644 index 00000000..70e65fb6 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/post_interaction/repository/InteractionRepository.java @@ -0,0 +1,19 @@ +package com.dnd.gongmuin.post_interaction.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.dnd.gongmuin.post_interaction.domain.Interaction; +import com.dnd.gongmuin.post_interaction.domain.InteractionType; + +public interface InteractionRepository extends JpaRepository { + + boolean existsByQuestionPostIdAndMemberIdAndType( + Long questionPostId, Long memberId, InteractionType type + ); + + Optional findByQuestionPostIdAndMemberIdAndType( + Long questionPostId, Long memberId, InteractionType type + ); +} diff --git a/src/main/java/com/dnd/gongmuin/post_interaction/service/InteractionService.java b/src/main/java/com/dnd/gongmuin/post_interaction/service/InteractionService.java new file mode 100644 index 00000000..70973c03 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/post_interaction/service/InteractionService.java @@ -0,0 +1,121 @@ +package com.dnd.gongmuin.post_interaction.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dnd.gongmuin.common.exception.runtime.NotFoundException; +import com.dnd.gongmuin.common.exception.runtime.ValidationException; +import com.dnd.gongmuin.post_interaction.domain.Interaction; +import com.dnd.gongmuin.post_interaction.domain.InteractionCount; +import com.dnd.gongmuin.post_interaction.domain.InteractionType; +import com.dnd.gongmuin.post_interaction.dto.InteractionMapper; +import com.dnd.gongmuin.post_interaction.dto.InteractionResponse; +import com.dnd.gongmuin.post_interaction.exception.InteractionErrorCode; +import com.dnd.gongmuin.post_interaction.repository.InteractionCountRepository; +import com.dnd.gongmuin.post_interaction.repository.InteractionRepository; +import com.dnd.gongmuin.question_post.domain.QuestionPost; +import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode; +import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class InteractionService { + + private final InteractionRepository interactionRepository; + private final InteractionCountRepository interactionCountRepository; + private final QuestionPostRepository questionPostRepository; + + @Transactional + public InteractionResponse activateInteraction( + Long questionPostId, + Long memberId, + InteractionType type // 북마크, 추천 + ) { + int count; + if (!interactionRepository.existsByQuestionPostIdAndMemberIdAndType // 상호 작용 존재x -> 저장 + (questionPostId, memberId, type) + ) { + count = createInteraction(questionPostId, memberId, type); + } else { // 존재 -> 값 업데이트 + count = updateInteractionAndCount(questionPostId, memberId, type, true); + } + return InteractionMapper.toPostInteractionResponse( + count, type + ); + } + + @Transactional + public InteractionResponse inactivateInteraction( + Long questionPostId, + Long memberId, + InteractionType type + ) { + int count = updateInteractionAndCount(questionPostId, memberId, type, false); + return InteractionMapper.toPostInteractionResponse( + count, type + ); + } + + private int createInteraction( + Long questionPostId, + Long memberId, + InteractionType type + ) { + validateIfPostExistsAndNotQuestioner(questionPostId, memberId); + interactionRepository.save( + InteractionMapper.toPostInteraction(questionPostId, memberId, type) + ); + return interactionCountRepository // 게시글 상호작용이 없어도 타 회원에 인해 게시글 상호작용 수가 있을 수 있음 + .findByQuestionPostIdAndType(questionPostId, type) + .orElseGet( + () -> interactionCountRepository + .save(InteractionMapper.toPostInteractionCount(questionPostId, type)) // 생성 시 count 1로 초기화 + ) + .getCount(); + } + + private void validateIfPostExistsAndNotQuestioner( + Long questionPostId, + Long memberId + ) { + QuestionPost questionPost = questionPostRepository.findById(questionPostId) + .orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST)); + if (questionPost.isQuestioner(memberId)) { + throw new ValidationException(InteractionErrorCode.ALREADY_UNINTERACTED); + } + } + + private int updateInteractionAndCount( + Long questionPostId, + Long memberId, + InteractionType type, + boolean isActivate + ) { + int count; + Interaction interaction = getPostInteraction(questionPostId, memberId, type); + InteractionCount interactionCount = getPostInteractionCount(questionPostId, type); + + if (isActivate) { //활성화 + interaction.updateIsInteractedTrue(); + count = interactionCount.increaseCount(); + } else { // 비활성화 + interaction.updateIsInteractedFalse(); + count = interactionCount.decreaseCount(); + } + return count; + } + + private Interaction getPostInteraction(Long questionPostId, Long memberId, InteractionType type) { + return interactionRepository.findByQuestionPostIdAndMemberIdAndType( + questionPostId, memberId, type + ).orElseThrow(() -> new NotFoundException(InteractionErrorCode.NOT_FOUND_POST_INTERACTION)); + } + + private InteractionCount getPostInteractionCount(Long questionPostId, InteractionType type) { + return interactionCountRepository + .findByQuestionPostIdAndType(questionPostId, type) + .orElseThrow(); + } +} diff --git a/src/main/java/com/dnd/gongmuin/question_post/controller/QuestionPostController.java b/src/main/java/com/dnd/gongmuin/question_post/controller/QuestionPostController.java index 2c9d224a..bdcc1ad5 100644 --- a/src/main/java/com/dnd/gongmuin/question_post/controller/QuestionPostController.java +++ b/src/main/java/com/dnd/gongmuin/question_post/controller/QuestionPostController.java @@ -17,6 +17,7 @@ import com.dnd.gongmuin.question_post.dto.request.RegisterQuestionPostRequest; import com.dnd.gongmuin.question_post.dto.response.QuestionPostDetailResponse; import com.dnd.gongmuin.question_post.dto.response.QuestionPostSimpleResponse; +import com.dnd.gongmuin.question_post.dto.response.RegisterQuestionPostResponse; import com.dnd.gongmuin.question_post.service.QuestionPostService; import io.swagger.v3.oas.annotations.Operation; @@ -36,11 +37,11 @@ public class QuestionPostController { @Operation(summary = "질문글 등록 API", description = "질문글을 등록한다") @ApiResponse(useReturnTypeSchema = true) @PostMapping - public ResponseEntity registerQuestionPost( + public ResponseEntity registerQuestionPost( @Valid @RequestBody RegisterQuestionPostRequest request, @AuthenticationPrincipal Member member ) { - QuestionPostDetailResponse response = questionPostService.registerQuestionPost(request, member); + RegisterQuestionPostResponse response = questionPostService.registerQuestionPost(request, member); return ResponseEntity.ok(response); } diff --git a/src/main/java/com/dnd/gongmuin/question_post/domain/QuestionPost.java b/src/main/java/com/dnd/gongmuin/question_post/domain/QuestionPost.java index 0638ee8a..e4bef737 100644 --- a/src/main/java/com/dnd/gongmuin/question_post/domain/QuestionPost.java +++ b/src/main/java/com/dnd/gongmuin/question_post/domain/QuestionPost.java @@ -82,8 +82,8 @@ private void addImages(List images) { }); } - public boolean isQuestioner(Member member) { - return Objects.equals(this.member.getId(), member.getId()); + public boolean isQuestioner(Long memberId) { + return Objects.equals(this.member.getId(), memberId); } public void updateIsChosen(Answer answer) { diff --git a/src/main/java/com/dnd/gongmuin/question_post/dto/QuestionPostMapper.java b/src/main/java/com/dnd/gongmuin/question_post/dto/QuestionPostMapper.java index 89b6192c..9a1526c5 100644 --- a/src/main/java/com/dnd/gongmuin/question_post/dto/QuestionPostMapper.java +++ b/src/main/java/com/dnd/gongmuin/question_post/dto/QuestionPostMapper.java @@ -10,6 +10,7 @@ import com.dnd.gongmuin.question_post.dto.response.MemberInfo; import com.dnd.gongmuin.question_post.dto.response.QuestionPostDetailResponse; import com.dnd.gongmuin.question_post.dto.response.QuestionPostSimpleResponse; +import com.dnd.gongmuin.question_post.dto.response.RegisterQuestionPostResponse; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -25,9 +26,36 @@ public static QuestionPost toQuestionPost(RegisterQuestionPostRequest request, M return QuestionPost.of(request.title(), request.content(), request.reward(), jobGroup, images, member); } - public static QuestionPostDetailResponse toQuestionPostDetailResponse(QuestionPost questionPost) { + public static QuestionPostDetailResponse toQuestionPostDetailResponse( + QuestionPost questionPost, + int recommendCount, + int savedCount + ) { Member member = questionPost.getMember(); return new QuestionPostDetailResponse( + questionPost.getId(), + questionPost.getTitle(), + questionPost.getContent(), + questionPost.getImages().stream() + .map(QuestionPostImage::getImageUrl).toList(), + questionPost.getReward(), + questionPost.getJobGroup().getLabel(), + new MemberInfo( + member.getId(), + member.getNickname(), + member.getJobGroup().getLabel() + ), + recommendCount, + savedCount, + questionPost.getCreatedAt().toString() + ); + } + + public static RegisterQuestionPostResponse toQuestionPostDetailResponse( + QuestionPost questionPost + ) { + Member member = questionPost.getMember(); + return new RegisterQuestionPostResponse( questionPost.getId(), questionPost.getTitle(), questionPost.getContent(), diff --git a/src/main/java/com/dnd/gongmuin/question_post/dto/response/QuestionPostDetailResponse.java b/src/main/java/com/dnd/gongmuin/question_post/dto/response/QuestionPostDetailResponse.java index f1f9c3f9..77c88460 100644 --- a/src/main/java/com/dnd/gongmuin/question_post/dto/response/QuestionPostDetailResponse.java +++ b/src/main/java/com/dnd/gongmuin/question_post/dto/response/QuestionPostDetailResponse.java @@ -10,6 +10,8 @@ public record QuestionPostDetailResponse( int reward, String targetJobGroup, MemberInfo memberInfo, + int recommendCount, + int savedCount, String createdAt ) { } \ No newline at end of file diff --git a/src/main/java/com/dnd/gongmuin/question_post/dto/response/RegisterQuestionPostResponse.java b/src/main/java/com/dnd/gongmuin/question_post/dto/response/RegisterQuestionPostResponse.java new file mode 100644 index 00000000..fc1633c5 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/question_post/dto/response/RegisterQuestionPostResponse.java @@ -0,0 +1,15 @@ +package com.dnd.gongmuin.question_post.dto.response; + +import java.util.List; + +public record RegisterQuestionPostResponse( + Long questionPostId, + String title, + String content, + List imageUrls, + int reward, + String targetJobGroup, + MemberInfo memberInfo, + String createdAt +) { +} \ No newline at end of file diff --git a/src/main/java/com/dnd/gongmuin/question_post/service/QuestionPostService.java b/src/main/java/com/dnd/gongmuin/question_post/service/QuestionPostService.java index e882f34e..9b7adf81 100644 --- a/src/main/java/com/dnd/gongmuin/question_post/service/QuestionPostService.java +++ b/src/main/java/com/dnd/gongmuin/question_post/service/QuestionPostService.java @@ -11,12 +11,16 @@ import com.dnd.gongmuin.common.exception.runtime.ValidationException; import com.dnd.gongmuin.member.domain.Member; import com.dnd.gongmuin.member.exception.MemberErrorCode; +import com.dnd.gongmuin.post_interaction.domain.InteractionCount; +import com.dnd.gongmuin.post_interaction.domain.InteractionType; +import com.dnd.gongmuin.post_interaction.repository.InteractionCountRepository; import com.dnd.gongmuin.question_post.domain.QuestionPost; import com.dnd.gongmuin.question_post.dto.QuestionPostMapper; import com.dnd.gongmuin.question_post.dto.request.QuestionPostSearchCondition; import com.dnd.gongmuin.question_post.dto.request.RegisterQuestionPostRequest; import com.dnd.gongmuin.question_post.dto.response.QuestionPostDetailResponse; import com.dnd.gongmuin.question_post.dto.response.QuestionPostSimpleResponse; +import com.dnd.gongmuin.question_post.dto.response.RegisterQuestionPostResponse; import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode; import com.dnd.gongmuin.question_post.repository.QuestionPostQueryRepository; import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; @@ -30,20 +34,31 @@ public class QuestionPostService { private final QuestionPostRepository questionPostRepository; private final QuestionPostQueryRepository questionPostQueryRepository; + private final InteractionCountRepository interactionCountRepository; + @Transactional - public QuestionPostDetailResponse registerQuestionPost(RegisterQuestionPostRequest request, Member member) { + public RegisterQuestionPostResponse registerQuestionPost( + RegisterQuestionPostRequest request, + Member member + ) { if (member.getCredit() < request.reward()) { throw new ValidationException(MemberErrorCode.NOT_ENOUGH_CREDIT); } QuestionPost questionPost = QuestionPostMapper.toQuestionPost(request, member); - return QuestionPostMapper.toQuestionPostDetailResponse(questionPostRepository.save(questionPost)); + return QuestionPostMapper.toQuestionPostDetailResponse( + questionPostRepository.save(questionPost) + ); } @Transactional(readOnly = true) public QuestionPostDetailResponse getQuestionPostById(Long questionPostId) { QuestionPost questionPost = questionPostRepository.findById(questionPostId) .orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST)); - return QuestionPostMapper.toQuestionPostDetailResponse(questionPost); + return QuestionPostMapper.toQuestionPostDetailResponse( + questionPost, + getCountByType(questionPostId, InteractionType.RECOMMEND), + getCountByType(questionPostId, InteractionType.SAVED) + ); } @Transactional(readOnly = true) @@ -56,4 +71,11 @@ public PageResponse searchQuestionPost( .map(QuestionPostMapper::toQuestionPostSimpleResponse); return PageMapper.toPageResponse(responsePage); } + + private int getCountByType(Long questionPostId, InteractionType type) { + return interactionCountRepository + .findByQuestionPostIdAndType(questionPostId, type) + .map(InteractionCount::getCount) + .orElse(0); + } } diff --git a/src/test/java/com/dnd/gongmuin/common/fixture/InteractionCountFixture.java b/src/test/java/com/dnd/gongmuin/common/fixture/InteractionCountFixture.java new file mode 100644 index 00000000..7f8c4476 --- /dev/null +++ b/src/test/java/com/dnd/gongmuin/common/fixture/InteractionCountFixture.java @@ -0,0 +1,25 @@ +package com.dnd.gongmuin.common.fixture; + +import org.springframework.test.util.ReflectionTestUtils; + +import com.dnd.gongmuin.post_interaction.domain.InteractionCount; +import com.dnd.gongmuin.post_interaction.domain.InteractionType; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class InteractionCountFixture { + + public static InteractionCount interactionCount( + InteractionType type, + Long questionPostId + ) { + InteractionCount interactionCount = InteractionCount.of( + type, + questionPostId + ); + ReflectionTestUtils.setField(interactionCount, "id", 1L); + return interactionCount; + } +} diff --git a/src/test/java/com/dnd/gongmuin/common/fixture/InteractionFixture.java b/src/test/java/com/dnd/gongmuin/common/fixture/InteractionFixture.java new file mode 100644 index 00000000..cc606725 --- /dev/null +++ b/src/test/java/com/dnd/gongmuin/common/fixture/InteractionFixture.java @@ -0,0 +1,27 @@ +package com.dnd.gongmuin.common.fixture; + +import org.springframework.test.util.ReflectionTestUtils; + +import com.dnd.gongmuin.post_interaction.domain.Interaction; +import com.dnd.gongmuin.post_interaction.domain.InteractionType; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class InteractionFixture { + + public static Interaction interaction( + InteractionType type, + Long memberId, + Long questionPostId + ) { + Interaction interaction = Interaction.of( + type, + memberId, + questionPostId + ); + ReflectionTestUtils.setField(interaction, "id", 1L); + return interaction; + } +} diff --git a/src/test/java/com/dnd/gongmuin/common/fixture/MemberFixture.java b/src/test/java/com/dnd/gongmuin/common/fixture/MemberFixture.java index e2f0acf1..e4f79c64 100644 --- a/src/test/java/com/dnd/gongmuin/common/fixture/MemberFixture.java +++ b/src/test/java/com/dnd/gongmuin/common/fixture/MemberFixture.java @@ -46,6 +46,14 @@ public static Member member3() { ); } + public static Member member4() { + return Member.of( + "소셜회원", + "KAKAO1234/member2@daum.net", + 20000 + ); + } + public static Member member(Long memberId) { Member member = Member.of( "김회원", diff --git a/src/test/java/com/dnd/gongmuin/member/controller/MemberControllerTest.java b/src/test/java/com/dnd/gongmuin/member/controller/MemberControllerTest.java index aee38a09..724c9aa3 100644 --- a/src/test/java/com/dnd/gongmuin/member/controller/MemberControllerTest.java +++ b/src/test/java/com/dnd/gongmuin/member/controller/MemberControllerTest.java @@ -5,15 +5,53 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import com.dnd.gongmuin.answer.domain.Answer; +import com.dnd.gongmuin.answer.repository.AnswerRepository; +import com.dnd.gongmuin.common.fixture.AnswerFixture; +import com.dnd.gongmuin.common.fixture.InteractionCountFixture; +import com.dnd.gongmuin.common.fixture.MemberFixture; +import com.dnd.gongmuin.common.fixture.QuestionPostFixture; import com.dnd.gongmuin.common.support.ApiTestSupport; +import com.dnd.gongmuin.member.domain.Member; import com.dnd.gongmuin.member.dto.request.UpdateMemberProfileRequest; +import com.dnd.gongmuin.member.repository.MemberRepository; +import com.dnd.gongmuin.post_interaction.domain.InteractionCount; +import com.dnd.gongmuin.post_interaction.domain.InteractionType; +import com.dnd.gongmuin.post_interaction.repository.InteractionCountRepository; +import com.dnd.gongmuin.question_post.domain.QuestionPost; +import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; @DisplayName("[MemberController] 통합테스트") class MemberControllerTest extends ApiTestSupport { + @Autowired + MemberRepository memberRepository; + + @Autowired + QuestionPostRepository questionPostRepository; + + @Autowired + AnswerRepository answerRepository; + + @Autowired + InteractionCountRepository interactionCountRepository; + + @AfterEach + void tearDown() { + answerRepository.deleteAll(); + memberRepository.deleteAll(); + questionPostRepository.deleteAll(); + } + @DisplayName("로그인 된 사용자 프로필 정보를 조회한다.") @Test void getMemberProfile() throws Exception { @@ -46,4 +84,83 @@ void updateMemberProfile() throws Exception { .andExpect(jsonPath("jobCategory").value("가스")) .andExpect(jsonPath("credit").value(10000)); } + + @DisplayName("로그인 된 회원이 작성한 질문을 전체 조회한다.") + @Test + void getQuestionPostsByMember() throws Exception { + // given + Member member = MemberFixture.member2(); + Member savedMember = memberRepository.save(member); + + QuestionPost questionPost1 = QuestionPostFixture.questionPost(loginMember, "첫 번째 게시글입니다."); + QuestionPost questionPost2 = QuestionPostFixture.questionPost(savedMember, "두 번째 게시글입니다."); + QuestionPost questionPost3 = QuestionPostFixture.questionPost(loginMember, "세 번째 게시글입니다."); + questionPostRepository.saveAll(List.of(questionPost1, questionPost2, questionPost3)); + + InteractionCount interactionCount1 = InteractionCountFixture.interactionCount(InteractionType.SAVED, + questionPost1.getId()); + InteractionCount interactionCount2 = InteractionCountFixture.interactionCount(InteractionType.RECOMMEND, + questionPost1.getId()); + InteractionCount interactionCount3 = InteractionCountFixture.interactionCount(InteractionType.SAVED, + questionPost3.getId()); + InteractionCount interactionCount4 = InteractionCountFixture.interactionCount(InteractionType.RECOMMEND, + questionPost3.getId()); + + ReflectionTestUtils.setField(interactionCount1, "id", 1L); + ReflectionTestUtils.setField(interactionCount1, "count", 10); + ReflectionTestUtils.setField(interactionCount2, "id", 2L); + ReflectionTestUtils.setField(interactionCount2, "count", 20); + ReflectionTestUtils.setField(interactionCount3, "id", 3L); + ReflectionTestUtils.setField(interactionCount3, "count", 30); + ReflectionTestUtils.setField(interactionCount4, "id", 4L); + ReflectionTestUtils.setField(interactionCount4, "count", 40); + + interactionCountRepository.saveAll( + List.of(interactionCount1, interactionCount2, interactionCount3, interactionCount4)); + + // when // then + mockMvc.perform(get("/api/members/question-posts") + .header(AUTHORIZATION, accessToken) + ) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andExpect(jsonPath("$.size").value(2)) + .andExpect(jsonPath("$.content[0].questionPostId").value(questionPost3.getId())) + .andExpect(jsonPath("$.content[0].savedTotalCount").value(30)) + .andExpect(jsonPath("$.content[0].recommendTotalCount").value(40)) + .andExpect(jsonPath("$.content[1].questionPostId").value(questionPost1.getId())) + .andExpect(jsonPath("$.content[1].savedTotalCount").value(10)) + .andExpect(jsonPath("$.content[1].recommendTotalCount").value(20)); + } + + @DisplayName("로그인 된 회원이 댓글 단 질문을 전체 조회한다.") + @Test + void getAnsweredQuestionPostsByMember() throws Exception { + // given + Member member = MemberFixture.member2(); + Member savedMember = memberRepository.save(member); + + QuestionPost questionPost1 = QuestionPostFixture.questionPost(loginMember, "첫 번째 게시글입니다."); + QuestionPost questionPost2 = QuestionPostFixture.questionPost(savedMember, "두 번째 게시글입니다."); + QuestionPost questionPost3 = QuestionPostFixture.questionPost(loginMember, "세 번째 게시글입니다."); + questionPostRepository.saveAll(List.of(questionPost1, questionPost2, questionPost3)); + + Answer answer1 = AnswerFixture.answer(questionPost2.getId(), loginMember); + Answer answer2 = AnswerFixture.answer(questionPost3.getId(), loginMember); + Answer answer3 = AnswerFixture.answer(questionPost3.getId(), loginMember); + answerRepository.saveAll(List.of(answer1, answer3)); + answerRepository.save(answer2); + + // when // then + mockMvc.perform(get("/api/members/answer-posts") + .header(AUTHORIZATION, accessToken) + ) + .andExpect(status().isOk()) + .andDo(MockMvcResultHandlers.print()) + .andExpect(jsonPath("$.size").value(2)) + .andExpect(jsonPath("$.content[0].questionPostId").value(questionPost3.getId())) + .andExpect(jsonPath("$.content[0].answerId").value(answer2.getId())) + .andExpect(jsonPath("$.content[1].questionPostId").value(questionPost2.getId())) + .andExpect(jsonPath("$.content[1].answerId").value(answer1.getId())); + } } diff --git a/src/test/java/com/dnd/gongmuin/member/repository/MemberRepositoryTest.java b/src/test/java/com/dnd/gongmuin/member/repository/MemberRepositoryTest.java index 3b377ccd..59b7344a 100644 --- a/src/test/java/com/dnd/gongmuin/member/repository/MemberRepositoryTest.java +++ b/src/test/java/com/dnd/gongmuin/member/repository/MemberRepositoryTest.java @@ -4,23 +4,60 @@ import static com.dnd.gongmuin.member.domain.JobGroup.*; import static org.assertj.core.api.Assertions.*; -import org.junit.jupiter.api.Disabled; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.test.util.ReflectionTestUtils; +import com.dnd.gongmuin.answer.domain.Answer; +import com.dnd.gongmuin.answer.repository.AnswerRepository; +import com.dnd.gongmuin.common.fixture.AnswerFixture; +import com.dnd.gongmuin.common.fixture.InteractionCountFixture; +import com.dnd.gongmuin.common.fixture.MemberFixture; +import com.dnd.gongmuin.common.fixture.QuestionPostFixture; import com.dnd.gongmuin.common.support.DataJpaTestSupport; import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.member.dto.response.AnsweredQuestionPostsByMemberResponse; +import com.dnd.gongmuin.member.dto.response.QuestionPostsByMemberResponse; +import com.dnd.gongmuin.post_interaction.domain.InteractionCount; +import com.dnd.gongmuin.post_interaction.domain.InteractionType; +import com.dnd.gongmuin.post_interaction.repository.InteractionCountRepository; +import com.dnd.gongmuin.question_post.domain.QuestionPost; +import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; -@Disabled class MemberRepositoryTest extends DataJpaTestSupport { + private final PageRequest pageRequest = PageRequest.of(0, 10); + @Autowired MemberRepository memberRepository; + @Autowired + QuestionPostRepository questionPostRepository; + + @Autowired + AnswerRepository answerRepository; + + @Autowired + InteractionCountRepository interactionCountRepository; + + @AfterEach + void tearDown() { + answerRepository.deleteAll(); + interactionCountRepository.deleteAll(); + questionPostRepository.deleteAll(); + memberRepository.deleteAll(); + } + @DisplayName("소셜이메일로 특정 회원을 조회한다.") @Test - void test() { + void findMemberBySocialEmail() { // given Member 공무인1 = createMember("공무인1", "영태", "kakao1234/gongmuin@nate.com", "gongumin@korea.kr"); Member savedMember = memberRepository.save(공무인1); @@ -32,6 +69,295 @@ void test() { assertThat(findMember.getNickname()).isEqualTo("공무인1"); } + @DisplayName("자신이 작성한 질문 목록만 조회할 수 있다.[상호작용 수 비포함]") + @Test + void getQuestionPostsByMember() { + // given + Member member1 = MemberFixture.member(); + Member member2 = MemberFixture.member2(); + memberRepository.saveAll(List.of(member1, member2)); + + QuestionPost questionPost1 = QuestionPostFixture.questionPost(member1, "첫 번째 게시글입니다."); + QuestionPost questionPost2 = QuestionPostFixture.questionPost(member1, "두 번째 게시글입니다."); + QuestionPost questionPost3 = QuestionPostFixture.questionPost(member2, "세 번째 게시글입니다."); + questionPostRepository.saveAll(List.of(questionPost1, questionPost2, questionPost3)); + + // when + Slice postsByMember = memberRepository.getQuestionPostsByMember(member1, + pageRequest); + + // then + Assertions.assertAll( + () -> assertThat(postsByMember).hasSize(2), + () -> assertThat(postsByMember).extracting(QuestionPostsByMemberResponse::title) + .containsExactly( + "두 번째 게시글입니다.", + "첫 번째 게시글입니다." + ), + () -> assertThat(postsByMember).extracting(QuestionPostsByMemberResponse::questionPostId) + .containsExactly( + questionPost2.getId(), + questionPost1.getId() + ) + ); + } + + @DisplayName("자신이 작성한 질문 목록만 조회할 수 있다.[상호작용 수 포함]") + @Test + void getQuestionPostsByMemberWithInteractionCount() { + // given + Member member1 = MemberFixture.member(); + Member member2 = MemberFixture.member2(); + memberRepository.saveAll(List.of(member1, member2)); + + QuestionPost questionPost1 = QuestionPostFixture.questionPost(member1, "첫 번째 게시글입니다."); + QuestionPost questionPost2 = QuestionPostFixture.questionPost(member1, "두 번째 게시글입니다."); + QuestionPost questionPost3 = QuestionPostFixture.questionPost(member2, "세 번째 게시글입니다."); + questionPostRepository.saveAll(List.of(questionPost1, questionPost2, questionPost3)); + + InteractionCount interactionCount1 = InteractionCountFixture.interactionCount(InteractionType.SAVED, + questionPost1.getId()); + InteractionCount interactionCount2 = InteractionCountFixture.interactionCount(InteractionType.RECOMMEND, + questionPost1.getId()); + ReflectionTestUtils.setField(interactionCount1, "id", 1L); + ReflectionTestUtils.setField(interactionCount1, "count", 10); + ReflectionTestUtils.setField(interactionCount2, "id", 2L); + ReflectionTestUtils.setField(interactionCount2, "count", 20); + interactionCountRepository.saveAll(List.of(interactionCount1, interactionCount2)); + + // when + Slice postsByMember = memberRepository.getQuestionPostsByMember(member1, + pageRequest); + + // then + Assertions.assertAll( + () -> assertThat(postsByMember).hasSize(2), + () -> assertThat(postsByMember).extracting(QuestionPostsByMemberResponse::questionPostId) + .containsExactly( + questionPost2.getId(), + questionPost1.getId() + ), + () -> assertThat(postsByMember).extracting(QuestionPostsByMemberResponse::title) + .containsExactly( + "두 번째 게시글입니다.", + "첫 번째 게시글입니다." + ), + () -> assertThat(postsByMember).extracting(QuestionPostsByMemberResponse::savedTotalCount) + .containsExactly( + 0, + 10 + ), + () -> assertThat(postsByMember).extracting(QuestionPostsByMemberResponse::recommendTotalCount) + .containsExactly( + 0, + 20 + ) + ); + } + + @DisplayName("자신이 댓글 단 질문 목록만 조회할 수 있다.[상호작용 수 미포함]") + @Test + void getAnsweredQuestionPostsByMember() { + // given + Member member1 = MemberFixture.member(); + Member member2 = MemberFixture.member2(); + memberRepository.saveAll(List.of(member1, member2)); + + QuestionPost questionPost1 = QuestionPostFixture.questionPost(member1, "첫 번째 게시글입니다."); + QuestionPost questionPost2 = QuestionPostFixture.questionPost(member1, "두 번째 게시글입니다."); + QuestionPost questionPost3 = QuestionPostFixture.questionPost(member2, "세 번째 게시글입니다."); + questionPostRepository.saveAll(List.of(questionPost1, questionPost2, questionPost3)); + + Answer answer1 = AnswerFixture.answer(questionPost1.getId(), member1); + Answer answer2 = AnswerFixture.answer(questionPost1.getId(), member2); + Answer answer3 = AnswerFixture.answer(questionPost2.getId(), member2); + Answer answer4 = AnswerFixture.answer(questionPost3.getId(), member1); + answerRepository.saveAll(List.of(answer1, answer2, answer3, answer4)); + + // when + Slice postsByMember = + memberRepository.getAnsweredQuestionPostsByMember(member1, pageRequest); + + // then + Assertions.assertAll( + () -> assertThat(postsByMember).hasSize(2), + () -> assertThat(postsByMember).extracting(AnsweredQuestionPostsByMemberResponse::title) + .containsExactly( + "세 번째 게시글입니다.", + "첫 번째 게시글입니다." + ), + () -> assertThat(postsByMember).extracting(AnsweredQuestionPostsByMemberResponse::questionPostId) + .containsExactly( + questionPost3.getId(), + questionPost1.getId() + ), + () -> assertThat(postsByMember).extracting(AnsweredQuestionPostsByMemberResponse::answerId) + .containsExactly( + answer4.getId(), + answer1.getId() + ) + ); + } + + @DisplayName("자신이 댓글 단 질문 목록만 조회할 수 있다.[상호작용 수 포함]") + @Test + void getAnsweredQuestionPostsByMemberWithInteractionCount() { + // given + Member member1 = MemberFixture.member(); + Member member2 = MemberFixture.member2(); + memberRepository.saveAll(List.of(member1, member2)); + + QuestionPost questionPost1 = QuestionPostFixture.questionPost(member1, "첫 번째 게시글입니다."); + QuestionPost questionPost2 = QuestionPostFixture.questionPost(member1, "두 번째 게시글입니다."); + QuestionPost questionPost3 = QuestionPostFixture.questionPost(member2, "세 번째 게시글입니다."); + questionPostRepository.saveAll(List.of(questionPost1, questionPost2, questionPost3)); + + Answer answer1 = AnswerFixture.answer(questionPost1.getId(), member1); + Answer answer2 = AnswerFixture.answer(questionPost1.getId(), member2); + Answer answer3 = AnswerFixture.answer(questionPost2.getId(), member2); + Answer answer4 = AnswerFixture.answer(questionPost3.getId(), member1); + answerRepository.saveAll(List.of(answer1, answer2, answer3, answer4)); + + InteractionCount interactionCount1 = InteractionCountFixture.interactionCount(InteractionType.SAVED, + questionPost1.getId()); + InteractionCount interactionCount2 = InteractionCountFixture.interactionCount(InteractionType.RECOMMEND, + questionPost1.getId()); + ReflectionTestUtils.setField(interactionCount1, "id", 1L); + ReflectionTestUtils.setField(interactionCount1, "count", 10); + ReflectionTestUtils.setField(interactionCount2, "id", 2L); + ReflectionTestUtils.setField(interactionCount2, "count", 20); + interactionCountRepository.saveAll(List.of(interactionCount1, interactionCount2)); + + // when + Slice postsByMember = + memberRepository.getAnsweredQuestionPostsByMember(member1, pageRequest); + + // then + Assertions.assertAll( + () -> assertThat(postsByMember).hasSize(2), + () -> assertThat(postsByMember).extracting(AnsweredQuestionPostsByMemberResponse::title) + .containsExactly( + "세 번째 게시글입니다.", + "첫 번째 게시글입니다." + ), + () -> assertThat(postsByMember).extracting(AnsweredQuestionPostsByMemberResponse::questionPostId) + .containsExactly( + questionPost3.getId(), + questionPost1.getId() + ), + () -> assertThat(postsByMember).extracting(AnsweredQuestionPostsByMemberResponse::answerId) + .containsExactly( + answer4.getId(), + answer1.getId() + ), + () -> assertThat(postsByMember).extracting(AnsweredQuestionPostsByMemberResponse::savedTotalCount) + .containsExactly( + 0, + 10 + ), + () -> assertThat(postsByMember).extracting(AnsweredQuestionPostsByMemberResponse::recommendTotalCount) + .containsExactly( + 0, + 20 + ) + ); + } + + @DisplayName("답변단 게시글이 존재하지 않으면 질문 목록의 Size는 0 이다.") + @Test + void whenNoAnsweredQuestionPosts_thenGetQuestionPosts() { + // given + Member member1 = MemberFixture.member(); + Member member2 = MemberFixture.member2(); + memberRepository.saveAll(List.of(member1, member2)); + + QuestionPost questionPost1 = QuestionPostFixture.questionPost(member1, "첫 번째 게시글입니다."); + QuestionPost questionPost2 = QuestionPostFixture.questionPost(member1, "두 번째 게시글입니다."); + QuestionPost questionPost3 = QuestionPostFixture.questionPost(member2, "세 번째 게시글입니다."); + questionPostRepository.saveAll(List.of(questionPost1, questionPost2, questionPost3)); + + Answer answer1 = AnswerFixture.answer(questionPost1.getId(), member1); + Answer answer2 = AnswerFixture.answer(questionPost2.getId(), member1); + Answer answer3 = AnswerFixture.answer(questionPost3.getId(), member1); + answerRepository.saveAll(List.of(answer1, answer2, answer3)); + + InteractionCount interactionCount1 = InteractionCountFixture.interactionCount(InteractionType.SAVED, + questionPost1.getId()); + InteractionCount interactionCount2 = InteractionCountFixture.interactionCount(InteractionType.RECOMMEND, + questionPost1.getId()); + ReflectionTestUtils.setField(interactionCount1, "id", 1L); + ReflectionTestUtils.setField(interactionCount1, "count", 10); + ReflectionTestUtils.setField(interactionCount2, "id", 2L); + ReflectionTestUtils.setField(interactionCount2, "count", 20); + interactionCountRepository.saveAll(List.of(interactionCount1, interactionCount2)); + + // when + Slice postsByMember = + memberRepository.getAnsweredQuestionPostsByMember(member2, pageRequest); + + // then + Assertions.assertAll( + () -> assertThat(postsByMember).hasSize(0), + () -> assertThat(postsByMember.getContent()).isEmpty() + ); + } + + @DisplayName("답변 단 질문을 가져올 때, 질문 내 답변이 여러 개면 가장 마지막 작성된 답변을 가져온다") + @Test + void whenAnsweredQuestionPosts_thenGetQuestionPostsAtRecently() { + // given + Member member1 = MemberFixture.member(); + Member member2 = MemberFixture.member(); + memberRepository.saveAll(List.of(member1, member2)); + + QuestionPost questionPost1 = QuestionPostFixture.questionPost(member1, "첫 번째 게시글입니다.22"); + QuestionPost questionPost2 = QuestionPostFixture.questionPost(member1, "두 번째 게시글입니다."); + QuestionPost questionPost3 = QuestionPostFixture.questionPost(member1, "세 번째 게시글입니다."); + questionPostRepository.saveAll(List.of(questionPost1, questionPost2, questionPost3)); + + Answer answer1 = AnswerFixture.answer(questionPost1.getId(), member1); + Answer answer2 = AnswerFixture.answer(questionPost1.getId(), member1); + Answer answer3 = AnswerFixture.answer(questionPost1.getId(), member2); + ReflectionTestUtils.setField(answer1, "content", "1번답변."); + ReflectionTestUtils.setField(answer2, "content", "2번답변."); + answerRepository.saveAll(List.of(answer1, answer3)); + answerRepository.save(answer2); + + // when + Slice postsByMember = + memberRepository.getAnsweredQuestionPostsByMember(member1, pageRequest); + + // then + Assertions.assertAll( + () -> postsByMember.forEach(post -> { + System.out.println("QuestionPostId: " + post.questionPostId()); + System.out.println("Title: " + post.title()); + System.out.println("Content: " + post.content()); + System.out.println("JobGroup: " + post.jobGroup()); + System.out.println("Reward: " + post.reward()); + System.out.println("UpdatedAt: " + post.questionPostUpdatedAt()); + System.out.println("IsChosen: " + post.isChosen()); + System.out.println("answerId: " + post.answerId()); + System.out.println("post.answerContent() = " + post.answerContent()); + System.out.println("post.answerUpdatedAt() = " + post.answerUpdatedAt()); + System.out.println("----------"); + }), + () -> assertThat(postsByMember).hasSize(1), + () -> assertThat(postsByMember).extracting(AnsweredQuestionPostsByMemberResponse::answerId) + .containsExactly( + answer2.getId() + ), + () -> assertThat(postsByMember).extracting(AnsweredQuestionPostsByMemberResponse::answerUpdatedAt) + .containsExactly( + answer2.getUpdatedAt().toString() + ), + () -> assertThat(postsByMember).extracting(AnsweredQuestionPostsByMemberResponse::answerContent) + .containsExactly( + answer2.getContent() + ) + ); + } + private Member createMember(String nickname, String socialName, String socialEmail, String officialEmail) { return Member.builder() .nickname(nickname) diff --git a/src/test/java/com/dnd/gongmuin/post_interaction/controller/InteractionControllerTest.java b/src/test/java/com/dnd/gongmuin/post_interaction/controller/InteractionControllerTest.java new file mode 100644 index 00000000..83a5f08a --- /dev/null +++ b/src/test/java/com/dnd/gongmuin/post_interaction/controller/InteractionControllerTest.java @@ -0,0 +1,100 @@ +package com.dnd.gongmuin.post_interaction.controller; + +import static org.springframework.http.HttpHeaders.*; +import static org.springframework.http.MediaType.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import com.dnd.gongmuin.common.fixture.InteractionCountFixture; +import com.dnd.gongmuin.common.fixture.InteractionFixture; +import com.dnd.gongmuin.common.fixture.MemberFixture; +import com.dnd.gongmuin.common.fixture.QuestionPostFixture; +import com.dnd.gongmuin.common.support.ApiTestSupport; +import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.post_interaction.domain.InteractionType; +import com.dnd.gongmuin.post_interaction.repository.InteractionCountRepository; +import com.dnd.gongmuin.post_interaction.repository.InteractionRepository; +import com.dnd.gongmuin.question_post.domain.QuestionPost; +import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; + +@DisplayName("[Interaction 통합 테스트]") +class InteractionControllerTest extends ApiTestSupport { + + @Autowired + private QuestionPostRepository questionPostRepository; + + @Autowired + private InteractionRepository interactionRepository; + + @Autowired + private InteractionCountRepository interactionCountRepository; + + @AfterEach + void teardown() { + memberRepository.deleteAll(); + questionPostRepository.deleteAll(); + } + + @DisplayName("[상호작용을 새로 활성화할 수 있다.]") + @Test + void activateInteraction_new() throws Exception { + Member questioner = memberRepository.save(MemberFixture.member4()); + QuestionPost questionPost = questionPostRepository.save( + QuestionPostFixture.questionPost(questioner) + ); + + mockMvc.perform(post("/api/question-posts/{questionPostId}/activated", questionPost.getId()) + .contentType(APPLICATION_JSON) + .header(AUTHORIZATION, accessToken) + .param("type", "추천") + ) + .andExpect(status().isOk()); + } + + @DisplayName("[기존 비활성화된 상호작용을 활성화할 수 있다.]") + @Test + void activateInteraction_old() throws Exception { + Member questioner = memberRepository.save(MemberFixture.member4()); + QuestionPost questionPost = questionPostRepository.save( + QuestionPostFixture.questionPost(questioner) + ); + interactionRepository.save(InteractionFixture.interaction(InteractionType.RECOMMEND, + loginMember.getId(), questionPost.getId())); + interactionCountRepository.save( + InteractionCountFixture.interactionCount(InteractionType.RECOMMEND, questionPost.getId()) + ); + + mockMvc.perform(post("/api/question-posts/{questionPostId}/inactivated", questionPost.getId()) + .contentType(APPLICATION_JSON) + .header(AUTHORIZATION, accessToken) + .param("type", "추천") + ) + .andExpect(status().isOk()); + } + + @DisplayName("[상호작용을 비활성화할 수 있다.]") + @Test + void inactivateInteraction() throws Exception { + Member questioner = memberRepository.save(MemberFixture.member4()); + QuestionPost questionPost = questionPostRepository.save( + QuestionPostFixture.questionPost(questioner) + ); + interactionRepository.save(InteractionFixture.interaction(InteractionType.RECOMMEND, + loginMember.getId(), questionPost.getId())); + interactionCountRepository.save( + InteractionCountFixture.interactionCount(InteractionType.RECOMMEND, questionPost.getId()) + ); + + mockMvc.perform(post("/api/question-posts/{questionPostId}/inactivated", questionPost.getId()) + .contentType(APPLICATION_JSON) + .header(AUTHORIZATION, accessToken) + .param("type", "추천") + ) + .andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/src/test/java/com/dnd/gongmuin/post_interaction/service/InteractionServiceTest.java b/src/test/java/com/dnd/gongmuin/post_interaction/service/InteractionServiceTest.java new file mode 100644 index 00000000..0fc78f0c --- /dev/null +++ b/src/test/java/com/dnd/gongmuin/post_interaction/service/InteractionServiceTest.java @@ -0,0 +1,177 @@ +package com.dnd.gongmuin.post_interaction.service; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.dnd.gongmuin.common.fixture.InteractionCountFixture; +import com.dnd.gongmuin.common.fixture.InteractionFixture; +import com.dnd.gongmuin.common.fixture.MemberFixture; +import com.dnd.gongmuin.common.fixture.QuestionPostFixture; +import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.post_interaction.domain.Interaction; +import com.dnd.gongmuin.post_interaction.domain.InteractionCount; +import com.dnd.gongmuin.post_interaction.domain.InteractionType; +import com.dnd.gongmuin.post_interaction.dto.InteractionResponse; +import com.dnd.gongmuin.post_interaction.repository.InteractionCountRepository; +import com.dnd.gongmuin.post_interaction.repository.InteractionRepository; +import com.dnd.gongmuin.question_post.domain.QuestionPost; +import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; + +@DisplayName("[InteractionService 테스트]") +@ExtendWith(MockitoExtension.class) +class InteractionServiceTest { + + private final Member questioner = MemberFixture.member(1L); + private final Member interactor = MemberFixture.member(2L); + + @Mock + private InteractionRepository interactionRepository; + + @Mock + private InteractionCountRepository interactionCountRepository; + + @Mock + private QuestionPostRepository questionPostRepository; + + @InjectMocks + private InteractionService interactionService; + + @DisplayName("[상호작용을 새로 활성화한다. 기존에 게시글 상호작용 수가 저장되어 있다.]") + @Test + void activateInteraction_create1() { + //given + InteractionType type = InteractionType.RECOMMEND; + QuestionPost questionPost = QuestionPostFixture.questionPost(1L, questioner); + Interaction interaction = Interaction.of(type, interactor.getId(), questionPost.getId()); + InteractionCount interactionCount = InteractionCount.of(type, interactor.getId()); + + given(interactionRepository.existsByQuestionPostIdAndMemberIdAndType( + questionPost.getId(), interactor.getId(), type + )).willReturn(false); // 생성 + given(questionPostRepository.findById(questionPost.getId())) + .willReturn(Optional.of(questionPost)); + given(interactionRepository.save(any(Interaction.class))) + .willReturn(interaction); + given(interactionCountRepository.findByQuestionPostIdAndType( + questionPost.getId(), type)).willReturn(Optional.of(interactionCount)); + + //when + InteractionResponse response = interactionService.activateInteraction(1L, 2L, + type); + + //then + assertAll( + () -> assertThat(response.count()).isEqualTo(1), + () -> assertThat(response.interactionType()).isEqualTo(type.getLabel()) + ); + } + + @DisplayName("[상호작용을 새로 활성화한다. 기존에 게시글 상호작용 수가 저장되어있지 않다.]") + @Test + void activateInteraction_create2() { + //given + InteractionType type = InteractionType.RECOMMEND; + QuestionPost questionPost = QuestionPostFixture.questionPost(1L, questioner); + Interaction interaction = Interaction.of(type, interactor.getId(), questionPost.getId()); + InteractionCount interactionCount = InteractionCount.of(type, interactor.getId()); + + given(interactionRepository.existsByQuestionPostIdAndMemberIdAndType( + questionPost.getId(), interactor.getId(), type + )).willReturn(false); // 생성 + given(questionPostRepository.findById(questionPost.getId())) + .willReturn(Optional.of(questionPost)); + given(interactionRepository.save(any(Interaction.class))) + .willReturn(interaction); + given(interactionCountRepository.findByQuestionPostIdAndType( + questionPost.getId(), type)).willReturn(Optional.empty()); + given(interactionCountRepository.save(any(InteractionCount.class))) + .willReturn(interactionCount); + + //when + InteractionResponse response + = interactionService.activateInteraction(1L, 2L, type); + + //then + assertAll( + () -> assertThat(response.count()).isEqualTo(1), + () -> assertThat(response.interactionType()).isEqualTo(type.getLabel()) + ); + } + + @DisplayName("[비활성화된 상호작용을 재활성화한다.]") + @Test + void activateInteraction_update() { + //given + InteractionType type = InteractionType.RECOMMEND; + QuestionPost questionPost = QuestionPostFixture.questionPost(1L, questioner); + Interaction interaction = InteractionFixture.interaction(type, interactor.getId(), + questionPost.getId()); + InteractionCount interactionCount = InteractionCountFixture.interactionCount(type, + interactor.getId()); + interaction.updateIsInteractedFalse(); + interactionCount.decreaseCount(); + + given(interactionRepository.existsByQuestionPostIdAndMemberIdAndType( + questionPost.getId(), interactor.getId(), type + )).willReturn(true); // 업데이트 + given(interactionRepository.findByQuestionPostIdAndMemberIdAndType( + questionPost.getId(), + interactor.getId(), + type + )).willReturn(Optional.of(interaction)); + given(interactionCountRepository.findByQuestionPostIdAndType( + interactionCount.getId(), type)) + .willReturn(Optional.of(interactionCount)); + + //when + InteractionResponse response = interactionService.activateInteraction(1L, 2L, + type); + + //then + assertAll( + () -> assertThat(response.count()).isEqualTo(1), + () -> assertThat(response.interactionType()).isEqualTo(type.getLabel()) + ); + } + + @DisplayName("[활성화된 상호작용을 비활성화한다.]") + @Test + void inactivateInteraction() { + //given + InteractionType type = InteractionType.RECOMMEND; + QuestionPost questionPost = QuestionPostFixture.questionPost(1L, questioner); + Interaction interaction = InteractionFixture.interaction(type, interactor.getId(), + questionPost.getId()); + InteractionCount interactionCount = InteractionCountFixture.interactionCount(type, + interactor.getId()); + + given(interactionRepository.findByQuestionPostIdAndMemberIdAndType( + questionPost.getId(), + interactor.getId(), + type + )).willReturn(Optional.of(interaction)); + given(interactionCountRepository.findByQuestionPostIdAndType( + interactionCount.getId(), type)) + .willReturn(Optional.of(interactionCount)); + + //when + InteractionResponse response = interactionService.inactivateInteraction(1L, 2L, + type); + + //then + assertAll( + () -> assertThat(response.count()).isZero(), + () -> assertThat(response.interactionType()).isEqualTo(type.getLabel()) + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/dnd/gongmuin/question_post/controller/QuestionPostControllerTest.java b/src/test/java/com/dnd/gongmuin/question_post/controller/QuestionPostControllerTest.java index 935b74b8..2a8ae51a 100644 --- a/src/test/java/com/dnd/gongmuin/question_post/controller/QuestionPostControllerTest.java +++ b/src/test/java/com/dnd/gongmuin/question_post/controller/QuestionPostControllerTest.java @@ -100,7 +100,9 @@ void getQuestionPostById() throws Exception { .andExpect(jsonPath("$.targetJobGroup").value(questionPost.getJobGroup().getLabel())) .andExpect(jsonPath("$.memberInfo.memberId").value(questionPost.getMember().getId())) .andExpect(jsonPath("$.memberInfo.nickname").value(questionPost.getMember().getNickname())) - .andExpect(jsonPath("$.memberInfo.memberJobGroup").value(questionPost.getMember().getJobGroup().getLabel()) + .andExpect(jsonPath("$.memberInfo.memberJobGroup").value(questionPost.getMember().getJobGroup().getLabel())) + .andExpect(jsonPath("$.recommendCount").value(0)) + .andExpect(jsonPath("$.savedCount").value(0) ); } diff --git a/src/test/java/com/dnd/gongmuin/question_post/service/QuestionPostServiceTest.java b/src/test/java/com/dnd/gongmuin/question_post/service/QuestionPostServiceTest.java index 89fe87d2..259c3369 100644 --- a/src/test/java/com/dnd/gongmuin/question_post/service/QuestionPostServiceTest.java +++ b/src/test/java/com/dnd/gongmuin/question_post/service/QuestionPostServiceTest.java @@ -15,12 +15,17 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import com.dnd.gongmuin.common.fixture.InteractionCountFixture; import com.dnd.gongmuin.common.fixture.MemberFixture; import com.dnd.gongmuin.common.fixture.QuestionPostFixture; import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.post_interaction.domain.InteractionCount; +import com.dnd.gongmuin.post_interaction.domain.InteractionType; +import com.dnd.gongmuin.post_interaction.repository.InteractionCountRepository; import com.dnd.gongmuin.question_post.domain.QuestionPost; import com.dnd.gongmuin.question_post.dto.request.RegisterQuestionPostRequest; import com.dnd.gongmuin.question_post.dto.response.QuestionPostDetailResponse; +import com.dnd.gongmuin.question_post.dto.response.RegisterQuestionPostResponse; import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; @DisplayName("[QuestionPostService 테스트]") @@ -32,6 +37,9 @@ class QuestionPostServiceTest { @Mock private QuestionPostRepository questionPostRepository; + @Mock + private InteractionCountRepository interactionCountRepository; + @InjectMocks private QuestionPostService questionPostService; @@ -53,7 +61,7 @@ void registerQuestionPost() { .willReturn(questionPost); //when - QuestionPostDetailResponse response = questionPostService.registerQuestionPost(request, member); + RegisterQuestionPostResponse response = questionPostService.registerQuestionPost(request, member); //then assertAll( @@ -64,18 +72,72 @@ void registerQuestionPost() { ); } - @DisplayName("[질문글 아이디로 질문글을 상세 조회할 수 있다.]") + @DisplayName("[질문글 아이디로 질문글을 상세 조회할 수 있다. 상호작용 이력 존재x]") @Test - void getQuestionPostById() { + void getQuestionPostById_noInteraction() { //given - QuestionPost questionPost = QuestionPostFixture.questionPost(1L); - given(questionPostRepository.findById(1L)) + Long questionPostId = 1L; + QuestionPost questionPost = QuestionPostFixture.questionPost(questionPostId); + given(questionPostRepository.findById(questionPostId)) .willReturn(Optional.of(questionPost)); + + given(interactionCountRepository.findByQuestionPostIdAndType( + questionPostId, + InteractionType.RECOMMEND + )).willReturn(Optional.empty()); + + given(interactionCountRepository.findByQuestionPostIdAndType( + questionPostId, + InteractionType.SAVED + )).willReturn(Optional.empty()); + //when - QuestionPostDetailResponse response = questionPostService.getQuestionPostById(questionPost.getId()); + QuestionPostDetailResponse response + = questionPostService.getQuestionPostById(questionPost.getId()); //then - assertThat(response.questionPostId()).isEqualTo(questionPost.getId()); + assertAll( + () -> assertThat(response.questionPostId()).isEqualTo(questionPost.getId()), + () -> assertThat(response.recommendCount()).isZero(), + () -> assertThat(response.savedCount()).isZero() + ); } + @DisplayName("[질문글 아이디로 질문글을 상세 조회할 수 있다. 상호작용 이력 존재]") + @Test + void getQuestionPostById_interaction() { + //given + Long questionPostId = 1L; + QuestionPost questionPost = QuestionPostFixture.questionPost(questionPostId); + given(questionPostRepository.findById(questionPostId)) + .willReturn(Optional.of(questionPost)); + + InteractionCount recommendCount + = InteractionCountFixture.interactionCount(InteractionType.RECOMMEND, questionPostId); + InteractionCount savedCount + = InteractionCountFixture.interactionCount(InteractionType.SAVED, questionPostId); + + given(interactionCountRepository.findByQuestionPostIdAndType( + questionPostId, + InteractionType.RECOMMEND + )).willReturn(Optional.of(recommendCount)); + + given(interactionCountRepository.findByQuestionPostIdAndType( + questionPostId, + InteractionType.SAVED + )).willReturn(Optional.of(savedCount)); + + //when + QuestionPostDetailResponse response + = questionPostService.getQuestionPostById(questionPost.getId()); + //then + assertAll( + () -> assertThat(response.questionPostId()) + .isEqualTo(questionPost.getId()), + () -> assertThat(response.recommendCount()) + .isEqualTo(recommendCount.getCount()).isEqualTo(1), + () -> assertThat(response.savedCount()) + .isEqualTo(savedCount.getCount()).isEqualTo(1) + ); + } }