From 53b0b08d8870b762cfa408b2d8c3b57cdc3adc94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=ED=98=B8?= <66300965+kdkdhoho@users.noreply.github.com> Date: Sun, 27 Oct 2024 21:00:44 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=EC=9A=A9=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=A3=BC=EC=A0=9C(=ED=86=A0=ED=94=BD)=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1/=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#318)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 사용자용 요청 주제(토픽) 생성/조회 API 구현 (#306) * chore: 충돌 해결 * feat: 사용자용 요청 주제 목록 조회 API URI를 WhiteList에 추가 (#306) * test: 토픽 생성 테스트 검증 수정 (#306) --- .../common/auth/AuthorizationInterceptor.java | 1 + .../domain/category/CategoryType.java | 19 +- .../category/CategoryTypeConverter.java | 2 + .../application/domain/list/ListEntity.java | 3 - .../list/application/service/ListService.java | 2 - .../topic/application/domain/Topic.java | 46 +++++ .../application/service/TopicService.java | 33 ++++ .../service/dto/ExposedTopicFindResponse.java | 60 +++++++ .../service/dto/TopicCreateRequest.java | 24 +++ .../topic/presentation/TopicController.java | 35 ++++ .../repository/CustomTopicRepository.java | 9 + .../repository/CustomTopicRepositoryImpl.java | 34 ++++ .../topic/repository/TopicRepository.java | 7 + .../com/listywave/common/IntegrationTest.java | 7 + .../application/service/TopicServiceTest.java | 163 ++++++++++++++++++ 15 files changed, 434 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/listywave/topic/application/domain/Topic.java create mode 100644 src/main/java/com/listywave/topic/application/service/TopicService.java create mode 100644 src/main/java/com/listywave/topic/application/service/dto/ExposedTopicFindResponse.java create mode 100644 src/main/java/com/listywave/topic/application/service/dto/TopicCreateRequest.java create mode 100644 src/main/java/com/listywave/topic/presentation/TopicController.java create mode 100644 src/main/java/com/listywave/topic/repository/CustomTopicRepository.java create mode 100644 src/main/java/com/listywave/topic/repository/CustomTopicRepositoryImpl.java create mode 100644 src/main/java/com/listywave/topic/repository/TopicRepository.java create mode 100644 src/test/java/com/listywave/topic/application/service/TopicServiceTest.java diff --git a/src/main/java/com/listywave/common/auth/AuthorizationInterceptor.java b/src/main/java/com/listywave/common/auth/AuthorizationInterceptor.java index 376bc647..79724474 100644 --- a/src/main/java/com/listywave/common/auth/AuthorizationInterceptor.java +++ b/src/main/java/com/listywave/common/auth/AuthorizationInterceptor.java @@ -37,6 +37,7 @@ public class AuthorizationInterceptor implements HandlerInterceptor { new UriAndMethod("/categories", GET), new UriAndMethod("/users/basic-profile-image", GET), new UriAndMethod("/users/basic-background-image", GET), + new UriAndMethod("/topics", GET) }; private final JwtManager jwtManager; diff --git a/src/main/java/com/listywave/list/application/domain/category/CategoryType.java b/src/main/java/com/listywave/list/application/domain/category/CategoryType.java index 47f69743..7cf7979a 100644 --- a/src/main/java/com/listywave/list/application/domain/category/CategoryType.java +++ b/src/main/java/com/listywave/list/application/domain/category/CategoryType.java @@ -2,7 +2,6 @@ import static com.listywave.common.exception.ErrorCode.RESOURCE_NOT_FOUND; -import com.fasterxml.jackson.annotation.JsonCreator; import com.listywave.common.exception.CustomException; import java.util.Arrays; import lombok.AllArgsConstructor; @@ -15,16 +14,18 @@ public enum CategoryType { ENTIRE("0", "전체"), MUSIC("1", "음악"), MOVIE_DRAMA("2", "영화&드라마"), - ENTERTAINMENT_ARTS("3","엔터&예술"), + ENTERTAINMENT_ARTS("3", "엔터&예술"), TRAVEL("4", "여행"), RESTAURANT_CAFE("5", "맛집&카페"), FOOD_RECIPES("6", "음식&레시피"), PLACE("7", "공간"), DAILYLIFE_THOUGHTS("8", "일상&생각"), HOBBY_LEISURE("9", "취미&레저"), - ETC("10", "기타") + ETC("10", "기타"), ; + private static final String ERROR_MESSAGE = "해당 카테고리는 존재하지 않습니다. 입력값: "; + private final String code; private final String viewName; @@ -32,14 +33,20 @@ public static CategoryType codeOf(String code) { return Arrays.stream(CategoryType.values()) .filter(t -> t.getCode().equals(code)) .findAny() - .orElseThrow(() -> new CustomException(RESOURCE_NOT_FOUND, "해당 카테고리코드는 존재하지 않습니다.")); + .orElseThrow(() -> new CustomException(RESOURCE_NOT_FOUND, ERROR_MESSAGE + code)); } - @JsonCreator public static CategoryType nameOf(String name) { return Arrays.stream(CategoryType.values()) .filter(categoryType -> categoryType.name().equalsIgnoreCase(name)) .findFirst() - .orElseThrow(() -> new CustomException(RESOURCE_NOT_FOUND, "해당 카테고리는 존재하지 않습니다.")); + .orElseThrow(() -> new CustomException(RESOURCE_NOT_FOUND, ERROR_MESSAGE + name)); + } + + public static CategoryType viewNameOf(String viewName) { + return Arrays.stream(CategoryType.values()) + .filter(categoryType -> categoryType.getViewName().equals(viewName)) + .findFirst() + .orElseThrow(() -> new CustomException(RESOURCE_NOT_FOUND, ERROR_MESSAGE + viewName)); } } diff --git a/src/main/java/com/listywave/list/application/domain/category/CategoryTypeConverter.java b/src/main/java/com/listywave/list/application/domain/category/CategoryTypeConverter.java index cab3022a..7dd6c340 100644 --- a/src/main/java/com/listywave/list/application/domain/category/CategoryTypeConverter.java +++ b/src/main/java/com/listywave/list/application/domain/category/CategoryTypeConverter.java @@ -1,7 +1,9 @@ package com.listywave.list.application.domain.category; import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +@Converter(autoApply = true) public class CategoryTypeConverter implements AttributeConverter { @Override diff --git a/src/main/java/com/listywave/list/application/domain/list/ListEntity.java b/src/main/java/com/listywave/list/application/domain/list/ListEntity.java index 02eb2f49..7264101b 100644 --- a/src/main/java/com/listywave/list/application/domain/list/ListEntity.java +++ b/src/main/java/com/listywave/list/application/domain/list/ListEntity.java @@ -12,13 +12,11 @@ import com.listywave.collaborator.application.domain.Collaborators; import com.listywave.common.exception.CustomException; import com.listywave.list.application.domain.category.CategoryType; -import com.listywave.list.application.domain.category.CategoryTypeConverter; import com.listywave.list.application.domain.item.Item; import com.listywave.list.application.domain.item.Items; import com.listywave.list.application.domain.label.Labels; import com.listywave.user.application.domain.User; import jakarta.persistence.Column; -import jakarta.persistence.Convert; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; @@ -56,7 +54,6 @@ public class ListEntity { private User user; @Column(name = "category_code", length = 10, nullable = false) - @Convert(converter = CategoryTypeConverter.class) private CategoryType category; @Embedded diff --git a/src/main/java/com/listywave/list/application/service/ListService.java b/src/main/java/com/listywave/list/application/service/ListService.java index 0f75670f..86d2a600 100644 --- a/src/main/java/com/listywave/list/application/service/ListService.java +++ b/src/main/java/com/listywave/list/application/service/ListService.java @@ -53,7 +53,6 @@ import java.util.List; import java.util.Set; import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; @@ -78,7 +77,6 @@ public class ListService { private final CollaboratorService collaboratorService; private final HistoryService historyService; private final ReactionService reactionService; - private final ApplicationEventPublisher applicationEventPublisher; public ListCreateResponse listCreate(ListCreateRequest request, Long loginUserId) { User user = userRepository.getById(loginUserId); diff --git a/src/main/java/com/listywave/topic/application/domain/Topic.java b/src/main/java/com/listywave/topic/application/domain/Topic.java new file mode 100644 index 00000000..bd2e0f40 --- /dev/null +++ b/src/main/java/com/listywave/topic/application/domain/Topic.java @@ -0,0 +1,46 @@ +package com.listywave.topic.application.domain; + +import static lombok.AccessLevel.PROTECTED; + +import com.listywave.common.BaseEntity; +import com.listywave.list.application.domain.category.CategoryType; +import com.listywave.list.application.domain.list.ListDescription; +import com.listywave.list.application.domain.list.ListTitle; +import com.listywave.user.application.domain.User; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = PROTECTED) +@AllArgsConstructor +public class Topic extends BaseEntity { + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "topic_user_fk")) + private User user; + + @Column(name = "category_code", length = 10, nullable = false) + private CategoryType category; + + @Embedded + private ListTitle title; + + @Embedded + private ListDescription description; + + @Column(nullable = false) + private boolean isAnonymous; + + @Column(nullable = false) + private boolean isExposed; +} diff --git a/src/main/java/com/listywave/topic/application/service/TopicService.java b/src/main/java/com/listywave/topic/application/service/TopicService.java new file mode 100644 index 00000000..72aaea35 --- /dev/null +++ b/src/main/java/com/listywave/topic/application/service/TopicService.java @@ -0,0 +1,33 @@ +package com.listywave.topic.application.service; + +import com.listywave.topic.application.domain.Topic; +import com.listywave.topic.application.service.dto.ExposedTopicFindResponse; +import com.listywave.topic.application.service.dto.TopicCreateRequest; +import com.listywave.topic.repository.TopicRepository; +import com.listywave.user.application.domain.User; +import com.listywave.user.repository.user.UserRepository; +import jakarta.annotation.Nullable; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class TopicService { + + private final UserRepository userRepository; + private final TopicRepository topicRepository; + + public void create(TopicCreateRequest request, Long userId) { + User user = userRepository.getById(userId); + Topic topic = request.toEntity(user); + topicRepository.save(topic); + } + + public ExposedTopicFindResponse findAllExposed(@Nullable Long cursorId, int size) { + List result = topicRepository.findAllExposed(cursorId, size); + return ExposedTopicFindResponse.of(result, size); + } +} diff --git a/src/main/java/com/listywave/topic/application/service/dto/ExposedTopicFindResponse.java b/src/main/java/com/listywave/topic/application/service/dto/ExposedTopicFindResponse.java new file mode 100644 index 00000000..07dff9bf --- /dev/null +++ b/src/main/java/com/listywave/topic/application/service/dto/ExposedTopicFindResponse.java @@ -0,0 +1,60 @@ +package com.listywave.topic.application.service.dto; + +import com.listywave.topic.application.domain.Topic; +import java.util.List; +import lombok.Builder; + +public record ExposedTopicFindResponse( + Long cursorId, + boolean hasNext, + List topics +) { + + public static ExposedTopicFindResponse of(List topics, int size) { + if (topics.isEmpty()) { + return new ExposedTopicFindResponse(null, false, List.of()); + } + + boolean hasNext = false; + if (topics.size() > size) { + hasNext = true; + topics.remove(topics.size() - 1); + } + Long cursorId = topics.get(topics.size() - 1).getId(); + List topicDtos = TopicDto.toList(topics); + + return new ExposedTopicFindResponse(cursorId, hasNext, topicDtos); + } + + @Builder + public record TopicDto( + Long id, + String categoryEngName, + String categoryKorName, + String title, + String description, + Long ownerId, + String ownerNickname, + boolean isAnonymous + ) { + + public static List toList(List topics) { + return topics.stream() + .map(TopicDto::of) + .toList(); + } + + public static TopicDto of(Topic topic) { + return TopicDto.builder() + .id(topic.getId()) + .categoryEngName(topic.getCategory().name()) + .categoryKorName(topic.getCategory().getViewName()) + .title(topic.getTitle().getValue()) + .description(topic.getDescription().getValue()) + .ownerId(topic.getUser().getId()) + .ownerNickname(topic.getUser().getNickname()) + .isAnonymous(topic.isAnonymous()) + .build(); + } + } +} diff --git a/src/main/java/com/listywave/topic/application/service/dto/TopicCreateRequest.java b/src/main/java/com/listywave/topic/application/service/dto/TopicCreateRequest.java new file mode 100644 index 00000000..db257919 --- /dev/null +++ b/src/main/java/com/listywave/topic/application/service/dto/TopicCreateRequest.java @@ -0,0 +1,24 @@ +package com.listywave.topic.application.service.dto; + +import com.listywave.list.application.domain.category.CategoryType; +import com.listywave.list.application.domain.list.ListDescription; +import com.listywave.list.application.domain.list.ListTitle; +import com.listywave.topic.application.domain.Topic; +import com.listywave.user.application.domain.User; + +public record TopicCreateRequest( + String categoryKorName, + String title, + String description, + boolean isAnonymous +) { + + public Topic toEntity(User user) { + return Topic.builder() + .user(user) + .category(CategoryType.viewNameOf(categoryKorName)) + .title(new ListTitle(title)) + .description(new ListDescription(description)) + .build(); + } +} diff --git a/src/main/java/com/listywave/topic/presentation/TopicController.java b/src/main/java/com/listywave/topic/presentation/TopicController.java new file mode 100644 index 00000000..11a70992 --- /dev/null +++ b/src/main/java/com/listywave/topic/presentation/TopicController.java @@ -0,0 +1,35 @@ +package com.listywave.topic.presentation; + +import com.listywave.common.auth.Auth; +import com.listywave.topic.application.service.TopicService; +import com.listywave.topic.application.service.dto.ExposedTopicFindResponse; +import com.listywave.topic.application.service.dto.TopicCreateRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class TopicController { + + private final TopicService topicService; + + @PostMapping("/topics") + ResponseEntity create(@RequestBody TopicCreateRequest request, @Auth Long userId) { + topicService.create(request, userId); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/topics") + ResponseEntity findAllExposed( + @RequestParam(required = false) Long cursorId, + @RequestParam(defaultValue = "10") int size + ) { + ExposedTopicFindResponse result = topicService.findAllExposed(cursorId, size); + return ResponseEntity.ok(result); + } +} diff --git a/src/main/java/com/listywave/topic/repository/CustomTopicRepository.java b/src/main/java/com/listywave/topic/repository/CustomTopicRepository.java new file mode 100644 index 00000000..f0faaa0f --- /dev/null +++ b/src/main/java/com/listywave/topic/repository/CustomTopicRepository.java @@ -0,0 +1,9 @@ +package com.listywave.topic.repository; + +import com.listywave.topic.application.domain.Topic; +import java.util.List; + +public interface CustomTopicRepository { + + List findAllExposed(Long cursorId, int size); +} diff --git a/src/main/java/com/listywave/topic/repository/CustomTopicRepositoryImpl.java b/src/main/java/com/listywave/topic/repository/CustomTopicRepositoryImpl.java new file mode 100644 index 00000000..f0f200fa --- /dev/null +++ b/src/main/java/com/listywave/topic/repository/CustomTopicRepositoryImpl.java @@ -0,0 +1,34 @@ +package com.listywave.topic.repository; + +import static com.listywave.topic.application.domain.QTopic.topic; +import static com.listywave.user.application.domain.QUser.user; + +import com.listywave.topic.application.domain.Topic; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class CustomTopicRepositoryImpl implements CustomTopicRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public List findAllExposed(Long cursorId, int size) { + return queryFactory + .selectFrom(topic) + .join(user).on(topic.user.id.eq(user.id)) + .where( + cursorIdLowerThan(cursorId), + topic.isExposed.isTrue() + ) + .limit(size + 1) + .orderBy(topic.id.desc()) + .fetch(); + } + + private static BooleanExpression cursorIdLowerThan(Long cursorId) { + return cursorId == null ? null : topic.id.lt(cursorId); + } +} diff --git a/src/main/java/com/listywave/topic/repository/TopicRepository.java b/src/main/java/com/listywave/topic/repository/TopicRepository.java new file mode 100644 index 00000000..0723b390 --- /dev/null +++ b/src/main/java/com/listywave/topic/repository/TopicRepository.java @@ -0,0 +1,7 @@ +package com.listywave.topic.repository; + +import com.listywave.topic.application.domain.Topic; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TopicRepository extends JpaRepository, CustomTopicRepository { +} diff --git a/src/test/java/com/listywave/common/IntegrationTest.java b/src/test/java/com/listywave/common/IntegrationTest.java index 6a9515e7..4e2cfa4e 100644 --- a/src/test/java/com/listywave/common/IntegrationTest.java +++ b/src/test/java/com/listywave/common/IntegrationTest.java @@ -22,6 +22,8 @@ import com.listywave.list.repository.list.ListRepository; import com.listywave.list.repository.reply.ReplyRepository; import com.listywave.mention.MentionRepository; +import com.listywave.topic.application.service.TopicService; +import com.listywave.topic.repository.TopicRepository; import com.listywave.user.application.domain.User; import com.listywave.user.application.service.UserService; import com.listywave.user.repository.follow.FollowRepository; @@ -75,12 +77,17 @@ public abstract class IntegrationTest { protected FolderService folderService; @Autowired protected FollowRepository followRepository; + @Autowired + protected TopicService topicService; + @Autowired + protected TopicRepository topicRepository; protected User dh, js, ej, sy; protected ListEntity list; @BeforeEach void setUp() { + topicRepository.deleteAllInBatch(); followRepository.deleteAllInBatch(); collectionRepository.deleteAllInBatch(); alarmRepository.deleteAllInBatch(); diff --git a/src/test/java/com/listywave/topic/application/service/TopicServiceTest.java b/src/test/java/com/listywave/topic/application/service/TopicServiceTest.java new file mode 100644 index 00000000..7cf2973d --- /dev/null +++ b/src/test/java/com/listywave/topic/application/service/TopicServiceTest.java @@ -0,0 +1,163 @@ +package com.listywave.topic.application.service; + +import static com.listywave.list.application.domain.category.CategoryType.DAILYLIFE_THOUGHTS; +import static com.listywave.list.application.domain.category.CategoryType.MUSIC; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.listywave.common.IntegrationTest; +import com.listywave.list.application.domain.list.ListDescription; +import com.listywave.list.application.domain.list.ListTitle; +import com.listywave.topic.application.domain.Topic; +import com.listywave.topic.application.service.dto.ExposedTopicFindResponse; +import com.listywave.topic.application.service.dto.ExposedTopicFindResponse.TopicDto; +import com.listywave.topic.application.service.dto.TopicCreateRequest; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class TopicServiceTest extends IntegrationTest { + + @Nested + class 토픽_생성 { + + @Test + void 토픽을_생성한다() { + // given + TopicCreateRequest request = new TopicCreateRequest(DAILYLIFE_THOUGHTS.getViewName(), "제일 좋아하는 여자 아이돌 TOP3", "여러분들은 어떤 여돌을 가장 좋아하나요?", false); + Long userId = ej.getId(); + + // when + topicService.create(request, userId); + + // then + List result = topicRepository.findAll(); + assertAll( + () -> assertThat(result).hasSize(1), + () -> { + Topic topic = result.get(0); + assertThat(topic.getCategory()).isEqualTo(DAILYLIFE_THOUGHTS); + assertThat(topic.getTitle().getValue()).isEqualTo("제일 좋아하는 여자 아이돌 TOP3"); + assertThat(topic.getDescription().getValue()).isEqualTo("여러분들은 어떤 여돌을 가장 좋아하나요?"); + assertThat(topic.isAnonymous()).isFalse(); + assertThat(topic.isExposed()).isFalse(); + } + ); + } + } + + @Nested + class 토픽_조회 { + + @Test + void 노출이_승인된_토픽만_조회한다() { + // given + List topics = List.of( + new Topic(dh, MUSIC, new ListTitle("1"), new ListDescription("1"), false, true), // O + new Topic(dh, MUSIC, new ListTitle("2"), new ListDescription("2"), false, false), + new Topic(dh, MUSIC, new ListTitle("3"), new ListDescription("3"), false, true), // O + new Topic(dh, MUSIC, new ListTitle("4"), new ListDescription("4"), false, false), + new Topic(dh, MUSIC, new ListTitle("5"), new ListDescription("5"), false, true), // O + new Topic(dh, MUSIC, new ListTitle("6"), new ListDescription("6"), false, false), + new Topic(dh, MUSIC, new ListTitle("7"), new ListDescription("7"), false, true), // O + new Topic(dh, MUSIC, new ListTitle("8"), new ListDescription("8"), false, false), + new Topic(dh, MUSIC, new ListTitle("9"), new ListDescription("9"), false, true), // O + new Topic(dh, MUSIC, new ListTitle("10"), new ListDescription("10"), false, false), + new Topic(dh, MUSIC, new ListTitle("11"), new ListDescription("11"), false, true), // O + new Topic(dh, MUSIC, new ListTitle("12"), new ListDescription("12"), false, false), + new Topic(dh, MUSIC, new ListTitle("13"), new ListDescription("13"), false, true) // O + ); + topicRepository.saveAll(topics); + + // when + int size = 5; + ExposedTopicFindResponse result = topicService.findAllExposed(null, size); + + // then + assertAll( + () -> assertThat(result.hasNext()).isTrue(), + () -> { + List topicDtos = result.topics(); + + assertThat(result.cursorId()).isEqualTo(topicDtos.get(topicDtos.size() - 1).id()); + assertThat(topicDtos).extracting("title") + .isEqualTo(List.of("13", "11", "9", "7", "5")); + } + ); + } + + @Test + void cursorId가_null이고_size가_10일_때_노출이_승인된_토픽을_조회한다() { + // given + List topics = List.of( + new Topic(dh, MUSIC, new ListTitle("1"), new ListDescription("1"), false, true), + new Topic(dh, MUSIC, new ListTitle("2"), new ListDescription("2"), false, true), + new Topic(dh, MUSIC, new ListTitle("3"), new ListDescription("3"), false, true), + new Topic(dh, MUSIC, new ListTitle("4"), new ListDescription("4"), false, true), + new Topic(dh, MUSIC, new ListTitle("5"), new ListDescription("5"), false, true), + new Topic(dh, MUSIC, new ListTitle("6"), new ListDescription("6"), false, true), + new Topic(dh, MUSIC, new ListTitle("7"), new ListDescription("7"), false, true), + new Topic(dh, MUSIC, new ListTitle("8"), new ListDescription("8"), false, true), + new Topic(dh, MUSIC, new ListTitle("9"), new ListDescription("9"), false, true), + new Topic(dh, MUSIC, new ListTitle("10"), new ListDescription("10"), false, true), + new Topic(dh, MUSIC, new ListTitle("11"), new ListDescription("11"), false, true), + new Topic(dh, MUSIC, new ListTitle("12"), new ListDescription("12"), false, true), + new Topic(dh, MUSIC, new ListTitle("13"), new ListDescription("13"), false, true) + ); + topicRepository.saveAll(topics); + + // when + int size = 10; + ExposedTopicFindResponse result = topicService.findAllExposed(null, size); + + // then + assertAll( + () -> assertThat(result.hasNext()).isTrue(), + () -> { + List topicDtos = result.topics(); + + assertThat(result.cursorId()).isEqualTo(topicDtos.get(topicDtos.size() - 1).id()); + assertThat(topicDtos).extracting("title") + .isEqualTo(List.of("13", "12", "11", "10", "9", "8", "7", "6", "5", "4")); + } + ); + } + + @Test + void cursorId가_5이고_size가_5일_때_노출이_승인된_토픽을_조회한다() { + // given + List topics = List.of( + new Topic(dh, MUSIC, new ListTitle("1"), new ListDescription("1"), false, true), + new Topic(dh, MUSIC, new ListTitle("2"), new ListDescription("2"), false, true), + new Topic(dh, MUSIC, new ListTitle("3"), new ListDescription("3"), false, true), + new Topic(dh, MUSIC, new ListTitle("4"), new ListDescription("4"), false, true), + new Topic(dh, MUSIC, new ListTitle("5"), new ListDescription("5"), false, true), + new Topic(dh, MUSIC, new ListTitle("6"), new ListDescription("6"), false, true), + new Topic(dh, MUSIC, new ListTitle("7"), new ListDescription("7"), false, true), + new Topic(dh, MUSIC, new ListTitle("8"), new ListDescription("8"), false, true), + new Topic(dh, MUSIC, new ListTitle("9"), new ListDescription("9"), false, true), + new Topic(dh, MUSIC, new ListTitle("10"), new ListDescription("10"), false, true), + new Topic(dh, MUSIC, new ListTitle("11"), new ListDescription("11"), false, true), + new Topic(dh, MUSIC, new ListTitle("12"), new ListDescription("12"), false, true), + new Topic(dh, MUSIC, new ListTitle("13"), new ListDescription("13"), false, true) + ); + topicRepository.saveAll(topics); + + // when + ExposedTopicFindResponse result = topicService.findAllExposed(5L, 5); + + // then + assertAll( + () -> assertThat(result.hasNext()).isFalse(), + () -> { + List topicDtos = result.topics(); + + assertThat(result.cursorId()).isEqualTo(topicDtos.get(topicDtos.size() - 1).id()); + assertThat(topicDtos).extracting("title") + .isEqualTo(List.of("4", "3", "2", "1")); + } + ); + + } + } +}