Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] 무한스크롤의 중복 컨텐츠 문제 해결 #192

Merged
merged 7 commits into from
Oct 1, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ public BaseResponse<SliceResponse<AnswerInfo>> getAnswerList(
@AuthMember Member member,
@Parameter(description = "질문 식별자")
@PathVariable(value = "questionId") Long questionId,
@Parameter(description = "Pull to Refresh 후 마지막 index")
@RequestParam(required = false) Long threshold,
@Parameter(description = "조회할 페이지 번호<br>0부터 시작")
@RequestParam(defaultValue = "0", required = false) Integer pageNumber,
@Parameter(description = "조회할 페이지 크기")
@RequestParam(defaultValue = "1000", required = false) Integer pageSize) {
return BaseResponse.onSuccess(answerService.getAnswerList(member.getId(), questionId, PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))));
return BaseResponse.onSuccess(answerService.getAnswerList(member.getId(), questionId, threshold, PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))));
}

@Operation(summary = "답변 수정 API", description = " 답변 수정 API 입니다." +
Expand Down Expand Up @@ -74,21 +76,25 @@ public BaseResponse<AnswerResponse.AnswerLike> toggleAnswerHeart(@AuthMember Mem
@GetMapping
public BaseResponse<SliceResponse<MemberAnswerInfo>> getMemberAnswer(
@AuthMember Member member,
@Parameter(description = "Pull to Refresh 후 마지막 index")
@RequestParam(required = false) Long threshold,
@Parameter(description = "조회할 페이지 번호<br>0부터 시작")
@RequestParam(defaultValue = "0", required = false) Integer pageNumber,
@Parameter(description = "조회할 페이지 크기")
@RequestParam(defaultValue = "1000", required = false) Integer pageSize) {
return BaseResponse.onSuccess(answerService.getMemberAnswer(member, PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))));
return BaseResponse.onSuccess(answerService.getMemberAnswer(member, threshold, PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))));
}

