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: offset 기반 리크루트 목록 조회 추가 #282

Merged
merged 10 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.ssafy.ssafsound.domain.recruit.service.RecruitService;
import com.ssafy.ssafsound.global.common.response.EnvelopeResponse;
import lombok.RequiredArgsConstructor;

import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.*;

Expand Down Expand Up @@ -57,23 +58,30 @@ public EnvelopeResponse<Void> deleteRecruit(@PathVariable Long recruitId, @Authe
return EnvelopeResponse.<Void>builder().build();
}

@GetMapping
public EnvelopeResponse<GetRecruitsResDto> getRecruits(GetRecruitsReqDto getRecruitsReqDto, Pageable pageable, @Authentication AuthenticatedMember memberInfo) {
return EnvelopeResponse.<GetRecruitsResDto>builder()
.data(recruitService.getRecruits(getRecruitsReqDto, pageable, memberInfo.getMemberId()))
@GetMapping("/cursor")
public EnvelopeResponse<GetRecruitsCursorResDto> getRecruitsByCursor(GetRecruitsReqDto getRecruitsReqDto, Pageable pageable, @Authentication AuthenticatedMember memberInfo) {
return EnvelopeResponse.<GetRecruitsCursorResDto>builder()
.data(recruitService.getRecruitsByCursor(getRecruitsReqDto, pageable, memberInfo.getMemberId()))
.build();
}

@GetMapping("/offset")
public EnvelopeResponse<GetRecruitOffsetResDto> getRecruitsByOffset(GetRecruitsReqDto getRecruitsReqDto, Pageable pageable, @Authentication AuthenticatedMember memberInfo) {
return EnvelopeResponse.<GetRecruitOffsetResDto>builder()
.data(recruitService.getRecruitsByOffset(getRecruitsReqDto, pageable, memberInfo.getMemberId()))
.build();
}

@GetMapping("/my-scrap")
public EnvelopeResponse<GetRecruitsResDto> getScrapedRecruits(Long cursor, Pageable pageable, @Authentication AuthenticatedMember memberInfo) {
return EnvelopeResponse.<GetRecruitsResDto>builder()
public EnvelopeResponse<GetRecruitsCursorResDto> getScrapedRecruits(Long cursor, Pageable pageable, @Authentication AuthenticatedMember memberInfo) {
return EnvelopeResponse.<GetRecruitsCursorResDto>builder()
.data(recruitService.getScrapedRecruits(memberInfo.getMemberId(), cursor, pageable))
.build();
}

@GetMapping("/joined")
public EnvelopeResponse<GetRecruitsResDto> getMemberJoinedRecruits(GetMemberJoinRecruitsReqDto getMemberJoinRecruitsReqDto, @Authentication AuthenticatedMember memberInfo) {
return EnvelopeResponse.<GetRecruitsResDto>builder()
public EnvelopeResponse<GetRecruitsCursorResDto> getMemberJoinedRecruits(GetMemberJoinRecruitsReqDto getMemberJoinRecruitsReqDto, @Authentication AuthenticatedMember memberInfo) {
return EnvelopeResponse.<GetRecruitsCursorResDto>builder()
.data(recruitService.getMemberJoinRecruits(getMemberJoinRecruitsReqDto, memberInfo.getMemberId()))
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.ssafy.ssafsound.domain.recruit.dto;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.ssafy.ssafsound.domain.recruit.domain.Recruit;
import lombok.Builder;
import lombok.Getter;
import org.springframework.data.domain.Page;

import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

@Getter
@Builder
public class GetRecruitOffsetResDto implements AddParticipantDto {
private List<RecruitElement> recruits;
private int currentPage;
private int totalPageCount;
@JsonIgnore
public List<Long> getRecruitsId() {
return recruits.stream().map(RecruitElement::getRecruitId).collect(Collectors.toList());
}

@JsonIgnore
public Map<Long, Map<String, RecruitParticipant>> getRecruitParticipantMapByRecruitIdAndRecruitType() {
Map<Long, Map<String, RecruitParticipant>> result = new TreeMap<>();
for(RecruitElement recruitElement: recruits) {
result.put(recruitElement.getRecruitId(), recruitElement.getRecruitParticipantMap());
}
return result;
}

public static GetRecruitOffsetResDto fromPageAndMemberId(Page<Recruit> recruitPages, Long memberId) {
List<RecruitElement> recruits = recruitPages.getContent()
.stream()
.map((recruit -> RecruitElement.fromRecruitAndLoginMemberId(recruit, memberId)))
.collect(Collectors.toList());

return GetRecruitOffsetResDto.builder()
.recruits(recruits)
.currentPage(recruitPages.getNumber())
.totalPageCount(recruitPages.getTotalPages())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

@Getter
@Builder
public class GetRecruitsResDto implements AddParticipantDto {
public class GetRecruitsCursorResDto implements AddParticipantDto {
private List<RecruitElement> recruits;
private Long nextCursor;
private Boolean isLast;
Expand All @@ -32,14 +32,14 @@ public Map<Long, Map<String, RecruitParticipant>> getRecruitParticipantMapByRecr
return result;
}

public static GetRecruitsResDto fromPageAndMemberId(Slice<Recruit> sliceRecruit, Long memberId) {
public static GetRecruitsCursorResDto fromPageAndMemberId(Slice<Recruit> sliceRecruit, Long memberId) {
List<RecruitElement> recruits = sliceRecruit.toList()
.stream()
.map((recruit -> RecruitElement.fromRecruitAndLoginMemberId(recruit, memberId)))
.collect(Collectors.toList());
Long nextCursor = recruits.isEmpty() ? -1L : recruits.get(recruits.size()-1).getRecruitId();

return GetRecruitsResDto.builder()
return GetRecruitsCursorResDto.builder()
.recruits(recruits)
.nextCursor(nextCursor)
.isLast(sliceRecruit.isLast())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
@Getter
@Builder
public class GetRecruitsReqDto {
private Long cursor;
private Integer next;

@NotBlank
private String category;
Comment on lines 12 to 16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Cursor 기반일때는 Long타입일텐데 Integer로 받아도 괜찮을까요?
  2. Cursor를 받을때는 디폴트가 -1, page를 받을때는 디폴트가 1로
    Pageble 또는 PageRequest의 page는 0부터 시작이니까 1을 빼주는 로직이 들어가야 할 것 같은데 cursor기반과 offset 기반의 페이지네이션을 하나의 dto로 관리하게 되면 문제가 발생하지 않을까 합니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Long.valueOf로 해당 부분을 감싸서 처리하고 있으므로 별도의 에러는 발생하지 않습니다.
    -> Integer 범위를 넘어설 정도로 게시글이 등록되는 경우 문제가 되겠지만, 현실적으로 불가능한 수치이므로 문제가 없다고 생각해요.

  2. Cursor
    -> PageRequest로 만들때, 해당 부분을 별도 처리하도록 수정하겠습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프론트 요청 사항으로 해당 부분 cursor, page로 스타일 일관되게 맞춘후, 해당 부분 인터페이스로 따로 빼서 쿼리 공유해서 쓸수 있게 해볼게요.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
import com.ssafy.ssafsound.domain.recruit.domain.Recruit;
import com.ssafy.ssafsound.domain.recruit.dto.AppliedRecruit;
import com.ssafy.ssafsound.domain.recruit.dto.GetRecruitsReqDto;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;

public interface RecruitDynamicQueryRepository {
Slice<Recruit> findRecruitByGetRecruitsReqDto(GetRecruitsReqDto dto, Pageable pageable);
Slice<Recruit> findRecruitSliceByGetRecruitsReqDto(GetRecruitsReqDto dto, Pageable pageable);
Page<Recruit> findRecruitPageByGetRecruitsReqDto(GetRecruitsReqDto dto, Pageable pageable);
Slice<Recruit> findMemberJoinRecruitWithCursorAndPageable(Long memberId, String category, Long cursorId, Pageable pageable);
Slice<Recruit> findMemberScrapRecruits(Long memberId, Long cursorId, Pageable pageable);

Slice<AppliedRecruit> findMemberAppliedRecruits(Long memberId, Long cursor, String category, String matchStatus, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@
import lombok.RequiredArgsConstructor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.data.domain.*;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;

Expand All @@ -44,63 +43,100 @@ public class RecruitDynamicQueryRepositoryImpl implements RecruitDynamicQueryRep
private final JPAQueryFactory jpaQueryFactory;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JPAQueryFactory 가져다 쓰면 따로 RecruitDynamicQueryRepository를 통해 Override 할 필요가 없지 않을까 해서 의견 남겨봅니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

배포후 제거해도 문제가없으면 삭제할게요.


@Override
public Slice<Recruit> findRecruitByGetRecruitsReqDto(GetRecruitsReqDto dto, Pageable pageable) {
// cursor base pagination (value -1 or null ignore search condition)
Long cursor = dto.getCursor();

// recruit category (STUDY | PROJECT)
BooleanExpression categoryEq = dto.getCategory() == null ? null : recruit.category.eq(Category.valueOf(dto.getCategory().toUpperCase()));

// recruit title contains search keyword
String keyword = dto.getKeyword();
BooleanExpression titleEq = StringUtils.hasText(keyword) ? recruit.title.contains(keyword) : null;

JPAQuery<Recruit> recruitDynamicQuery = jpaQueryFactory.selectFrom(recruit)
.where(recruitIdLtThanCursor(cursor), categoryEq, titleEq);

// recruit skill
List<String> skills = dto.getSkills();
if(skills!=null && skills.size() > 0) {
String metaDataType = MetaDataType.SKILL.name();
List<MetaData> containSkills = skills.stream()
.map(skillName->metaDataConsumer.getMetaData(metaDataType, skillName))
.collect(Collectors.toList());

JPQLQuery<Long> recruitSkillContainRecruitIds = JPAExpressions
.select(recruitSkill.recruit.id)
.from(recruitSkill)
.innerJoin(recruitSkill.recruit, recruit)
.where(recruitSkill.skill.in(containSkills));

recruitDynamicQuery.where(recruit.id.in(recruitSkillContainRecruitIds));
}

// recruit types limitation
List<String> recruitTypes = dto.getRecruitTypes();
if(dto.getCategory() != null && dto.getCategory().toUpperCase().equals(Category.PROJECT.name()) && recruitTypes!=null && !recruitTypes.isEmpty()) {
String metaDataType = MetaDataType.RECRUIT_TYPE.name();
List<MetaData> containRecruitTypes = recruitTypes.stream()
.map(recruitType->metaDataConsumer.getMetaData(metaDataType, recruitType))
.collect(Collectors.toList());

JPQLQuery<Long> limitationContainRecruitIds = JPAExpressions
.select(recruitLimitation.recruit.id)
.from(recruitLimitation)
.innerJoin(recruitLimitation.recruit, recruit)
.where(recruitLimitation.type.in(containRecruitTypes));

recruitDynamicQuery.where(recruit.id.in(limitationContainRecruitIds));
}

public Slice<Recruit> findRecruitSliceByGetRecruitsReqDto(GetRecruitsReqDto dto, Pageable pageable) {
JPAQuery<Recruit> recruitDynamicQuery = findRecruitByGetRecruitsReqDto(dto);
Integer cursor = dto.getNext();
List<Recruit> recruits = recruitDynamicQuery
.where(recruit.finishedRecruit.eq(dto.isFinished()))
.where(recruitIdLtThanCursor(Long.valueOf(cursor)))
.limit(pageable.getPageSize()+1)
.orderBy(recruit.id.desc())
.fetch();
boolean hasNext = pageable.isPaged() && recruits.size() > pageable.getPageSize();
return new SliceImpl<>(hasNext ? recruits.subList(0, pageable.getPageSize()) : recruits, pageable, hasNext);
}

@Override
public Page<Recruit> findRecruitPageByGetRecruitsReqDto(GetRecruitsReqDto dto, Pageable pageable) {
JPAQuery<Recruit> recruitDynamicQuery = findRecruitByGetRecruitsReqDto(dto);
List<Recruit> recruits = recruitDynamicQuery
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(recruit.id.desc())
.fetch();

JPAQuery<Long> countQuery = jpaQueryFactory
.select(recruit.count())
.from(recruit)
.where(
recruitCategoryEq(dto.getCategory()),
recruitTitleEq(dto.getKeyword()),
recruit.finishedRecruit.eq(dto.isFinished()),
recruitTypeContains(dto.getCategory(), dto.getRecruitTypes()),
recruitSkillContains(dto.getSkills())
);

return PageableExecutionUtils.getPage(recruits, pageable, countQuery::fetchOne);
}

private JPAQuery<Recruit> findRecruitByGetRecruitsReqDto(GetRecruitsReqDto dto) {
JPAQuery<Recruit> recruitDynamicQuery = jpaQueryFactory.selectFrom(recruit);
recruitDynamicQuery
.where(
recruitCategoryEq(dto.getCategory()),
recruitTitleEq(dto.getKeyword()),
recruit.finishedRecruit.eq(dto.isFinished()),
recruitTypeContains(dto.getCategory(), dto.getRecruitTypes()),
recruitSkillContains(dto.getSkills())
);
return recruitDynamicQuery;
}

private BooleanExpression recruitCategoryEq(String category) {
return category == null ? null : recruit.category.eq(Category.valueOf(category.toUpperCase()));
}
private BooleanExpression recruitTitleEq(String keyword) {
return StringUtils.hasText(keyword) ? recruit.title.contains(keyword) : null;
}
private BooleanExpression recruitTypeContains(String category, List<String> recruitTypes) {
boolean isNotProject = category == null || category.toUpperCase().equals(Category.PROJECT.name());
boolean isEmpty = recruitTypes == null || recruitTypes.isEmpty();
if(isNotProject || isEmpty) {
return null;
}

String metaDataType = MetaDataType.RECRUIT_TYPE.name();
List<MetaData> containRecruitTypes = recruitTypes.stream()
.map(recruitType->metaDataConsumer.getMetaData(metaDataType, recruitType))
.collect(Collectors.toList());

JPQLQuery<Long> limitationContainRecruitIds = JPAExpressions
.select(recruitLimitation.recruit.id)
.from(recruitLimitation)
.innerJoin(recruitLimitation.recruit, recruit)
.where(recruitLimitation.type.in(containRecruitTypes));

return recruit.id.in(limitationContainRecruitIds);
}

private BooleanExpression recruitSkillContains(List<String> skills) {
boolean isEmpty = skills==null || skills.isEmpty();
if(isEmpty) {
return null;
}

String metaDataType = MetaDataType.SKILL.name();
List<MetaData> containSkills = skills.stream()
.map(skillName->metaDataConsumer.getMetaData(metaDataType, skillName))
.collect(Collectors.toList());

JPQLQuery<Long> recruitSkillContainRecruitIds = JPAExpressions
.select(recruitSkill.recruit.id)
.from(recruitSkill)
.innerJoin(recruitSkill.recruit, recruit)
.where(recruitSkill.skill.in(containSkills));
return recruit.id.in(recruitSkillContainRecruitIds);
}

@Override
public Slice<Recruit> findMemberJoinRecruitWithCursorAndPageable(Long memberId, String category, Long cursor, Pageable pageable) {
BooleanExpression applicationRecruitCategoryEq =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.ssafy.ssafsound.global.common.exception.GlobalErrorInfo;
import com.ssafy.ssafsound.global.common.exception.ResourceNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
Expand Down Expand Up @@ -129,20 +130,32 @@ public void deleteRecruit(Long recruitId, Long memberId) {
}

@Transactional(readOnly = true)
public GetRecruitsResDto getRecruits(GetRecruitsReqDto getRecruitsReqDto, Pageable pageable, Long loginMemberId) {
// 페이지네이션 조건에 따라 프로젝트/스터디 글 목록을 조회한다.
Slice<Recruit> recruitPages = recruitRepository.findRecruitByGetRecruitsReqDto(getRecruitsReqDto, pageable);
GetRecruitsResDto recruitsResDto = GetRecruitsResDto.fromPageAndMemberId(recruitPages, loginMemberId);
public GetRecruitsCursorResDto getRecruitsByCursor(GetRecruitsReqDto getRecruitsReqDto, Pageable pageable, Long loginMemberId) {
Slice<Recruit> recruitPages = recruitRepository.findRecruitSliceByGetRecruitsReqDto(getRecruitsReqDto, pageable);
GetRecruitsCursorResDto recruitsResDto = GetRecruitsCursorResDto.fromPageAndMemberId(recruitPages, loginMemberId);
if(!recruitsResDto.getRecruits().isEmpty()) {
addRecruitParticipants(recruitsResDto);
}
return recruitsResDto;
}

@Transactional(readOnly = true)
public GetRecruitsResDto getScrapedRecruits(Long memberId, Long cursor, Pageable pageable) {
public GetRecruitOffsetResDto getRecruitsByOffset(GetRecruitsReqDto getRecruitsReqDto, Pageable pageable, Long loginMemberId) {
Integer pageOffset = getRecruitsReqDto.getNext();

PageRequest pageRequest = PageRequest.of(pageOffset== null || pageOffset == -1 ? 0 : pageOffset, pageable.getPageSize());
Page<Recruit> recruitPages = recruitRepository.findRecruitPageByGetRecruitsReqDto(getRecruitsReqDto, pageRequest);
GetRecruitOffsetResDto recruitsResDto = GetRecruitOffsetResDto.fromPageAndMemberId(recruitPages, loginMemberId);
if(!recruitsResDto.getRecruits().isEmpty()) {
addRecruitParticipants(recruitsResDto);
}
return recruitsResDto;
}

@Transactional(readOnly = true)
public GetRecruitsCursorResDto getScrapedRecruits(Long memberId, Long cursor, Pageable pageable) {
Slice<Recruit> recruitPages = recruitRepository.findMemberScrapRecruits(memberId, cursor, pageable);
GetRecruitsResDto recruitsResDto = GetRecruitsResDto.fromPageAndMemberId(recruitPages, memberId);
GetRecruitsCursorResDto recruitsResDto = GetRecruitsCursorResDto.fromPageAndMemberId(recruitPages, memberId);
return recruitsResDto;
}

Expand All @@ -158,7 +171,7 @@ public void expiredRecruit(Long recruitId, Long memberId) {
}

@Transactional(readOnly = true)
public GetRecruitsResDto getMemberJoinRecruits(GetMemberJoinRecruitsReqDto recruitsReqDto, Long loginMemberId) {
public GetRecruitsCursorResDto getMemberJoinRecruits(GetMemberJoinRecruitsReqDto recruitsReqDto, Long loginMemberId) {
Long memberId = recruitsReqDto.getMemberId();
Member member = memberRepository.findById(memberId).orElseThrow(()->new ResourceNotFoundException(GlobalErrorInfo.NOT_FOUND));

Expand All @@ -167,7 +180,7 @@ public GetRecruitsResDto getMemberJoinRecruits(GetMemberJoinRecruitsReqDto recru
}

Slice<Recruit> recruitPages = recruitRepository.findMemberJoinRecruitWithCursorAndPageable(memberId, recruitsReqDto.getCategory(), recruitsReqDto.getCursor(), PageRequest.ofSize(recruitsReqDto.getSize()));
GetRecruitsResDto recruitsResDto = GetRecruitsResDto.fromPageAndMemberId(recruitPages, loginMemberId);
GetRecruitsCursorResDto recruitsResDto = GetRecruitsCursorResDto.fromPageAndMemberId(recruitPages, loginMemberId);
if(!recruitsResDto.getRecruits().isEmpty()) {
addRecruitParticipants(recruitsResDto);
}
Expand Down
Loading
Loading