Skip to content

Commit

Permalink
[feat #42] 스크랩 목록 조회 API (#50)
Browse files Browse the repository at this point in the history
* [feat] : 스크랩 전제 조회 관려 에러코드 추가

* [feat] : 스크랩 전체 조회 응답 DTO 추가

* [feat] : 스크랩 전체 조회 QueryDSL 추가

* [feat] : 스크랩 전체 조회 API Controller/Service 로직 추가

* [test] : InteractionFixture 정적 생성메서드 추가

* [test] : 스크랩 전체 조회 Controller/Repository 테스트 추가

* [style] : 코드 포맷팅

* [rename] : 마이페이지 게시글 관련 에러코드 통일 및 메시지 내용 변경

* [fix] : 마이페이지 게시글 조회 관련 API Path  일관성을 위한 수정

* [test] : 마이페이지 게시글 조회 관련 API 변경으로 인한 수정

* [fix] : 마이페이지 게시글 조회 시 updatedAt -> createdAt 정렬 순으로 변경

* [fix] : Coalesce를 통한 null 값 처리
  • Loading branch information
dudxo authored Aug 17, 2024
1 parent 75bde48 commit c9bdf8c
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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.BookmarksByMemberResponse;
import com.dnd.gongmuin.member.dto.response.MemberProfileResponse;
import com.dnd.gongmuin.member.dto.response.QuestionPostsByMemberResponse;
import com.dnd.gongmuin.member.service.MemberService;
Expand Down Expand Up @@ -64,7 +65,7 @@ public ResponseEntity<PageResponse<QuestionPostsByMemberResponse>> getQuestionPo

@Operation(summary = "댓글 단 질문 전체 조회 API", description = "댓글 단 질문을 전체 조회한다.")
@ApiResponse(useReturnTypeSchema = true)
@GetMapping("/answer-posts")
@GetMapping("/question-posts/answers")
public ResponseEntity<PageResponse<AnsweredQuestionPostsByMemberResponse>> getAnsweredQuestionPostsByMember(
@AuthenticationPrincipal Member member,
Pageable pageable) {
Expand All @@ -74,4 +75,16 @@ public ResponseEntity<PageResponse<AnsweredQuestionPostsByMemberResponse>> getAn
return ResponseEntity.ok(response);
}

@Operation(summary = "스크랩 질문 전체 조회 API", description = "스크랩한 질문을 전체 조회한다.")
@ApiResponse(useReturnTypeSchema = true)
@GetMapping("/question-posts/bookmarks")
public ResponseEntity<PageResponse<BookmarksByMemberResponse>> getBookmarksByMember(
@AuthenticationPrincipal Member member,
Pageable pageable) {
PageResponse<BookmarksByMemberResponse> response =
memberService.getBookmarksByMember(member, pageable);

return ResponseEntity.ok(response);
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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;

Expand All @@ -23,8 +22,8 @@ public record AnsweredQuestionPostsByMemberResponse(
@QueryProjection
public AnsweredQuestionPostsByMemberResponse(
QuestionPost questionPost,
InteractionCount savedCount,
InteractionCount recommendCount,
int savedTotalCount,
int recommendTotalCount,
Answer answer) {
this(
questionPost.getId(),
Expand All @@ -34,15 +33,11 @@ public AnsweredQuestionPostsByMemberResponse(
questionPost.getReward(),
questionPost.getUpdatedAt().toString(),
questionPost.getIsChosen(),
extractTotalCount(savedCount),
extractTotalCount(recommendCount),
savedTotalCount,
recommendTotalCount,
answer.getId(),
answer.getContent(),
answer.getUpdatedAt().toString()
);
}

private static int extractTotalCount(InteractionCount interactionCount) {
return interactionCount != null ? interactionCount.getCount() : 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.dnd.gongmuin.member.dto.response;

import com.dnd.gongmuin.question_post.domain.QuestionPost;
import com.querydsl.core.annotations.QueryProjection;

public record BookmarksByMemberResponse(
Long questionPostId,
String title,
String content,
String jobGroup,
int reward,
String updatedAt,
boolean isChosen,
int savedTotalCount,
int recommendTotalCount
) {
@QueryProjection
public BookmarksByMemberResponse(
QuestionPost questionPost,
int savedTotalCount,
int recommendTotalCount
) {
this(
questionPost.getId(),
questionPost.getTitle(),
questionPost.getContent(),
questionPost.getJobGroup().getLabel(),
questionPost.getReward(),
questionPost.getUpdatedAt().toString(),
questionPost.getIsChosen(),
savedTotalCount,
recommendTotalCount
);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
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;

Expand All @@ -19,8 +18,8 @@ public record QuestionPostsByMemberResponse(
@QueryProjection
public QuestionPostsByMemberResponse(
QuestionPost questionPost,
InteractionCount savedCount,
InteractionCount recommendCount
int savedTotalCount,
int recommendTotalCount
) {
this(
questionPost.getId(),
Expand All @@ -30,13 +29,8 @@ public QuestionPostsByMemberResponse(
questionPost.getReward(),
questionPost.getUpdatedAt().toString(),
questionPost.getIsChosen(),
extractTotalCount(savedCount),
extractTotalCount(recommendCount)
savedTotalCount,
recommendTotalCount
);
}

private static int extractTotalCount(InteractionCount interactionCount) {
return interactionCount != null ? interactionCount.getCount() : 0;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ public enum MemberErrorCode implements ErrorCode {
LOGOUT_FAILED("로그아웃을 실패했습니다.", "MEMBER_003"),
NOT_ENOUGH_CREDIT("보유한 크레딧이 부족합니다.", "MEMBER_004"),
UPDATE_PROFILE_FAILED("프로필 수정에 실패했습니다.", "MEMBER_005"),
QUESTION_POSTS_BY_MEMBER_FAILED("작성한 게시글 목록을 찾는 도중 실패했습니다.", "MEMBER_006"),
ANSWERED_QUESTION_POSTS_BY_MEMBER_FAILED("댓글 단 게시글 목록을 찾는 도중 실패했습니다.", "MEMBER_007");
QUESTION_POSTS_BY_MEMBER_FAILED("마이페이지 게시글 목록을 불러오는데 실패했습니다", "MEMBER_006");

private final String message;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@

import com.dnd.gongmuin.member.domain.Member;
import com.dnd.gongmuin.member.dto.response.AnsweredQuestionPostsByMemberResponse;
import com.dnd.gongmuin.member.dto.response.BookmarksByMemberResponse;
import com.dnd.gongmuin.member.dto.response.QuestionPostsByMemberResponse;

public interface MemberCustom {
Slice<QuestionPostsByMemberResponse> getQuestionPostsByMember(Member member, Pageable pageable);

Slice<AnsweredQuestionPostsByMemberResponse> getAnsweredQuestionPostsByMember(Member member, Pageable pageable);

Slice<BookmarksByMemberResponse> getBookmarksByMember(Member member, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
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.BookmarksByMemberResponse;
import com.dnd.gongmuin.member.dto.response.QAnsweredQuestionPostsByMemberResponse;
import com.dnd.gongmuin.member.dto.response.QBookmarksByMemberResponse;
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.QInteraction;
import com.dnd.gongmuin.post_interaction.domain.QInteractionCount;
import com.dnd.gongmuin.question_post.domain.QQuestionPost;
import com.querydsl.jpa.JPAExpressions;
Expand All @@ -32,14 +35,18 @@ public Slice<QuestionPostsByMemberResponse> getQuestionPostsByMember(Member memb
QInteractionCount recommend = new QInteractionCount("RECOMMEND");

List<QuestionPostsByMemberResponse> content = queryFactory
.select(new QQuestionPostsByMemberResponse(qp, saved, recommend))
.select(new QQuestionPostsByMemberResponse(
qp,
saved.count.coalesce(0).as("savedTotalCount"),
recommend.count.coalesce(0).as("recommendTotalCount")
))
.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())
.orderBy(qp.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1L)
.fetch();
Expand All @@ -60,7 +67,12 @@ public Slice<AnsweredQuestionPostsByMemberResponse> getAnsweredQuestionPostsByMe

List<AnsweredQuestionPostsByMemberResponse> content =
queryFactory
.select(new QAnsweredQuestionPostsByMemberResponse(qp, saved, recommend, aw1))
.select(new QAnsweredQuestionPostsByMemberResponse(
qp,
saved.count.coalesce(0).as("savedTotalCount"),
recommend.count.coalesce(0).as("recommendTotalCount"),
aw1
))
.from(qp)
.join(aw1)
.on(aw1.id.eq(
Expand All @@ -81,7 +93,7 @@ public Slice<AnsweredQuestionPostsByMemberResponse> getAnsweredQuestionPostsByMe
.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())
.orderBy(qp.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1L)
.fetch();
Expand All @@ -91,6 +103,38 @@ public Slice<AnsweredQuestionPostsByMemberResponse> getAnsweredQuestionPostsByMe
return new SliceImpl<>(content, pageable, hasNext);
}

@Override
public Slice<BookmarksByMemberResponse> getBookmarksByMember(Member member, Pageable pageable) {
QQuestionPost qp = QQuestionPost.questionPost;
QInteraction ir = QInteraction.interaction;
QInteractionCount saved = new QInteractionCount("SAVED");
QInteractionCount recommend = new QInteractionCount("RECOMMEND");

List<BookmarksByMemberResponse> content = queryFactory
.select(new QBookmarksByMemberResponse(
qp,
saved.count.coalesce(0).as("savedTotalCount"),
recommend.count.coalesce(0).as("recommendTotalCount")
))
.from(qp)
.join(ir)
.on(qp.id.eq(ir.questionPostId).and(ir.type.eq(InteractionType.SAVED)))
.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(ir.memberId.eq(member.getId()))
.orderBy(qp.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1L)
.fetch();

boolean hasNext = hasNext(pageable.getPageSize(), content);

return new SliceImpl<>(content, pageable, hasNext);
}

// .on(qp.id.eq(ir.questionPostId).and(ir.type.eq(InteractionType.SAVED)))
private <T> boolean hasNext(int pageSize, List<T> content) {
if (content.size() <= pageSize) {
return false;
Expand Down
15 changes: 14 additions & 1 deletion src/main/java/com/dnd/gongmuin/member/service/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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.BookmarksByMemberResponse;
import com.dnd.gongmuin.member.dto.response.LogoutResponse;
import com.dnd.gongmuin.member.dto.response.MemberProfileResponse;
import com.dnd.gongmuin.member.dto.response.QuestionPostsByMemberResponse;
Expand Down Expand Up @@ -216,7 +217,19 @@ public PageResponse<AnsweredQuestionPostsByMemberResponse> getAnsweredQuestionPo

return PageMapper.toPageResponse(responsePage);
} catch (Exception e) {
throw new NotFoundException(MemberErrorCode.ANSWERED_QUESTION_POSTS_BY_MEMBER_FAILED);
throw new NotFoundException(MemberErrorCode.QUESTION_POSTS_BY_MEMBER_FAILED);
}
}

public PageResponse<BookmarksByMemberResponse> getBookmarksByMember(
Member member, Pageable pageable) {
try {
Slice<BookmarksByMemberResponse> responsePage =
memberRepository.getBookmarksByMember(member, pageable);

return PageMapper.toPageResponse(responsePage);
} catch (Exception e) {
throw new NotFoundException(MemberErrorCode.QUESTION_POSTS_BY_MEMBER_FAILED);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,17 @@ public static Interaction interaction(
ReflectionTestUtils.setField(interaction, "id", 1L);
return interaction;
}

public static Interaction interaction2(
InteractionType type,
Long memberId,
Long questionPostId
) {
Interaction interaction = Interaction.of(
type,
memberId,
questionPostId
);
return interaction;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@
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.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.member.dto.request.UpdateMemberProfileRequest;
import com.dnd.gongmuin.member.repository.MemberRepository;
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.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;

Expand All @@ -45,6 +48,9 @@ class MemberControllerTest extends ApiTestSupport {
@Autowired
InteractionCountRepository interactionCountRepository;

@Autowired
InteractionRepository interactionRepository;

@AfterEach
void tearDown() {
answerRepository.deleteAll();
Expand Down Expand Up @@ -152,7 +158,7 @@ void getAnsweredQuestionPostsByMember() throws Exception {
answerRepository.save(answer2);

// when // then
mockMvc.perform(get("/api/members/answer-posts")
mockMvc.perform(get("/api/members/question-posts/answers")
.header(AUTHORIZATION, accessToken)
)
.andExpect(status().isOk())
Expand All @@ -163,4 +169,37 @@ void getAnsweredQuestionPostsByMember() throws Exception {
.andExpect(jsonPath("$.content[1].questionPostId").value(questionPost2.getId()))
.andExpect(jsonPath("$.content[1].answerId").value(answer1.getId()));
}

@DisplayName("로그인 된 회원의 스크랩 질문을 전체 조회한다.")
@Test
void getBookmarksByMember() 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));

Interaction interaction1 = InteractionFixture.interaction2(InteractionType.SAVED, loginMember.getId(),
questionPost1.getId());
Interaction interaction3 = InteractionFixture.interaction2(InteractionType.RECOMMEND, loginMember.getId(),
questionPost2.getId());
Interaction interaction2 = InteractionFixture.interaction2(InteractionType.SAVED, loginMember.getId(),
questionPost3.getId());
Interaction interaction4 = InteractionFixture.interaction2(InteractionType.RECOMMEND, loginMember.getId(),
questionPost3.getId());
interactionRepository.saveAll(List.of(interaction1, interaction2, interaction3, interaction4));

// when // then
mockMvc.perform(get("/api/members/question-posts/bookmarks")
.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[1].questionPostId").value(questionPost1.getId()));
}
}
Loading

0 comments on commit c9bdf8c

Please sign in to comment.