diff --git a/src/main/java/konkuk/thip/book/adapter/in/web/response/BookDetailSearchResponse.java b/src/main/java/konkuk/thip/book/adapter/in/web/response/BookDetailSearchResponse.java index 522fb354d..9dbac1b31 100644 --- a/src/main/java/konkuk/thip/book/adapter/in/web/response/BookDetailSearchResponse.java +++ b/src/main/java/konkuk/thip/book/adapter/in/web/response/BookDetailSearchResponse.java @@ -18,12 +18,12 @@ public record BookDetailSearchResponse( public static BookDetailSearchResponse of(BookDetailSearchResult result) { return new BookDetailSearchResponse( - result.naverDetailBook().title(), - result.naverDetailBook().imageUrl(), - result.naverDetailBook().author(), - result.naverDetailBook().publisher(), - result.naverDetailBook().isbn(), - result.naverDetailBook().description(), + result.bookDetail().title(), + result.bookDetail().imageUrl(), + result.bookDetail().author(), + result.bookDetail().publisher(), + result.bookDetail().isbn(), + result.bookDetail().description(), result.recruitingRoomCount(), result.readCount(), result.isSaved()); diff --git a/src/main/java/konkuk/thip/book/adapter/in/web/response/BookSearchListResponse.java b/src/main/java/konkuk/thip/book/adapter/in/web/response/BookSearchListResponse.java index be5e8431a..4c3b8bc61 100644 --- a/src/main/java/konkuk/thip/book/adapter/in/web/response/BookSearchListResponse.java +++ b/src/main/java/konkuk/thip/book/adapter/in/web/response/BookSearchListResponse.java @@ -1,10 +1,10 @@ package konkuk.thip.book.adapter.in.web.response; -import konkuk.thip.book.adapter.out.api.dto.NaverBookParseResult; +import konkuk.thip.book.adapter.out.api.dto.BookSearchResult; import java.util.List; -import static konkuk.thip.book.adapter.out.api.naver.NaverApiUtil.PAGE_SIZE; +import static konkuk.thip.book.adapter.out.api.dto.BookSearchResult.PAGE_SIZE; public record BookSearchListResponse( List searchResult, // 책 목록 @@ -15,13 +15,13 @@ public record BookSearchListResponse( boolean last, // 마지막 페이지 여부 boolean first // 첫 페이지 여부 ) { - public static BookSearchListResponse of(NaverBookParseResult result, int page) { + public static BookSearchListResponse of(BookSearchResult result, int page) { int totalElements = result.total(); int totalPages = (int) Math.ceil((double) totalElements / PAGE_SIZE); boolean last = (page >= totalPages); boolean first = (page == 1); - List bookSearchDtos = result.naverBooks().stream() + List bookSearchDtos = result.books().stream() .map(BookSearchDto::of) .toList(); @@ -42,13 +42,13 @@ public record BookSearchDto( String publisher, String isbn ) { - public static BookSearchDto of(NaverBookParseResult.NaverBook naverBook) { + public static BookSearchDto of(BookSearchResult.BookSummary book) { return new BookSearchDto( - naverBook.title(), - naverBook.imageUrl(), - naverBook.author(), - naverBook.publisher(), - naverBook.isbn() + book.title(), + book.imageUrl(), + book.author(), + book.publisher(), + book.isbn() ); } } diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java index 64f4b1671..789bba183 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/CompositeBookApiAdapter.java @@ -1,9 +1,9 @@ package konkuk.thip.book.adapter.out.api; import konkuk.thip.book.adapter.out.api.aladin.AladinApiClient; -import konkuk.thip.book.adapter.out.api.dto.NaverBookParseResult; -import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; -import konkuk.thip.book.adapter.out.api.naver.NaverApiClient; +import konkuk.thip.book.adapter.out.api.aladin.AladinApiUtil; +import konkuk.thip.book.adapter.out.api.dto.BookSearchResult; +import konkuk.thip.book.adapter.out.api.dto.BookDetailResult; import konkuk.thip.book.application.port.out.BookApiQueryPort; import konkuk.thip.book.domain.Book; import lombok.RequiredArgsConstructor; @@ -13,17 +13,16 @@ @RequiredArgsConstructor public class CompositeBookApiAdapter implements BookApiQueryPort { - private final NaverApiClient naverApiClient; private final AladinApiClient aladinApiClient; @Override - public NaverBookParseResult findBooksByKeyword(String keyword, int start) { - return naverApiClient.findBooksByKeyword(keyword, start); + public BookSearchResult findBooksByKeyword(String keyword, int start) { + return aladinApiClient.findBooksByKeyword(keyword, start); } @Override - public NaverDetailBookParseResult findDetailBookByIsbn(String isbn) { - return naverApiClient.findDetailBookByIsbn(isbn); + public BookDetailResult findDetailBookByIsbn(String isbn) { + return aladinApiClient.findDetailBookByIsbn(isbn); } @Override @@ -33,22 +32,19 @@ public Integer findPageCountByIsbn(String isbn) { @Override public Book loadBookWithPageByIsbn(String isbn) { - // 1. naver 상세정보 조회 api 로 책 상세정보(without page) load - NaverDetailBookParseResult detailBookByKeyword = findDetailBookByIsbn(isbn); + // 상세정보 + page 정보를 알라딘 ItemLookUp 한 번의 호출로 함께 조회 + AladinApiUtil.AladinDetailResult result = aladinApiClient.findDetailBookWithPageCountByIsbn(isbn); + BookDetailResult detail = result.detail(); - // 2. 알라딘으로부터 책 page 정보 load - Integer pageCount = findPageCountByIsbn(isbn); - - // 3. pageCount 정보를 포함한 Book 반환 return Book.withoutId( - detailBookByKeyword.title(), + detail.title(), isbn, - detailBookByKeyword.author(), + detail.author(), false, // TODO : 추후 BestSeller 도입되면 고려해야함 - detailBookByKeyword.publisher(), - detailBookByKeyword.imageUrl(), - pageCount, - detailBookByKeyword.description() + detail.publisher(), + detail.imageUrl(), + result.pageCount(), + detail.description() ); } } diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiClient.java b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiClient.java index 3b6b12abd..96b67b6a4 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiClient.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiClient.java @@ -1,5 +1,7 @@ package konkuk.thip.book.adapter.out.api.aladin; +import konkuk.thip.book.adapter.out.api.dto.BookSearchResult; +import konkuk.thip.book.adapter.out.api.dto.BookDetailResult; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -9,7 +11,19 @@ public class AladinApiClient { private final AladinApiUtil aladinApiUtil; + public BookSearchResult findBooksByKeyword(String keyword, int start) { + return aladinApiUtil.searchBooks(keyword, start); + } + + public BookDetailResult findDetailBookByIsbn(String isbn) { + return aladinApiUtil.getBookDetail(isbn); + } + public Integer findPageCountByIsbn(String isbn) { return aladinApiUtil.getPageCount(isbn); } + + public AladinApiUtil.AladinDetailResult findDetailBookWithPageCountByIsbn(String isbn) { + return aladinApiUtil.getBookDetailWithPageCount(isbn); + } } diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiParam.java b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiParam.java index a6c51c27a..559f6755f 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiParam.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiParam.java @@ -6,7 +6,9 @@ public enum AladinApiParam { OUTPUT("js"), API_VERSION("20131101"), SUB_INFO_PARSING_KEY("subInfo"), - PAGE_COUNT_PARSING_KEY("itemPage"); + PAGE_COUNT_PARSING_KEY("itemPage"), + QUERY_TYPE("Keyword"), + SEARCH_TARGET("Book"); private final String value; diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java index 45d936d64..721a2e3a1 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import konkuk.thip.book.adapter.out.api.dto.BookSearchResult; +import konkuk.thip.book.adapter.out.api.dto.BookDetailResult; import konkuk.thip.common.exception.ExternalApiException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -10,16 +12,21 @@ import org.springframework.web.client.RestTemplate; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import static konkuk.thip.book.adapter.out.api.aladin.AladinApiParam.*; -import static konkuk.thip.common.exception.code.ErrorCode.BOOK_ALADIN_API_ISBN_NOT_FOUND; -import static konkuk.thip.common.exception.code.ErrorCode.BOOK_ALADIN_API_PARSING_ERROR; +import static konkuk.thip.book.adapter.out.api.dto.BookSearchResult.PAGE_SIZE; +import static konkuk.thip.common.exception.code.ErrorCode.*; @Component @RequiredArgsConstructor @Slf4j public class AladinApiUtil { + // 알라딘 정책상 한 검색어당 실제로 조회 가능한 결과는 최대 200건까지로 제한된다. + private static final int MAX_TOTAL_RESULTS = 200; + private final RestTemplate restTemplate; private final ObjectMapper objectMapper; @@ -29,6 +36,10 @@ public class AladinApiUtil { @Value("${aladin.baseUrl}") private String baseUrl; + @Value("${aladin.searchUrl}") + private String searchUrl; + + public record AladinDetailResult(BookDetailResult detail, Integer pageCount) {} private String buildLookupUrl(String isbn) { return String.format( @@ -41,7 +52,34 @@ private String buildLookupUrl(String isbn) { ); } + // Query 값은 RestTemplate가 URI 템플릿 변수로 한 번만 인코딩하도록 {query} 플레이스홀더로 남겨둔다. + // 여기서 미리 URLEncoder로 인코딩해버리면 RestTemplate이 다시 인코딩(이중 인코딩)하여 키워드가 깨진다. + private String buildSearchUrlTemplate(int page) { + return String.format( + searchUrl + "ttbkey=%s&Query={query}&QueryType=%s&SearchTarget=%s&MaxResults=%s&start=%d&output=%s&Version=%s", + ttbKey, + QUERY_TYPE.getValue(), + SEARCH_TARGET.getValue(), + PAGE_SIZE, + page, + OUTPUT.getValue(), + API_VERSION.getValue() + ); + } + public Integer getPageCount(String isbn) { + return fetchDetail(isbn).pageCount(); + } + + public BookDetailResult getBookDetail(String isbn) { + return fetchDetail(isbn).detail(); + } + + public AladinDetailResult getBookDetailWithPageCount(String isbn) { + return fetchDetail(isbn); + } + + private AladinDetailResult fetchDetail(String isbn) { String url = buildLookupUrl(isbn); String response = restTemplate.getForObject(url, String.class); @@ -50,19 +88,63 @@ public Integer getPageCount(String isbn) { JsonNode items = jsonNode.path("item"); // json 응답 결과에 item 키값이 없는 경우 + // TODO : 알라딘으로부터 page 정보가 없으면 ?? + // 보상 시나리오 : 유저에게 "page 정보를 찾을 수 없는 책입니다. 직접 page 정보를 입력하세요" 라고 안내 + // 일단 지금은 exception throw 만 진행 if (!items.isArray() || items.isEmpty()) { - // TODO : 알라딘으로부터 page 정보가 없으면 ?? - // 보상 시나리오 : 유저에게 "page 정보를 찾을 수 없는 책입니다. 직접 page 정보를 입력하세요" 라고 안내 - // 일단 지금은 exception throw 만 진행 throw new ExternalApiException(BOOK_ALADIN_API_ISBN_NOT_FOUND); } - JsonNode subInfo = items.get(0).path(SUB_INFO_PARSING_KEY.getValue()); + JsonNode item = items.get(0); + JsonNode subInfo = item.path(SUB_INFO_PARSING_KEY.getValue()); + int pageCount = subInfo.path(PAGE_COUNT_PARSING_KEY.getValue()).asInt(); - return subInfo.path(PAGE_COUNT_PARSING_KEY.getValue()).asInt(); + BookDetailResult detail = BookDetailResult.builder() + .title(item.path("title").asText()) + .imageUrl(item.path("cover").asText()) + .author(item.path("author").asText()) + .publisher(item.path("publisher").asText()) + .isbn(item.path("isbn13").asText()) + .description(item.path("description").asText()) + .build(); + + return new AladinDetailResult(detail, pageCount); } catch (IOException e) { throw new ExternalApiException(BOOK_ALADIN_API_PARSING_ERROR); } } -} + public BookSearchResult searchBooks(String keyword, int start) { + // BookSearchService가 넘기는 start는 (page-1)*PAGE_SIZE+1 형태의 아이템 오프셋이므로 + // 알라딘이 요구하는 "페이지 번호"로 역산한다. + int page = ((start - 1) / PAGE_SIZE) + 1; + String urlTemplate = buildSearchUrlTemplate(page); + String response = restTemplate.getForObject(urlTemplate, String.class, keyword); + + try { + JsonNode jsonNode = objectMapper.readTree(response); + // totalResults는 전체 매칭 건수를 그대로 보고하지만 실제로 조회 가능한 건 MAX_TOTAL_RESULTS까지뿐이므로, + // 페이지네이션(totalPages/last)이 정확히 그 지점에서 끝나도록 캡핑한다. + int total = Math.min(jsonNode.path("totalResults").asInt(), MAX_TOTAL_RESULTS); + int startIndex = jsonNode.path("startIndex").asInt(); + + List books = new ArrayList<>(); + JsonNode items = jsonNode.path("item"); + if (items.isArray()) { + for (JsonNode item : items) { + books.add(BookSearchResult.BookSummary.builder() + .title(item.path("title").asText()) + .imageUrl(item.path("cover").asText()) + .author(item.path("author").asText()) + .publisher(item.path("publisher").asText()) + .isbn(item.path("isbn13").asText()) + .build()); + } + } + + return BookSearchResult.of(books, total, startIndex); + } catch (IOException e) { + throw new ExternalApiException(BOOK_ALADIN_API_PARSING_ERROR); + } + } +} diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverDetailBookParseResult.java b/src/main/java/konkuk/thip/book/adapter/out/api/dto/BookDetailResult.java similarity index 86% rename from src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverDetailBookParseResult.java rename to src/main/java/konkuk/thip/book/adapter/out/api/dto/BookDetailResult.java index 511455a40..2d7addd67 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverDetailBookParseResult.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/dto/BookDetailResult.java @@ -4,7 +4,7 @@ import lombok.Builder; @Builder -public record NaverDetailBookParseResult( +public record BookDetailResult( String title, String imageUrl, String author, diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverBookParseResult.java b/src/main/java/konkuk/thip/book/adapter/out/api/dto/BookSearchResult.java similarity index 51% rename from src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverBookParseResult.java rename to src/main/java/konkuk/thip/book/adapter/out/api/dto/BookSearchResult.java index 1ee7ec7af..f52dc6e5f 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverBookParseResult.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/dto/BookSearchResult.java @@ -5,19 +5,22 @@ import java.util.List; -public record NaverBookParseResult( - List naverBooks, +public record BookSearchResult( + List books, int total, int start) { + + public static final int PAGE_SIZE = 10; + @Builder - public record NaverBook( + public record BookSummary( String title, String imageUrl, String author, String publisher, String isbn ) {} - public static NaverBookParseResult of(List books, int total, int start) { - return new NaverBookParseResult(books, total, start); + public static BookSearchResult of(List books, int total, int start) { + return new BookSearchResult(books, total, start); } } diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiClient.java b/src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiClient.java index 54d29ba8e..be1bd956a 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiClient.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiClient.java @@ -1,7 +1,7 @@ package konkuk.thip.book.adapter.out.api.naver; -import konkuk.thip.book.adapter.out.api.dto.NaverBookParseResult; -import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; +import konkuk.thip.book.adapter.out.api.dto.BookSearchResult; +import konkuk.thip.book.adapter.out.api.dto.BookDetailResult; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -11,12 +11,12 @@ public class NaverApiClient { private final NaverApiUtil naverApiUtil; - public NaverBookParseResult findBooksByKeyword(String keyword, int start) { + public BookSearchResult findBooksByKeyword(String keyword, int start) { String xml = naverApiUtil.searchBook(keyword, start); // 네이버 API 호출 return NaverBookXmlParser.parseBookList(xml); // XML 파싱 + 페이징 정보 포함 } - public NaverDetailBookParseResult findDetailBookByIsbn(String isbn) { + public BookDetailResult findDetailBookByIsbn(String isbn) { String xml = naverApiUtil.detailSearchBook(isbn); // 네이버 API 호출 return NaverBookXmlParser.parseBookDetail(xml); // XML 파싱 } diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverBookXmlParser.java b/src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverBookXmlParser.java index e01a1e1c3..88b5a1e30 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverBookXmlParser.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverBookXmlParser.java @@ -1,7 +1,7 @@ package konkuk.thip.book.adapter.out.api.naver; -import konkuk.thip.book.adapter.out.api.dto.NaverBookParseResult; -import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; +import konkuk.thip.book.adapter.out.api.dto.BookSearchResult; +import konkuk.thip.book.adapter.out.api.dto.BookDetailResult; import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.exception.ExternalApiException; import org.apache.commons.lang3.StringEscapeUtils; @@ -19,8 +19,8 @@ public class NaverBookXmlParser { private static final int MAX_DESCRIPTION_LENGTH = 3000; private static final String DESCRIPTION_SUFFIX = "..."; - public static NaverBookParseResult parseBookList(String xml) { - List naverBooks = new ArrayList<>(); + public static BookSearchResult parseBookList(String xml) { + List books = new ArrayList<>(); int total = -1; int start = -1; try { @@ -35,65 +35,64 @@ public static NaverBookParseResult parseBookList(String xml) { String author = getTagValue(item, "author"); String publisher = getTagValue(item, "publisher"); String isbn = getTagValue(item, "isbn"); - NaverBookParseResult.NaverBook naverBook = NaverBookParseResult.NaverBook.builder() + BookSearchResult.BookSummary naverBook = BookSearchResult.BookSummary.builder() .title(title) .imageUrl(imageUrl) .author(author) .publisher(publisher) .isbn(isbn) .build(); - naverBooks.add(naverBook); + books.add(naverBook); } } } catch (Exception e) { throw new ExternalApiException(BOOK_NAVER_API_PARSING_ERROR); } - return NaverBookParseResult.of(naverBooks, total, start); + return BookSearchResult.of(books, total, start); } - public static NaverDetailBookParseResult parseBookDetail(String xml) { + public static BookDetailResult parseBookDetail(String xml) { try { Element channel = getFirstChannel(xml); - if (channel != null) { - int total = 0; - String totalStr = getTagValue(channel, "total"); - if (totalStr != null) total = Integer.parseInt(totalStr); + if (channel == null) throw new ExternalApiException(BOOK_NAVER_API_PARSING_ERROR); - // total이 0이면 isbn에 해당하는 책이 없음(잘못 넘어온 isbn 예외처리) - if (total == 0) throw new BusinessException(BOOK_NAVER_API_ISBN_NOT_FOUND); + int total = 0; + String totalStr = getTagValue(channel, "total"); + if (totalStr != null) total = Integer.parseInt(totalStr); - List items = getItemElements(channel); - if (!items.isEmpty()) { - Element item = items.get(0); - String title = StringEscapeUtils.unescapeHtml4(getTagValue(item, "title")); - String imageUrl = getTagValue(item, "image"); - String author = getTagValue(item, "author"); - String publisher = getTagValue(item, "publisher"); - String isbn = getTagValue(item, "isbn"); - String rawDescription = StringEscapeUtils.unescapeHtml4(getTagValue(item, "description")); - String description; - if (rawDescription.length() > MAX_DESCRIPTION_LENGTH) { - description = rawDescription.substring(0, MAX_DESCRIPTION_LENGTH - DESCRIPTION_SUFFIX.length()) + DESCRIPTION_SUFFIX; - } else { - description = rawDescription; - } - - return NaverDetailBookParseResult.builder() - .title(title) - .imageUrl(imageUrl) - .author(author) - .publisher(publisher) - .isbn(isbn) - .description(description) - .build(); - } + // total이 0이면 isbn에 해당하는 책이 없음(잘못 넘어온 isbn 예외처리) + if (total == 0) throw new BusinessException(BOOK_NAVER_API_ISBN_NOT_FOUND); + + List items = getItemElements(channel); + if (items.isEmpty()) throw new ExternalApiException(BOOK_NAVER_API_PARSING_ERROR); + + Element item = items.get(0); + String title = StringEscapeUtils.unescapeHtml4(getTagValue(item, "title")); + String imageUrl = getTagValue(item, "image"); + String author = getTagValue(item, "author"); + String publisher = getTagValue(item, "publisher"); + String isbn = getTagValue(item, "isbn"); + String rawDescription = StringEscapeUtils.unescapeHtml4(getTagValue(item, "description")); + String description; + if (rawDescription.length() > MAX_DESCRIPTION_LENGTH) { + description = rawDescription.substring(0, MAX_DESCRIPTION_LENGTH - DESCRIPTION_SUFFIX.length()) + DESCRIPTION_SUFFIX; + } else { + description = rawDescription; } - } catch (BusinessException e) { + + return BookDetailResult.builder() + .title(title) + .imageUrl(imageUrl) + .author(author) + .publisher(publisher) + .isbn(isbn) + .description(description) + .build(); + } catch (BusinessException | ExternalApiException e) { throw e; } catch (Exception e) { throw new ExternalApiException(BOOK_NAVER_API_PARSING_ERROR); } - return null; } private static Document parseXml(String xml) throws Exception { diff --git a/src/main/java/konkuk/thip/book/application/port/in/BookSearchUseCase.java b/src/main/java/konkuk/thip/book/application/port/in/BookSearchUseCase.java index 4f7cdfed9..563c5725a 100644 --- a/src/main/java/konkuk/thip/book/application/port/in/BookSearchUseCase.java +++ b/src/main/java/konkuk/thip/book/application/port/in/BookSearchUseCase.java @@ -1,10 +1,10 @@ package konkuk.thip.book.application.port.in; -import konkuk.thip.book.adapter.out.api.dto.NaverBookParseResult; +import konkuk.thip.book.adapter.out.api.dto.BookSearchResult; import konkuk.thip.book.application.port.in.dto.BookDetailSearchResult; public interface BookSearchUseCase { - NaverBookParseResult searchBooks(String keyword, int page, Long userId, boolean isFinalized); + BookSearchResult searchBooks(String keyword, int page, Long userId, boolean isFinalized); BookDetailSearchResult searchDetailBooks(String isbn, Long userId); } diff --git a/src/main/java/konkuk/thip/book/application/port/in/dto/BookDetailSearchResult.java b/src/main/java/konkuk/thip/book/application/port/in/dto/BookDetailSearchResult.java index 17ffe4a8e..136f15091 100644 --- a/src/main/java/konkuk/thip/book/application/port/in/dto/BookDetailSearchResult.java +++ b/src/main/java/konkuk/thip/book/application/port/in/dto/BookDetailSearchResult.java @@ -1,22 +1,22 @@ package konkuk.thip.book.application.port.in.dto; -import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; +import konkuk.thip.book.adapter.out.api.dto.BookDetailResult; public record BookDetailSearchResult( - NaverDetailBookParseResult naverDetailBook, + BookDetailResult bookDetail, int recruitingRoomCount, int readCount, boolean isSaved ) { - public static BookDetailSearchResult of(NaverDetailBookParseResult naverDetailBook , + public static BookDetailSearchResult of(BookDetailResult bookDetail, int recruitingRoomCount, int readCount, boolean isSaved) { return new BookDetailSearchResult( - naverDetailBook, + bookDetail, recruitingRoomCount, readCount, isSaved); 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 803f57b82..64ac1b654 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 @@ -1,14 +1,14 @@ package konkuk.thip.book.application.port.out; -import konkuk.thip.book.adapter.out.api.dto.NaverBookParseResult; -import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; +import konkuk.thip.book.adapter.out.api.dto.BookSearchResult; +import konkuk.thip.book.adapter.out.api.dto.BookDetailResult; import konkuk.thip.book.domain.Book; public interface BookApiQueryPort { - NaverBookParseResult findBooksByKeyword(String keyword, int start); + BookSearchResult findBooksByKeyword(String keyword, int start); - NaverDetailBookParseResult findDetailBookByIsbn(String isbn); + BookDetailResult findDetailBookByIsbn(String isbn); Integer findPageCountByIsbn(String isbn); 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 50d90c1a5..25818d777 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookMostSearchRankService.java @@ -1,6 +1,6 @@ package konkuk.thip.book.application.service; -import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; +import konkuk.thip.book.adapter.out.api.dto.BookDetailResult; 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; @@ -64,13 +64,13 @@ public void updateDailySearchRank() { .build() ), () -> { - // DB에 없으면 Naver API에서 상세 정보 조회 - NaverDetailBookParseResult naverResult = bookApiQueryPort.findDetailBookByIsbn(isbn); + // DB에 없으면 외부 API에서 상세 정보 조회 + BookDetailResult bookDetail = bookApiQueryPort.findDetailBookByIsbn(isbn); bookRankDetails.add( BookMostSearchResult.BookRankInfo.builder() .rank(rank.getAndIncrement()) - .title(naverResult.title()) - .imageUrl(naverResult.imageUrl()) + .title(bookDetail.title()) + .imageUrl(bookDetail.imageUrl()) .isbn(isbn) .build() ); 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 3cf3a9196..6f393b38b 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java @@ -1,6 +1,5 @@ package konkuk.thip.book.application.service; -import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; import konkuk.thip.book.application.port.in.BookSavedUseCase; import konkuk.thip.book.application.port.in.dto.BookIsSavedResult; import konkuk.thip.book.application.port.out.BookApiQueryPort; @@ -48,17 +47,8 @@ public BookIsSavedResult changeSavedBook(String isbn, boolean isSaveRequest, Lon } private Book registerBookByIsbn(String isbn) { - // 저장 요청이면 네이버 API로 책 정보 조회 후 저장 - NaverDetailBookParseResult naverResult = bookApiQueryPort.findDetailBookByIsbn(isbn); - Book newBook = Book.withoutId( - naverResult.title(), - naverResult.isbn(), - naverResult.author(), - false, - naverResult.publisher(), - naverResult.imageUrl(), - null, - naverResult.description()); + // 저장 요청이면 외부 API로 책 정보 + 페이지 수를 함께 조회 후 저장 + Book newBook = bookApiQueryPort.loadBookWithPageByIsbn(isbn); Long savedBookId = bookCommandPort.save(newBook); return bookCommandPort.findById(savedBookId); 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 617a27d4d..65702d1cf 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSearchService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSearchService.java @@ -1,7 +1,7 @@ package konkuk.thip.book.application.service; -import konkuk.thip.book.adapter.out.api.dto.NaverBookParseResult; -import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; +import konkuk.thip.book.adapter.out.api.dto.BookSearchResult; +import konkuk.thip.book.adapter.out.api.dto.BookDetailResult; import konkuk.thip.book.application.port.in.BookSearchUseCase; import konkuk.thip.book.application.port.in.dto.BookDetailSearchResult; import konkuk.thip.book.application.port.out.BookApiQueryPort; @@ -25,7 +25,7 @@ import java.util.HashSet; import java.util.Set; -import static konkuk.thip.book.adapter.out.api.naver.NaverApiUtil.PAGE_SIZE; +import static konkuk.thip.book.adapter.out.api.dto.BookSearchResult.PAGE_SIZE; import static konkuk.thip.common.exception.code.ErrorCode.*; import static konkuk.thip.recentSearch.domain.value.RecentSearchType.BOOK_SEARCH; @@ -47,7 +47,7 @@ public class BookSearchService implements BookSearchUseCase { @Override @Transactional - public NaverBookParseResult searchBooks(String keyword, int page, Long userId, boolean isFinalized) { + public BookSearchResult searchBooks(String keyword, int page, Long userId, boolean isFinalized) { if (keyword == null || keyword.isBlank()) { throw new BusinessException(BOOK_KEYWORD_REQUIRED); @@ -59,7 +59,7 @@ public NaverBookParseResult searchBooks(String keyword, int page, Long userId, b int start = (page - 1) * PAGE_SIZE + 1; //검색 시작 위치 - NaverBookParseResult result = bookApiQueryPort.findBooksByKeyword(keyword, start); + BookSearchResult result = bookApiQueryPort.findBooksByKeyword(keyword, start); int totalElements = result.total(); int totalPages = (totalElements + PAGE_SIZE - 1) / PAGE_SIZE; @@ -81,7 +81,7 @@ public BookDetailSearchResult searchDetailBooks(String isbn,Long userId) { User user = userCommandPort.findById(userId); //책 상세정보 - NaverDetailBookParseResult naverDetailBookParseResult = bookApiQueryPort.findDetailBookByIsbn(isbn); + BookDetailResult bookDetailResult = bookApiQueryPort.findDetailBookByIsbn(isbn); //책 검색순위 정보 업데이트 bookRedisCommandPort.incrementBookSearchCount(isbn,LocalDate.now()); @@ -96,7 +96,7 @@ public BookDetailSearchResult searchDetailBooks(String isbn,Long userId) { boolean isSaved = bookQueryPort.existsSavedBookByUserIdAndBookId(user.getId(), book.getId()); return BookDetailSearchResult.of( - naverDetailBookParseResult, + bookDetailResult, recruitingRoomCount, readCount, isSaved @@ -104,7 +104,7 @@ public BookDetailSearchResult searchDetailBooks(String isbn,Long userId) { }) .orElseGet(() -> BookDetailSearchResult.of( - naverDetailBookParseResult, + bookDetailResult, 0, // 모집 중인 방 개수 0, // 읽기 참여자 수 false // 저장 여부 diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index fa5b606cd..de9dde63c 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -223,7 +223,8 @@ public enum SwaggerResponseDescription { TAG_NAME_NOT_MATCH, TAG_SHOULD_BE_UNIQUE, TAG_LIST_SIZE_OVERFLOW, - BOOK_NAVER_API_PARSING_ERROR, + BOOK_ALADIN_API_PARSING_ERROR, + BOOK_ALADIN_API_ISBN_NOT_FOUND, URL_INVALID_DOMAIN, URL_USER_ID_MISMATCH ))), @@ -322,15 +323,12 @@ public enum SwaggerResponseDescription { BOOK_SEARCH_PAGE_OUT_OF_RANGE, BOOK_KEYWORD_REQUIRED, BOOK_PAGE_NUMBER_INVALID, - BOOK_NAVER_API_PARSING_ERROR, - BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED, - BOOK_NAVER_API_RESPONSE_ERROR + BOOK_ALADIN_API_PARSING_ERROR ))), BOOK_DETAIL_SEARCH(new LinkedHashSet<>(Set.of( BOOK_NOT_FOUND, - BOOK_NAVER_API_PARSING_ERROR, - BOOK_NAVER_API_ISBN_NOT_FOUND, - BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED + BOOK_ALADIN_API_PARSING_ERROR, + BOOK_ALADIN_API_ISBN_NOT_FOUND ))), POPULAR_BOOK_SEARCH(new LinkedHashSet<>(Set.of( USER_NOT_FOUND, diff --git a/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java b/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java index b439dfcb5..9e7e66b72 100644 --- a/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java +++ b/src/main/java/konkuk/thip/feed/application/service/FeedCreateService.java @@ -1,6 +1,5 @@ package konkuk.thip.feed.application.service; -import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; import konkuk.thip.book.application.port.out.BookApiQueryPort; import konkuk.thip.book.application.port.out.BookCommandPort; import konkuk.thip.book.domain.Book; @@ -76,7 +75,7 @@ private void sendNotifications(FeedCreateCommand command, Long savedFeedId) { } /** - * ISBN으로 책을 조회하고, 없으면 외부 API(Naver)에서 상세 정보를 조회해 새로 저장 후 ID 반환 + * ISBN으로 책을 조회하고, 없으면 외부 API에서 상세 정보(+페이지 수)를 조회해 새로 저장 후 ID 반환 */ private Long findOrCreateBookByIsbn(String isbn) { return bookCommandPort.findByIsbn(isbn) @@ -85,19 +84,10 @@ private Long findOrCreateBookByIsbn(String isbn) { } /** - * 외부 API(Naver)를 통해 상세 책 정보를 조회하고 Book 도메인으로 저장 + * 외부 API를 통해 상세 책 정보와 페이지 수를 함께 조회하여 Book 도메인으로 저장 */ private Long saveNewBookWithFromExternalApi(String isbn) { - NaverDetailBookParseResult detailBookByKeyword = bookApiQueryPort.findDetailBookByIsbn(isbn); - Book newBook = Book.withoutId( - detailBookByKeyword.title(), - detailBookByKeyword.isbn(), - detailBookByKeyword.author(), - false, // TODO : 추후 BestSeller 도입 시 로직 수정 - detailBookByKeyword.publisher(), - detailBookByKeyword.imageUrl(), - null, - detailBookByKeyword.description()); + Book newBook = bookApiQueryPort.loadBookWithPageByIsbn(isbn); return bookCommandPort.save(newBook); } } diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java index 608f77ece..adb10fde5 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchApiTest.java @@ -131,7 +131,7 @@ void searchDetailBooks_ReturnsCorrectResult() { assertThat(result.recruitingRoomCount()).isEqualTo(1); assertThat(result.readCount()).isEqualTo(1); assertThat(result.isSaved()).isTrue(); - assertThat(result.naverDetailBook()).isNotNull(); + assertThat(result.bookDetail()).isNotNull(); } @Test