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/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)