@Operation(summary = "좋아한 답변 조회 API", description = " 좋아한 답변 조회 API 입니다.")
@GetMapping("/heart")
public BaseResponse<SliceResponse<MemberAnswerInfo>> getMemberHeartAnswer(
@AuthMember Member member,
@Parameter(description = "Pull to Refresh 후 마지막 index")
@RequestParam(required = false) Long threshold,
@Parameter(description = "조회할 페이지 번호<br>0부터 시작")
@RequestParam(defaultValue = "0", required = false) Integer pageNumber,
@Parameter(description = "조회할 페이지 크기")
@RequestParam(defaultValue = "1000", required = false) Integer pageSize) {
return BaseResponse.onSuccess(answerService.getMemberHeartAnswer(member, PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))));
return BaseResponse.onSuccess(answerService.getMemberHeartAnswer(member, threshold, PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,16 @@
public interface AnswerRepository extends JpaRepository<Answer, Long> {
Optional<Answer> findById(Long answerId);

Slice<Answer> findByIdIn(Set<Long> answerIds, Pageable pageable);
Slice<Answer> findByIdInAndIdIsLessThanEqual(Set<Long> answerIds, Long lastIndex, Pageable pageable);

boolean existsByQuestionAndMember(Question question, Member member);

@Query("SELECT a AS answer, (r IS NOT NULL) AS isReported " +
"FROM Answer a " +
"LEFT JOIN " +
"Report r ON r.answer = a " +
"WHERE a.question.id = :questionId")
Slice<AnswerInfoInterface> findByQuestion(
@Param("questionId") Long questionId,
Pageable pageable);
"WHERE a.id <= :lastIndex AND a.question.id = :questionId")
Slice<AnswerInfoInterface> findByQuestion(@Param("questionId") Long questionId, Long lastIndex, Pageable pageable);

@Query("SELECT a FROM Answer a WHERE a.member = :member and a.deletedAt is null")
Slice<Answer> findByMember(@Param("member") Member member, Pageable pageable);
Slice<Answer> findByMemberAndIdIsLessThanEqual(@Param("member") Member member, Long lastIndex, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import com.server.capple.global.common.SliceResponse;
import org.springframework.data.domain.Pageable;


public interface AnswerService {
AnswerResponse.AnswerId createAnswer(Member member, Long questionId, AnswerRequest request);

Expand All @@ -21,9 +20,9 @@ public interface AnswerService {

AnswerResponse.AnswerLike toggleAnswerHeart(Member loginMember, Long answerId);

SliceResponse<AnswerInfo> getAnswerList(Long memberId, Long questionId, Pageable pageable);
SliceResponse<AnswerInfo> getAnswerList(Long memberId, Long questionId, Long lastIndex, Pageable pageable);

SliceResponse<MemberAnswerInfo> getMemberAnswer(Member member, Pageable pageable);
SliceResponse<MemberAnswerInfo> getMemberAnswer(Member member, Long lastIndex, Pageable pageable);

SliceResponse<MemberAnswerInfo> getMemberHeartAnswer(Member member, Pageable pageable);
SliceResponse<MemberAnswerInfo> getMemberHeartAnswer(Member member, Long lastIndex, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,16 @@ public AnswerResponse.AnswerId deleteAnswer(Member loginMember, Long answerId) {
@Override
public AnswerLike toggleAnswerHeart(Member loginMember, Long answerId) {
Member member = memberService.findMember(loginMember.getId());

answerRepository.findById(answerId).orElseThrow(() -> new RestApiException(AnswerErrorCode.ANSWER_NOT_FOUND));
Boolean isLiked = answerHeartRedisRepository.toggleAnswerHeart(member.getId(), answerId);
return new AnswerLike(answerId, isLiked);
}

@Override
public SliceResponse<AnswerInfo> getAnswerList(Long memberId, Long questionId, Pageable pageable) {
Slice<AnswerInfoInterface> answerInfoSliceInterface = answerRepository.findByQuestion(questionId, pageable);
public SliceResponse<AnswerInfo> getAnswerList(Long memberId, Long questionId, Long lastIndex, Pageable pageable) {
lastIndex = getLastIndex(lastIndex);
Slice<AnswerInfoInterface> answerInfoSliceInterface = answerRepository.findByQuestion(questionId, lastIndex, pageable);
lastIndex = getLastIndexFromAnswerInfoInterface(lastIndex, answerInfoSliceInterface);
return SliceResponse.toSliceResponse(answerInfoSliceInterface, answerInfoSliceInterface.getContent().stream().map(
answerInfoDto -> answerMapper.toAnswerInfo(
answerInfoDto.getAnswer(),
Expand All @@ -102,33 +104,37 @@ public SliceResponse<AnswerInfo> getAnswerList(Long memberId, Long questionId, P
answerHeartRedisRepository.isMemberLikedAnswer(memberId, answerInfoDto.getAnswer().getId()),
answerInfoDto.getAnswer().getMember().getId().equals(memberId)
)
).toList(), null);
).toList(), lastIndex.toString(), null);
}

// 유저가 작성한 답변 조회
@Override
public SliceResponse<MemberAnswerInfo> getMemberAnswer(Member member, Pageable pageable) {
Slice<Answer> answerSlice = answerRepository.findByMember(member, pageable);
public SliceResponse<MemberAnswerInfo> getMemberAnswer(Member member, Long lastIndex, Pageable pageable) {
lastIndex = getLastIndex(lastIndex);
Slice<Answer> answerSlice = answerRepository.findByMemberAndIdIsLessThanEqual(member, lastIndex, pageable);
lastIndex = getLastIndexFromAnswer(lastIndex, answerSlice);
return SliceResponse.toSliceResponse(
answerSlice, answerSlice.getContent().stream()
.map(answer -> answerMapper.toMemberAnswerInfo(
answer,
answerHeartRedisRepository.getAnswerHeartsCount(answer.getId()),
answerHeartRedisRepository.isMemberLikedAnswer(member.getId(), answer.getId())
)).toList(), null
)).toList(), lastIndex.toString(), null
);
}

// 유저가 좋아한 답변 조회
// 유저가 좋아한 답변 조회 //TODO 좋아요니까 좋아요한 순으로 정렬해야할거같은데 Answer의 createAt으로 하고 있음
@Override
public SliceResponse<MemberAnswerInfo> getMemberHeartAnswer(Member member, Pageable pageable) {
Slice<Answer> answerSlice = answerRepository.findByIdIn(answerHeartRedisRepository.getMemberHeartsAnswer(member.getId()), pageable);
public SliceResponse<MemberAnswerInfo> getMemberHeartAnswer(Member member, Long lastIndex, Pageable pageable) {
lastIndex = getLastIndex(lastIndex);
Slice<Answer> answerSlice = answerRepository.findByIdInAndIdIsLessThanEqual(answerHeartRedisRepository.getMemberHeartsAnswer(member.getId()), lastIndex, pageable);
lastIndex = getLastIndexFromAnswer(lastIndex, answerSlice);
return SliceResponse.toSliceResponse(answerSlice, answerSlice.getContent().stream()
.map(answer -> answerMapper.toMemberAnswerInfo(
answer,
answerHeartRedisRepository.getAnswerHeartsCount(answer.getId()),
answerHeartRedisRepository.isMemberLikedAnswer(member.getId(), answer.getId())
)).toList(), null
)).toList(), lastIndex.toString(), null
);
}

Expand All @@ -146,4 +152,16 @@ public Answer findAnswer(Long answerId) {
() -> new RestApiException(AnswerErrorCode.ANSWER_NOT_FOUND)
);
}

private Long getLastIndex(Long lastIndex) {
return lastIndex == null ? Long.MAX_VALUE : lastIndex;
}

private Long getLastIndexFromAnswerInfoInterface(Long lastIndex, Slice<AnswerInfoInterface> answerInfoSliceInterface) {
return lastIndex == Long.MAX_VALUE ? answerInfoSliceInterface.stream().map(AnswerInfoInterface::getAnswer).map(Answer::getId).max(Long::compareTo).get() : lastIndex;
}

private Long getLastIndexFromAnswer(Long lastIndex, Slice<Answer> answerSlice) {
return lastIndex == Long.MAX_VALUE ? answerSlice.stream().map(Answer::getId).max(Long::compareTo).get() : lastIndex;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.server.capple.global.common.BaseResponse;
import com.server.capple.global.common.SliceResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand Down Expand Up @@ -46,9 +47,11 @@ private BaseResponse<BoardId> createBoard(@AuthMember Member member,
private BaseResponse<SliceResponse<BoardInfo>> getBoardsByBoardTypeWithRedis(
@AuthMember Member member,
@RequestParam(name = "boardType", required = false) BoardType boardType,
@Parameter(description = "Pull to Refresh 후 마지막 index")
@RequestParam(required = false) Long threshold,
@RequestParam(defaultValue = "0", required = false) Integer pageNumber, @RequestParam(defaultValue = "1000", required = false) Integer pageSize
) {
return BaseResponse.onSuccess(boardService.getBoardsByBoardTypeWithRedis(member, boardType, PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))));
return BaseResponse.onSuccess(boardService.getBoardsByBoardTypeWithRedis(member, boardType, threshold, PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))));
}

@Operation(summary = "카테고리별 게시글 조회", description = "카테고리별 게시글을 조회합니다.")
Expand All @@ -59,9 +62,11 @@ private BaseResponse<SliceResponse<BoardInfo>> getBoardsByBoardTypeWithRedis(
private BaseResponse<SliceResponse<BoardInfo>> getBoardsByBoardType(
@AuthMember Member member,
@RequestParam(name = "boardType", required = false) BoardType boardType,
@Parameter(description = "Pull to Refresh 후 마지막 index")
@RequestParam(required = false) Long threshold,
@RequestParam(defaultValue = "0", required = false) Integer pageNumber, @RequestParam(defaultValue = "1000", required = false) Integer pageSize
) {
return BaseResponse.onSuccess(boardService.getBoardsByBoardType(member, boardType, PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))));
return BaseResponse.onSuccess(boardService.getBoardsByBoardType(member, boardType, threshold, PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))));
}

