diff --git a/src/main/java/com/depromeet/domain/feed/api/FeedController.java b/src/main/java/com/depromeet/domain/feed/api/FeedController.java index 0eeccfc07..8b6675e1d 100644 --- a/src/main/java/com/depromeet/domain/feed/api/FeedController.java +++ b/src/main/java/com/depromeet/domain/feed/api/FeedController.java @@ -3,6 +3,7 @@ import com.depromeet.domain.feed.application.FeedService; import com.depromeet.domain.feed.dto.response.FeedOneByProfileResponse; import com.depromeet.domain.feed.dto.response.FeedOneResponse; +import com.depromeet.domain.feed.dto.response.FeedSliceResponse; import com.depromeet.domain.mission.domain.MissionVisibility; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -29,10 +30,19 @@ public List feedFindAll( return feedService.findAllFeedByVisibility(visibility); } - @Operation(summary = "피드 탭", description = "피드 탭을 조회합니다.") + @Operation(summary = "피드 탭 (페이지네이션)", description = "피드 탭을 조회합니다.") @GetMapping("/me") - public List feedFindAll() { - return feedService.findAllFeed(); + public FeedSliceResponse feedFindByPage( + @RequestParam int size, + @RequestParam(required = false) Long lastId, + @RequestParam(value = "visibility", required = false) MissionVisibility visibility) { + if (visibility == MissionVisibility.ALL) { + // 전체 피드 탭 + return feedService.findAllFeed(size, lastId); + } else { + // 팔로워 피드 탭 + return feedService.findFollowerFeed(size, lastId); + } } @Operation(summary = "프로필 피드", description = "피드 탭을 조회합니다.") diff --git a/src/main/java/com/depromeet/domain/feed/application/FeedService.java b/src/main/java/com/depromeet/domain/feed/application/FeedService.java index 9cb374b98..f150931fc 100644 --- a/src/main/java/com/depromeet/domain/feed/application/FeedService.java +++ b/src/main/java/com/depromeet/domain/feed/application/FeedService.java @@ -2,6 +2,7 @@ import com.depromeet.domain.feed.dto.response.FeedOneByProfileResponse; import com.depromeet.domain.feed.dto.response.FeedOneResponse; +import com.depromeet.domain.feed.dto.response.FeedSliceResponse; import com.depromeet.domain.follow.dao.MemberRelationRepository; import com.depromeet.domain.follow.domain.MemberRelation; import com.depromeet.domain.member.dao.MemberRepository; @@ -15,6 +16,7 @@ import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -43,6 +45,28 @@ public List findAllFeedByVisibility(MissionVisibility visibilit return missionRecordRepository.findFeedAll(sourceMembers); } + // 전체 피드 탭 + @Transactional(readOnly = true) + public FeedSliceResponse findAllFeed(int size, Long lastId) { + final List members = memberRepository.findAll(); + Slice feedByVisibilityAndPage = + missionRecordRepository.findFeedByVisibilityAndPage( + size, lastId, members, List.of(MissionVisibility.ALL)); + return FeedSliceResponse.from(feedByVisibilityAndPage); + } + + // 팔로워 피드 탭 + @Transactional(readOnly = true) + public FeedSliceResponse findFollowerFeed(int size, Long lastId) { + final Member currentMember = memberUtil.getCurrentMember(); + List sourceMembers = getSourceMembers(currentMember.getId()); + + sourceMembers.add(currentMember); + Slice feedAllByPage = + missionRecordRepository.findFeedAllByPage(size, lastId, sourceMembers); + return FeedSliceResponse.from(feedAllByPage); + } + @Transactional(readOnly = true) public List findAllFeed() { final Member currentMember = memberUtil.getCurrentMember(); diff --git a/src/main/java/com/depromeet/domain/feed/dto/response/FeedSliceResponse.java b/src/main/java/com/depromeet/domain/feed/dto/response/FeedSliceResponse.java new file mode 100644 index 000000000..26576cb56 --- /dev/null +++ b/src/main/java/com/depromeet/domain/feed/dto/response/FeedSliceResponse.java @@ -0,0 +1,13 @@ +package com.depromeet.domain.feed.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import org.springframework.data.domain.Slice; + +public record FeedSliceResponse( + @Schema(description = "피드 데이터") List content, + @Schema(description = "마지막 페이지 여부") Boolean last) { + public static FeedSliceResponse from(Slice feedResponses) { + return new FeedSliceResponse(feedResponses.getContent(), feedResponses.isLast()); + } +} diff --git a/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java b/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java index a2eca4302..2d38d4d51 100644 --- a/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java +++ b/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java @@ -15,9 +15,6 @@ import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; import org.springframework.stereotype.Repository; @Repository @@ -130,18 +127,4 @@ private BooleanExpression ltMissionId(Long lastId) { private BooleanExpression durationStatusFinishedEq() { return mission.durationStatus.eq(DurationStatus.FINISHED); } - - // 무한 스크롤 방식 처리하는 메서드 - private Slice checkLastPage(int size, List result) { - - boolean hasNext = false; - - // 조회한 결과 개수가 요청한 페이지 사이즈보다 크면 뒤에 더 있음, next = true - if (result.size() > size) { - hasNext = true; - result.remove(size); - } - Pageable pageable = Pageable.unpaged(); - return new SliceImpl<>(result, pageable, hasNext); - } } diff --git a/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryCustom.java b/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryCustom.java index 75c9212f0..b42cd35ff 100644 --- a/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryCustom.java +++ b/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryCustom.java @@ -6,6 +6,7 @@ import com.depromeet.domain.missionRecord.domain.MissionRecord; import java.time.YearMonth; import java.util.List; +import org.springframework.data.domain.Slice; public interface MissionRecordRepositoryCustom { @@ -23,4 +24,9 @@ List findFeedByVisibility( boolean isCompletedMissionExistsToday(Long missionId); void deleteByMissionRecordId(Long missionRecordId); + + Slice findFeedAllByPage(int size, Long lastId, List members); + + Slice findFeedByVisibilityAndPage( + int size, Long lastId, List members, List visibility); } diff --git a/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryImpl.java b/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryImpl.java index 7af9b57e0..719868bc1 100644 --- a/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryImpl.java +++ b/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryImpl.java @@ -16,6 +16,10 @@ import java.time.YearMonth; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; import org.springframework.stereotype.Repository; @Repository @@ -101,6 +105,48 @@ public List findFeedByVisibility( .fetch(); } + @Override + public Slice findFeedAllByPage(int size, Long lastId, List members) { + return findFeedByVisibilityAndPage( + size, lastId, members, List.of(MissionVisibility.FOLLOWER, MissionVisibility.ALL)); + } + + @Override + public Slice findFeedByVisibilityAndPage( + int size, Long lastId, List members, List visibilities) { + List feedList = + jpaQueryFactory + .select( + Projections.constructor( + FeedOneResponse.class, + member.id, + member.profile.nickname, + member.profile.profileImageUrl, + mission.id, + mission.name, + missionRecord.id, + missionRecord.remark, + missionRecord.imageUrl, + missionRecord.duration, + mission.startedAt, + mission.finishedAt, + missionRecord.startedAt)) + .from(missionRecord) + .leftJoin(missionRecord.mission, mission) + .on(mission.id.eq(missionRecord.mission.id)) + .leftJoin(mission.member, member) + .on(mission.member.id.eq(missionRecord.mission.member.id)) + .where( + ltMissionRecordId(lastId), + missionRecord.mission.member.in(members), + missionRecord.mission.visibility.in(visibilities), + uploadStatusCompleteEq()) + .orderBy(missionRecord.finishedAt.desc()) + .limit((long) size + 1) + .fetch(); + return checkLastPage(size, feedList); + } + @Override public List findFeedAllByMemberId( Long memberId, List visibilities) { @@ -140,4 +186,26 @@ private BooleanExpression dayEq(int day) { private BooleanExpression uploadStatusCompleteEq() { return missionRecord.uploadStatus.eq(ImageUploadStatus.COMPLETE); } + + // no-offset 방식 처리하는 메서드 + private BooleanExpression ltMissionRecordId(Long lastId) { + if (lastId == null) { + return null; + } + return missionRecord.id.lt(lastId); + } + + // 무한 스크롤 방식 처리하는 메서드 + private Slice checkLastPage(int size, List result) { + + boolean hasNext = false; + + // 조회한 결과 개수가 요청한 페이지 사이즈보다 크면 뒤에 더 있음, next = true + if (result.size() > size) { + hasNext = true; + result.remove(size); + } + Pageable pageable = PageRequest.ofSize(size); + return new SliceImpl<>(result, pageable, hasNext); + } }