From e318f82463e0e1e48e90a8bd17b05efd66bf2d2c Mon Sep 17 00:00:00 2001 From: khs960616 Date: Mon, 23 Oct 2023 02:44:36 +0900 Subject: [PATCH] =?UTF-8?q?feature:=20offset=EA=B8=B0=EB=B0=98=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=84=A4=EC=9D=B4=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recruit/controller/RecruitController.java | 24 ++++--- .../recruit/dto/GetRecruitOffsetResDto.java | 46 +++++++++++++ .../recruit/dto/GetRecruitsCursorResDto.java | 6 +- .../recruit/service/RecruitService.java | 27 ++++++-- .../controller/RecruitControllerTest.java | 66 ++++++++++++++++--- 5 files changed, 143 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/ssafy/ssafsound/domain/recruit/dto/GetRecruitOffsetResDto.java diff --git a/src/main/java/com/ssafy/ssafsound/domain/recruit/controller/RecruitController.java b/src/main/java/com/ssafy/ssafsound/domain/recruit/controller/RecruitController.java index 3f1ee7bb..1086a669 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/recruit/controller/RecruitController.java +++ b/src/main/java/com/ssafy/ssafsound/domain/recruit/controller/RecruitController.java @@ -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.*; @@ -57,23 +58,30 @@ public EnvelopeResponse deleteRecruit(@PathVariable Long recruitId, @Authe return EnvelopeResponse.builder().build(); } - @GetMapping - public EnvelopeResponse getRecruits(GetRecruitsReqDto getRecruitsReqDto, Pageable pageable, @Authentication AuthenticatedMember memberInfo) { - return EnvelopeResponse.builder() - .data(recruitService.getRecruits(getRecruitsReqDto, pageable, memberInfo.getMemberId())) + @GetMapping("/cursor") + public EnvelopeResponse getRecruitsByCursor(GetRecruitsReqDto getRecruitsReqDto, Pageable pageable, @Authentication AuthenticatedMember memberInfo) { + return EnvelopeResponse.builder() + .data(recruitService.getRecruitsByCursor(getRecruitsReqDto, pageable, memberInfo.getMemberId())) + .build(); + } + + @GetMapping("/offset") + public EnvelopeResponse getRecruitsByOffset(GetRecruitsReqDto getRecruitsReqDto, Pageable pageable, @Authentication AuthenticatedMember memberInfo) { + return EnvelopeResponse.builder() + .data(recruitService.getRecruitsByOffset(getRecruitsReqDto, pageable, memberInfo.getMemberId())) .build(); } @GetMapping("/my-scrap") - public EnvelopeResponse getScrapedRecruits(Long cursor, Pageable pageable, @Authentication AuthenticatedMember memberInfo) { - return EnvelopeResponse.builder() + public EnvelopeResponse getScrapedRecruits(Long cursor, Pageable pageable, @Authentication AuthenticatedMember memberInfo) { + return EnvelopeResponse.builder() .data(recruitService.getScrapedRecruits(memberInfo.getMemberId(), cursor, pageable)) .build(); } @GetMapping("/joined") - public EnvelopeResponse getMemberJoinedRecruits(GetMemberJoinRecruitsReqDto getMemberJoinRecruitsReqDto, @Authentication AuthenticatedMember memberInfo) { - return EnvelopeResponse.builder() + public EnvelopeResponse getMemberJoinedRecruits(GetMemberJoinRecruitsReqDto getMemberJoinRecruitsReqDto, @Authentication AuthenticatedMember memberInfo) { + return EnvelopeResponse.builder() .data(recruitService.getMemberJoinRecruits(getMemberJoinRecruitsReqDto, memberInfo.getMemberId())) .build(); } diff --git a/src/main/java/com/ssafy/ssafsound/domain/recruit/dto/GetRecruitOffsetResDto.java b/src/main/java/com/ssafy/ssafsound/domain/recruit/dto/GetRecruitOffsetResDto.java new file mode 100644 index 00000000..54c87eb5 --- /dev/null +++ b/src/main/java/com/ssafy/ssafsound/domain/recruit/dto/GetRecruitOffsetResDto.java @@ -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 recruits; + private int currentPage; + private int totalPageCount; + @JsonIgnore + public List getRecruitsId() { + return recruits.stream().map(RecruitElement::getRecruitId).collect(Collectors.toList()); + } + + @JsonIgnore + public Map> getRecruitParticipantMapByRecruitIdAndRecruitType() { + Map> result = new TreeMap<>(); + for(RecruitElement recruitElement: recruits) { + result.put(recruitElement.getRecruitId(), recruitElement.getRecruitParticipantMap()); + } + return result; + } + + public static GetRecruitOffsetResDto fromPageAndMemberId(Page recruitPages, Long memberId) { + List 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(); + } +} diff --git a/src/main/java/com/ssafy/ssafsound/domain/recruit/dto/GetRecruitsCursorResDto.java b/src/main/java/com/ssafy/ssafsound/domain/recruit/dto/GetRecruitsCursorResDto.java index 2b5054be..1244e369 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/recruit/dto/GetRecruitsCursorResDto.java +++ b/src/main/java/com/ssafy/ssafsound/domain/recruit/dto/GetRecruitsCursorResDto.java @@ -13,7 +13,7 @@ @Getter @Builder -public class GetRecruitsResDto implements AddParticipantDto { +public class GetRecruitsCursorResDto implements AddParticipantDto { private List recruits; private Long nextCursor; private Boolean isLast; @@ -32,14 +32,14 @@ public Map> getRecruitParticipantMapByRecr return result; } - public static GetRecruitsResDto fromPageAndMemberId(Slice sliceRecruit, Long memberId) { + public static GetRecruitsCursorResDto fromPageAndMemberId(Slice sliceRecruit, Long memberId) { List 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()) diff --git a/src/main/java/com/ssafy/ssafsound/domain/recruit/service/RecruitService.java b/src/main/java/com/ssafy/ssafsound/domain/recruit/service/RecruitService.java index b8665eeb..1f6e2d2a 100644 --- a/src/main/java/com/ssafy/ssafsound/domain/recruit/service/RecruitService.java +++ b/src/main/java/com/ssafy/ssafsound/domain/recruit/service/RecruitService.java @@ -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; @@ -129,10 +130,9 @@ public void deleteRecruit(Long recruitId, Long memberId) { } @Transactional(readOnly = true) - public GetRecruitsResDto getRecruits(GetRecruitsReqDto getRecruitsReqDto, Pageable pageable, Long loginMemberId) { - // 페이지네이션 조건에 따라 프로젝트/스터디 글 목록을 조회한다. + public GetRecruitsCursorResDto getRecruitsByCursor(GetRecruitsReqDto getRecruitsReqDto, Pageable pageable, Long loginMemberId) { Slice recruitPages = recruitRepository.findRecruitSliceByGetRecruitsReqDto(getRecruitsReqDto, pageable); - GetRecruitsResDto recruitsResDto = GetRecruitsResDto.fromPageAndMemberId(recruitPages, loginMemberId); + GetRecruitsCursorResDto recruitsResDto = GetRecruitsCursorResDto.fromPageAndMemberId(recruitPages, loginMemberId); if(!recruitsResDto.getRecruits().isEmpty()) { addRecruitParticipants(recruitsResDto); } @@ -140,9 +140,22 @@ public GetRecruitsResDto getRecruits(GetRecruitsReqDto getRecruitsReqDto, Pageab } @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 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 recruitPages = recruitRepository.findMemberScrapRecruits(memberId, cursor, pageable); - GetRecruitsResDto recruitsResDto = GetRecruitsResDto.fromPageAndMemberId(recruitPages, memberId); + GetRecruitsCursorResDto recruitsResDto = GetRecruitsCursorResDto.fromPageAndMemberId(recruitPages, memberId); return recruitsResDto; } @@ -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)); @@ -167,7 +180,7 @@ public GetRecruitsResDto getMemberJoinRecruits(GetMemberJoinRecruitsReqDto recru } Slice 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); } diff --git a/src/test/java/com/ssafy/ssafsound/domain/recruit/controller/RecruitControllerTest.java b/src/test/java/com/ssafy/ssafsound/domain/recruit/controller/RecruitControllerTest.java index 6db6f046..de95d33b 100644 --- a/src/test/java/com/ssafy/ssafsound/domain/recruit/controller/RecruitControllerTest.java +++ b/src/test/java/com/ssafy/ssafsound/domain/recruit/controller/RecruitControllerTest.java @@ -200,23 +200,23 @@ void deleteRecruit() { getEnvelopPatternWithNoContent())); } - @DisplayName("리크루트 목록 조회") + @DisplayName("커서 기반 리크루트 목록 조회") @Test - void getRecruits() { - doReturn(RecruitFixture.GET_RECRUITS_RES_DTO) + void getRecruitsByCursor() { + doReturn(RecruitFixture.GET_RECRUITS_CURSOR_RES_DTO) .when(recruitService) - .getRecruits(any(), any(), any()); + .getRecruitsByCursor(any(), any(), any()); restDocs .cookie(ACCESS_TOKEN) - .when().get("/recruits?size=20&category=project&keyword=사이드&isFinished=false&recruitTypes=백엔드&recruitTypes=프론트엔드&skills=Spring&skills=React") + .when().get("/recruits/cursor?size=20&category=project&keyword=사이드&isFinished=false&recruitTypes=백엔드&recruitTypes=프론트엔드&skills=Spring&skills=React") .then().log().all() .assertThat() .statusCode(HttpStatus.OK.value()) .apply(document("recruit/recruits", requestCookieAccessTokenOptional(), requestParameters( - parameterWithName("cursor").optional().description("다음 조회 커서 default(초기화면)에서는 미포함"), + parameterWithName("next").optional().description("조회할 커서 번호 default(초기화면)에서는 미포함"), parameterWithName("size").description("페이징 사이즈"), parameterWithName("category").description("카테고리 project|study"), parameterWithName("keyword").description("리크루트 게시글 제목 검색 키워드"), @@ -250,10 +250,60 @@ void getRecruits() { ); } + @DisplayName("offset 기반 리크루트 목록 조회") + @Test + void getRecruitsByOffset() { + doReturn(RecruitFixture.GET_RECRUITS_OFFSET_RES_DTO) + .when(recruitService) + .getRecruitsByOffset(any(), any(), any()); + + restDocs + .cookie(ACCESS_TOKEN) + .when().get("/recruits/offset?size=20&category=project&keyword=사이드&isFinished=false&recruitTypes=백엔드&recruitTypes=프론트엔드&skills=Spring&skills=React") + .then().log().all() + .assertThat() + .statusCode(HttpStatus.OK.value()) + .apply(document("recruit/recruits", + requestCookieAccessTokenOptional(), + requestParameters( + parameterWithName("next").optional().description("조회할 page 번호"), + parameterWithName("size").description("페이징 사이즈"), + parameterWithName("category").description("카테고리 project|study"), + parameterWithName("keyword").description("리크루트 게시글 제목 검색 키워드"), + parameterWithName("isFinished").description("리크루트 종료 여부"), + parameterWithName("recruitTypes").description("리크루트 모집파트, 메타데이터-리크루트 목록 조회 참고"), + parameterWithName("skills").description("리크루트와 연관된 기술 스택, 메타데이터-스킬 목록 조회 참고") + ), + getEnvelopPatternWithData().andWithPrefix("data.", + fieldWithPath("currentPage").type(JsonFieldType.NUMBER).description("현재 조회한 페이지 번호"), + fieldWithPath("totalPageCount").type(JsonFieldType.NUMBER).description("total 페이지 갯수") + ).andWithPrefix("data.recruits[].", + fieldWithPath("recruitId").type(JsonFieldType.NUMBER).description("리크루트 id"), + fieldWithPath("category").type(JsonFieldType.STRING).description("카테고리 project|study"), + fieldWithPath("mine").type(JsonFieldType.BOOLEAN).description("내가 쓴 글 여부(토큰 기준)"), + fieldWithPath("title").type(JsonFieldType.STRING).description("리크루트 모집글 제목, 글자 수 제한 50자"), + fieldWithPath("content").type(JsonFieldType.STRING).description("리크루트 본문 요약본 최대 50자"), + fieldWithPath("recruitEnd").type(JsonFieldType.STRING).description("yyyy-MM-dd 모집 종료 일자"), + fieldWithPath("finishedRecruit").type(JsonFieldType.BOOLEAN).description("리크루트 종료 여부"), + fieldWithPath("participants[].members[]").type(JsonFieldType.ARRAY).description("리크루트 참여 멤버 ").optional() + ).andWithPrefix("data.recruits[].skills[].", + fieldWithPath("skillId").type(JsonFieldType.NUMBER).description("스킬 id 미사용"), + fieldWithPath("name").type(JsonFieldType.STRING).description("리크루트와 연관된 기술 스택명, 메타데이터-스킬 목록 조회 참고") + ).andWithPrefix("data.recruits[].participants[].", + fieldWithPath("recruitType").type(JsonFieldType.STRING).description("리크루트 모집파트, 메타데이터-리크루트 목록 조회 참고"), + fieldWithPath("limit").type(JsonFieldType.NUMBER).description("리크루트 모집 인원 제한 1명이상 10명 이하") + ).andWithPrefix("data.recruits[].participants[].members[].", + fieldWithPath("nickname").type(JsonFieldType.STRING).description("리크루트 참여자 닉네임"), + fieldWithPath("isMajor").type(JsonFieldType.BOOLEAN).description("리크루트 참여자 전공 여부") + ) + ) + ); + } + @DisplayName("스크랩 된 리크루트 목록 조회") @Test void getScrapedRecruits() { - doReturn(RecruitFixture.GET_RECRUITS_RES_DTO) + doReturn(RecruitFixture.GET_RECRUITS_CURSOR_RES_DTO) .when(recruitService) .getScrapedRecruits(any(), any(), any()); @@ -298,7 +348,7 @@ void getScrapedRecruits() { @DisplayName("사용자 참여 확정 리크루트 목록 조회") @Test void getMemberJoinedRecruits() { - doReturn(RecruitFixture.GET_RECRUITS_RES_DTO) + doReturn(RecruitFixture.GET_RECRUITS_CURSOR_RES_DTO) .when(recruitService) .getMemberJoinRecruits(any(), any());