@Operation(summary = "게시글 검색 API", description = "게시글을 검색합니다. 자유게시판에서만 검색이 가능합니다.")
Expand All @@ -71,9 +76,11 @@ private BaseResponse<SliceResponse<BoardInfo>> getBoardsByBoardType(
@GetMapping("/search")
private BaseResponse<SliceResponse<BoardInfo>> searchBoardsByKeyword(
@AuthMember Member member, @RequestParam(name = "keyword") String keyword,
@Parameter(description = "Pull to Refresh 후 마지막 index")
@RequestParam(required = false) Long threshold,
@RequestParam(defaultValue = "0", required = false) Integer pageNumber,
@RequestParam(defaultValue = "1000", required = false) Integer pageSize) {
return BaseResponse.onSuccess(boardService.searchBoardsByKeyword(member, keyword, PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))));
return BaseResponse.onSuccess(boardService.searchBoardsByKeyword(member, keyword, threshold, PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))));
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,31 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface BoardRepository extends JpaRepository<Board, Long> {

@Query("SELECT DISTINCT b AS board, " +
"(CASE WHEN bh.isLiked = TRUE THEN TRUE ELSE FALSE END) AS isLike, " +
"(CASE WHEN b.writer = :member THEN TRUE ELSE FALSE END) AS isMine " +
"FROM Board b " +
"LEFT JOIN BoardHeart bh ON b = bh.board AND bh.member = :member " +
"WHERE :boardType IS NULL OR b.boardType = :boardType")
Slice<BoardInfoInterface> findBoardInfosByMemberAndBoardType(Member member, BoardType boardType, Pageable pageable);
"WHERE (:boardType IS NULL OR b.boardType = :boardType) AND b.id <= :lastIndex")
Slice<BoardInfoInterface> findBoardInfosByMemberAndBoardTypeAndIdIsLessThanEqual(Member member, BoardType boardType, Long lastIndex, Pageable pageable);

