diff --git a/src/main/java/com/listywave/topic/application/service/TopicService.java b/src/main/java/com/listywave/topic/application/service/TopicService.java index 72aaea35..1addf96e 100644 --- a/src/main/java/com/listywave/topic/application/service/TopicService.java +++ b/src/main/java/com/listywave/topic/application/service/TopicService.java @@ -3,6 +3,7 @@ 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.application.service.dto.TopicFindResponse; import com.listywave.topic.repository.TopicRepository; import com.listywave.user.application.domain.User; import com.listywave.user.repository.user.UserRepository; @@ -30,4 +31,10 @@ public ExposedTopicFindResponse findAllExposed(@Nullable Long cursorId, int size List result = topicRepository.findAllExposed(cursorId, size); return ExposedTopicFindResponse.of(result, size); } + + public TopicFindResponse findAll(@Nullable Long cursorId, int size) { + List result = topicRepository.findAll(cursorId, size); + long totalCount = (topicRepository.count() / size) + 1; + return TopicFindResponse.from(result, size, totalCount); + } } diff --git a/src/main/java/com/listywave/topic/application/service/dto/TopicFindResponse.java b/src/main/java/com/listywave/topic/application/service/dto/TopicFindResponse.java new file mode 100644 index 00000000..c2ed981b --- /dev/null +++ b/src/main/java/com/listywave/topic/application/service/dto/TopicFindResponse.java @@ -0,0 +1,69 @@ +package com.listywave.topic.application.service.dto; + +import com.listywave.topic.application.domain.Topic; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Builder; + +@Builder +public record TopicFindResponse( + boolean hasNext, + long totalCount, + Long cursorId, + List topics +) { + + public static TopicFindResponse from(List topics, int size, long totalCount) { + if (topics.isEmpty()) { + return new TopicFindResponse(false, totalCount, null, List.of()); + } + + boolean hasNext = false; + if (topics.size() > size) { + hasNext = true; + topics.remove(topics.size() - 1); + } + long cursorId = topics.get(topics.size() - 1).getId(); + + return TopicFindResponse.builder() + .hasNext(hasNext) + .totalCount(totalCount) + .cursorId(cursorId) + .topics(TopicDto.toList(topics)) + .build(); + } + + @Builder + public record TopicDto( + String categoryEngName, + String categoryKorName, + String title, + String description, + LocalDateTime createdDate, + Long ownerId, + String ownerNickname, + boolean isAnonymous, + boolean isExposed + ) { + + public static List toList(List topics) { + return topics.stream() + .map(TopicDto::of) + .toList(); + } + + private static TopicDto of(Topic topic) { + return TopicDto.builder() + .categoryEngName(topic.getCategory().name()) + .categoryKorName(topic.getCategory().getViewName()) + .title(topic.getTitle().getValue()) + .description(topic.getDescription().getValue()) + .createdDate(topic.getCreatedDate()) + .ownerId(topic.getUser().getId()) + .ownerNickname(topic.getUser().getNickname()) + .isAnonymous(topic.isAnonymous()) + .isExposed(topic.isExposed()) + .build(); + } + } +} diff --git a/src/main/java/com/listywave/topic/presentation/TopicController.java b/src/main/java/com/listywave/topic/presentation/TopicController.java index 11a70992..50dc5adf 100644 --- a/src/main/java/com/listywave/topic/presentation/TopicController.java +++ b/src/main/java/com/listywave/topic/presentation/TopicController.java @@ -4,6 +4,7 @@ import com.listywave.topic.application.service.TopicService; import com.listywave.topic.application.service.dto.ExposedTopicFindResponse; import com.listywave.topic.application.service.dto.TopicCreateRequest; +import com.listywave.topic.application.service.dto.TopicFindResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -32,4 +33,13 @@ ResponseEntity findAllExposed( ExposedTopicFindResponse result = topicService.findAllExposed(cursorId, size); return ResponseEntity.ok(result); } + + @GetMapping("/admin/topics") + ResponseEntity findAll( + @RequestParam(required = false) Long cursorId, + @RequestParam(defaultValue = "5") int size + ) { + TopicFindResponse result = topicService.findAll(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 index f0faaa0f..b1c8a43c 100644 --- a/src/main/java/com/listywave/topic/repository/CustomTopicRepository.java +++ b/src/main/java/com/listywave/topic/repository/CustomTopicRepository.java @@ -6,4 +6,6 @@ public interface CustomTopicRepository { List findAllExposed(Long cursorId, int size); + + List findAll(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 index f0f200fa..70a6d57f 100644 --- a/src/main/java/com/listywave/topic/repository/CustomTopicRepositoryImpl.java +++ b/src/main/java/com/listywave/topic/repository/CustomTopicRepositoryImpl.java @@ -6,6 +6,7 @@ import com.listywave.topic.application.domain.Topic; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.annotation.Nullable; import java.util.List; import lombok.RequiredArgsConstructor; @@ -15,7 +16,7 @@ public class CustomTopicRepositoryImpl implements CustomTopicRepository { private final JPAQueryFactory queryFactory; @Override - public List findAllExposed(Long cursorId, int size) { + public List findAllExposed(@Nullable Long cursorId, int size) { return queryFactory .selectFrom(topic) .join(user).on(topic.user.id.eq(user.id)) @@ -28,7 +29,24 @@ public List findAllExposed(Long cursorId, int size) { .fetch(); } - private static BooleanExpression cursorIdLowerThan(Long cursorId) { + private BooleanExpression cursorIdLowerThan(Long cursorId) { return cursorId == null ? null : topic.id.lt(cursorId); } + + @Override + public List findAll(@Nullable Long cursorId, int size) { + return queryFactory + .selectFrom(topic) + .join(user).on(topic.user.id.eq(user.id)) + .where( + cursorIdGreaterThan(cursorId) + ) + .limit(size + 1) + .orderBy(topic.id.asc()) + .fetch(); + } + + private BooleanExpression cursorIdGreaterThan(Long cursorId) { + return cursorId == null ? null : topic.id.gt(cursorId); + } } diff --git a/src/test/java/com/listywave/topic/application/service/TopicServiceTest.java b/src/test/java/com/listywave/topic/application/service/TopicServiceTest.java index 7cf2973d..aaf4414c 100644 --- a/src/test/java/com/listywave/topic/application/service/TopicServiceTest.java +++ b/src/test/java/com/listywave/topic/application/service/TopicServiceTest.java @@ -12,6 +12,7 @@ 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 com.listywave.topic.application.service.dto.TopicFindResponse; import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -47,7 +48,7 @@ class 토픽_생성 { } @Nested - class 토픽_조회 { + class 사용자용_토픽_조회 { @Test void 노출이_승인된_토픽만_조회한다() { @@ -124,7 +125,7 @@ class 토픽_조회 { } @Test - void cursorId가_5이고_size가_5일_때_노출이_승인된_토픽을_조회한다() { + void cursorId가_뒤에서_다섯_번째고_size가_5일_때_노출이_승인된_토픽을_조회한다() { // given List topics = List.of( new Topic(dh, MUSIC, new ListTitle("1"), new ListDescription("1"), false, true), @@ -144,20 +145,121 @@ class 토픽_조회 { topicRepository.saveAll(topics); // when - ExposedTopicFindResponse result = topicService.findAllExposed(5L, 5); + long cursorId = topics.get(8).getId(); + ExposedTopicFindResponse result = topicService.findAllExposed(cursorId, 5); // then assertAll( - () -> assertThat(result.hasNext()).isFalse(), + () -> assertThat(result.hasNext()).isTrue(), () -> { List topicDtos = result.topics(); - assertThat(result.cursorId()).isEqualTo(topicDtos.get(topicDtos.size() - 1).id()); + assertThat(result.cursorId()).isEqualTo(topics.get(3).getId()); assertThat(topicDtos).extracting("title") - .isEqualTo(List.of("4", "3", "2", "1")); + .isEqualTo(List.of("8", "7", "6", "5", "4")); } ); } } + + @Nested + class 관리자용_토픽_조회 { + + @Test + void cursorId가_null이고_size가_10일_때_모든_토픽을_조회한다() { + // 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 + TopicFindResponse result = topicService.findAll(null, 10); + + // then + assertAll( + () -> assertThat(result.hasNext()).isTrue(), + () -> assertThat(result.totalCount()).isEqualTo(2), + () -> assertThat(result.topics()).extracting("title") + .isEqualTo(List.of("1", "2", "3", "4", "5", "6", "7", "8", "9", "10")) + ); + } + + @Test + void cursorId가_열_번째_ID이고_size가_10일_때_모든_토픽을_조회한다() { + // 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 + TopicFindResponse result = topicService.findAll(topics.get(9).getId(), 10); + + // then + assertAll( + () -> assertThat(result.hasNext()).isFalse(), + () -> assertThat(result.totalCount()).isEqualTo(2), + () -> assertThat(result.topics()).extracting("title") + .isEqualTo(List.of("11", "12", "13")) + ); + } + + @Test + void cursorId가_다섯_번째이고_size가_5일_때_모든_토픽을_조회한다() { + // 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 + TopicFindResponse result = topicService.findAll(topics.get(4).getId(), 5); + + // then + assertAll( + () -> assertThat(result.hasNext()).isTrue(), + () -> assertThat(result.totalCount()).isEqualTo(3), + () -> assertThat(result.topics()).extracting("title") + .isEqualTo(List.of("6", "7", "8", "9", "10")) + ); + } + } }