From 7f5ec675fc6d9a1f250b9d576b6d1aa20b813177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:14:01 +0900 Subject: [PATCH 01/30] [feat] BookCommandRedisAdapter (#48) --- .../persistence/BookCommandRedisAdapter.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java new file mode 100644 index 000000000..357c872b1 --- /dev/null +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java @@ -0,0 +1,65 @@ +package konkuk.thip.book.adapter.out.persistence; + +import konkuk.thip.book.application.port.out.BookRedisCommandPort; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class BookCommandRedisAdapter implements BookRedisCommandPort { + + private final RedisTemplate redisTemplate; + private final DateTimeFormatter DAILY_KEY_FORMATTER = + DateTimeFormatter.ofPattern("yyyyMMdd"); + + @Value("${app.redis.search-count-prefix}") + private String searchCountPrefix; + + @Value("${app.redis.search-rank-prefix}") + private String searchRankPrefix; + + + @Override + public void incrementBookSearchCount(String isbn, LocalDate date) { + String redisKey = makeRedisKey(searchCountPrefix, date); + redisTemplate.opsForZSet().incrementScore(redisKey, isbn, 1.0); + } + + @Override + public void saveBookSearchRank(List isbns, List scores, LocalDate date) { + String redisKey = makeRedisKey(searchRankPrefix, date); + for (int i = 0; i < isbns.size(); i++) { + redisTemplate.opsForZSet().add(redisKey, isbns.get(i), scores.get(i)); + } + redisTemplate.expire(redisKey, Duration.ofDays(7)); + } + + @Override + public void deleteBookSearchRank(LocalDate date) { + deleteZSetKey(searchRankPrefix, date); + } + + @Override + public void deleteBookSearchCount(LocalDate date) { + deleteZSetKey(searchCountPrefix, date); + } + + private void deleteZSetKey(String prefix, LocalDate date) { + String redisKey = makeRedisKey(prefix, date); + redisTemplate.delete(redisKey); + } + + private String makeRedisKey(String prefix, LocalDate date) { + String dateStr = date.format(DAILY_KEY_FORMATTER); + return prefix + dateStr; + } + + +} From 15870ee93de790b81df785fdb35f79173038852b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:14:14 +0900 Subject: [PATCH 02/30] [feat] BookJpaRepository.findByIsbnIn (#48) --- .../thip/book/adapter/out/persistence/BookJpaRepository.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookJpaRepository.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookJpaRepository.java index 538045ca5..1570af68f 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookJpaRepository.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookJpaRepository.java @@ -3,8 +3,10 @@ import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface BookJpaRepository extends JpaRepository { Optional findByIsbn(String isbn); + List findByIsbnIn(List isbnList); } From e3971c2af207021739a22f79def0c655b3830f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:14:41 +0900 Subject: [PATCH 03/30] [test] BookMostSearchedBooksControllerTest(#48) --- .../BookMostSearchedBooksControllerTest.java | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java new file mode 100644 index 000000000..0569484a5 --- /dev/null +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java @@ -0,0 +1,172 @@ +package konkuk.thip.book.adapter.in.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; +import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; +import org.junit.jupiter.api.AfterEach; +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; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@ActiveProfiles("test") +class BookMostSearchedBooksControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private AliasJpaRepository aliasJpaRepository; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private ObjectMapper objectMapper; + + + private LocalDate today; + private String rankKey; + + + private final DateTimeFormatter DAILY_KEY_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + + @Value("${app.redis.search-count-prefix}") + private String searchCountPrefix; + + @Value("${app.redis.search-rank-prefix}") + private String searchRankPrefix; + + @BeforeEach + void setUp() { + + AliasJpaEntity alias = aliasJpaRepository.save(AliasJpaEntity.builder() + .value("책벌레") + .color("blue") + .imageUrl("http://image.url") + .build()); + + UserJpaEntity user = userJpaRepository.save(UserJpaEntity.builder() + .oauth2Id("kakao_432708231") + .nickname("User1") + .imageUrl("https://avatar1.jpg") + .role(UserRole.USER) + .aliasForUserJpaEntity(alias) + .build()); + + } + + @AfterEach + void tearDown() { + userJpaRepository.deleteAll(); + aliasJpaRepository.deleteAll(); + } + + + @Test + @DisplayName("오늘 랭킹 Top 5를 정상적으로 조회한다") + void getMostSearchedBooks_returnsRankList() throws Exception { + + // given 오늘 날짜의 랭킹 키에 테스트용 데이터 3개를 저장. + today = LocalDate.now(); + rankKey = searchRankPrefix + today.format(DAILY_KEY_FORMATTER); + + redisTemplate.opsForZSet().add(rankKey, "9788954682152", 10); + redisTemplate.opsForZSet().add(rankKey, "9788991742178", 5); + redisTemplate.opsForZSet().add(rankKey, "9791198783400", 7); + + Long userId = userJpaRepository.findAll().get(0).getUserId(); + + // when + ResultActions result = mockMvc.perform(get("/books/most-searched") + .requestAttr("userId", userId) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.bookList").isArray()) + .andExpect(jsonPath("$.data.bookList.length()").value(3)); + + + String json = result.andReturn().getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(json); + JsonNode rankArray = jsonNode.path("data").path("bookRankInfos"); + + assertThat(rankArray).isNotNull(); + assertThat(rankArray.size()).isEqualTo(3); + + + // 점수 내림차순 확인 (Redis에서 직접 점수 확인) + double previousScore = Double.MAX_VALUE; + for (JsonNode bookRankInfo : rankArray) { + String isbn = bookRankInfo.path("isbn").asText(); + Double score = redisTemplate.opsForZSet().score(rankKey, isbn); + assertThat(score).isLessThanOrEqualTo(previousScore); + previousScore = score; + } + + // 저장된 isbn이 포함되어 있는지 확인 + List isbns = List.of("9788954682152", "9788991742178", "9791198783400"); + for (JsonNode bookRankInfo : rankArray) { + assertThat(isbns.contains(bookRankInfo.path("isbn").asText())).isTrue(); + } + } + + @Test + @DisplayName("랭킹 데이터가 없으면 빈 리스트를 반환한다") + void getMostSearchedBooks_returnsEmptyList_whenNoData() throws Exception { + + // given 오늘 날짜의 랭킹 키를 삭제해서 데이터가 없는 상태로 만든다 + today = LocalDate.now(); + rankKey = searchRankPrefix + today.format(DAILY_KEY_FORMATTER); + redisTemplate.delete(rankKey); + + Long userId = userJpaRepository.findAll().get(0).getUserId(); + + // when + ResultActions result = mockMvc.perform(get("/books/most-searched") + .requestAttr("userId", userId) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.bookList").isArray()) + .andExpect(jsonPath("$.data.bookList.length()").value(0)); + + String json = result.andReturn().getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(json); + JsonNode bookList = jsonNode.path("data").path("bookList"); + + assertThat(bookList).isNotNull(); + assertThat(bookList.size()).isEqualTo(0); + } + +} From 7eb5073a61edc79612fc9d4734f9eb0f03248fff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:14:54 +0900 Subject: [PATCH 04/30] [test] BookSearchRankServiceTest (#48) --- .../service/BookSearchRankServiceTest.java | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/test/java/konkuk/thip/book/application/service/BookSearchRankServiceTest.java diff --git a/src/test/java/konkuk/thip/book/application/service/BookSearchRankServiceTest.java b/src/test/java/konkuk/thip/book/application/service/BookSearchRankServiceTest.java new file mode 100644 index 000000000..397fe0e32 --- /dev/null +++ b/src/test/java/konkuk/thip/book/application/service/BookSearchRankServiceTest.java @@ -0,0 +1,89 @@ +package konkuk.thip.book.application.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; + +import konkuk.thip.book.application.port.out.BookRedisCommandPort; +import konkuk.thip.book.application.port.out.BookRedisQueryPort; +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; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +public class BookSearchRankServiceTest { + + @Autowired + private BookRedisQueryPort bookRedisQueryPort; + + @Autowired + private BookRedisCommandPort bookRedisCommandPort; + + @Autowired + private RedisTemplate redisTemplate; + + private final DateTimeFormatter DAILY_KEY_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + + private LocalDate yesterday; + + @Value("${app.redis.search-count-prefix}") + private String searchCountPrefix; + + + @BeforeEach + void setup() { + + // given: 어제 날짜의 카운트 키에 테스트용 데이터 3개를 저장 + yesterday = LocalDate.now().minusDays(1); + String countKey = searchCountPrefix + yesterday.format(DAILY_KEY_FORMATTER); + + redisTemplate.opsForZSet().add(countKey, "9788954682152", 10); + redisTemplate.opsForZSet().add(countKey, "9788991742178", 5); + redisTemplate.opsForZSet().add(countKey, "9791198783400", 7); + + } + + @Test + @DisplayName("어제의 검색 카운트 Top 5를 집계하여 랭킹 키에 저장한다") + void updateDailySearchRank_shouldAggregateAndSaveTop5() { + + // given 어제 날짜의 카운트 키에 데이터가 저장되어 있음 + List> top5 = bookRedisQueryPort.getBookSearchCountTopN(yesterday, 5); + + // when 기존 랭킹/카운트 키를 삭제하고, Top 5를 랭킹 키에 저장 + bookRedisCommandPort.deleteBookSearchRank(yesterday); + bookRedisCommandPort.deleteBookSearchCount(yesterday); + + if (!top5.isEmpty()) { + List isbns = top5.stream().map(Map.Entry::getKey).toList(); + List scores = top5.stream().map(Map.Entry::getValue).toList(); + bookRedisCommandPort.saveBookSearchRank(isbns, scores, yesterday); + } + + // then 랭킹 키에 Top 5가 내림차순으로 저장되어 있어야 함 + List> savedTop5 = bookRedisQueryPort.getBookSearchRank(yesterday, 5); + + assertThat(savedTop5).isNotEmpty(); + assertThat(savedTop5.size()).isLessThanOrEqualTo(5); + + // 점수 내림차순 확인 + double previousScore = Double.MAX_VALUE; + for (Map.Entry entry : savedTop5) { + assertThat(entry.getValue()).isLessThanOrEqualTo(previousScore); + previousScore = entry.getValue(); + } + + // 테스트용으로 저장된 isbn 포함 여부 확인 + assertThat(savedTop5.stream().map(Map.Entry::getKey)) + .containsAnyOf("9788954682152", "9788991742178", "9791198783400"); + } +} From 0117897923a2016988e26b9ab60f28d98d635d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:17:53 +0900 Subject: [PATCH 05/30] =?UTF-8?q?[test]=20=EC=B1=85=20=EB=A0=9D=ED=82=B9?= =?UTF-8?q?=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=8A=A4=EC=BC=80?= =?UTF-8?q?=EC=A4=84=EB=9F=AC=20=EC=9E=91=EC=84=B1=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/BookMostSearchRankService.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java diff --git a/src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java b/src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java new file mode 100644 index 000000000..cd980d1e0 --- /dev/null +++ b/src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java @@ -0,0 +1,45 @@ +package konkuk.thip.book.application.service; + +import konkuk.thip.book.application.port.out.BookRedisCommandPort; +import konkuk.thip.book.application.port.out.BookRedisQueryPort; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class BookMostSearchRankService { + + private final BookRedisQueryPort bookRedisQueryPort; + private final BookRedisCommandPort bookRedisCommandPort; + + // 매일 0시 실행 + @Async + @Scheduled(cron = "0 0 0 * * *") + public void updateDailySearchRank() { + + LocalDate yesterday = LocalDate.now().minusDays(1); + + // 전날 검색 카운트 Top 5 조회 + List> top5 = bookRedisQueryPort.getBookSearchCountTopN(yesterday, 5); + + // 기존 랭킹 키 삭제 + bookRedisCommandPort.deleteBookSearchRank(yesterday); + // 전날 카운트 키 삭제 + bookRedisCommandPort.deleteBookSearchCount(yesterday); + + // Top 5 저장 + if (!top5.isEmpty()) { + List isbns = top5.stream().map(Map.Entry::getKey).collect(Collectors.toList()); + List scores = top5.stream().map(Map.Entry::getValue).collect(Collectors.toList()); + bookRedisCommandPort.saveBookSearchRank(isbns, scores, yesterday); + } + } + +} From ebd6d8f76bb46864196b608a0e02d4660ae833c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:18:10 +0900 Subject: [PATCH 06/30] =?UTF-8?q?[test]=20response=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?dto=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/GetBookMostSearchResponse.java | 22 +++++++++++++++++++ .../port/in/dto/BookMostSearchResult.java | 13 +++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookMostSearchResponse.java create mode 100644 src/main/java/konkuk/thip/book/application/port/in/dto/BookMostSearchResult.java diff --git a/src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookMostSearchResponse.java b/src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookMostSearchResponse.java new file mode 100644 index 000000000..44fafa3e2 --- /dev/null +++ b/src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookMostSearchResponse.java @@ -0,0 +1,22 @@ +package konkuk.thip.book.adapter.in.web.response; + +import konkuk.thip.book.application.port.in.dto.BookMostSearchResult; +import lombok.Builder; + +import java.util.List; + +public record GetBookMostSearchResponse( + List bookList +) { + @Builder + public record BookRankInfo( + int rank, + String title, + String imageUrl, + String isbn + ) {} + + public static GetBookMostSearchResponse of(BookMostSearchResult bookMostSearchResult) { + return new GetBookMostSearchResponse(bookMostSearchResult.bookList()); + } +} diff --git a/src/main/java/konkuk/thip/book/application/port/in/dto/BookMostSearchResult.java b/src/main/java/konkuk/thip/book/application/port/in/dto/BookMostSearchResult.java new file mode 100644 index 000000000..9ae401dbd --- /dev/null +++ b/src/main/java/konkuk/thip/book/application/port/in/dto/BookMostSearchResult.java @@ -0,0 +1,13 @@ +package konkuk.thip.book.application.port.in.dto; + +import konkuk.thip.book.adapter.in.web.response.GetBookMostSearchResponse; + +import java.util.List; + +public record BookMostSearchResult( + List bookList +) { + public static BookMostSearchResult of(List bookList) { + return new BookMostSearchResult(bookList); + } +} \ No newline at end of file From 8751a96ef52cbc66692a979685b0b4ef6aa25ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:18:45 +0900 Subject: [PATCH 07/30] [feat] BookMostSearchService.getMostSearchedBooks (#48) --- .../service/BookMostSearchService.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java diff --git a/src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java b/src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java new file mode 100644 index 000000000..e850156b4 --- /dev/null +++ b/src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java @@ -0,0 +1,80 @@ +package konkuk.thip.book.application.service; + +import konkuk.thip.book.adapter.in.web.response.GetBookMostSearchResponse; +import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; +import konkuk.thip.book.application.port.in.BookMostSearchUseCase; +import konkuk.thip.book.application.port.in.dto.BookMostSearchResult; +import konkuk.thip.book.application.port.out.BookApiQueryPort; +import konkuk.thip.book.application.port.out.BookQueryPort; +import konkuk.thip.book.application.port.out.BookRedisQueryPort; +import konkuk.thip.book.domain.Book; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class BookMostSearchService implements BookMostSearchUseCase { + + private final BookQueryPort bookQueryPort; + private final BookRedisQueryPort bookRedisQueryPort; + private final BookApiQueryPort bookApiQueryPort; + + @Override + public BookMostSearchResult getMostSearchedBooks(Long userId) { + + // 오늘 날짜 기준 + LocalDate today = LocalDate.now(); + + // Redis에서 오늘 날짜 기준 랭킹 Top 5 조회 + List> top5 = bookRedisQueryPort.getBookSearchRank(today, 5); + + if (top5 == null || top5.isEmpty()) { + return BookMostSearchResult.of(Collections.emptyList()); //PM과 상의 후 결정 + } + + // isbn 리스트 추출 + List isbnList = top5.stream() + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + // DB에서 isbn으로 책 정보 조회 + List books = bookQueryPort.findByIsbnIn(isbnList); + // isbn -> Book 매핑 + Map bookMap = books.stream() + .collect(Collectors.toMap(Book::getIsbn, b -> b)); + + List bookRankInfos = new ArrayList<>(); + int rank = 1; + for (Map.Entry entry : top5) { + String isbn = entry.getKey(); + Book book = bookMap.get(isbn); + if (book == null) { + // DB에 없으면 Naver API에서 상세 정보 조회 + NaverDetailBookParseResult naverResult = bookApiQueryPort.findDetailBookByKeyword(isbn); + bookRankInfos.add(GetBookMostSearchResponse.BookRankInfo.builder() + .rank(rank++) + .title(naverResult.title()) + .imageUrl(naverResult.imageUrl()) + .isbn(isbn) + .build()); + } else { + bookRankInfos.add(GetBookMostSearchResponse.BookRankInfo.builder() + .rank(rank++) + .title(book.getTitle()) + .imageUrl(book.getImageUrl()) + .isbn(isbn) + .build()); + } + } + + return BookMostSearchResult.of(bookRankInfos); + + } +} \ No newline at end of file From 51db2a4040ceffe5245018cd92d9315db7b6b435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:18:58 +0900 Subject: [PATCH 08/30] =?UTF-8?q?[feat]=20=EA=B0=80=EC=9E=A5=EB=A7=8E?= =?UTF-8?q?=EC=9D=B4=20=EA=B2=80=EC=83=89=EB=90=9C=20=EC=B1=85=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=9C=A0=EC=A6=88=EC=BC=80=EC=9D=B4=EC=8A=A4=20(#4?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../book/application/port/in/BookMostSearchUseCase.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/konkuk/thip/book/application/port/in/BookMostSearchUseCase.java diff --git a/src/main/java/konkuk/thip/book/application/port/in/BookMostSearchUseCase.java b/src/main/java/konkuk/thip/book/application/port/in/BookMostSearchUseCase.java new file mode 100644 index 000000000..336f8a7b4 --- /dev/null +++ b/src/main/java/konkuk/thip/book/application/port/in/BookMostSearchUseCase.java @@ -0,0 +1,7 @@ +package konkuk.thip.book.application.port.in; + +import konkuk.thip.book.application.port.in.dto.BookMostSearchResult; + +public interface BookMostSearchUseCase { + BookMostSearchResult getMostSearchedBooks(Long userId); +} From ecec33cb609f9b902f7da940b1140c94fabab635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:19:05 +0900 Subject: [PATCH 09/30] =?UTF-8?q?[feat]=20=EA=B0=80=EC=9E=A5=EB=A7=8E?= =?UTF-8?q?=EC=9D=B4=20=EA=B2=80=EC=83=89=EB=90=9C=20=EC=B1=85=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/book/adapter/in/web/BookQueryController.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java b/src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java index de8dad678..b1e7de2a9 100644 --- a/src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java +++ b/src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java @@ -2,8 +2,10 @@ import jakarta.validation.constraints.Pattern; import konkuk.thip.book.adapter.in.web.response.GetBookDetailSearchResponse; +import konkuk.thip.book.adapter.in.web.response.GetBookMostSearchResponse; import konkuk.thip.book.adapter.in.web.response.GetBookSearchListResponse; import konkuk.thip.book.application.port.in.BookSearchUseCase; +import konkuk.thip.book.application.port.in.BookMostSearchUseCase; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; import lombok.RequiredArgsConstructor; @@ -16,6 +18,8 @@ public class BookQueryController { private final BookSearchUseCase bookSearchUseCase; + private final BookMostSearchUseCase bookMostSearchUseCase; + //책 검색결과 조회 @GetMapping("/books") @@ -36,4 +40,11 @@ public BaseResponse getBookDetailSearch(@PathVariab return BaseResponse.ok(GetBookDetailSearchResponse.of(bookSearchUseCase.searchDetailBooks(isbn,userId))); } + //가장 많이 검색된 책 조회 + @GetMapping("/books/most-searched") + public BaseResponse getMostSearchedBooks(@UserId final Long userId) { + + return BaseResponse.ok(GetBookMostSearchResponse.of(bookMostSearchUseCase.getMostSearchedBooks(userId))); + } + } From 5df05e463482616b76d13fe56a0b78c46f1ff4ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:19:19 +0900 Subject: [PATCH 10/30] [feat] BookQueryPersistenceAdapter.findByIsbnIn (#48) --- .../BookQueryPersistenceAdapter.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java new file mode 100644 index 000000000..17beba160 --- /dev/null +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java @@ -0,0 +1,27 @@ +package konkuk.thip.book.adapter.out.persistence; + +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.mapper.BookMapper; +import konkuk.thip.book.application.port.out.BookQueryPort; +import konkuk.thip.book.domain.Book; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.stream.Collectors; + +@Repository +@RequiredArgsConstructor +public class BookQueryPersistenceAdapter implements BookQueryPort { + + private final BookJpaRepository bookJpaRepository; + private final BookMapper bookMapper; + + @Override + public List findByIsbnIn(List isbnList) { + List entities = bookJpaRepository.findByIsbnIn(isbnList); + return entities.stream() + .map(bookMapper::toDomainEntity) + .collect(Collectors.toList()); + } +} From abd294c2c9401e7f3805180b5e3253be61e825dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:19:27 +0900 Subject: [PATCH 11/30] [feat] BookQueryPort.findByIsbnIn (#48) --- .../thip/book/application/port/out/BookQueryPort.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java index a795fd146..a56689c99 100644 --- a/src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java +++ b/src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java @@ -1,5 +1,9 @@ package konkuk.thip.book.application.port.out; -public interface BookQueryPort { +import konkuk.thip.book.domain.Book; + +import java.util.List; +public interface BookQueryPort { + List findByIsbnIn(List isbnList); } From c3b276b13476fd2f9d26a63d6f8fe9e975ae74b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:19:46 +0900 Subject: [PATCH 12/30] [feat] BookQueryRedisAdapter (#48) --- .../persistence/BookQueryRedisAdapter.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java new file mode 100644 index 000000000..be457cb23 --- /dev/null +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java @@ -0,0 +1,57 @@ +package konkuk.thip.book.adapter.out.persistence; + +import konkuk.thip.book.application.port.out.BookRedisQueryPort; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class BookQueryRedisAdapter implements BookRedisQueryPort { + + private final RedisTemplate redisTemplate; + private final DateTimeFormatter DAILY_KEY_FORMATTER = + DateTimeFormatter.ofPattern("yyyyMMdd"); + + @Value("${app.redis.search-count-prefix}") + private String searchCountPrefix; + + @Value("${app.redis.search-rank-prefix}") + private String searchRankPrefix; + + + @Override + public List> getBookSearchRank(LocalDate date, int topN) { + return getTopNFromZSet(searchRankPrefix, date, topN); + } + + @Override + public List> getBookSearchCountTopN(LocalDate date, int topN) { + return getTopNFromZSet(searchCountPrefix, date, topN); + } + + private List> getTopNFromZSet(String prefix, LocalDate date, int topN) { + String dateStr = date.format(DAILY_KEY_FORMATTER); + String redisKey = prefix + dateStr; + Set> topNSet = redisTemplate.opsForZSet() + .reverseRangeWithScores(redisKey, 0, topN - 1); + + if (topNSet == null) return Collections.emptyList(); + return topNSet.stream() + .map(tuple -> Map.entry(tuple.getValue(), tuple.getScore())) + .collect(Collectors.toList()); + } + + + +} From da1a08dbb7c1e78fe8b53812bac4470bdb444835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:20:01 +0900 Subject: [PATCH 13/30] =?UTF-8?q?[feat]=20=EB=A0=88=EB=94=94=EC=8A=A4?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20port=20=EC=9E=91=EC=84=B1=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/port/out/BookRedisCommandPort.java | 11 +++++++++++ .../book/application/port/out/BookRedisQueryPort.java | 10 ++++++++++ 2 files changed, 21 insertions(+) create mode 100644 src/main/java/konkuk/thip/book/application/port/out/BookRedisCommandPort.java create mode 100644 src/main/java/konkuk/thip/book/application/port/out/BookRedisQueryPort.java diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookRedisCommandPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookRedisCommandPort.java new file mode 100644 index 000000000..0d666f8fa --- /dev/null +++ b/src/main/java/konkuk/thip/book/application/port/out/BookRedisCommandPort.java @@ -0,0 +1,11 @@ +package konkuk.thip.book.application.port.out; + +import java.time.LocalDate; +import java.util.List; + +public interface BookRedisCommandPort { + void incrementBookSearchCount(String isbn, LocalDate date); + void saveBookSearchRank(List isbns, List scores, LocalDate date); + void deleteBookSearchRank(LocalDate date); + void deleteBookSearchCount(LocalDate date); +} diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookRedisQueryPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookRedisQueryPort.java new file mode 100644 index 000000000..59f267b32 --- /dev/null +++ b/src/main/java/konkuk/thip/book/application/port/out/BookRedisQueryPort.java @@ -0,0 +1,10 @@ +package konkuk.thip.book.application.port.out; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +public interface BookRedisQueryPort { + List> getBookSearchRank(LocalDate date, int topN); + List> getBookSearchCountTopN(LocalDate date, int topN); +} From 19184a74794f0e4acc41c81b8c3aae376b46a930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:20:19 +0900 Subject: [PATCH 14/30] =?UTF-8?q?[feat]=20=EC=B1=85=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=EA=B2=80=EC=83=89=EC=8B=9C=20=EA=B2=80=EC=83=89=EC=88=9C?= =?UTF-8?q?=EC=9C=84=20=EC=A0=95=EB=B3=B4=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/book/application/service/BookSearchService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/book/application/service/BookSearchService.java b/src/main/java/konkuk/thip/book/application/service/BookSearchService.java index 3ec280a6c..0b139598f 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSearchService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSearchService.java @@ -7,6 +7,7 @@ import konkuk.thip.book.application.port.in.dto.BookDetailSearchResult; import konkuk.thip.book.application.port.out.BookCommandPort; import konkuk.thip.book.application.port.out.BookApiQueryPort; +import konkuk.thip.book.application.port.out.BookRedisCommandPort; import konkuk.thip.book.domain.Book; import konkuk.thip.common.exception.BusinessException; import konkuk.thip.feed.application.port.out.FeedQueryPort; @@ -40,6 +41,7 @@ public class BookSearchService implements BookSearchUseCase { private final SavedQueryPort savedQueryPort; private final RecentSearchCommandPort recentSearchCommandPort; private final BookCommandPort bookCommandPort; + private final BookRedisCommandPort bookRedisCommandPort; private final UserCommandPort userCommandPort; @@ -85,9 +87,11 @@ public BookDetailSearchResult searchDetailBooks(String isbn,Long userId) { //책 상세정보 NaverDetailBookParseResult naverDetailBookParseResult = bookApiQueryPort.findDetailBookByKeyword(isbn); - Optional bookOpt = bookCommandPort.findByIsbn(isbn); + //책 검색순위 정보 업데이트 + bookRedisCommandPort.incrementBookSearchCount(isbn,LocalDate.now()); + if (bookOpt.isEmpty()) { // 책이 없으면 기본값으로 반환 return BookDetailSearchResult.of( From 6f78577516d90f6ab1ca1848ad8cb771b8f0e56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:20:58 +0900 Subject: [PATCH 15/30] =?UTF-8?q?[feat]=20RedisConfig=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/config/RedisConfig.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/main/java/konkuk/thip/config/RedisConfig.java diff --git a/src/main/java/konkuk/thip/config/RedisConfig.java b/src/main/java/konkuk/thip/config/RedisConfig.java new file mode 100644 index 000000000..088dde588 --- /dev/null +++ b/src/main/java/konkuk/thip/config/RedisConfig.java @@ -0,0 +1,37 @@ + +package konkuk.thip.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(connectionFactory); + // 일반적인 key:value의 경우 시리얼라이저 + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + return redisTemplate; + } + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + +} From 5a23a8210b3bd9cf7cfcd6ed8b735a8dcaee44cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:21:12 +0900 Subject: [PATCH 16/30] =?UTF-8?q?[feat]=20@=20EnableScheduling=20=20=20=20?= =?UTF-8?q?=20=20=20=20@=20EnableAsync=20=EC=B6=94=EA=B0=80=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/ThipServerApplication.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/konkuk/thip/ThipServerApplication.java b/src/main/java/konkuk/thip/ThipServerApplication.java index 57f951a56..4ee0caadb 100644 --- a/src/main/java/konkuk/thip/ThipServerApplication.java +++ b/src/main/java/konkuk/thip/ThipServerApplication.java @@ -3,8 +3,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; @EnableJpaAuditing +@EnableScheduling +@EnableAsync @SpringBootApplication public class ThipServerApplication { From 0f663b5ef982ac3b47648af90b75e7a9924da5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 03:49:20 +0900 Subject: [PATCH 17/30] [test] test (#48) --- src/test/java/konkuk/thip/ThipServerApplicationTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/konkuk/thip/ThipServerApplicationTests.java b/src/test/java/konkuk/thip/ThipServerApplicationTests.java index 3acca870f..82ff3c417 100644 --- a/src/test/java/konkuk/thip/ThipServerApplicationTests.java +++ b/src/test/java/konkuk/thip/ThipServerApplicationTests.java @@ -12,4 +12,5 @@ class ThipServerApplicationTests { void contextLoads() { } + } From 31266bdc5440ebc6e894f03e22ddcfb081c2bf6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 04:06:42 +0900 Subject: [PATCH 18/30] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/BookMostSearchedBooksControllerTest.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java index 0569484a5..8c2fe87c6 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java @@ -57,9 +57,6 @@ class BookMostSearchedBooksControllerTest { private final DateTimeFormatter DAILY_KEY_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); - @Value("${app.redis.search-count-prefix}") - private String searchCountPrefix; - @Value("${app.redis.search-rank-prefix}") private String searchRankPrefix; @@ -117,7 +114,7 @@ void getMostSearchedBooks_returnsRankList() throws Exception { String json = result.andReturn().getResponse().getContentAsString(); JsonNode jsonNode = objectMapper.readTree(json); - JsonNode rankArray = jsonNode.path("data").path("bookRankInfos"); + JsonNode rankArray = jsonNode.path("data").path("bookList"); assertThat(rankArray).isNotNull(); assertThat(rankArray.size()).isEqualTo(3); From 003d72c4ae193951e4f6cd1d5daf472edf9f71e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sun, 6 Jul 2025 20:41:49 +0900 Subject: [PATCH 19/30] =?UTF-8?q?[refactor]=20ci=20=EB=A0=88=EB=94=94?= =?UTF-8?q?=EC=8A=A4=20=ED=83=AC=ED=94=8C=EB=A6=BF=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-workflow.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index ec5c7bd85..a9b9b0b22 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -35,21 +35,26 @@ jobs: settings-path: ${{ github.workspace }} # location for the settings.xml file - - name: 🧾 Create application.yml from secret +# - name: 🧾 Create application.yml from secret +# run: | +# mkdir -p ${{ env.RESOURCE_PATH }} +# mkdir -p ${{ env.TEST_RESOURCE_PATH }} +# echo "${{ secrets.APPLICATION_YML_DEV }}" > ${{ env.RESOURCE_PATH }}/application.yml +# echo "${{ secrets.APPLICATION_YML_TEST }}" > ${{ env.TEST_RESOURCE_PATH }}/application-test.yml + - name: 🧾 Create application.yml from secret (base64) run: | mkdir -p ${{ env.RESOURCE_PATH }} mkdir -p ${{ env.TEST_RESOURCE_PATH }} - echo "${{ secrets.APPLICATION_YML_DEV }}" > ${{ env.RESOURCE_PATH }}/application.yml - echo "${{ secrets.APPLICATION_YML_TEST }}" > ${{ env.TEST_RESOURCE_PATH }}/application-test.yml - + echo "${{ secrets.APPLICATION_YML_DEV }}" | base64 --decode > ${{ env.RESOURCE_PATH }}/application.yml + echo "${{ secrets.APPLICATION_YML_TEST }}" | base64 --decode > ${{ env.TEST_RESOURCE_PATH }}/application-test.yml - name: 👏🏻 grant execute permission for gradlew run: chmod +x gradlew -# - name: 🚀 Start Redis -# uses: supercharge/redis-github-action@1.7.0 -# with: -# redis-version: 7 + - name: 🚀 Start Redis + uses: supercharge/redis-github-action@1.7.0 + with: + redis-version: 7 - name: 🐘 build with Gradle run: ./gradlew build \ No newline at end of file From 96a58404b737a4edaeb0c1a336d8209496e1971f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 7 Jul 2025 21:15:03 +0900 Subject: [PATCH 20/30] =?UTF-8?q?[fix]=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=EC=B1=85=20=EA=B2=80=EC=83=89=EC=88=9C=EC=9C=84=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/book/application/service/BookSearchService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/book/application/service/BookSearchService.java b/src/main/java/konkuk/thip/book/application/service/BookSearchService.java index 41f6ff91d..4263da925 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSearchService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSearchService.java @@ -7,6 +7,7 @@ import konkuk.thip.book.application.port.in.dto.BookDetailSearchResult; import konkuk.thip.book.application.port.out.BookCommandPort; import konkuk.thip.book.application.port.out.BookApiQueryPort; +import konkuk.thip.book.application.port.out.BookRedisCommandPort; import konkuk.thip.book.domain.Book; import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.exception.EntityNotFoundException; @@ -41,6 +42,7 @@ public class BookSearchService implements BookSearchUseCase { private final RecentSearchCommandPort recentSearchCommandPort; private final BookCommandPort bookCommandPort; private final UserCommandPort userCommandPort; + private final BookRedisCommandPort bookRedisCommandPort; @Override @@ -85,6 +87,9 @@ public BookDetailSearchResult searchDetailBooks(String isbn,Long userId) { //책 상세정보 NaverDetailBookParseResult naverDetailBookParseResult = bookApiQueryPort.findDetailBookByKeyword(isbn); + //책 검색순위 정보 업데이트 + bookRedisCommandPort.incrementBookSearchCount(isbn,LocalDate.now()); + Book book; try { // DB에서 책 정보 조회 (없으면 예외 발생) @@ -99,7 +104,6 @@ public BookDetailSearchResult searchDetailBooks(String isbn,Long userId) { ); } - //이책에 모집중인 모임방 개수 int recruitingRoomCount = getRecruitingRoomCount(book); // 이책에 읽기 참여중인 사용자 수 From cedaba88a874207fb4421dd6a47c8fd1a16b4625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 7 Jul 2025 21:57:08 +0900 Subject: [PATCH 21/30] =?UTF-8?q?[refactor]=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=AA=85=20=EB=B3=80=EA=B2=BD=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/book/adapter/out/api/BookApiNaverApiAdapter.java | 2 +- .../konkuk/thip/book/application/port/out/BookApiQueryPort.java | 2 +- .../thip/book/application/service/BookMostSearchService.java | 2 +- .../konkuk/thip/book/application/service/BookSavedService.java | 2 +- .../konkuk/thip/book/application/service/BookSearchService.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/BookApiNaverApiAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/api/BookApiNaverApiAdapter.java index 3a2eedfe8..c52747689 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/BookApiNaverApiAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/BookApiNaverApiAdapter.java @@ -19,7 +19,7 @@ public NaverBookParseResult findBooksByKeyword(String keyword, int start) { } @Override - public NaverDetailBookParseResult findDetailBookByKeyword(String isbn) { + public NaverDetailBookParseResult findDetailBookByIsbn(String isbn) { String xml = naverApiUtil.detailSearchBook(isbn); // 네이버 API 호출 return NaverBookXmlParser.parseBookDetail(xml); // XML 파싱 } diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookApiQueryPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookApiQueryPort.java index 1bbf8fdc4..145dee188 100644 --- a/src/main/java/konkuk/thip/book/application/port/out/BookApiQueryPort.java +++ b/src/main/java/konkuk/thip/book/application/port/out/BookApiQueryPort.java @@ -5,5 +5,5 @@ public interface BookApiQueryPort { NaverBookParseResult findBooksByKeyword(String keyword, int start); - NaverDetailBookParseResult findDetailBookByKeyword(String isbn); + NaverDetailBookParseResult findDetailBookByIsbn(String isbn); } diff --git a/src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java b/src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java index e850156b4..7ee98b2ff 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java @@ -57,7 +57,7 @@ public BookMostSearchResult getMostSearchedBooks(Long userId) { Book book = bookMap.get(isbn); if (book == null) { // DB에 없으면 Naver API에서 상세 정보 조회 - NaverDetailBookParseResult naverResult = bookApiQueryPort.findDetailBookByKeyword(isbn); + NaverDetailBookParseResult naverResult = bookApiQueryPort.findDetailBookByIsbn(isbn); bookRankInfos.add(GetBookMostSearchResponse.BookRankInfo.builder() .rank(rank++) .title(naverResult.title()) diff --git a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java index 258b7a76f..4872f1674 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java @@ -44,7 +44,7 @@ public BookIsSavedResult changeSavedBook(String isbn, boolean isSave, Long userI } // 저장 요청이면 네이버 API로 책 정보 조회 후 저장 - NaverDetailBookParseResult naverResult = bookApiQueryPort.findDetailBookByKeyword(isbn); + NaverDetailBookParseResult naverResult = bookApiQueryPort.findDetailBookByIsbn(isbn); Book newBook = Book.withoutId( naverResult.title(), naverResult.isbn(), diff --git a/src/main/java/konkuk/thip/book/application/service/BookSearchService.java b/src/main/java/konkuk/thip/book/application/service/BookSearchService.java index 4263da925..d1ec358f8 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSearchService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSearchService.java @@ -85,7 +85,7 @@ public BookDetailSearchResult searchDetailBooks(String isbn,Long userId) { User user = userCommandPort.findById(userId); //책 상세정보 - NaverDetailBookParseResult naverDetailBookParseResult = bookApiQueryPort.findDetailBookByKeyword(isbn); + NaverDetailBookParseResult naverDetailBookParseResult = bookApiQueryPort.findDetailBookByIsbn(isbn); //책 검색순위 정보 업데이트 bookRedisCommandPort.incrementBookSearchCount(isbn,LocalDate.now()); From 3c56997529638faa40ae444e6d38ccc13b857bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 8 Jul 2025 04:52:35 +0900 Subject: [PATCH 22/30] =?UTF-8?q?[refactor]=20=EC=B1=85=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EC=A0=95=EB=B3=B4=20=EC=BA=90=EC=8B=B1=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/BookCommandRedisAdapter.java | 30 ++++++++++++-- .../port/out/BookRedisCommandPort.java | 4 ++ .../service/BookMostSearchRankService.java | 39 ++++++++++++++++++- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java index 357c872b1..ebfcc1235 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java @@ -1,6 +1,10 @@ package konkuk.thip.book.adapter.out.persistence; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import konkuk.thip.book.application.port.in.dto.BookMostSearchResult; import konkuk.thip.book.application.port.out.BookRedisCommandPort; +import konkuk.thip.common.exception.ExternalApiException; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; @@ -11,6 +15,8 @@ import java.time.format.DateTimeFormatter; import java.util.List; +import static konkuk.thip.common.exception.code.ErrorCode.JSON_PROCESSING_ERROR; + @Component @RequiredArgsConstructor public class BookCommandRedisAdapter implements BookRedisCommandPort { @@ -25,6 +31,10 @@ public class BookCommandRedisAdapter implements BookRedisCommandPort { @Value("${app.redis.search-rank-prefix}") private String searchRankPrefix; + @Value("${app.redis.search-rank-detail-prefix}") + private String searchRankDetailPrefix; + + private final ObjectMapper objectMapper; @Override public void incrementBookSearchCount(String isbn, LocalDate date) { @@ -41,15 +51,28 @@ public void saveBookSearchRank(List isbns, List scores, LocalDat redisTemplate.expire(redisKey, Duration.ofDays(7)); } + @Override + public void saveBookSearchRankDetail(List bookRankDetails, LocalDate date) { + String redisKey = makeRedisKey(searchRankDetailPrefix, date); + String detailJson = null; + try { + detailJson = objectMapper.writeValueAsString(bookRankDetails); + } catch (JsonProcessingException e) { + throw new ExternalApiException(JSON_PROCESSING_ERROR); + } + redisTemplate.opsForValue().set(redisKey, detailJson); + } + @Override public void deleteBookSearchRank(LocalDate date) { deleteZSetKey(searchRankPrefix, date); } @Override - public void deleteBookSearchCount(LocalDate date) { - deleteZSetKey(searchCountPrefix, date); - } + public void deleteBookSearchCount(LocalDate date) { deleteZSetKey(searchCountPrefix, date); } + + @Override + public void deleteBookSearchRankDetail(LocalDate date) { deleteZSetKey(searchRankDetailPrefix, date); } private void deleteZSetKey(String prefix, LocalDate date) { String redisKey = makeRedisKey(prefix, date); @@ -61,5 +84,4 @@ private String makeRedisKey(String prefix, LocalDate date) { return prefix + dateStr; } - } diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookRedisCommandPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookRedisCommandPort.java index 0d666f8fa..c2bc9e749 100644 --- a/src/main/java/konkuk/thip/book/application/port/out/BookRedisCommandPort.java +++ b/src/main/java/konkuk/thip/book/application/port/out/BookRedisCommandPort.java @@ -1,11 +1,15 @@ package konkuk.thip.book.application.port.out; +import konkuk.thip.book.application.port.in.dto.BookMostSearchResult; + import java.time.LocalDate; import java.util.List; public interface BookRedisCommandPort { void incrementBookSearchCount(String isbn, LocalDate date); void saveBookSearchRank(List isbns, List scores, LocalDate date); + void saveBookSearchRankDetail(List bookRankDetails, LocalDate date); void deleteBookSearchRank(LocalDate date); void deleteBookSearchCount(LocalDate date); + void deleteBookSearchRankDetail(LocalDate date); } diff --git a/src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java b/src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java index cd980d1e0..cc3c8b4eb 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java @@ -1,13 +1,20 @@ package konkuk.thip.book.application.service; +import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; +import konkuk.thip.book.application.port.in.dto.BookMostSearchResult; +import konkuk.thip.book.application.port.out.BookApiQueryPort; +import konkuk.thip.book.application.port.out.BookCommandPort; import konkuk.thip.book.application.port.out.BookRedisCommandPort; import konkuk.thip.book.application.port.out.BookRedisQueryPort; +import konkuk.thip.book.domain.Book; +import konkuk.thip.common.exception.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -16,8 +23,10 @@ @RequiredArgsConstructor public class BookMostSearchRankService { + private final BookApiQueryPort bookApiQueryPort; private final BookRedisQueryPort bookRedisQueryPort; private final BookRedisCommandPort bookRedisCommandPort; + private final BookCommandPort bookCommandPort; // 매일 0시 실행 @Async @@ -29,17 +38,43 @@ public void updateDailySearchRank() { // 전날 검색 카운트 Top 5 조회 List> top5 = bookRedisQueryPort.getBookSearchCountTopN(yesterday, 5); + // 기존 랭킹 책 상제정보 키 삭제 + bookRedisCommandPort.deleteBookSearchRankDetail(yesterday); // 기존 랭킹 키 삭제 bookRedisCommandPort.deleteBookSearchRank(yesterday); // 전날 카운트 키 삭제 bookRedisCommandPort.deleteBookSearchCount(yesterday); // Top 5 저장 - if (!top5.isEmpty()) { + if (!top5.isEmpty()) { //PM과 상의 후 결정 List isbns = top5.stream().map(Map.Entry::getKey).collect(Collectors.toList()); List scores = top5.stream().map(Map.Entry::getValue).collect(Collectors.toList()); bookRedisCommandPort.saveBookSearchRank(isbns, scores, yesterday); + + // Top 5 상세정보 저장 추가 + List bookRankDetails = new ArrayList<>(); + int rank = 1; + for (String isbn : isbns) { + try { + Book book = bookCommandPort.findByIsbn(isbn); + bookRankDetails.add(BookMostSearchResult.BookRankInfo.builder() + .rank(rank++) + .title(book.getTitle()) + .imageUrl(book.getImageUrl()) + .isbn(isbn) + .build()); + } catch (EntityNotFoundException e) { + // DB에 없으면 Naver API에서 상세 정보 조회 + NaverDetailBookParseResult naverResult = bookApiQueryPort.findDetailBookByIsbn(isbn); + bookRankDetails.add(BookMostSearchResult.BookRankInfo.builder() + .rank(rank++) + .title(naverResult.title()) + .imageUrl(naverResult.imageUrl()) + .isbn(isbn) + .build()); + } + } + bookRedisCommandPort.saveBookSearchRankDetail(bookRankDetails, yesterday); } } - } From a826f7dbf1a6d5ee563cd6461c4e6c55ef1704de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 8 Jul 2025 04:52:44 +0900 Subject: [PATCH 23/30] =?UTF-8?q?[refactor]=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#4?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/common/exception/code/ErrorCode.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 5de2c7ec8..83489ffac 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -23,6 +23,8 @@ public enum ErrorCode implements ResponseCode { AUTH_LOGIN_FAILED(HttpStatus.UNAUTHORIZED, 40104, "로그인에 실패했습니다."), AUTH_UNSUPPORTED_SOCIAL_LOGIN(HttpStatus.UNAUTHORIZED, 40105, "지원하지 않는 소셜 로그인입니다."), + JSON_PROCESSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 50000, "JSON 직렬화/역직렬화에 실패했습니다."), + /* 60000부터 비즈니스 예외 */ /** * 60000 : alias error From 691bcbbd77bd95c9a8bc3a31a47da229fdc3018c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 8 Jul 2025 04:52:55 +0900 Subject: [PATCH 24/30] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookMostSearchedBooksControllerTest.java | 88 ++++++++++--------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java index 8c2fe87c6..796692ea9 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import konkuk.thip.book.application.port.in.dto.BookMostSearchResult; import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserRole; @@ -51,14 +52,14 @@ class BookMostSearchedBooksControllerTest { private ObjectMapper objectMapper; - private LocalDate today; + private LocalDate yesterday; private String rankKey; private final DateTimeFormatter DAILY_KEY_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); - @Value("${app.redis.search-rank-prefix}") - private String searchRankPrefix; + @Value("${app.redis.search-rank-detail-prefix}") + private String searchRankDetailPrefix; @BeforeEach void setUp() { @@ -85,18 +86,35 @@ void tearDown() { aliasJpaRepository.deleteAll(); } - @Test - @DisplayName("오늘 랭킹 Top 5를 정상적으로 조회한다") + @DisplayName("어제 랭킹 Top 5를 정상적으로 조회한다") void getMostSearchedBooks_returnsRankList() throws Exception { - - // given 오늘 날짜의 랭킹 키에 테스트용 데이터 3개를 저장. - today = LocalDate.now(); - rankKey = searchRankPrefix + today.format(DAILY_KEY_FORMATTER); - - redisTemplate.opsForZSet().add(rankKey, "9788954682152", 10); - redisTemplate.opsForZSet().add(rankKey, "9788991742178", 5); - redisTemplate.opsForZSet().add(rankKey, "9791198783400", 7); + // given: 어제 날짜의 랭킹 상세정보(JSON)를 Redis에 저장 + yesterday = LocalDate.now().minusDays(1); + String detailKey = searchRankDetailPrefix + yesterday.format(DAILY_KEY_FORMATTER); + + List bookRankInfos = List.of( + BookMostSearchResult.BookRankInfo.builder() + .rank(1) + .title("책1") + .imageUrl("http://image1.jpg") + .isbn("9788954682152") + .build(), + BookMostSearchResult.BookRankInfo.builder() + .rank(2) + .title("책2") + .imageUrl("http://image2.jpg") + .isbn("9788991742178") + .build(), + BookMostSearchResult.BookRankInfo.builder() + .rank(3) + .title("책3") + .imageUrl("http://image3.jpg") + .isbn("9791198783400") + .build() + ); + String json = objectMapper.writeValueAsString(bookRankInfos); + redisTemplate.opsForValue().set(detailKey, json); Long userId = userJpaRepository.findAll().get(0).getUserId(); @@ -111,39 +129,25 @@ void getMostSearchedBooks_returnsRankList() throws Exception { .andExpect(jsonPath("$.data.bookList").isArray()) .andExpect(jsonPath("$.data.bookList.length()").value(3)); + String responseJson = result.andReturn().getResponse().getContentAsString(); + JsonNode bookList = objectMapper.readTree(responseJson).path("data").path("bookList"); - String json = result.andReturn().getResponse().getContentAsString(); - JsonNode jsonNode = objectMapper.readTree(json); - JsonNode rankArray = jsonNode.path("data").path("bookList"); - - assertThat(rankArray).isNotNull(); - assertThat(rankArray.size()).isEqualTo(3); - - - // 점수 내림차순 확인 (Redis에서 직접 점수 확인) - double previousScore = Double.MAX_VALUE; - for (JsonNode bookRankInfo : rankArray) { - String isbn = bookRankInfo.path("isbn").asText(); - Double score = redisTemplate.opsForZSet().score(rankKey, isbn); - assertThat(score).isLessThanOrEqualTo(previousScore); - previousScore = score; - } + assertThat(bookList).isNotNull(); + assertThat(bookList.size()).isEqualTo(3); - // 저장된 isbn이 포함되어 있는지 확인 - List isbns = List.of("9788954682152", "9788991742178", "9791198783400"); - for (JsonNode bookRankInfo : rankArray) { - assertThat(isbns.contains(bookRankInfo.path("isbn").asText())).isTrue(); - } + // 순서 및 필드 검증 + assertThat(bookList.get(0).path("isbn").asText()).isEqualTo("9788954682152"); + assertThat(bookList.get(1).path("isbn").asText()).isEqualTo("9788991742178"); + assertThat(bookList.get(2).path("isbn").asText()).isEqualTo("9791198783400"); } @Test @DisplayName("랭킹 데이터가 없으면 빈 리스트를 반환한다") void getMostSearchedBooks_returnsEmptyList_whenNoData() throws Exception { - - // given 오늘 날짜의 랭킹 키를 삭제해서 데이터가 없는 상태로 만든다 - today = LocalDate.now(); - rankKey = searchRankPrefix + today.format(DAILY_KEY_FORMATTER); - redisTemplate.delete(rankKey); + // given: 어제 날짜의 랭킹 상세정보 키를 삭제 + yesterday = LocalDate.now().minusDays(1); + String detailKey = searchRankDetailPrefix + yesterday.format(DAILY_KEY_FORMATTER); + redisTemplate.delete(detailKey); Long userId = userJpaRepository.findAll().get(0).getUserId(); @@ -158,12 +162,12 @@ void getMostSearchedBooks_returnsEmptyList_whenNoData() throws Exception { .andExpect(jsonPath("$.data.bookList").isArray()) .andExpect(jsonPath("$.data.bookList.length()").value(0)); - String json = result.andReturn().getResponse().getContentAsString(); - JsonNode jsonNode = objectMapper.readTree(json); - JsonNode bookList = jsonNode.path("data").path("bookList"); + String responseJson = result.andReturn().getResponse().getContentAsString(); + JsonNode bookList = objectMapper.readTree(responseJson).path("data").path("bookList"); assertThat(bookList).isNotNull(); assertThat(bookList.size()).isEqualTo(0); } + } From 9acf60f811d29c882ff1c7766b06797a6c502f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 8 Jul 2025 04:53:14 +0900 Subject: [PATCH 25/30] =?UTF-8?q?[refactor]=20=EC=9D=B4=EB=84=88=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/response/GetBookMostSearchResponse.java | 10 +--------- .../port/in/dto/BookMostSearchResult.java | 13 +++++++++++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookMostSearchResponse.java b/src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookMostSearchResponse.java index 44fafa3e2..8f14f1008 100644 --- a/src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookMostSearchResponse.java +++ b/src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookMostSearchResponse.java @@ -6,16 +6,8 @@ import java.util.List; public record GetBookMostSearchResponse( - List bookList + List bookList ) { - @Builder - public record BookRankInfo( - int rank, - String title, - String imageUrl, - String isbn - ) {} - public static GetBookMostSearchResponse of(BookMostSearchResult bookMostSearchResult) { return new GetBookMostSearchResponse(bookMostSearchResult.bookList()); } diff --git a/src/main/java/konkuk/thip/book/application/port/in/dto/BookMostSearchResult.java b/src/main/java/konkuk/thip/book/application/port/in/dto/BookMostSearchResult.java index 9ae401dbd..0862bef8e 100644 --- a/src/main/java/konkuk/thip/book/application/port/in/dto/BookMostSearchResult.java +++ b/src/main/java/konkuk/thip/book/application/port/in/dto/BookMostSearchResult.java @@ -1,13 +1,22 @@ package konkuk.thip.book.application.port.in.dto; import konkuk.thip.book.adapter.in.web.response.GetBookMostSearchResponse; +import lombok.Builder; import java.util.List; public record BookMostSearchResult( - List bookList + List bookList ) { - public static BookMostSearchResult of(List bookList) { + @Builder + public record BookRankInfo( + int rank, + String title, + String imageUrl, + String isbn + ) {} + + public static BookMostSearchResult of(List bookList) { return new BookMostSearchResult(bookList); } } \ No newline at end of file From 671ca86f2278e9c317d5fd3fd1fa88e249cd129a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 8 Jul 2025 04:53:36 +0900 Subject: [PATCH 26/30] =?UTF-8?q?[refactor]=20=EC=95=88=EC=93=B0=EB=8A=94?= =?UTF-8?q?=20=ED=95=A8=EC=88=98=20=EC=A0=95=EB=A6=AC=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/BookQueryPersistenceAdapter.java | 12 ------------ .../book/application/port/out/BookQueryPort.java | 5 ----- 2 files changed, 17 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java index 17beba160..d91fcf0fc 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java @@ -1,15 +1,10 @@ package konkuk.thip.book.adapter.out.persistence; -import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.mapper.BookMapper; import konkuk.thip.book.application.port.out.BookQueryPort; -import konkuk.thip.book.domain.Book; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; -import java.util.List; -import java.util.stream.Collectors; - @Repository @RequiredArgsConstructor public class BookQueryPersistenceAdapter implements BookQueryPort { @@ -17,11 +12,4 @@ public class BookQueryPersistenceAdapter implements BookQueryPort { private final BookJpaRepository bookJpaRepository; private final BookMapper bookMapper; - @Override - public List findByIsbnIn(List isbnList) { - List entities = bookJpaRepository.findByIsbnIn(isbnList); - return entities.stream() - .map(bookMapper::toDomainEntity) - .collect(Collectors.toList()); - } } diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java index a56689c99..186919761 100644 --- a/src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java +++ b/src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java @@ -1,9 +1,4 @@ package konkuk.thip.book.application.port.out; -import konkuk.thip.book.domain.Book; - -import java.util.List; - public interface BookQueryPort { - List findByIsbnIn(List isbnList); } From 80eb90e88d3be1e6bf66396d6570253478d05a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 8 Jul 2025 04:53:52 +0900 Subject: [PATCH 27/30] =?UTF-8?q?[refactor]=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/BookQueryRedisAdapter.java | 26 ++++++++ .../port/out/BookRedisQueryPort.java | 3 + .../service/BookMostSearchService.java | 59 ++----------------- 3 files changed, 34 insertions(+), 54 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java index be457cb23..954dd333f 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java @@ -1,6 +1,12 @@ package konkuk.thip.book.adapter.out.persistence; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import konkuk.thip.book.application.port.in.dto.BookMostSearchResult; import konkuk.thip.book.application.port.out.BookRedisQueryPort; +import konkuk.thip.common.exception.ExternalApiException; +import konkuk.thip.common.exception.code.ErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; @@ -29,6 +35,10 @@ public class BookQueryRedisAdapter implements BookRedisQueryPort { @Value("${app.redis.search-rank-prefix}") private String searchRankPrefix; + @Value("${app.redis.search-rank-detail-prefix}") + private String searchRankDetailPrefix; + + private final ObjectMapper objectMapper; @Override public List> getBookSearchRank(LocalDate date, int topN) { @@ -52,6 +62,22 @@ private List> getTopNFromZSet(String prefix, LocalDate .collect(Collectors.toList()); } + @Override + public List getYesterdayBookRankInfos(LocalDate date) { + String redisKey = searchRankDetailPrefix + date.format(DAILY_KEY_FORMATTER); + String json = redisTemplate.opsForValue().get(redisKey); + if (json == null || json.isBlank()) { + return List.of(); + } + try { + return objectMapper.readValue( + json, + new TypeReference>() {} + ); + } catch (JsonProcessingException e) { + throw new ExternalApiException(ErrorCode.JSON_PROCESSING_ERROR); + } + } } diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookRedisQueryPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookRedisQueryPort.java index 59f267b32..c149107e6 100644 --- a/src/main/java/konkuk/thip/book/application/port/out/BookRedisQueryPort.java +++ b/src/main/java/konkuk/thip/book/application/port/out/BookRedisQueryPort.java @@ -1,5 +1,7 @@ package konkuk.thip.book.application.port.out; +import konkuk.thip.book.application.port.in.dto.BookMostSearchResult; + import java.time.LocalDate; import java.util.List; import java.util.Map; @@ -7,4 +9,5 @@ public interface BookRedisQueryPort { List> getBookSearchRank(LocalDate date, int topN); List> getBookSearchCountTopN(LocalDate date, int topN); + List getYesterdayBookRankInfos(LocalDate date); } diff --git a/src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java b/src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java index 7ee98b2ff..8555506f2 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookMostSearchService.java @@ -1,79 +1,30 @@ package konkuk.thip.book.application.service; -import konkuk.thip.book.adapter.in.web.response.GetBookMostSearchResponse; -import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; import konkuk.thip.book.application.port.in.BookMostSearchUseCase; import konkuk.thip.book.application.port.in.dto.BookMostSearchResult; -import konkuk.thip.book.application.port.out.BookApiQueryPort; -import konkuk.thip.book.application.port.out.BookQueryPort; import konkuk.thip.book.application.port.out.BookRedisQueryPort; -import konkuk.thip.book.domain.Book; +import konkuk.thip.user.application.port.out.UserCommandPort; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class BookMostSearchService implements BookMostSearchUseCase { - private final BookQueryPort bookQueryPort; private final BookRedisQueryPort bookRedisQueryPort; - private final BookApiQueryPort bookApiQueryPort; + private final UserCommandPort userCommandPort; @Override public BookMostSearchResult getMostSearchedBooks(Long userId) { - // 오늘 날짜 기준 - LocalDate today = LocalDate.now(); + userCommandPort.findById(userId); - // Redis에서 오늘 날짜 기준 랭킹 Top 5 조회 - List> top5 = bookRedisQueryPort.getBookSearchRank(today, 5); - - if (top5 == null || top5.isEmpty()) { - return BookMostSearchResult.of(Collections.emptyList()); //PM과 상의 후 결정 - } - - // isbn 리스트 추출 - List isbnList = top5.stream() - .map(Map.Entry::getKey) - .collect(Collectors.toList()); - - // DB에서 isbn으로 책 정보 조회 - List books = bookQueryPort.findByIsbnIn(isbnList); - // isbn -> Book 매핑 - Map bookMap = books.stream() - .collect(Collectors.toMap(Book::getIsbn, b -> b)); - - List bookRankInfos = new ArrayList<>(); - int rank = 1; - for (Map.Entry entry : top5) { - String isbn = entry.getKey(); - Book book = bookMap.get(isbn); - if (book == null) { - // DB에 없으면 Naver API에서 상세 정보 조회 - NaverDetailBookParseResult naverResult = bookApiQueryPort.findDetailBookByIsbn(isbn); - bookRankInfos.add(GetBookMostSearchResponse.BookRankInfo.builder() - .rank(rank++) - .title(naverResult.title()) - .imageUrl(naverResult.imageUrl()) - .isbn(isbn) - .build()); - } else { - bookRankInfos.add(GetBookMostSearchResponse.BookRankInfo.builder() - .rank(rank++) - .title(book.getTitle()) - .imageUrl(book.getImageUrl()) - .isbn(isbn) - .build()); - } - } + LocalDate yesterday = LocalDate.now().minusDays(1); + List bookRankInfos = bookRedisQueryPort.getYesterdayBookRankInfos(yesterday); return BookMostSearchResult.of(bookRankInfos); } From de5fd7d676aa40827af97c9a6d8f8f09cc7cc066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 8 Jul 2025 05:00:43 +0900 Subject: [PATCH 28/30] =?UTF-8?q?[refactor]=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/common/exception/code/ErrorCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 83489ffac..42da90c96 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -23,7 +23,7 @@ public enum ErrorCode implements ResponseCode { AUTH_LOGIN_FAILED(HttpStatus.UNAUTHORIZED, 40104, "로그인에 실패했습니다."), AUTH_UNSUPPORTED_SOCIAL_LOGIN(HttpStatus.UNAUTHORIZED, 40105, "지원하지 않는 소셜 로그인입니다."), - JSON_PROCESSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 50000, "JSON 직렬화/역직렬화에 실패했습니다."), + JSON_PROCESSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 50100, "JSON 직렬화/역직렬화에 실패했습니다."), /* 60000부터 비즈니스 예외 */ /** From 5cd9e04111e57842aa8c7d7f602f5a317969672b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 8 Jul 2025 11:30:38 +0900 Subject: [PATCH 29/30] =?UTF-8?q?[refactor]=20=EB=A0=88=EB=94=94=EC=8A=A4?= =?UTF-8?q?=20=EC=BB=A4=EB=A7=A8=EB=93=9C/=EC=BF=BC=EB=A6=AC=20=EC=96=B4?= =?UTF-8?q?=EB=8C=91=ED=84=B0=20=ED=95=98=EB=82=98=EB=A1=9C=20=ED=95=A9?= =?UTF-8?q?=EC=B9=A8=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/BookQueryRedisAdapter.java | 83 ------------------- ...edisAdapter.java => BookRedisAdapter.java} | 52 +++++++++++- 2 files changed, 50 insertions(+), 85 deletions(-) delete mode 100644 src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java rename src/main/java/konkuk/thip/book/adapter/out/persistence/{BookCommandRedisAdapter.java => BookRedisAdapter.java} (60%) diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java deleted file mode 100644 index 954dd333f..000000000 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryRedisAdapter.java +++ /dev/null @@ -1,83 +0,0 @@ -package konkuk.thip.book.adapter.out.persistence; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import konkuk.thip.book.application.port.in.dto.BookMostSearchResult; -import konkuk.thip.book.application.port.out.BookRedisQueryPort; -import konkuk.thip.common.exception.ExternalApiException; -import konkuk.thip.common.exception.code.ErrorCode; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ZSetOperations; -import org.springframework.stereotype.Component; - -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -@Component -@RequiredArgsConstructor -public class BookQueryRedisAdapter implements BookRedisQueryPort { - - private final RedisTemplate redisTemplate; - private final DateTimeFormatter DAILY_KEY_FORMATTER = - DateTimeFormatter.ofPattern("yyyyMMdd"); - - @Value("${app.redis.search-count-prefix}") - private String searchCountPrefix; - - @Value("${app.redis.search-rank-prefix}") - private String searchRankPrefix; - - @Value("${app.redis.search-rank-detail-prefix}") - private String searchRankDetailPrefix; - - private final ObjectMapper objectMapper; - - @Override - public List> getBookSearchRank(LocalDate date, int topN) { - return getTopNFromZSet(searchRankPrefix, date, topN); - } - - @Override - public List> getBookSearchCountTopN(LocalDate date, int topN) { - return getTopNFromZSet(searchCountPrefix, date, topN); - } - - private List> getTopNFromZSet(String prefix, LocalDate date, int topN) { - String dateStr = date.format(DAILY_KEY_FORMATTER); - String redisKey = prefix + dateStr; - Set> topNSet = redisTemplate.opsForZSet() - .reverseRangeWithScores(redisKey, 0, topN - 1); - - if (topNSet == null) return Collections.emptyList(); - return topNSet.stream() - .map(tuple -> Map.entry(tuple.getValue(), tuple.getScore())) - .collect(Collectors.toList()); - } - - @Override - public List getYesterdayBookRankInfos(LocalDate date) { - String redisKey = searchRankDetailPrefix + date.format(DAILY_KEY_FORMATTER); - String json = redisTemplate.opsForValue().get(redisKey); - - if (json == null || json.isBlank()) { - return List.of(); - } - try { - return objectMapper.readValue( - json, - new TypeReference>() {} - ); - } catch (JsonProcessingException e) { - throw new ExternalApiException(ErrorCode.JSON_PROCESSING_ERROR); - } - } - -} diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java similarity index 60% rename from src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java rename to src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java index ebfcc1235..66f762426 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandRedisAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java @@ -1,25 +1,33 @@ package konkuk.thip.book.adapter.out.persistence; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import konkuk.thip.book.application.port.in.dto.BookMostSearchResult; import konkuk.thip.book.application.port.out.BookRedisCommandPort; +import konkuk.thip.book.application.port.out.BookRedisQueryPort; import konkuk.thip.common.exception.ExternalApiException; +import konkuk.thip.common.exception.code.ErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; import org.springframework.stereotype.Component; import java.time.Duration; import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import static konkuk.thip.common.exception.code.ErrorCode.JSON_PROCESSING_ERROR; @Component @RequiredArgsConstructor -public class BookCommandRedisAdapter implements BookRedisCommandPort { +public class BookRedisAdapter implements BookRedisQueryPort, BookRedisCommandPort { private final RedisTemplate redisTemplate; private final DateTimeFormatter DAILY_KEY_FORMATTER = @@ -36,6 +44,46 @@ public class BookCommandRedisAdapter implements BookRedisCommandPort { private final ObjectMapper objectMapper; + @Override + public List> getBookSearchRank(LocalDate date, int topN) { + return getTopNFromZSet(searchRankPrefix, date, topN); + } + + @Override + public List> getBookSearchCountTopN(LocalDate date, int topN) { + return getTopNFromZSet(searchCountPrefix, date, topN); + } + + private List> getTopNFromZSet(String prefix, LocalDate date, int topN) { + String dateStr = date.format(DAILY_KEY_FORMATTER); + String redisKey = prefix + dateStr; + Set> topNSet = redisTemplate.opsForZSet() + .reverseRangeWithScores(redisKey, 0, topN - 1); + + if (topNSet == null) return Collections.emptyList(); + return topNSet.stream() + .map(tuple -> Map.entry(tuple.getValue(), tuple.getScore())) + .collect(Collectors.toList()); + } + + @Override + public List getYesterdayBookRankInfos(LocalDate date) { + String redisKey = searchRankDetailPrefix + date.format(DAILY_KEY_FORMATTER); + String json = redisTemplate.opsForValue().get(redisKey); + + if (json == null || json.isBlank()) { + return List.of(); + } + try { + return objectMapper.readValue( + json, + new TypeReference>() {} + ); + } catch (JsonProcessingException e) { + throw new ExternalApiException(ErrorCode.JSON_PROCESSING_ERROR); + } + } + @Override public void incrementBookSearchCount(String isbn, LocalDate date) { String redisKey = makeRedisKey(searchCountPrefix, date); @@ -54,7 +102,7 @@ public void saveBookSearchRank(List isbns, List scores, LocalDat @Override public void saveBookSearchRankDetail(List bookRankDetails, LocalDate date) { String redisKey = makeRedisKey(searchRankDetailPrefix, date); - String detailJson = null; + String detailJson; try { detailJson = objectMapper.writeValueAsString(bookRankDetails); } catch (JsonProcessingException e) { From 5e535579794698e3e2eeeac401258f3fc66d2a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 8 Jul 2025 11:30:49 +0900 Subject: [PATCH 30/30] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../book/adapter/in/web/BookMostSearchedBooksControllerTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java index 796692ea9..d90a218ad 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookMostSearchedBooksControllerTest.java @@ -53,7 +53,6 @@ class BookMostSearchedBooksControllerTest { private LocalDate yesterday; - private String rankKey; private final DateTimeFormatter DAILY_KEY_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");