Skip to content

Commit

Permalink
[feat #41] 나의활동 API (#47)
Browse files Browse the repository at this point in the history
* [feat] : MemberQueryDSL Repo 추가

* [feat] : 작성한 질문 전체 조회 응답 DTO 추가

* [feat] : MemberErrorCode 추가

* [feat] : 작성한 질문 전체 조회 컨트롤러/비즈니스 로직 작성

* [test] : 특정 회원 작성 질문 목록 검색 테스트

* [feat] : 댓글 단 질문 목록 응답 DTO 추가

* [feat] : 댓글 단 질문 목록 QueryDSL 추가

* [feat] : 댓글 단 질문 목록 에러코드 추가

* [feat] : 댓글 단 질문 전체 조회 컨트롤러/비즈니스 로직 추가

* [test] : 특정 회원 답변 작성 질문 목록 리스트 조회 테스트

* [test] 마이페이지 - 나의활동 API 통합 테스트 추가

* [feat #38] 질문글 상호작용 API (#44)

* [style] : 정적 팩토리 메서드 네이밍 변경

* [style] : enum 라벨 필드명 변경

* [feat] : 게시글 상호작용 에러코드 추가

* [feat] : postInteraction mapper 추가

* [feat] : postInteraction 상태 변경 메서드 추가

* [feat] : postInteractionCount 필드 변경 메서드 추가

* [feat] : 게시글 상호작용 응답 추가

* [feat] : 게시글 상호작용, 게시글 상호작용수 repository 추가

* [feat] : 게시글 상호작용(추천, 북마크) 활성화, 비활성화 비즈니스 로직 작성

* [feat] : 게시글 상호작용(추천, 북마크) 활성화, 비활성화 API 메서드 작성

* [feat] : 상호작용 에러 코드 추가 및 네이밍 변경

* [refactor] : 질문글 비즈니스 메서드 파라미터 변경

* [feat] : 질문글 상호작용 생성 전 검증 로직 추가

* [test] : 상호작용, 상호작용 수 fixture 추가

* [fix] : 상태변경 로직 오류 수정

* [feat] : 상태변경 에러코드 수정

* [feat] : 응답 필드에 라벨로 반환

* [test] : 상호작용 활성화/비활성화 단위 테스트

* [style] : 코드 리포멧팅

* [feat] : 컨트롤러 파라미터 어노테이션 수정

* [feat] : 게시글 상호작용 생성 시 기존에 게시글 상호작용 수 엔티티 존재하는지 검증

* [test] : 게시글 상호작용 생성 시 기존에 게시글 상호작용 수 엔티티 존재하는지 로직 추가에 대한 테스트 반영

* [test] : 이메일 중복 저장 방지를 위해 memberFixture 객체 추가

* [fix] : 도메인 비즈니스 로직 오류 수정

* [test] : 게시글 상호작용 통합 테스트 작성

* [rename] : 상호작용, 상호작용 수 엔티티명 변경

* [style] : 코드 리포멧팅

* [feat #45] 게시글 상세 조회 응답 필드 추가 (#46)

* [feat] : 질문글 등록 응답 dto 생성

* [feat] : 상세 조회 응답 dto에 추천수, 북마크수 필드 추가

* [feat] : 상세 조회 DTO, 등록 DTO 분리

* [feat] : 북마크수, 추천수 구하는 비즈니스 로직 추가

* [refactor] : fixture 메서드명 수정

* [feat] : 북마크수, 추천수 구하는 비즈니스 로직 테스트

* [feat] : API별 DTO 분리 반영

* [test] : dto 응답 필드 추가 반영

* [style] : 코드 리포멧팅

* [fix] : DTO mapper 함수명 수정

* [feat] : 엔티티 필드명 수정

* [fix] : PostInteractionCount -> InteractionCount 네이밍 변경으로 인한 오류 수정

* [test] : repoisitory 테스트 실패 수정

* [fix] : 게시글 스크랩/추천해요 총 개수 제대로 매핑되도록 쿼리 수정

* [fix] : 작성 질문/답변 단 질문 응답 DTO 스크랩/추천해요 충 개수 필드 수정

* [test] : 스크랩/추천해요 총 개수에 관한 테스트 추가

* [feat] : 답변 단 질문 목록 조회시 가장 최신 답변 1개만 가져오도록 쿼리 추가

* [test] : 답변 단 질문 목록 조회 관련 Repostiory/Controller 테스트 작성

* [fix] : 연산 타입 일치화(+1 -> +1L)

* [fix] : 질문 목록 id 값이 아닌 updatedAt() 내림차순으로 정렬 변경

---------

Co-authored-by: Son Gahyun <[email protected]>
  • Loading branch information
dudxo and hyun2371 authored Aug 16, 2024
1 parent 061f022 commit 30a7944
Show file tree
Hide file tree
Showing 10 changed files with 715 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -31,6 +35,7 @@ public class MemberController {
@GetMapping("/profile")
public ResponseEntity<MemberProfileResponse> getMemberProfile(@AuthenticationPrincipal Member member) {
MemberProfileResponse response = memberService.getMemberProfile(member);

return ResponseEntity.ok(response);
}

Expand All @@ -41,6 +46,31 @@ public ResponseEntity<MemberProfileResponse> 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<PageResponse<QuestionPostsByMemberResponse>> getQuestionPostsByMember(
@AuthenticationPrincipal Member member,
Pageable pageable) {
PageResponse<QuestionPostsByMemberResponse> response =
memberService.getQuestionPostsByMember(member, pageable);

return ResponseEntity.ok(response);
}

@Operation(summary = "댓글 단 질문 전체 조회 API", description = "댓글 단 질문을 전체 조회한다.")
@ApiResponse(useReturnTypeSchema = true)
@GetMapping("/answer-posts")
public ResponseEntity<PageResponse<AnsweredQuestionPostsByMemberResponse>> getAnsweredQuestionPostsByMember(
@AuthenticationPrincipal Member member,
Pageable pageable) {
PageResponse<AnsweredQuestionPostsByMemberResponse> response =
memberService.getAnsweredQuestionPostsByMember(member, pageable);

return ResponseEntity.ok(response);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/dnd/gongmuin/member/repository/MemberCustom.java
Original file line number Diff line number Diff line change
@@ -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<QuestionPostsByMemberResponse> getQuestionPostsByMember(Member member, Pageable pageable);

Slice<AnsweredQuestionPostsByMemberResponse> getAnsweredQuestionPostsByMember(Member member, Pageable pageable);
}
101 changes: 101 additions & 0 deletions src/main/java/com/dnd/gongmuin/member/repository/MemberCustomImpl.java
Original file line number Diff line number Diff line change
@@ -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<QuestionPostsByMemberResponse> getQuestionPostsByMember(Member member, Pageable pageable) {
QQuestionPost qp = QQuestionPost.questionPost;
QInteractionCount saved = new QInteractionCount("SAVED");
QInteractionCount recommend = new QInteractionCount("RECOMMEND");

List<QuestionPostsByMemberResponse> 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<AnsweredQuestionPostsByMemberResponse> 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<AnsweredQuestionPostsByMemberResponse> 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 <T> boolean hasNext(int pageSize, List<T> content) {
if (content.size() <= pageSize) {
return false;
}
content.remove(pageSize);
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import com.dnd.gongmuin.member.domain.Member;

public interface MemberRepository extends JpaRepository<Member, Long> {
public interface MemberRepository extends JpaRepository<Member, Long>, MemberCustom {
Optional<Member> findBySocialEmail(String socialEmail);

boolean existsByNickname(String nickname);
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/com/dnd/gongmuin/member/service/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -189,4 +195,28 @@ public MemberProfileResponse updateMemberProfile(UpdateMemberProfileRequest requ
throw new ValidationException(MemberErrorCode.UPDATE_PROFILE_FAILED);
}
}

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

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

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

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

0 comments on commit 30a7944

Please sign in to comment.