diff --git a/src/main/java/com/jisungin/api/book/BookController.java b/src/main/java/com/jisungin/api/book/BookController.java index 521c0e8..73a0841 100644 --- a/src/main/java/com/jisungin/api/book/BookController.java +++ b/src/main/java/com/jisungin/api/book/BookController.java @@ -2,18 +2,16 @@ import com.jisungin.api.ApiResponse; import com.jisungin.api.book.request.BookCreateRequest; -import com.jisungin.api.book.request.BookPageRequest; import com.jisungin.application.OffsetLimit; import com.jisungin.application.PageResponse; import com.jisungin.application.book.BestSellerService; import com.jisungin.application.book.BookService; -import com.jisungin.application.book.response.BestSellerResponse; -import com.jisungin.application.book.response.BookResponse; import com.jisungin.application.book.response.BookFindAllResponse; +import com.jisungin.application.book.response.BookResponse; +import com.jisungin.application.book.response.BookWithRankingResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -44,8 +42,11 @@ public ApiResponse> getBooks( } @GetMapping("/books/best-seller") - public ApiResponse> getBestSellers(@ModelAttribute BookPageRequest page) { - return ApiResponse.ok(bestSellerService.getBestSellers(page.toService())); + public ApiResponse> getBestSellers( + @RequestParam(required = false, defaultValue = "1") Integer page, + @RequestParam(required = false, defaultValue = "5") Integer size + ) { + return ApiResponse.ok(bestSellerService.getBestSellers(OffsetLimit.ofRange(page, size))); } @PostMapping("/books") diff --git a/src/main/java/com/jisungin/api/book/request/BookPageRequest.java b/src/main/java/com/jisungin/api/book/request/BookPageRequest.java deleted file mode 100644 index c5c858f..0000000 --- a/src/main/java/com/jisungin/api/book/request/BookPageRequest.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.jisungin.api.book.request; - -import com.jisungin.application.book.request.BookServicePageRequest; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@NoArgsConstructor -public class BookPageRequest { - - private Integer page; - private Integer size; - - @Builder - private BookPageRequest(Integer page, Integer size) { - this.page = page != null ? page : 1; - this.size = size != null ? size : 5; - } - - public BookServicePageRequest toService() { - return BookServicePageRequest.builder() - .page(page) - .size(size) - .build(); - } - -} diff --git a/src/main/java/com/jisungin/application/OffsetLimit.java b/src/main/java/com/jisungin/application/OffsetLimit.java index 4c9a7ee..4f8fbbf 100644 --- a/src/main/java/com/jisungin/application/OffsetLimit.java +++ b/src/main/java/com/jisungin/application/OffsetLimit.java @@ -35,6 +35,13 @@ public static OffsetLimit of(Integer page, Integer size, String order) { .build(); } + public static OffsetLimit ofRange(Integer page, Integer size) { + return OffsetLimit.builder() + .offset(calculateOffset(page, size)) + .limit(page * size - 1) + .build(); + } + private static Integer calculateOffset(Integer page, Integer size) { return (max(1, page) - 1) * min(size, MAX_SIZE); } diff --git a/src/main/java/com/jisungin/application/book/BestSellerService.java b/src/main/java/com/jisungin/application/book/BestSellerService.java index 9f4dbf3..5ef7f19 100644 --- a/src/main/java/com/jisungin/application/book/BestSellerService.java +++ b/src/main/java/com/jisungin/application/book/BestSellerService.java @@ -1,12 +1,13 @@ package com.jisungin.application.book; +import com.jisungin.application.OffsetLimit; import com.jisungin.application.PageResponse; import com.jisungin.application.book.event.BestSellerUpdatedEvent; -import com.jisungin.application.book.request.BookServicePageRequest; -import com.jisungin.application.book.response.BestSellerResponse; +import com.jisungin.application.book.response.BookWithRankingResponse; import com.jisungin.domain.book.repository.BestSellerRepository; +import com.jisungin.infra.crawler.CrawledBook; import com.jisungin.infra.crawler.Crawler; -import com.jisungin.infra.crawler.CrawlingBook; +import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; @@ -20,16 +21,20 @@ public class BestSellerService { private final BestSellerRepository bestSellerRepository; private final ApplicationEventPublisher eventPublisher; - public PageResponse getBestSellers(BookServicePageRequest page) { - return bestSellerRepository.findBestSellerByPage(page); - } + public PageResponse getBestSellers(OffsetLimit offsetLimit) { + List response = bestSellerRepository.findBooksWithRank(offsetLimit.getOffset(), + offsetLimit.getLimit()); + + Long count = bestSellerRepository.count(); + return PageResponse.of(response.size(), count, response); + } public void updateBestSellers() { - Map crawledBooks = crawler.crawlBestSellerBook(); + Map crawledBookMap = crawler.crawlBestSellerBook(); - bestSellerRepository.updateAll(crawledBooks); - eventPublisher.publishEvent(new BestSellerUpdatedEvent(crawledBooks)); + bestSellerRepository.updateAll(crawledBookMap); + eventPublisher.publishEvent(new BestSellerUpdatedEvent(crawledBookMap)); } } \ No newline at end of file diff --git a/src/main/java/com/jisungin/application/book/BookService.java b/src/main/java/com/jisungin/application/book/BookService.java index 70bdf3a..768bd6f 100644 --- a/src/main/java/com/jisungin/application/book/BookService.java +++ b/src/main/java/com/jisungin/application/book/BookService.java @@ -3,9 +3,9 @@ import com.jisungin.application.OffsetLimit; import com.jisungin.application.PageResponse; import com.jisungin.application.book.request.BookCreateServiceRequest; +import com.jisungin.application.book.request.BookCreateServiceRequests; import com.jisungin.application.book.response.BookFindAllResponse; import com.jisungin.application.book.response.BookResponse; -import com.jisungin.application.talkroom.response.TalkRoomQueryResponse; import com.jisungin.domain.book.Book; import com.jisungin.domain.book.repository.BookRepository; import com.jisungin.domain.rating.repository.RatingRepository; @@ -13,6 +13,7 @@ import com.jisungin.exception.ErrorCode; import com.jisungin.infra.crawler.Crawler; import java.util.List; +import java.util.Set; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -56,17 +57,11 @@ public BookResponse createBook(BookCreateServiceRequest request) { } @Transactional - public void addNewBooks(List requests) { - requests.stream() - .filter(request -> !bookRepository.existsBookByIsbn(request.getIsbn())) - .map(BookCreateServiceRequest::toEntity) - .forEach(bookRepository::save); - } + public void addNewBooks(BookCreateServiceRequests requests) { + Set existIsbns = bookRepository.findExistIsbns(requests.getIsbns()); + List newBooks = requests.toEntitiesNotInclude(existIsbns); - private List extractTalkRoomIds(List talkRooms) { - return talkRooms.stream() - .map(TalkRoomQueryResponse::getId) - .toList(); + bookRepository.saveAll(newBooks); } } diff --git a/src/main/java/com/jisungin/application/book/event/BestSellerUpdatedEvent.java b/src/main/java/com/jisungin/application/book/event/BestSellerUpdatedEvent.java index 830713b..92d90a4 100644 --- a/src/main/java/com/jisungin/application/book/event/BestSellerUpdatedEvent.java +++ b/src/main/java/com/jisungin/application/book/event/BestSellerUpdatedEvent.java @@ -1,16 +1,24 @@ package com.jisungin.application.book.event; -import com.jisungin.infra.crawler.CrawlingBook; +import com.jisungin.application.book.request.BookCreateServiceRequest; +import com.jisungin.infra.crawler.CrawledBook; +import java.util.List; import java.util.Map; import lombok.Getter; @Getter public class BestSellerUpdatedEvent { - private final Map crawledBooks; + private final Map crawledBookMap; - public BestSellerUpdatedEvent(Map crawledBooks) { - this.crawledBooks = crawledBooks; + public BestSellerUpdatedEvent(Map crawledBookMap) { + this.crawledBookMap = crawledBookMap; + } + + public List getServiceRequests() { + return crawledBookMap.values().stream() + .map(CrawledBook::toServiceRequest) + .toList(); } } diff --git a/src/main/java/com/jisungin/application/book/event/BestSellerUpdatedEventListener.java b/src/main/java/com/jisungin/application/book/event/BestSellerUpdatedEventListener.java index b4ae241..7144c97 100644 --- a/src/main/java/com/jisungin/application/book/event/BestSellerUpdatedEventListener.java +++ b/src/main/java/com/jisungin/application/book/event/BestSellerUpdatedEventListener.java @@ -1,10 +1,7 @@ package com.jisungin.application.book.event; import com.jisungin.application.book.BookService; -import com.jisungin.application.book.request.BookCreateServiceRequest; -import com.jisungin.infra.crawler.CrawlingBook; -import java.util.List; -import java.util.Map; +import com.jisungin.application.book.request.BookCreateServiceRequests; import lombok.RequiredArgsConstructor; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @@ -17,13 +14,7 @@ public class BestSellerUpdatedEventListener { @EventListener public void handleBestSellerUpdatedEvent(BestSellerUpdatedEvent event) { - Map crawledBook = event.getCrawledBooks(); - - List bookCreateServiceRequests = crawledBook.values().stream() - .map(CrawlingBook::toServiceRequest) - .toList(); - - bookService.addNewBooks(bookCreateServiceRequests); + bookService.addNewBooks(BookCreateServiceRequests.of(event.getServiceRequests())); } } diff --git a/src/main/java/com/jisungin/application/book/request/BookCreateServiceRequest.java b/src/main/java/com/jisungin/application/book/request/BookCreateServiceRequest.java index 0db9095..1ccecf9 100644 --- a/src/main/java/com/jisungin/application/book/request/BookCreateServiceRequest.java +++ b/src/main/java/com/jisungin/application/book/request/BookCreateServiceRequest.java @@ -1,7 +1,6 @@ package com.jisungin.application.book.request; import com.jisungin.domain.book.Book; -import com.jisungin.infra.crawler.CrawlingBook; import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/com/jisungin/application/book/request/BookCreateServiceRequests.java b/src/main/java/com/jisungin/application/book/request/BookCreateServiceRequests.java new file mode 100644 index 0000000..d107556 --- /dev/null +++ b/src/main/java/com/jisungin/application/book/request/BookCreateServiceRequests.java @@ -0,0 +1,38 @@ +package com.jisungin.application.book.request; + +import com.jisungin.domain.book.Book; +import java.util.List; +import java.util.Set; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class BookCreateServiceRequests { + + private final List requests; + + @Builder + private BookCreateServiceRequests(List requests) { + this.requests = requests; + } + + public static BookCreateServiceRequests of(List requests) { + return BookCreateServiceRequests.builder() + .requests(requests) + .build(); + } + + public List getIsbns() { + return requests.stream() + .map(BookCreateServiceRequest::getIsbn) + .toList(); + } + + public List toEntitiesNotInclude(Set existIsbns) { + return requests.stream() + .filter(request -> !existIsbns.contains(request.getIsbn())) + .map(BookCreateServiceRequest::toEntity) + .toList(); + } + +} diff --git a/src/main/java/com/jisungin/application/book/response/BestSellerResponse.java b/src/main/java/com/jisungin/application/book/response/BestSellerResponse.java deleted file mode 100644 index 3b0c2d6..0000000 --- a/src/main/java/com/jisungin/application/book/response/BestSellerResponse.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.jisungin.application.book.response; - -import java.time.LocalDateTime; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -public class BestSellerResponse { - - private Long ranking; - private String isbn; - private String title; - private String publisher; - private String thumbnail; - private String[] authors; - private LocalDateTime dateTime; - - @Builder - private BestSellerResponse(Long ranking, String isbn, String title, String publisher, String thumbnail, - String[] authors, LocalDateTime dateTime) { - this.ranking = ranking; - this.isbn = isbn; - this.title = title; - this.publisher = publisher; - this.thumbnail = thumbnail; - this.authors = authors; - this.dateTime = dateTime; - } - - public static BestSellerResponse of(Long ranking, String isbn, String title, String publisher, String thumbnail, - String[] authors, LocalDateTime dateTime) { - return BestSellerResponse.builder() - .ranking(ranking) - .isbn(isbn) - .title(title) - .publisher(publisher) - .thumbnail(thumbnail) - .authors(authors) - .dateTime(dateTime) - .build(); - } - -} diff --git a/src/main/java/com/jisungin/application/book/response/BookWithRankingResponse.java b/src/main/java/com/jisungin/application/book/response/BookWithRankingResponse.java new file mode 100644 index 0000000..0dba4cb --- /dev/null +++ b/src/main/java/com/jisungin/application/book/response/BookWithRankingResponse.java @@ -0,0 +1,58 @@ +package com.jisungin.application.book.response; + +import com.jisungin.infra.crawler.CrawledBook; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class BookWithRankingResponse { + + private Long ranking; + private String isbn; + private String title; + private String publisher; + private String thumbnail; + private String[] authors; + private LocalDateTime dateTime; + + @Builder + private BookWithRankingResponse(Long ranking, String isbn, String title, String publisher, String thumbnail, + String[] authors, LocalDateTime dateTime) { + this.ranking = ranking; + this.isbn = isbn; + this.title = title; + this.publisher = publisher; + this.thumbnail = thumbnail; + this.authors = authors; + this.dateTime = dateTime; + } + + public static BookWithRankingResponse of(Long ranking, String isbn, String title, String publisher, String thumbnail, + String[] authors, LocalDateTime dateTime) { + return BookWithRankingResponse.builder() + .ranking(ranking) + .isbn(isbn) + .title(title) + .publisher(publisher) + .thumbnail(thumbnail) + .authors(authors) + .dateTime(dateTime) + .build(); + } + + public static BookWithRankingResponse ofRankIncrement(Long ranking, CrawledBook crawledBook) { + return BookWithRankingResponse.builder() + .ranking(ranking + 1) + .isbn(crawledBook.getIsbn()) + .title(crawledBook.getTitle()) + .publisher(crawledBook.getPublisher()) + .thumbnail(crawledBook.getThumbnail()) + .authors(crawledBook.getAuthors()) + .dateTime(crawledBook.getDateTime()) + .build(); + } + +} diff --git a/src/main/java/com/jisungin/application/search/SearchService.java b/src/main/java/com/jisungin/application/search/SearchService.java index fa516ec..ddac768 100644 --- a/src/main/java/com/jisungin/application/search/SearchService.java +++ b/src/main/java/com/jisungin/application/search/SearchService.java @@ -1,27 +1,23 @@ package com.jisungin.application.search; +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ZSetOperations; import org.springframework.stereotype.Service; -import java.util.List; -import java.util.Set; - @Slf4j @Service +@RequiredArgsConstructor public class SearchService { private final RedisTemplate redisTemplate; - public SearchService(@Qualifier("redisTemplateSecond") RedisTemplate redisTemplate) { - this.redisTemplate = redisTemplate; - } - public void searchKeyword(String keyword) { ZSetOperations zset = redisTemplate.opsForZSet(); - zset.incrementScore("ranking", keyword, 1); // 점수 증가 + zset.incrementScore("ranking", keyword, 1); } public List getRankKeywords() { diff --git a/src/main/java/com/jisungin/config/RedisConfig.java b/src/main/java/com/jisungin/config/RedisConfig.java index 5b232ec..1e24852 100644 --- a/src/main/java/com/jisungin/config/RedisConfig.java +++ b/src/main/java/com/jisungin/config/RedisConfig.java @@ -1,22 +1,13 @@ package com.jisungin.config; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.data.redis.cache.RedisCacheConfiguration; -import org.springframework.data.redis.cache.RedisCacheManager; -import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration @@ -33,35 +24,19 @@ public class RedisConfig { @Value("${spring.data.redis.password}") private String password; - @Primary - @Bean(name = "redisConnectionFactoryFirst") - public LettuceConnectionFactory redisConnectionFactoryFirst() { - RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); - - redisStandaloneConfiguration.setHostName(host); - redisStandaloneConfiguration.setPort(port); - redisStandaloneConfiguration.setPassword(password); - redisStandaloneConfiguration.setDatabase(0); - - return new LettuceConnectionFactory(redisStandaloneConfiguration); - } - - @Bean(name = "redisConnectionFactorySecond") - public LettuceConnectionFactory redisConnectionFactorySecond() { + @Bean + public LettuceConnectionFactory redisConnectionFactory() { RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); redisStandaloneConfiguration.setHostName(host); redisStandaloneConfiguration.setPort(port); redisStandaloneConfiguration.setPassword(password); - redisStandaloneConfiguration.setDatabase(1); return new LettuceConnectionFactory(redisStandaloneConfiguration); } - @Primary - @Bean(name = "redisTemplateFirst") - public RedisTemplate redisTemplateFirst( - @Qualifier("redisConnectionFactoryFirst") LettuceConnectionFactory connectionFactory) { + @Bean + public RedisTemplate redisTemplate(LettuceConnectionFactory connectionFactory) { RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); @@ -76,34 +51,5 @@ public RedisTemplate redisTemplateFirst( return redisTemplate; } - @Bean(name = "redisTemplateSecond") - public RedisTemplate redisTemplateSecond( - @Qualifier("redisConnectionFactorySecond") LettuceConnectionFactory connectionFactory) { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(connectionFactory); - redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class)); - return redisTemplate; - } - - @Bean - public RedisMessageListenerContainer redisMessageListenerContainer( - @Qualifier("redisConnectionFactoryFirst") RedisConnectionFactory connectionFactory) { - RedisMessageListenerContainer container = new RedisMessageListenerContainer(); - container.setConnectionFactory(connectionFactory); - return container; - } - - @Bean - public RedisCacheManager redisCacheManager( - @Qualifier("redisConnectionFactorySecond") RedisConnectionFactory connectionFactory) { - RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() - .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); - - return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory) - .cacheDefaults(redisCacheConfiguration) - .build(); - } } diff --git a/src/main/java/com/jisungin/domain/book/repository/BestSellerRedisRepository.java b/src/main/java/com/jisungin/domain/book/repository/BestSellerRedisRepository.java index da7d5b9..62ec60f 100644 --- a/src/main/java/com/jisungin/domain/book/repository/BestSellerRedisRepository.java +++ b/src/main/java/com/jisungin/domain/book/repository/BestSellerRedisRepository.java @@ -1,17 +1,13 @@ package com.jisungin.domain.book.repository; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.jisungin.application.PageResponse; -import com.jisungin.application.book.request.BookServicePageRequest; -import com.jisungin.application.book.response.BestSellerResponse; -import com.jisungin.exception.BusinessException; -import com.jisungin.exception.ErrorCode; -import com.jisungin.infra.crawler.CrawlingBook; -import java.util.Comparator; +import com.jisungin.application.book.response.BookWithRankingResponse; +import com.jisungin.infra.JsonConverter; +import com.jisungin.infra.crawler.CrawledBook; +import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.stream.IntStream; +import java.util.Optional; +import java.util.Set; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @@ -21,65 +17,49 @@ public class BestSellerRedisRepository implements BestSellerRepository { private static final String BEST_SELLER_REDIS_KEY = "BEST_SELLER"; - private final ObjectMapper om; + + private final JsonConverter converter; private final RedisTemplate redisTemplate; @Override - public PageResponse findBestSellerByPage(BookServicePageRequest request) { - List queryResponse = redisTemplate.opsForHash() - .multiGet(BEST_SELLER_REDIS_KEY, createHashKeys(request.extractStartIndex(), request.extractEndIndex())) - .stream() - .map(this::parseToBestSellerResponse) - .sorted(Comparator.comparing(BestSellerResponse::getRanking)) - .toList(); - - return PageResponse.builder() - .size(request.getSize()) - .totalCount(redisTemplate.opsForHash().size(BEST_SELLER_REDIS_KEY)) - .queryResponse(queryResponse) - .build(); + public Long count() { + return redisTemplate.opsForZSet().zCard(BEST_SELLER_REDIS_KEY); } @Override - public List findAll() { - Map bestSellers = redisTemplate.opsForHash().entries(BEST_SELLER_REDIS_KEY); + public List findBooksWithRank(Integer offset, Integer limit) { + Set bookJsonSet = Optional.ofNullable( + redisTemplate.opsForZSet().range(BEST_SELLER_REDIS_KEY, offset, limit)) + .orElse(Collections.emptySet()); - return bestSellers.values().stream() - .map(this::parseToBestSellerResponse) - .sorted(Comparator.comparing(BestSellerResponse::getRanking)) + return bookJsonSet.stream() + .map(json -> { + Long rank = redisTemplate.opsForZSet().rank(BEST_SELLER_REDIS_KEY, json); + return BookWithRankingResponse.ofRankIncrement(rank, converter.fromJson(json, CrawledBook.class)); + }) .toList(); } @Override - public void updateAll(Map bestSellers) { - bestSellers.forEach((key, value) -> redisTemplate.opsForHash() - .put(BEST_SELLER_REDIS_KEY, String.valueOf(key), parseToBestSellerResponseJson(key, value))); - } + public List findAll() { + Set bookJsonSet = Optional.ofNullable( + redisTemplate.opsForZSet().range(BEST_SELLER_REDIS_KEY, 0, -1)) + .orElse(Collections.emptySet()); - private BestSellerResponse parseToBestSellerResponse(Object value) { - try { - return om.readValue((String) value, BestSellerResponse.class); - } catch (JsonProcessingException e) { - throw new BusinessException(ErrorCode.BOOK_INVALID_INFO); - } + return bookJsonSet.stream() + .map(json -> { + Long rank = redisTemplate.opsForZSet().rank(BEST_SELLER_REDIS_KEY, json); + return BookWithRankingResponse.ofRankIncrement(rank, converter.fromJson(json, CrawledBook.class)); + }) + .toList(); } - private String parseToBestSellerResponseJson(Long key, CrawlingBook book) { - try { - return om.writeValueAsString( - BestSellerResponse.of(key, book.getIsbn(), book.getTitle(), book.getPublisher(), - book.getThumbnail(), book.getAuthors(), book.getDateTime())); - } catch (JsonProcessingException e) { - throw new BusinessException(ErrorCode.BOOK_INVALID_INFO); - } - } + @Override + public void updateAll(Map crawledBookMap) { + redisTemplate.delete(BEST_SELLER_REDIS_KEY); - private List createHashKeys(Integer startIndex, Integer endIndex) { - return IntStream.rangeClosed(startIndex, endIndex) - .boxed() - .map(Object::toString) - .map(obj -> (Object) obj) - .toList(); + crawledBookMap.forEach((key, value) -> redisTemplate.opsForZSet() + .add(BEST_SELLER_REDIS_KEY, converter.toJson(value), key)); } } diff --git a/src/main/java/com/jisungin/domain/book/repository/BestSellerRepository.java b/src/main/java/com/jisungin/domain/book/repository/BestSellerRepository.java index 3f40fa4..7910421 100644 --- a/src/main/java/com/jisungin/domain/book/repository/BestSellerRepository.java +++ b/src/main/java/com/jisungin/domain/book/repository/BestSellerRepository.java @@ -1,18 +1,18 @@ package com.jisungin.domain.book.repository; -import com.jisungin.application.PageResponse; -import com.jisungin.application.book.request.BookServicePageRequest; -import com.jisungin.application.book.response.BestSellerResponse; -import com.jisungin.infra.crawler.CrawlingBook; +import com.jisungin.application.book.response.BookWithRankingResponse; +import com.jisungin.infra.crawler.CrawledBook; import java.util.List; import java.util.Map; public interface BestSellerRepository { - List findAll(); + Long count(); - PageResponse findBestSellerByPage(BookServicePageRequest request); + List findAll(); - void updateAll(Map bestSellers); + List findBooksWithRank(Integer offset, Integer limit); + + void updateAll(Map crawledBookMap); } diff --git a/src/main/java/com/jisungin/domain/book/repository/BookRepository.java b/src/main/java/com/jisungin/domain/book/repository/BookRepository.java index c297202..3b589b6 100644 --- a/src/main/java/com/jisungin/domain/book/repository/BookRepository.java +++ b/src/main/java/com/jisungin/domain/book/repository/BookRepository.java @@ -1,7 +1,11 @@ package com.jisungin.domain.book.repository; import com.jisungin.domain.book.Book; +import java.util.List; +import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -9,4 +13,7 @@ public interface BookRepository extends JpaRepository, BookReposit Boolean existsBookByIsbn(String isbn); + @Query("SELECT b.isbn FROM Book b WHERE b.isbn IN :isbns") + Set findExistIsbns(@Param("isbns") List isbns); + } diff --git a/src/main/java/com/jisungin/infra/JsonConverter.java b/src/main/java/com/jisungin/infra/JsonConverter.java new file mode 100644 index 0000000..f51b072 --- /dev/null +++ b/src/main/java/com/jisungin/infra/JsonConverter.java @@ -0,0 +1,30 @@ +package com.jisungin.infra; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class JsonConverter { + + private final ObjectMapper om; + + public T fromJson(String json, Class clazz) { + try { + return om.readValue(json, clazz); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException(); + } + } + + public String toJson(Object obj) { + try { + return om.writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException(); + } + } + +} diff --git a/src/main/java/com/jisungin/infra/crawler/CrawlingBook.java b/src/main/java/com/jisungin/infra/crawler/CrawledBook.java similarity index 72% rename from src/main/java/com/jisungin/infra/crawler/CrawlingBook.java rename to src/main/java/com/jisungin/infra/crawler/CrawledBook.java index 123ddd0..49fef35 100644 --- a/src/main/java/com/jisungin/infra/crawler/CrawlingBook.java +++ b/src/main/java/com/jisungin/infra/crawler/CrawledBook.java @@ -4,9 +4,12 @@ import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; +import net.minidev.json.annotate.JsonIgnore; @Getter -public class CrawlingBook { +@NoArgsConstructor +public class CrawledBook { private String title; private String content; @@ -18,8 +21,8 @@ public class CrawlingBook { private LocalDateTime dateTime; @Builder - private CrawlingBook(String title, String content, String isbn, String publisher, String imageUrl, String thumbnail, - String authors, LocalDateTime dateTime) { + private CrawledBook(String title, String content, String isbn, String publisher, String imageUrl, String thumbnail, + String authors, LocalDateTime dateTime) { this.title = title; this.content = content; this.isbn = isbn; @@ -30,9 +33,9 @@ private CrawlingBook(String title, String content, String isbn, String publisher this.dateTime = dateTime; } - public static CrawlingBook of(String title, String content, String isbn, String publisher, String imageUrl, - String thumbnail, String authors, LocalDateTime dateTime) { - return CrawlingBook.builder() + public static CrawledBook of(String title, String content, String isbn, String publisher, String imageUrl, + String thumbnail, String authors, LocalDateTime dateTime) { + return CrawledBook.builder() .title(title) .content(content) .isbn(isbn) @@ -57,6 +60,10 @@ public BookCreateServiceRequest toServiceRequest() { .build(); } + public boolean isBlankIsbn() { + return isbn == null || isbn.isBlank(); + } + private String[] convertAuthorsToArr(String authors) { return authors.split(" 저| 공저| 글| 편저| 원저| 기획|&")[0].split(","); } diff --git a/src/main/java/com/jisungin/infra/crawler/Crawler.java b/src/main/java/com/jisungin/infra/crawler/Crawler.java index 5ffc118..0a2fb9b 100644 --- a/src/main/java/com/jisungin/infra/crawler/Crawler.java +++ b/src/main/java/com/jisungin/infra/crawler/Crawler.java @@ -4,7 +4,7 @@ public interface Crawler { - CrawlingBook crawlBook(String isbn); - Map crawlBestSellerBook(); + CrawledBook crawlBook(String isbn); + Map crawlBestSellerBook(); } diff --git a/src/main/java/com/jisungin/infra/crawler/Parser.java b/src/main/java/com/jisungin/infra/crawler/Parser.java index 5d61182..0a5f502 100644 --- a/src/main/java/com/jisungin/infra/crawler/Parser.java +++ b/src/main/java/com/jisungin/infra/crawler/Parser.java @@ -6,7 +6,7 @@ public interface Parser { String parseIsbn(Document doc); - CrawlingBook parseBook(Document doc); + CrawledBook parseBook(Document doc); Map parseBestSellerBookId(Document doc); } diff --git a/src/main/java/com/jisungin/infra/crawler/Yes24Crawler.java b/src/main/java/com/jisungin/infra/crawler/Yes24Crawler.java index 15e1225..55d52f9 100644 --- a/src/main/java/com/jisungin/infra/crawler/Yes24Crawler.java +++ b/src/main/java/com/jisungin/infra/crawler/Yes24Crawler.java @@ -5,8 +5,10 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +@Slf4j @Component @RequiredArgsConstructor public class Yes24Crawler implements Crawler { @@ -15,26 +17,34 @@ public class Yes24Crawler implements Crawler { private final Parser parser; @Override - public CrawlingBook crawlBook(String isbn) { + public CrawledBook crawlBook(String isbn) { String bookId = parser.parseIsbn(fetcher.fetchIsbn(isbn)); return parser.parseBook(fetcher.fetchBook(bookId)); } @Override - public Map crawlBestSellerBook() { - Map bestSellerBookIds = parser.parseBestSellerBookId(fetcher.fetchBestSellerBookId()); - Map bestSellerBooks = new ConcurrentHashMap<>(); + public Map crawlBestSellerBook() { + Map crawledBookIds = parser.parseBestSellerBookId(fetcher.fetchBestSellerBookId()); + Map crawledBookMap = new ConcurrentHashMap<>(); - List> futures = bestSellerBookIds.entrySet().stream() + List> futures = crawledBookIds.entrySet().stream() .map(entry -> CompletableFuture.supplyAsync(() -> parser.parseBook(fetcher.fetchBook(entry.getValue()))) - .thenAccept(crawlingBook -> bestSellerBooks.put(entry.getKey(), crawlingBook)) - .exceptionally(throwable -> null)) + .thenAccept(crawledBook -> { + if (!crawledBook.isBlankIsbn()) { + crawledBookMap.put(entry.getKey(), crawledBook); + } + }) + .exceptionally(throwable -> { + log.warn("[WARN] 19세 이상 도서는 조회할 수 없습니다."); + + return null; + })) .toList(); CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).join(); - return bestSellerBooks; + return crawledBookMap; } } diff --git a/src/main/java/com/jisungin/infra/crawler/Yes24Parser.java b/src/main/java/com/jisungin/infra/crawler/Yes24Parser.java index 86b0619..04eb805 100644 --- a/src/main/java/com/jisungin/infra/crawler/Yes24Parser.java +++ b/src/main/java/com/jisungin/infra/crawler/Yes24Parser.java @@ -35,7 +35,7 @@ public String parseIsbn(Document doc) { } @Override - public CrawlingBook parseBook(Document doc) { + public CrawledBook parseBook(Document doc) { String json = doc.select(bookJsonCss).html(); String title = parseJsonToString(json, "$.name"); @@ -47,7 +47,7 @@ public CrawlingBook parseBook(Document doc) { String content = Jsoup.clean(doc.select(bookContentCss).text(), Safelist.none()); LocalDateTime dateTime = parseDate(parseJsonToString(json, "$.workExample[0].datePublished")); - return CrawlingBook.of(title, content, isbn, publisher, imageUrl, thumbnail, authors, dateTime); + return CrawledBook.of(title, content, isbn, publisher, imageUrl, thumbnail, authors, dateTime); } @Override diff --git a/src/test/java/com/jisungin/application/book/BestSellerServiceTest.java b/src/test/java/com/jisungin/application/book/BestSellerServiceTest.java index 37cfbef..4698235 100644 --- a/src/test/java/com/jisungin/application/book/BestSellerServiceTest.java +++ b/src/test/java/com/jisungin/application/book/BestSellerServiceTest.java @@ -7,23 +7,21 @@ import static org.mockito.Mockito.when; import com.jisungin.RedisTestContainer; +import com.jisungin.application.OffsetLimit; import com.jisungin.application.PageResponse; -import com.jisungin.application.book.BestSellerService; import com.jisungin.application.book.event.BestSellerUpdatedEvent; import com.jisungin.application.book.event.BestSellerUpdatedEventListener; -import com.jisungin.application.book.request.BookServicePageRequest; -import com.jisungin.application.book.response.BestSellerResponse; +import com.jisungin.application.book.response.BookWithRankingResponse; import com.jisungin.domain.book.repository.BestSellerRedisRepository; import com.jisungin.domain.book.repository.BookRepository; +import com.jisungin.infra.crawler.CrawledBook; import com.jisungin.infra.crawler.Crawler; -import com.jisungin.infra.crawler.CrawlingBook; import com.jisungin.infra.s3.S3FileManager; import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.IntStream; -import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -31,11 +29,8 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.test.context.event.RecordApplicationEvents; @SpringBootTest -@RecordApplicationEvents -@Slf4j public class BestSellerServiceTest extends RedisTestContainer { @Autowired @@ -68,15 +63,12 @@ public void tearDown() { @DisplayName("베스트 셀러 페이지를 조회한다.") public void getBestSellers() { // given - Map bestSellers = createSampleBestSellers(); - - bestSellerRedisRepository.updateAll(bestSellers); + bestSellerRedisRepository.updateAll(createCrawledBookMap()); // when - PageResponse result = bestSellerService.getBestSellers(BookServicePageRequest.builder() - .page(1) - .size(5) - .build()); + PageResponse result = bestSellerService.getBestSellers(OffsetLimit + .ofRange(1, 5)); + // then assertThat(result.getSize()).isEqualTo(5); assertThat(result.getTotalCount()).isEqualTo(6); @@ -95,15 +87,15 @@ public void getBestSellers() { @DisplayName("베스트 셀러를 갱신 한다.") public void updateBestSellers() { // given - Map bestSellers = createSampleBestSellers(); + Map crawledBookMap = createCrawledBookMap(); - when(crawler.crawlBestSellerBook()).thenReturn(bestSellers); + when(crawler.crawlBestSellerBook()).thenReturn(crawledBookMap); // when bestSellerService.updateBestSellers(); // then - List bookResponses = bestSellerRedisRepository.findAll(); + List bookResponses = bestSellerRedisRepository.findAll(); assertThat(bookResponses.size()).isEqualTo(6); assertThat(bookResponses).extracting("title", "isbn", "publisher", "authors") @@ -121,8 +113,8 @@ public void updateBestSellers() { @DisplayName("베스트 셀러를 갱신하면 DB에 새로 등록된 책을 저장하는 이벤트가 발생한다.") public void updateBestSellerEventRaised() { // given - Map bestSellers = createSampleBestSellers(); - when(crawler.crawlBestSellerBook()).thenReturn(bestSellers); + Map crawledBookMap = createCrawledBookMap(); + when(crawler.crawlBestSellerBook()).thenReturn(crawledBookMap); // when bestSellerService.updateBestSellers(); @@ -131,11 +123,11 @@ public void updateBestSellerEventRaised() { verify(eventEventListener).handleBestSellerUpdatedEvent(any(BestSellerUpdatedEvent.class)); } - private static Map createSampleBestSellers() { + private static Map createCrawledBookMap() { return IntStream.rangeClosed(1, 6) .boxed() .collect(Collectors.toMap(Long::valueOf, - i -> CrawlingBook.of("title" + i, "content" + i, "isbn" + i, + i -> CrawledBook.of("title" + i, "content" + i, "isbn" + i, "publisher" + i, "imageUrl" + i, "thumbnail" + i, "author" + i, LocalDateTime.of(2024, 1, 1, 0, 0)))); } diff --git a/src/test/java/com/jisungin/application/book/BookServiceTest.java b/src/test/java/com/jisungin/application/book/BookServiceTest.java index 07eb8c3..ef4dc53 100644 --- a/src/test/java/com/jisungin/application/book/BookServiceTest.java +++ b/src/test/java/com/jisungin/application/book/BookServiceTest.java @@ -24,7 +24,7 @@ import com.jisungin.domain.user.repository.UserRepository; import com.jisungin.exception.BusinessException; import com.jisungin.infra.crawler.Crawler; -import com.jisungin.infra.crawler.CrawlingBook; +import com.jisungin.infra.crawler.CrawledBook; import java.time.LocalDateTime; import java.util.List; import java.util.stream.IntStream; @@ -161,7 +161,7 @@ public void createBook() { .build(); when(crawler.crawlBook(request.getIsbn())) - .thenReturn(CrawlingBook.of("도서 제목", "도서 내용", "123456789X", "도서 출판사", + .thenReturn(CrawledBook.of("도서 제목", "도서 내용", "123456789X", "도서 출판사", "도서 imageUrl", "도서 썸네일", "도서 작가1,도서 작가2", registeredDateTime)); // when diff --git a/src/test/java/com/jisungin/application/search/SearchServiceTest.java b/src/test/java/com/jisungin/application/search/SearchServiceTest.java index eed749a..8dc155d 100644 --- a/src/test/java/com/jisungin/application/search/SearchServiceTest.java +++ b/src/test/java/com/jisungin/application/search/SearchServiceTest.java @@ -1,32 +1,28 @@ package com.jisungin.application.search; +import static org.assertj.core.api.Assertions.assertThat; + import com.jisungin.RedisTestContainer; import com.jisungin.infra.s3.S3FileManager; +import java.util.List; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ZSetOperations; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.test.context.event.RecordApplicationEvents; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest -@RecordApplicationEvents public class SearchServiceTest extends RedisTestContainer { @Autowired private SearchService searchService; @Autowired - private @Qualifier("redisTemplateSecond") RedisTemplate redisTemplate; + private RedisTemplate redisTemplate; @MockBean private S3FileManager s3FileManager; @@ -46,7 +42,7 @@ void searchSaveRanking() { //then ZSetOperations zset = redisTemplate.opsForZSet(); Double score = zset.score("ranking", keyword); - assertThat(score).isEqualTo(1.0); // 검색어의 점수가 1.0인지 확인 + assertThat(score).isEqualTo(1.0); } @@ -61,7 +57,7 @@ void getRankKeywords() { //then List rankKeywords = searchService.getRankKeywords(); - Assertions.assertThat(rankKeywords).contains(keyword); // 랭킹에 추가된 검색어가 있는지 확인 + Assertions.assertThat(rankKeywords).contains(keyword); } } \ No newline at end of file diff --git a/src/test/java/com/jisungin/docs/book/BookControllerDocsTest.java b/src/test/java/com/jisungin/docs/book/BookControllerDocsTest.java index 9a78feb..1bb50d2 100644 --- a/src/test/java/com/jisungin/docs/book/BookControllerDocsTest.java +++ b/src/test/java/com/jisungin/docs/book/BookControllerDocsTest.java @@ -26,14 +26,12 @@ import com.jisungin.api.book.BookController; import com.jisungin.api.book.request.BookCreateRequest; -import com.jisungin.api.book.request.BookPageRequest; import com.jisungin.application.OffsetLimit; import com.jisungin.application.PageResponse; import com.jisungin.application.book.BestSellerService; import com.jisungin.application.book.BookService; import com.jisungin.application.book.request.BookCreateServiceRequest; -import com.jisungin.application.book.request.BookServicePageRequest; -import com.jisungin.application.book.response.BestSellerResponse; +import com.jisungin.application.book.response.BookWithRankingResponse; import com.jisungin.application.book.response.BookFindAllResponse; import com.jisungin.application.book.response.BookResponse; import com.jisungin.docs.RestDocsSupport; @@ -57,11 +55,13 @@ protected Object initController() { @Test @DisplayName("도서 단건 조회 API") public void getBook() throws Exception { + // given String isbn = "0000000000001"; given(bookService.getBook(anyString())) .willReturn(createBookResponseWithIsbn(isbn)); + // when // then mockMvc.perform(get("/v1/books/{isbn}", isbn) .accept(APPLICATION_JSON)) .andDo(print()) @@ -93,12 +93,14 @@ public void getBook() throws Exception { @Test @DisplayName("도서 페이징 조회 API") public void getBooks() throws Exception { - List queryResponse = createSimpleBookResponse(); - PageResponse response = PageResponse.of(queryResponse.size(), queryResponse.size(), - queryResponse); + // given + List response = createBookFindAllResponse(); + PageResponse pageResponse = PageResponse.of(response.size(), response.size(), + response); - given(bookService.getBooks(any(OffsetLimit.class))).willReturn(response); + given(bookService.getBooks(any(OffsetLimit.class))).willReturn(pageResponse); + // when // then mockMvc.perform(get("/v1/books") .param("page", "1") .param("size", "10") @@ -136,22 +138,19 @@ public void getBooks() throws Exception { @Test @DisplayName("베스트 셀러 조회 API") public void getBestSellers() throws Exception { - BookPageRequest params = BookPageRequest.builder() - .page(1) - .size(5) - .build(); - - List queryResponse = createBestSellerResponse(); + // given + List response = createBookWithRankingResponse(); - PageResponse response = PageResponse.of(queryResponse.size(), - queryResponse.size(), queryResponse); + PageResponse pageResponse = PageResponse.of(response.size(), + response.size(), response); - given(bestSellerService.getBestSellers(any(BookServicePageRequest.class))) - .willReturn(response); + given(bestSellerService.getBestSellers(any(OffsetLimit.class))) + .willReturn(pageResponse); + // when // then mockMvc.perform(get("/v1/books/best-seller") - .param("page", String.valueOf(params.getPage())) - .param("size", String.valueOf(params.getSize())) + .param("page", "1") + .param("size", "5") .accept(APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()) @@ -184,6 +183,7 @@ public void getBestSellers() throws Exception { @Test @DisplayName("도서 생성 API") public void createBook() throws Exception { + // given BookCreateRequest request = BookCreateRequest.builder() .isbn("0000000000001") .title("book title") @@ -197,6 +197,7 @@ public void createBook() throws Exception { given(bookService.createBook(any(BookCreateServiceRequest.class))) .willReturn(createBookResponseWithIsbn(request.getIsbn())); + // when // then mockMvc.perform(post("/v1/books") .accept(APPLICATION_JSON) .contentType(APPLICATION_JSON) @@ -247,7 +248,7 @@ private BookResponse createBookResponseWithIsbn(String isbn) { .build(); } - private List createSimpleBookResponse() { + private List createBookFindAllResponse() { return IntStream.rangeClosed(1, 5) .mapToObj(i -> BookFindAllResponse.builder() .isbn("000000000000" + i) @@ -260,9 +261,9 @@ private List createSimpleBookResponse() { .toList(); } - private List createBestSellerResponse() { + private List createBookWithRankingResponse() { return LongStream.rangeClosed(1, 5) - .mapToObj(i -> BestSellerResponse.builder() + .mapToObj(i -> BookWithRankingResponse.builder() .ranking(i) .isbn("00000000000" + i) .title("book title" + i) diff --git a/src/test/java/com/jisungin/infra/Yes24CrawlerTest.java b/src/test/java/com/jisungin/infra/Yes24CrawlerTest.java index 9269539..6986cd6 100644 --- a/src/test/java/com/jisungin/infra/Yes24CrawlerTest.java +++ b/src/test/java/com/jisungin/infra/Yes24CrawlerTest.java @@ -8,7 +8,7 @@ import com.jisungin.exception.BusinessException; import com.jisungin.exception.ErrorCode; -import com.jisungin.infra.crawler.CrawlingBook; +import com.jisungin.infra.crawler.CrawledBook; import com.jisungin.infra.crawler.Yes24Crawler; import com.jisungin.infra.crawler.Yes24Fetcher; import com.jisungin.infra.crawler.Yes24Parser; @@ -45,21 +45,19 @@ public void crawlingBook() { Document isbnDocument = mock(Document.class); Document bookDocument = mock(Document.class); - LocalDateTime registeredTime = LocalDateTime.of(2024, 1, 1, 0, 0); - - CrawlingBook crawlingBook = CrawlingBook.of("도서 제목", "도서 내용", "도서 ISBN", - "도서 출판사", "도서 이미지 링크", "도서 썸네일", "도서 작가1", registeredTime); + CrawledBook crawledBook = createCrawledBookWithIsbn("0000000000"); when(fetcher.fetchIsbn(isbn)).thenReturn(isbnDocument); when(fetcher.fetchBook(bookId)).thenReturn(bookDocument); + when(parser.parseIsbn(isbnDocument)).thenReturn(bookId); - when(parser.parseBook(bookDocument)).thenReturn(crawlingBook); + when(parser.parseBook(bookDocument)).thenReturn(crawledBook); // when - CrawlingBook expectedCrawlingBook = crawler.crawlBook(isbn); + CrawledBook result = crawler.crawlBook(isbn); // then - assertThat(expectedCrawlingBook).isEqualTo(crawlingBook); + assertThat(result).isEqualTo(crawledBook); } @Test @@ -94,23 +92,91 @@ public void crawlingBestSeller() { when(fetcher.fetchBook("00001")).thenReturn(fetchBookDoc1); when(fetcher.fetchBook("00002")).thenReturn(fetchBookDoc2); - CrawlingBook book1 = CrawlingBook.of("책 제목1", "책 내용1", "책 ISBN1", "책 출판사1", - "책 이미지 URL1", "책 썸네일1", "책 저자1, 책 저자2", - LocalDateTime.of(2024, 1, 1, 0, 0)); - CrawlingBook book2 = CrawlingBook.of("책 제목2", "책 내용2", "책 ISBN2", "책 출판사2", - "책 이미지 URL2", "책 썸네일2", "책 저자3, 책 저자4", - LocalDateTime.of(2024, 1, 1, 0, 0)); + CrawledBook book1 = createCrawledBookWithIsbn("1"); + CrawledBook book2 = createCrawledBookWithIsbn("2"); when(parser.parseBook(fetchBookDoc1)).thenReturn(book1); when(parser.parseBook(fetchBookDoc2)).thenReturn(book2); // when - Map bestSellerBooks = crawler.crawlBestSellerBook(); + Map result = crawler.crawlBestSellerBook(); // then - assertThat(bestSellerBooks.size()).isEqualTo(2); - assertThat(bestSellerBooks.get(1L)).isEqualTo(book1); - assertThat(bestSellerBooks.get(2L)).isEqualTo(book2); + assertThat(result.size()).isEqualTo(2); + assertThat(result.get(1L)).isEqualTo(book1); + assertThat(result.get(2L)).isEqualTo(book2); + } + + @Test + @DisplayName("ISBN이 없는 도서를 조회한다.") + public void crawledBookWithoutISBN() { + Document bestSellerBookIdsDoc = mock(Document.class); + Document fetchBookDoc1 = mock(Document.class); + Document fetchBookDoc2 = mock(Document.class); + + Map crawledBookIds = new HashMap<>(); + + crawledBookIds.put(1L, "000001"); + crawledBookIds.put(2L, "000002"); + + when(fetcher.fetchBestSellerBookId()).thenReturn(bestSellerBookIdsDoc); + when(parser.parseBestSellerBookId(any(Document.class))).thenReturn(crawledBookIds); + + when(fetcher.fetchBook("000001")).thenReturn(fetchBookDoc1); + when(fetcher.fetchBook("000002")).thenReturn(fetchBookDoc2); + + CrawledBook book1 = createCrawledBookWithIsbn(""); + CrawledBook book2 = createCrawledBookWithIsbn(""); + + when(parser.parseBook(fetchBookDoc1)).thenReturn(book1); + when(parser.parseBook(fetchBookDoc2)).thenReturn(book2); + + // when + Map result = crawler.crawlBestSellerBook(); + + // then + assertThat(result).hasSize(0); + } + + @Test + @DisplayName("19세 이상 도서는 조회할 수 없다.") + public void crawlingBookWithAdultBook() { + // given + Document bestSellerBookIdsDoc = mock(Document.class); + Document fetchBookDoc1 = mock(Document.class); + Document fetchBookDoc2 = mock(Document.class); + + Map crawledBookIds = new HashMap<>(); + crawledBookIds.put(1L, "000001"); + crawledBookIds.put(2L, "000002"); + + when(fetcher.fetchBestSellerBookId()).thenReturn(bestSellerBookIdsDoc); + when(parser.parseBestSellerBookId(any(Document.class))).thenReturn(crawledBookIds); + + when(fetcher.fetchBook("000001")).thenReturn(fetchBookDoc1); + when(fetcher.fetchBook("000002")).thenReturn(fetchBookDoc2); + + when(parser.parseBook(fetchBookDoc1)).thenReturn(null); + when(parser.parseBook(fetchBookDoc2)).thenReturn(null); + + // when + Map result = crawler.crawlBestSellerBook(); + + // then + assertThat(result).hasSize(0); + } + + private static CrawledBook createCrawledBookWithIsbn(String isbn) { + return CrawledBook.builder() + .title("도서 제목" + isbn) + .content("도서 내용" + isbn) + .isbn(isbn) + .publisher("도서 출판사" + isbn) + .imageUrl("www.image-url.com/" + isbn) + .thumbnail("www.image-thumbnail.com/" + isbn) + .authors("도서 저자" + isbn) + .dateTime(LocalDateTime.of(2024, 1, 1, 0, 0)) + .build(); } }