@Query("SELECT DISTINCT b AS board, " +
"(CASE WHEN bh.isLiked = TRUE THEN TRUE ELSE FALSE END) AS isLike, " +
"(CASE WHEN b.writer = :member THEN TRUE ELSE FALSE END) AS isMine " +
"FROM Board b " +
"LEFT JOIN BoardHeart bh ON b = bh.board AND bh.member = :member " +
"WHERE b.content LIKE %:keyword% AND b.boardType = 0 ORDER BY b.createdAt DESC") //FREETYPE = 0
Slice<BoardInfoInterface> findBoardInfosByMemberAndKeyword(Member member, String keyword, Pageable pageable);
"WHERE b.id <= :lastIndex AND b.content LIKE %:keyword% AND b.boardType = 0") //FREETYPE = 0
Slice<BoardInfoInterface> findBoardInfosByMemberAndKeywordAndIdIsLessThanEqual(Member member, String keyword, Long lastIndex, Pageable pageable);


//redis 성능 테스트용
@Query("SELECT DISTINCT b AS board, " +
"(CASE WHEN b.writer = :member THEN TRUE ELSE FALSE END) AS isMine " +
"FROM Board b " +
"WHERE :boardType IS NULL OR b.boardType = :boardType ORDER BY b.createdAt DESC")
Slice<BoardInfoInterface> findBoardInfosForRedis(Member member, BoardType boardType, Pageable pageable);
"WHERE (:boardType IS NULL OR b.boardType = :boardType) AND b.id <= :lastIndex")
Slice<BoardInfoInterface> findBoardInfosForRedisAndIdIsLessThanEqual(Member member, BoardType boardType, Long lastIndex, Pageable pageable);

@Query("SELECT COUNT(b) FROM Board b")
Integer getBoardCount();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
public interface BoardService {
BoardId createBoard(Member member, BoardType boardType, String content);

SliceResponse<BoardInfo> getBoardsByBoardTypeWithRedis(Member member, BoardType boardType, Pageable pageable);
SliceResponse<BoardInfo> getBoardsByBoardTypeWithRedis(Member member, BoardType boardType, Long lastIndex, Pageable pageable);

SliceResponse<BoardInfo> getBoardsByBoardType(Member member, BoardType boardType, Pageable pageable);
SliceResponse<BoardInfo> getBoardsByBoardType(Member member, BoardType boardType, Long lastIndex, Pageable pageable);

SliceResponse<BoardInfo> searchBoardsByKeyword(Member member, String keyword, Pageable pageable);
SliceResponse<BoardInfo> searchBoardsByKeyword(Member member, String keyword, Long lastIndex, Pageable pageable);

BoardId deleteBoard(Member member, Long boardId);

Expand Down
Loading
Loading