diff --git a/src/main/java/ddingdong/ddingdongBE/domain/feed/api/FeedApi.java b/src/main/java/ddingdong/ddingdongBE/domain/feed/api/FeedApi.java index 8c66dab4..42a7e42f 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/feed/api/FeedApi.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/feed/api/FeedApi.java @@ -2,7 +2,7 @@ import ddingdong.ddingdongBE.domain.feed.controller.dto.response.ClubFeedPageResponse; import ddingdong.ddingdongBE.domain.feed.controller.dto.response.FeedResponse; -import ddingdong.ddingdongBE.domain.feed.controller.dto.response.NewestFeedPerClubPageResponse; +import ddingdong.ddingdongBE.domain.feed.controller.dto.response.FeedPageResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -21,28 +21,28 @@ public interface FeedApi { @Operation(summary = "특정 동아리 피드 페이지 조회 API") @ApiResponse(responseCode = "200", description = "특정 동아리 피드 페이지 조회 성공", - content = @Content(schema = @Schema(implementation = ClubFeedPageResponse.class))) + content = @Content(schema = @Schema(implementation = ClubFeedPageResponse.class))) @ResponseStatus(HttpStatus.OK) @GetMapping("/clubs/{clubId}/feeds") ClubFeedPageResponse getFeedPageByClub( - @PathVariable("clubId") Long clubId, - @RequestParam(value = "size", defaultValue = "9") int size, - @RequestParam(value = "currentCursorId", defaultValue = "-1") Long currentCursorId - ); + @PathVariable("clubId") Long clubId, + @RequestParam(value = "size", defaultValue = "9") int size, + @RequestParam(value = "currentCursorId", defaultValue = "-1") Long currentCursorId + ); @Operation(summary = "모든 동아리 최신 피드 페이지 조회 API") @ApiResponse(responseCode = "200", description = "모든 동아리 최신 피드 페이지 조회 성공", - content = @Content(schema = @Schema(implementation = NewestFeedPerClubPageResponse.class))) + content = @Content(schema = @Schema(implementation = FeedPageResponse.class))) @ResponseStatus(HttpStatus.OK) @GetMapping("/feeds") - NewestFeedPerClubPageResponse getNewestFeedPerClub( - @RequestParam(value = "size", defaultValue = "9") int size, - @RequestParam(value = "currentCursorId", defaultValue = "-1") Long currentCursorId + FeedPageResponse getAllFeedPage( + @RequestParam(value = "size", defaultValue = "9") int size, + @RequestParam(value = "currentCursorId", defaultValue = "-1") Long currentCursorId ); @Operation(summary = "동아리 피드 상세 조회 API") @ApiResponse(responseCode = "200", description = "동아리 피드 상세 조회 API", - content = @Content(schema = @Schema(implementation = FeedResponse.class))) + content = @Content(schema = @Schema(implementation = FeedResponse.class))) @ResponseStatus(HttpStatus.OK) @GetMapping("/feeds/{feedId}") FeedResponse getByFeedId(@PathVariable("feedId") Long feedId); diff --git a/src/main/java/ddingdong/ddingdongBE/domain/feed/controller/FeedController.java b/src/main/java/ddingdong/ddingdongBE/domain/feed/controller/FeedController.java index 28cf01ea..62bbc69f 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/feed/controller/FeedController.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/feed/controller/FeedController.java @@ -3,11 +3,11 @@ import ddingdong.ddingdongBE.domain.feed.api.FeedApi; import ddingdong.ddingdongBE.domain.feed.controller.dto.response.ClubFeedPageResponse; import ddingdong.ddingdongBE.domain.feed.controller.dto.response.FeedResponse; -import ddingdong.ddingdongBE.domain.feed.controller.dto.response.NewestFeedPerClubPageResponse; +import ddingdong.ddingdongBE.domain.feed.controller.dto.response.FeedPageResponse; import ddingdong.ddingdongBE.domain.feed.service.FacadeFeedService; import ddingdong.ddingdongBE.domain.feed.service.dto.query.ClubFeedPageQuery; import ddingdong.ddingdongBE.domain.feed.service.dto.query.FeedQuery; -import ddingdong.ddingdongBE.domain.feed.service.dto.query.NewestFeedPerClubPageQuery; +import ddingdong.ddingdongBE.domain.feed.service.dto.query.FeedPageQuery; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.RestController; @@ -19,21 +19,21 @@ public class FeedController implements FeedApi { @Override public ClubFeedPageResponse getFeedPageByClub( - Long clubId, - int size, - Long currentCursorId + Long clubId, + int size, + Long currentCursorId ) { ClubFeedPageQuery clubFeedPageQuery = facadeFeedService.getFeedPageByClub(clubId, size, currentCursorId); return ClubFeedPageResponse.from(clubFeedPageQuery); } @Override - public NewestFeedPerClubPageResponse getNewestFeedPerClub( - int size, - Long currentCursorId + public FeedPageResponse getAllFeedPage( + int size, + Long currentCursorId ) { - NewestFeedPerClubPageQuery newestFeedPerClubPageQuery = facadeFeedService.getNewestFeedPerClubPage(size, currentCursorId); - return NewestFeedPerClubPageResponse.from(newestFeedPerClubPageQuery); + FeedPageQuery feedPageQuery = facadeFeedService.getAllFeedPage(size, currentCursorId); + return FeedPageResponse.from(feedPageQuery); } @Override diff --git a/src/main/java/ddingdong/ddingdongBE/domain/feed/controller/dto/response/NewestFeedPerClubPageResponse.java b/src/main/java/ddingdong/ddingdongBE/domain/feed/controller/dto/response/FeedPageResponse.java similarity index 64% rename from src/main/java/ddingdong/ddingdongBE/domain/feed/controller/dto/response/NewestFeedPerClubPageResponse.java rename to src/main/java/ddingdong/ddingdongBE/domain/feed/controller/dto/response/FeedPageResponse.java index 626d98ad..9b1070ee 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/feed/controller/dto/response/NewestFeedPerClubPageResponse.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/feed/controller/dto/response/FeedPageResponse.java @@ -1,31 +1,31 @@ package ddingdong.ddingdongBE.domain.feed.controller.dto.response; import ddingdong.ddingdongBE.domain.feed.service.dto.query.FeedListQuery; -import ddingdong.ddingdongBE.domain.feed.service.dto.query.NewestFeedPerClubPageQuery; +import ddingdong.ddingdongBE.domain.feed.service.dto.query.FeedPageQuery; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; import lombok.Builder; -public record NewestFeedPerClubPageResponse( - @ArraySchema(schema = @Schema(name = "동아리 최신 피드 정보", implementation = NewestFeedListResponse.class)) - List newestFeeds, +public record FeedPageResponse( + @ArraySchema(schema = @Schema(name = "동아리 최신 피드 정보", implementation = FeedListResponse.class)) + List newestFeeds, @Schema(name = "피드 페이지 정보", implementation = PagingResponse.class) PagingResponse pagingInfo ) { - public static NewestFeedPerClubPageResponse from( - NewestFeedPerClubPageQuery newestFeedPerClubPageQuery) { - List newestFeeds = newestFeedPerClubPageQuery.feedListQueries() + public static FeedPageResponse from( + FeedPageQuery feedPageQuery) { + List newestFeeds = feedPageQuery.feedListQueries() .stream() - .map(NewestFeedListResponse::from) + .map(FeedListResponse::from) .toList(); - return new NewestFeedPerClubPageResponse(newestFeeds, - PagingResponse.from(newestFeedPerClubPageQuery.pagingQuery())); + return new FeedPageResponse(newestFeeds, + PagingResponse.from(feedPageQuery.pagingQuery())); } @Builder - public record NewestFeedListResponse( + public record FeedListResponse( @Schema(description = "피드 ID", example = "1") Long id, @Schema(description = "피드 썸네일 CDN URL", example = "https://%s.s3.%s.amazonaws.com/%s/%s/%s") @@ -38,8 +38,8 @@ public record NewestFeedListResponse( String feedType ) { - public static NewestFeedListResponse from(FeedListQuery query) { - return NewestFeedListResponse.builder() + public static FeedListResponse from(FeedListQuery query) { + return FeedListResponse.builder() .id(query.id()) .thumbnailOriginUrl(query.thumbnailOriginUrl()) .thumbnailCdnUrl(query.thumbnailCdnUrl()) diff --git a/src/main/java/ddingdong/ddingdongBE/domain/feed/repository/FeedRepository.java b/src/main/java/ddingdong/ddingdongBE/domain/feed/repository/FeedRepository.java index 01e2475f..2f4407ab 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/feed/repository/FeedRepository.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/feed/repository/FeedRepository.java @@ -33,41 +33,32 @@ or exists ( limit :size """, nativeQuery = true) Slice findPageByClubIdOrderById( - @Param("clubId") Long clubId, - @Param("size") int size, - @Param("currentCursorId") Long currentCursorId + @Param("clubId") Long clubId, + @Param("size") int size, + @Param("currentCursorId") Long currentCursorId ); @Query(value = """ select * from feed f - where f.id in - (select max(id) - from feed - where deleted_at is null - and ( - f.feed_type != 'VIDEO' - or exists ( - select 1 - from ( - select id - from file_meta_data - where entity_id = f.id - and domain_type = 'FEED_VIDEO' - ) filtered_fm - join vod_processing_job vpj - on filtered_fm.id = vpj.file_meta_data_id - where vpj.convert_job_status = 'COMPLETE' - ) - ) - GROUP BY club_id) - and (:currentCursorId = -1 or id < :currentCursorId) - ORDER BY id DESC - limit :size - """, - nativeQuery = true) - Slice findNewestPerClubPage( - @Param("size") int size, - @Param("currentCursorId") Long currentCursorId + where f.deleted_at is null + and ( + f.feed_type != 'VIDEO' + or exists ( + select 1 + from file_meta_data fm + join vod_processing_job vpj on fm.id = vpj.file_meta_data_id + where fm.entity_id = f.id + and fm.domain_type = 'FEED_VIDEO' + and vpj.convert_job_status = 'COMPLETE' + ) + ) + and (:currentCursorId = -1 or f.id < :currentCursorId) + ORDER BY f.id DESC + limit :size + """, nativeQuery = true) + Slice getAllFeedPage( + @Param("size") int size, + @Param("currentCursorId") Long currentCursorId ); } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/feed/service/FacadeFeedService.java b/src/main/java/ddingdong/ddingdongBE/domain/feed/service/FacadeFeedService.java index 1259d183..a437f433 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/feed/service/FacadeFeedService.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/feed/service/FacadeFeedService.java @@ -6,7 +6,7 @@ import ddingdong.ddingdongBE.domain.feed.service.dto.query.FeedFileInfoQuery; import ddingdong.ddingdongBE.domain.feed.service.dto.query.FeedListQuery; import ddingdong.ddingdongBE.domain.feed.service.dto.query.FeedQuery; -import ddingdong.ddingdongBE.domain.feed.service.dto.query.NewestFeedPerClubPageQuery; +import ddingdong.ddingdongBE.domain.feed.service.dto.query.FeedPageQuery; import ddingdong.ddingdongBE.domain.feed.service.dto.query.PagingQuery; import java.util.List; import lombok.RequiredArgsConstructor; @@ -36,10 +36,10 @@ public ClubFeedPageQuery getFeedPageByClub(Long clubId, int size, Long currentCu return ClubFeedPageQuery.of(feedListQueries, pagingQuery); } - public NewestFeedPerClubPageQuery getNewestFeedPerClubPage(int size, Long currentCursorId) { - Slice feedPage = feedService.getNewestFeedPerClubPage(size, currentCursorId); + public FeedPageQuery getAllFeedPage(int size, Long currentCursorId) { + Slice feedPage = feedService.getAllFeedPage(size, currentCursorId); if (feedPage == null) { - return NewestFeedPerClubPageQuery.createEmpty(); + return FeedPageQuery.createEmpty(); } List completeFeeds = feedPage.getContent(); @@ -47,7 +47,7 @@ public NewestFeedPerClubPageQuery getNewestFeedPerClubPage(int size, Long curren .toList(); PagingQuery pagingQuery = PagingQuery.of(currentCursorId, completeFeeds, feedPage.hasNext()); - return NewestFeedPerClubPageQuery.of(feedListQueries, pagingQuery); + return FeedPageQuery.of(feedListQueries, pagingQuery); } public FeedQuery getById(Long feedId) { diff --git a/src/main/java/ddingdong/ddingdongBE/domain/feed/service/FeedService.java b/src/main/java/ddingdong/ddingdongBE/domain/feed/service/FeedService.java index 98b41dc7..23690db1 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/feed/service/FeedService.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/feed/service/FeedService.java @@ -8,7 +8,7 @@ public interface FeedService { Slice getFeedPageByClubId(Long clubId, int size, Long currentCursorId); - Slice getNewestFeedPerClubPage(int size, Long currentCursorId); + Slice getAllFeedPage(int size, Long currentCursorId); Feed getById(Long feedId); diff --git a/src/main/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedService.java b/src/main/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedService.java index 44e83989..92333b2e 100644 --- a/src/main/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedService.java +++ b/src/main/java/ddingdong/ddingdongBE/domain/feed/service/GeneralFeedService.java @@ -27,8 +27,8 @@ public Slice getFeedPageByClubId(Long clubId, int size, Long currentCursor } @Override - public Slice getNewestFeedPerClubPage(int size, Long currentCursorId) { - Slice feedPages = feedRepository.findNewestPerClubPage(size + 1, currentCursorId); + public Slice getAllFeedPage(int size, Long currentCursorId) { + Slice feedPages = feedRepository.getAllFeedPage(size + 1, currentCursorId); return buildSlice(feedPages, size); } diff --git a/src/main/java/ddingdong/ddingdongBE/domain/feed/service/dto/query/FeedPageQuery.java b/src/main/java/ddingdong/ddingdongBE/domain/feed/service/dto/query/FeedPageQuery.java new file mode 100644 index 00000000..bf94a0a3 --- /dev/null +++ b/src/main/java/ddingdong/ddingdongBE/domain/feed/service/dto/query/FeedPageQuery.java @@ -0,0 +1,18 @@ +package ddingdong.ddingdongBE.domain.feed.service.dto.query; + +import java.util.Collections; +import java.util.List; + +public record FeedPageQuery( + List feedListQueries, + PagingQuery pagingQuery +) { + + public static FeedPageQuery of(List feedListQueries, PagingQuery pagingQuery) { + return new FeedPageQuery(feedListQueries, pagingQuery); + } + + public static FeedPageQuery createEmpty() { + return new FeedPageQuery(Collections.emptyList(), PagingQuery.createEmpty()); + } +} diff --git a/src/main/java/ddingdong/ddingdongBE/domain/feed/service/dto/query/NewestFeedPerClubPageQuery.java b/src/main/java/ddingdong/ddingdongBE/domain/feed/service/dto/query/NewestFeedPerClubPageQuery.java deleted file mode 100644 index 2c9770c1..00000000 --- a/src/main/java/ddingdong/ddingdongBE/domain/feed/service/dto/query/NewestFeedPerClubPageQuery.java +++ /dev/null @@ -1,18 +0,0 @@ -package ddingdong.ddingdongBE.domain.feed.service.dto.query; - -import java.util.Collections; -import java.util.List; - -public record NewestFeedPerClubPageQuery( - List feedListQueries, - PagingQuery pagingQuery -) { - - public static NewestFeedPerClubPageQuery of(List feedListQueries, PagingQuery pagingQuery) { - return new NewestFeedPerClubPageQuery(feedListQueries, pagingQuery); - } - - public static NewestFeedPerClubPageQuery createEmpty() { - return new NewestFeedPerClubPageQuery(Collections.emptyList(), PagingQuery.createEmpty()); - } -} diff --git a/src/test/java/ddingdong/ddingdongBE/common/fixture/ClubFixture.java b/src/test/java/ddingdong/ddingdongBE/common/fixture/ClubFixture.java index 97f13dce..60667ef8 100644 --- a/src/test/java/ddingdong/ddingdongBE/common/fixture/ClubFixture.java +++ b/src/test/java/ddingdong/ddingdongBE/common/fixture/ClubFixture.java @@ -27,4 +27,21 @@ public static Club createClub(final User user) { .score(Score.from(BigDecimal.valueOf(85.5))) .build(); } + + public static Club createClub() { + return Club.builder() + .clubMembers(new ArrayList<>()) + .name("컴퓨터공학과 동아리") + .category("학술") + .tag("프로그래밍, 개발, IT") + .leader("김동아") + .phoneNumber(PhoneNumber.from("010-1234-5678")) + .location(Location.from("S3014")) // S + 4자리 숫자 + .regularMeeting("매주 수요일 18:00") + .introduction("컴퓨터공학과 학생들이 함께 공부하고 프로젝트를 진행하는 동아리입니다.") + .activity("알고리즘 스터디, 웹 개발 프로젝트, 해커톤 참가") + .ideal("함께 성장하는 개발자 커뮤니티") + .score(Score.from(BigDecimal.valueOf(85.5))) + .build(); + } } diff --git a/src/test/java/ddingdong/ddingdongBE/common/fixture/FeedFixture.java b/src/test/java/ddingdong/ddingdongBE/common/fixture/FeedFixture.java new file mode 100644 index 00000000..0b7de200 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/common/fixture/FeedFixture.java @@ -0,0 +1,32 @@ +package ddingdong.ddingdongBE.common.fixture; + +import ddingdong.ddingdongBE.domain.club.entity.Club; +import ddingdong.ddingdongBE.domain.feed.entity.Feed; +import ddingdong.ddingdongBE.domain.feed.entity.FeedType; + +public class FeedFixture { + + public static Feed createImageFeed(Club club, String content) { + return Feed.builder() + .club(club) + .feedType(FeedType.IMAGE) + .activityContent(content) + .build(); + } + + public static Feed createVideoFeed(Club club, String content) { + return Feed.builder() + .club(club) + .feedType(FeedType.VIDEO) + .activityContent(content) + .build(); + } + + public static Feed createFeed(Club club, FeedType feedType, String content) { + return Feed.builder() + .club(club) + .feedType(feedType) + .activityContent(content) + .build(); + } +} diff --git a/src/test/java/ddingdong/ddingdongBE/common/fixture/VodProcessingJobFixture.java b/src/test/java/ddingdong/ddingdongBE/common/fixture/VodProcessingJobFixture.java new file mode 100644 index 00000000..22caad50 --- /dev/null +++ b/src/test/java/ddingdong/ddingdongBE/common/fixture/VodProcessingJobFixture.java @@ -0,0 +1,40 @@ +package ddingdong.ddingdongBE.common.fixture; + +import com.github.f4b6a3.uuid.UuidCreator; +import ddingdong.ddingdongBE.domain.filemetadata.entity.DomainType; +import ddingdong.ddingdongBE.domain.filemetadata.entity.FileMetaData; +import ddingdong.ddingdongBE.domain.filemetadata.entity.FileStatus; +import ddingdong.ddingdongBE.domain.vodprocessing.entity.ConvertJobStatus; +import ddingdong.ddingdongBE.domain.vodprocessing.entity.VodProcessingJob; + +public class VodProcessingJobFixture { + + public static FileMetaData createFileMetaData(Long feedId) { + return FileMetaData.builder() + .id(UuidCreator.getTimeOrderedEpoch()) + .fileKey("test-key-" + feedId) + .fileName("test-" + feedId + ".mp4") + .domainType(DomainType.FEED_VIDEO) + .entityId(feedId) + .fileStatus(FileStatus.COUPLED) + .build(); + } + + public static VodProcessingJob createCompleteVodProcessingJob(FileMetaData fileMetaData) { + return VodProcessingJob.builder() + .fileMetaData(fileMetaData) + .convertJobId(UuidCreator.getTimeOrderedEpoch().toString()) + .userId("testUser") + .convertJobStatus(ConvertJobStatus.COMPLETE) + .build(); + } + + public static VodProcessingJob createPendingVodProcessingJob(FileMetaData fileMetaData) { + return VodProcessingJob.builder() + .fileMetaData(fileMetaData) + .convertJobId(UuidCreator.getTimeOrderedEpoch().toString()) + .userId("testUser") + .convertJobStatus(ConvertJobStatus.PENDING) + .build(); + } +} diff --git a/src/test/java/ddingdong/ddingdongBE/domain/feed/repository/FeedRepositoryTest.java b/src/test/java/ddingdong/ddingdongBE/domain/feed/repository/FeedRepositoryTest.java index 92737988..af99b798 100644 --- a/src/test/java/ddingdong/ddingdongBE/domain/feed/repository/FeedRepositoryTest.java +++ b/src/test/java/ddingdong/ddingdongBE/domain/feed/repository/FeedRepositoryTest.java @@ -1,26 +1,19 @@ package ddingdong.ddingdongBE.domain.feed.repository; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; -import com.navercorp.fixturemonkey.FixtureMonkey; +import ddingdong.ddingdongBE.common.fixture.ClubFixture; +import ddingdong.ddingdongBE.common.fixture.FeedFixture; +import ddingdong.ddingdongBE.common.fixture.VodProcessingJobFixture; import ddingdong.ddingdongBE.common.support.DataJpaTestSupport; -import ddingdong.ddingdongBE.common.support.FixtureMonkeyFactory; import ddingdong.ddingdongBE.domain.club.entity.Club; import ddingdong.ddingdongBE.domain.club.repository.ClubRepository; import ddingdong.ddingdongBE.domain.feed.entity.Feed; -import ddingdong.ddingdongBE.domain.feed.entity.FeedType; -import ddingdong.ddingdongBE.domain.filemetadata.entity.DomainType; import ddingdong.ddingdongBE.domain.filemetadata.entity.FileMetaData; -import ddingdong.ddingdongBE.domain.filemetadata.entity.FileStatus; import ddingdong.ddingdongBE.domain.filemetadata.repository.FileMetaDataRepository; -import ddingdong.ddingdongBE.domain.scorehistory.entity.Score; -import ddingdong.ddingdongBE.domain.vodprocessing.entity.ConvertJobStatus; -import ddingdong.ddingdongBE.domain.vodprocessing.entity.VodProcessingJob; import ddingdong.ddingdongBE.domain.vodprocessing.repository.VodProcessingJobRepository; -import java.math.BigDecimal; import java.util.List; -import java.util.UUID; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -29,10 +22,10 @@ class FeedRepositoryTest extends DataJpaTestSupport { @Autowired - private ClubRepository clubRepository; + private FeedRepository feedRepository; @Autowired - private FeedRepository feedRepository; + private ClubRepository clubRepository; @Autowired private FileMetaDataRepository fileMetaDataRepository; @@ -40,342 +33,198 @@ class FeedRepositoryTest extends DataJpaTestSupport { @Autowired private VodProcessingJobRepository vodProcessingJobRepository; - private final FixtureMonkey fixture = FixtureMonkeyFactory.getNotNullBuilderIntrospectorMonkey(); + @DisplayName("클럽별 피드 페이지 조회 - IMAGE 타입 피드는 조회된다") + @Test + void findPageByClubIdOrderById_ImageFeeds() { + // given + Club club = clubRepository.save(ClubFixture.createClub()); + Feed imageFeed1 = feedRepository.save(FeedFixture.createImageFeed(club, "이미지 피드 1")); + Feed imageFeed2 = feedRepository.save(FeedFixture.createImageFeed(club, "이미지 피드 2")); + + // when + Slice result = feedRepository.findPageByClubIdOrderById(club.getId(), 10, -1L); + + // then + assertSoftly(softly -> { + softly.assertThat(result.getContent()).hasSize(2); + softly.assertThat(result.getContent()).extracting(Feed::getActivityContent) + .containsExactly("이미지 피드 2", "이미지 피드 1"); + }); + } + + @DisplayName("클럽별 피드 페이지 조회 - VIDEO 타입 피드 중 VOD 처리 완료된 피드만 조회된다") + @Test + void findPageByClubIdOrderById_VideoFeedsWithCompleteVod() { + // given + Club club = clubRepository.save(ClubFixture.createClub()); + Feed videoFeed1 = feedRepository.save(FeedFixture.createVideoFeed(club, "비디오 피드 1")); + Feed videoFeed2 = feedRepository.save(FeedFixture.createVideoFeed(club, "비디오 피드 2")); + + // 첫 번째 비디오만 VOD 처리 완료 + FileMetaData fileMetaData = fileMetaDataRepository.save(VodProcessingJobFixture.createFileMetaData(videoFeed1.getId())); + vodProcessingJobRepository.save(VodProcessingJobFixture.createCompleteVodProcessingJob(fileMetaData)); + + // when + Slice result = feedRepository.findPageByClubIdOrderById(club.getId(), 10, -1L); + + // then + assertSoftly(softly -> { + softly.assertThat(result.getContent()).hasSize(1); + softly.assertThat(result.getContent().get(0).getActivityContent()).isEqualTo("비디오 피드 1"); + }); + } + + @DisplayName("클럽별 피드 페이지 조회 - 커서 기반 페이지네이션이 동작한다") + @Test + void findPageByClubIdOrderById_CursorPagination() { + // given + Club club = clubRepository.save(ClubFixture.createClub()); + Feed feed1 = feedRepository.save(FeedFixture.createImageFeed(club, "피드 1")); + Feed feed2 = feedRepository.save(FeedFixture.createImageFeed(club, "피드 2")); + Feed feed3 = feedRepository.save(FeedFixture.createImageFeed(club, "피드 3")); - @BeforeEach - void setUp() { - feedRepository.deleteAll(); + // when + Slice firstPage = feedRepository.findPageByClubIdOrderById(club.getId(), 2, -1L); + Long lastId = firstPage.getContent().get(firstPage.getContent().size() - 1).getId(); + Slice secondPage = feedRepository.findPageByClubIdOrderById(club.getId(), 2, lastId); + + // then + assertSoftly(softly -> { + softly.assertThat(firstPage.getContent()).hasSize(2); + softly.assertThat(secondPage.getContent()).hasSize(1); + softly.assertThat(secondPage.getContent().get(0).getActivityContent()).isEqualTo("피드 1"); + }); + } + + @DisplayName("클럽별 피드 페이지 조회 - 삭제된 피드는 조회되지 않는다") + @Test + void findPageByClubIdOrderById_ExcludesDeletedFeeds() { + // given + Club club = clubRepository.save(ClubFixture.createClub()); + Feed activeFeed = feedRepository.save(FeedFixture.createImageFeed(club, "활성 피드")); + Feed deletedFeed = feedRepository.save(FeedFixture.createImageFeed(club, "삭제된 피드")); + + // 피드 삭제 (soft delete) + feedRepository.delete(deletedFeed); feedRepository.flush(); - clubRepository.deleteAll(); - clubRepository.flush(); + + // when + Slice result = feedRepository.findPageByClubIdOrderById(club.getId(), 10, -1L); + + // then + assertSoftly(softly -> { + softly.assertThat(result.getContent()).hasSize(1); + softly.assertThat(result.getContent().get(0).getActivityContent()).isEqualTo("활성 피드"); + }); } - @DisplayName("모든 동아리의 최신 피드 페이지를 주어진 정보에 맞춰 반환한다.") + @DisplayName("전체 최신 피드 페이지 조회 - IMAGE 타입 피드는 조회된다") @Test - void findNewestPerClubPage() { + void getAllFeedPage_ImageFeeds() { // given - Club club1 = fixture.giveMeBuilder(Club.class) - .set("id", null) - .set("name", "카우1") - .set("user", null) - .set("score", Score.from(BigDecimal.ZERO)) - .set("clubMembers", null) - .sample(); - Club club2 = fixture.giveMeBuilder(Club.class) - .set("id", null) - .set("name", "카우2") - .set("user", null) - .set("score", Score.from(BigDecimal.ZERO)) - .set("clubMembers", null) - .sample(); - Club club3 = fixture.giveMeBuilder(Club.class) - .set("id", null) - .set("name", "카우3") - .set("user", null) - .set("score", Score.from(BigDecimal.ZERO)) - .set("clubMembers", null) - .sample(); - Club savedClub1 = clubRepository.save(club1); - Club savedClub2 = clubRepository.save(club2); - Club savedClub3 = clubRepository.save(club3); - - Feed feed1 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub1) - .set("activityContent", "내용 1 올드") - .set("feedType", FeedType.IMAGE) - .sample(); - Feed feed2 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub1) - .set("activityContent", "내용 1 최신") - .set("feedType", FeedType.IMAGE) - .sample(); - Feed feed3 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub2) - .set("activityContent", "내용 2 올드") - .set("feedType", FeedType.IMAGE) - .sample(); - Feed feed4 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub2) - .set("activityContent", "내용 2 최신") - .set("feedType", FeedType.IMAGE) - .sample(); - Feed feed5 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub3) - .set("activityContent", "내용 3 올드") - .set("feedType", FeedType.IMAGE) - .sample(); - Feed feed6 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub3) - .set("activityContent", "내용 3 최신") - .set("feedType", FeedType.IMAGE) - .sample(); - feedRepository.saveAll(List.of(feed1, feed2, feed3, feed4, feed5, feed6)); - - int size = 2; - Long currentCursorId = -1L; + Club club1 = clubRepository.save(ClubFixture.createClub()); + Club club2 = clubRepository.save(ClubFixture.createClub()); + Feed imageFeed1 = feedRepository.save(FeedFixture.createImageFeed(club1, "이미지 피드 1")); + Feed imageFeed2 = feedRepository.save(FeedFixture.createImageFeed(club2, "이미지 피드 2")); + // when - Slice newestFeeds = feedRepository.findNewestPerClubPage(size, currentCursorId); + Slice result = feedRepository.getAllFeedPage(10, -1L); // then - List feeds = newestFeeds.getContent(); - assertThat(feeds.size()).isEqualTo(2); - assertThat(feeds.get(0).getId()).isEqualTo(6); - assertThat(feeds.get(1).getId()).isEqualTo(4); + assertSoftly(softly -> { + softly.assertThat(result.getContent()).hasSize(2); + softly.assertThat(result.getContent()).extracting(Feed::getActivityContent) + .containsExactly("이미지 피드 2", "이미지 피드 1"); + }); } - @DisplayName("size 개수보다 남은 feed의 개수가 적다면, 그 수만큼 페이지로 반환한다.") + @DisplayName("전체 최신 피드 페이지 조회 - VIDEO 타입 피드 중 VOD 처리 완료된 피드만 조회된다") @Test - void 페이지네이션_남은_개수가_사이즈보다_적은경우() { + void getAllFeedPage_VideoFeedsWithCompleteVod() { // given - Club club = fixture.giveMeBuilder(Club.class) - .set("id", null) - .set("name", "카우") - .set("user", null) - .set("score", Score.from(BigDecimal.ZERO)) - .set("clubMembers", null) - .sample(); - Club savedClub = clubRepository.save(club); - - Feed feed1 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub) - .set("activityContent", "내용1") - .set("feedType", FeedType.IMAGE) - .sample(); - Feed feed2 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub) - .set("activityContent", "내용2") - .set("feedType", FeedType.IMAGE) - .sample(); - Feed feed3 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub) - .set("activityContent", "내용3") - .set("feedType", FeedType.IMAGE) - .sample(); - Feed feed4 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub) - .set("activityContent", "내용4") - .set("feedType", FeedType.IMAGE) - .sample(); - feedRepository.saveAll(List.of(feed1, feed2, feed3, feed4)); - - Long clubId = savedClub.getId(); - int size = 2; - Long cursorId = 2L; + Club club1 = clubRepository.save(ClubFixture.createClub()); + Club club2 = clubRepository.save(ClubFixture.createClub()); + Feed videoFeed1 = feedRepository.save(FeedFixture.createVideoFeed(club1, "비디오 피드 1")); + Feed videoFeed2 = feedRepository.save(FeedFixture.createVideoFeed(club2, "비디오 피드 2")); + + // 첫 번째 비디오만 VOD 처리 완료 + FileMetaData fileMetaData = fileMetaDataRepository.save(VodProcessingJobFixture.createFileMetaData(videoFeed1.getId())); + vodProcessingJobRepository.save(VodProcessingJobFixture.createCompleteVodProcessingJob(fileMetaData)); + // when - Slice page = feedRepository.findPageByClubIdOrderById(clubId, size, cursorId); + Slice result = feedRepository.getAllFeedPage(10, -1L); + // then - List feeds = page.getContent(); - assertThat(feeds.size()).isEqualTo(1); - assertThat(feeds.get(0).getId()).isEqualTo(1); - assertThat(feeds.get(0).getActivityContent()).isEqualTo(feed1.getActivityContent()); + assertSoftly(softly -> { + softly.assertThat(result.getContent()).hasSize(1); + softly.assertThat(result.getContent().get(0).getActivityContent()).isEqualTo("비디오 피드 1"); + }); } - @DisplayName("cursorId보다 작은 Feed를 size 개수만큼 페이지로 반환한다.") + @DisplayName("전체 최신 피드 페이지 조회 - 커서 기반 페이지네이션이 동작한다") @Test - void findPageByClubIdOrderById() { + void getAllFeedPage_CursorPagination() { // given - Club club = fixture.giveMeBuilder(Club.class) - .set("id", null) - .set("name", "카우") - .set("user", null) - .set("score", Score.from(BigDecimal.ZERO)) - .set("clubMembers", null) - .sample(); - Club savedClub = clubRepository.save(club); - - Feed feed1 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub) - .set("activityContent", "내용1") - .set("feedType", FeedType.IMAGE) - .sample(); - Feed feed2 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub) - .set("activityContent", "내용2") - .set("feedType", FeedType.IMAGE) - .sample(); - Feed feed3 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub) - .set("activityContent", "내용3") - .set("feedType", FeedType.IMAGE) - .sample(); - Feed feed4 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub) - .set("activityContent", "내용4") - .set("feedType", FeedType.IMAGE) - .sample(); - feedRepository.saveAll(List.of(feed1, feed2, feed3, feed4)); - - Long clubId = savedClub.getId(); - int size = 2; - Long cursorId = 4L; + Club club1 = clubRepository.save(ClubFixture.createClub()); + Club club2 = clubRepository.save(ClubFixture.createClub()); + Club club3 = clubRepository.save(ClubFixture.createClub()); + Feed feed1 = feedRepository.save(FeedFixture.createImageFeed(club1, "피드 1")); + Feed feed12 = feedRepository.save(FeedFixture.createImageFeed(club1, "피드 12")); + Feed feed2 = feedRepository.save(FeedFixture.createImageFeed(club2, "피드 2")); + Feed feed3 = feedRepository.save(FeedFixture.createImageFeed(club3, "피드 3")); + // when - Slice page = feedRepository.findPageByClubIdOrderById(clubId, size, cursorId); + Slice firstPage = feedRepository.getAllFeedPage(2, -1L); + Long lastId = firstPage.getContent().get(firstPage.getContent().size() - 1).getId(); + Slice secondPage = feedRepository.getAllFeedPage(2, lastId); + // then - List feeds = page.getContent(); - assertThat(feeds.size()).isEqualTo(2); - assertThat(feeds.get(0).getId()).isEqualTo(feed3.getId()); - assertThat(feeds.get(0).getActivityContent()).isEqualTo(feed3.getActivityContent()); - assertThat(feeds.get(1).getId()).isEqualTo(feed2.getId()); - assertThat(feeds.get(1).getActivityContent()).isEqualTo(feed2.getActivityContent()); + assertSoftly(softly -> { + softly.assertThat(firstPage.getContent()).hasSize(2); + softly.assertThat(secondPage.getContent()).hasSize(2); + softly.assertThat(secondPage.getContent().get(0).getActivityContent()).isEqualTo("피드 12"); + }); } - @DisplayName("동아리 피드 목록 조회 - VIDEO 피드일 경우 vodJopProcessingJob 상태가 COMPLETE인것만 조회 ") + @DisplayName("전체 최신 피드 페이지 조회 - 삭제된 피드는 조회되지 않는다") @Test - void 동아리_피드_목록_조회() { + void getAllFeedPage_ExcludesDeletedFeeds() { // given - Club club = fixture.giveMeBuilder(Club.class) - .set("id", null) - .set("name", "카우") - .set("user", null) - .set("score", Score.from(BigDecimal.ZERO)) - .set("clubMembers", null) - .sample(); - Club savedClub = clubRepository.save(club); - - Feed feed1 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub) - .set("activityContent", "내용1") - .set("feedType", FeedType.VIDEO) - .sample(); - Feed feed2 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub) - .set("activityContent", "내용2") - .set("feedType", FeedType.VIDEO) - .sample(); - feedRepository.saveAll(List.of(feed1, feed2)); - - UUID id = UUID.randomUUID(); - UUID id2 = UUID.randomUUID(); - DomainType domainType = DomainType.FEED_VIDEO; - FileMetaData fileMetaData = FileMetaData.builder() - .id(id) - .fileKey("123") - .fileName("1234.png") - .domainType(domainType) - .entityId(feed1.getId()) - .fileStatus(FileStatus.COUPLED) - .build(); - FileMetaData fileMetaData2 = FileMetaData.builder() - .id(id2) - .fileKey("123") - .fileName("1234.png") - .domainType(domainType) - .entityId(feed2.getId()) - .fileStatus(FileStatus.COUPLED) - .build(); - fileMetaDataRepository.saveAll(List.of(fileMetaData, fileMetaData2)); - - VodProcessingJob vodProcessingJob1 = fixture.giveMeBuilder(VodProcessingJob.class) - .set("id", null) - .set("vodProcessingNotification", null) - .set("fileMetaData", fileMetaData) - .set("convertJobStatus", ConvertJobStatus.COMPLETE) - .sample(); - VodProcessingJob vodProcessingJob2 = fixture.giveMeBuilder(VodProcessingJob.class) - .set("id", null) - .set("vodProcessingNotification", null) - .set("fileMetaData", fileMetaData2) - .set("convertJobStatus", ConvertJobStatus.PENDING) - .sample(); - vodProcessingJobRepository.saveAll(List.of(vodProcessingJob1, vodProcessingJob2)); - Long clubId = savedClub.getId(); - int size = 2; - Long currentCursorId = -1L; + Club club1 = clubRepository.save(ClubFixture.createClub()); + Club club2 = clubRepository.save(ClubFixture.createClub()); + Feed activeFeed = feedRepository.save(FeedFixture.createImageFeed(club1, "활성 피드")); + Feed deletedFeed = feedRepository.save(FeedFixture.createImageFeed(club2, "삭제된 피드")); + + // 피드 삭제 (soft delete) + feedRepository.delete(deletedFeed); + feedRepository.flush(); + // when - Slice findFeedsByClub = feedRepository.findPageByClubIdOrderById(clubId, size, currentCursorId); + Slice result = feedRepository.getAllFeedPage(10, -1L); // then - List feeds = findFeedsByClub.getContent(); - assertThat(feeds.size()).isEqualTo(1); - assertThat(feeds.get(0).getId()).isEqualTo(1); + assertSoftly(softly -> { + softly.assertThat(result.getContent()).hasSize(1); + softly.assertThat(result.getContent().get(0).getActivityContent()).isEqualTo("활성 피드"); + }); } - @DisplayName("모든 동아리 최신 피드 조회 - VIDEO 피드일 경우 vodJopProcessingJob 상태가 COMPLETE인것만 조회 ") + @DisplayName("전체 최신 피드 페이지 조회 - 여러 클럽의 피드가 최신순으로 정렬된다") @Test - void 모든_동아리_최신_피드_조회() { + void getAllFeedPage_OrderedByIdDesc() { // given - Club club = fixture.giveMeBuilder(Club.class) - .set("id", null) - .set("name", "카우") - .set("user", null) - .set("score", Score.from(BigDecimal.ZERO)) - .set("clubMembers", null) - .sample(); - Club savedClub = clubRepository.save(club); - - Feed feed1 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub) - .set("activityContent", "내용1") - .set("feedType", FeedType.VIDEO) - .sample(); - Feed feed2 = fixture.giveMeBuilder(Feed.class) - .set("id", null) - .set("club", savedClub) - .set("activityContent", "내용2") - .set("feedType", FeedType.VIDEO) - .sample(); - feedRepository.saveAll(List.of(feed1, feed2)); - - UUID id = UUID.randomUUID(); - UUID id2 = UUID.randomUUID(); - DomainType domainType = DomainType.FEED_VIDEO; - FileMetaData fileMetaData = FileMetaData.builder() - .id(id) - .fileKey("123") - .fileName("1234.png") - .domainType(domainType) - .entityId(feed1.getId()) - .fileStatus(FileStatus.COUPLED) - .build(); - FileMetaData fileMetaData2 = FileMetaData.builder() - .id(id2) - .fileKey("123") - .fileName("1234.png") - .domainType(domainType) - .entityId(feed2.getId()) - .fileStatus(FileStatus.COUPLED) - .build(); - fileMetaDataRepository.saveAll(List.of(fileMetaData, fileMetaData2)); - - VodProcessingJob vodProcessingJob1 = fixture.giveMeBuilder(VodProcessingJob.class) - .set("id", null) - .set("vodProcessingNotification", null) - .set("fileMetaData", fileMetaData) - .set("convertJobStatus", ConvertJobStatus.COMPLETE) - .sample(); - VodProcessingJob vodProcessingJob2 = fixture.giveMeBuilder(VodProcessingJob.class) - .set("id", null) - .set("vodProcessingNotification", null) - .set("fileMetaData", fileMetaData2) - .set("convertJobStatus", ConvertJobStatus.COMPLETE) - .sample(); - vodProcessingJobRepository.saveAll(List.of(vodProcessingJob1, vodProcessingJob2)); - - int size = 2; - Long currentCursorId = -1L; + Club club1 = clubRepository.save(ClubFixture.createClub()); + Club club2 = clubRepository.save(ClubFixture.createClub()); + + Feed oldFeed = feedRepository.save(FeedFixture.createImageFeed(club1, "오래된 피드")); + Feed newFeed = feedRepository.save(FeedFixture.createImageFeed(club2, "최신 피드")); + // when - Slice findFeedsByClub = feedRepository.findNewestPerClubPage(size, currentCursorId); + Slice result = feedRepository.getAllFeedPage(10, -1L); // then - List feeds = findFeedsByClub.getContent(); - assertThat(feeds.size()).isEqualTo(1); - assertThat(feeds.get(0).getId()).isEqualTo(2); + List feeds = result.getContent(); + assertThat(feeds.get(0).getId()).isGreaterThan(feeds.get(1).getId()); } + }