From f3293b880865169652b9ebed863c8d8446b33ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A4=80=ED=98=84?= Date: Sun, 18 Jan 2026 02:57:01 +0900 Subject: [PATCH] feat: search refactor --- README.md | 1 - .../controller/BookSearchController.java | 23 ------- .../domain/library/books/entity/Books.java | 7 +- .../books/service/BookImportService.java | 12 +++- .../search/controller/SearchController.java | 66 ++++++++++++++++++ .../search/dto/BookSearchItemResponse.java | 19 +++++ .../domain/search/dto/BookSearchResponse.java | 15 ++++ .../search/service/BookSearchService.java | 69 +++++++++++++++++++ 8 files changed, 186 insertions(+), 26 deletions(-) delete mode 100644 README.md delete mode 100644 booklog/src/main/java/com/example/booklog/domain/library/books/controller/BookSearchController.java create mode 100644 booklog/src/main/java/com/example/booklog/domain/search/controller/SearchController.java create mode 100644 booklog/src/main/java/com/example/booklog/domain/search/dto/BookSearchItemResponse.java create mode 100644 booklog/src/main/java/com/example/booklog/domain/search/dto/BookSearchResponse.java create mode 100644 booklog/src/main/java/com/example/booklog/domain/search/service/BookSearchService.java diff --git a/README.md b/README.md deleted file mode 100644 index 7861df0..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# BookLog-BackEnd \ No newline at end of file diff --git a/booklog/src/main/java/com/example/booklog/domain/library/books/controller/BookSearchController.java b/booklog/src/main/java/com/example/booklog/domain/library/books/controller/BookSearchController.java deleted file mode 100644 index 24b0361..0000000 --- a/booklog/src/main/java/com/example/booklog/domain/library/books/controller/BookSearchController.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.booklog.domain.library.books.controller; - -import com.example.booklog.domain.library.books.dto.BookSearchResponse; -import com.example.booklog.domain.library.books.service.BookImportService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/v1/books") -public class BookSearchController { - - private final BookImportService bookImportService; - - @GetMapping("/search") - public BookSearchResponse search( - @RequestParam String query, - @RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "10") int size - ) { - return bookImportService.searchAndUpsert(query, page, size); - } -} diff --git a/booklog/src/main/java/com/example/booklog/domain/library/books/entity/Books.java b/booklog/src/main/java/com/example/booklog/domain/library/books/entity/Books.java index a333f58..91c43b9 100644 --- a/booklog/src/main/java/com/example/booklog/domain/library/books/entity/Books.java +++ b/booklog/src/main/java/com/example/booklog/domain/library/books/entity/Books.java @@ -139,8 +139,13 @@ public void addBookAuthor(BookAuthors bookAuthor) { } public void replaceBookAuthors(List newMappings) { + // ✅ orphan removal을 위해 기존 매핑 clear this.bookAuthors.clear(); - for (BookAuthors m : newMappings) addBookAuthor(m); + + // ✅ 새로운 매핑 추가 + for (BookAuthors m : newMappings) { + addBookAuthor(m); + } } public void addBookGenre(BookGenres bookGenre) { diff --git a/booklog/src/main/java/com/example/booklog/domain/library/books/service/BookImportService.java b/booklog/src/main/java/com/example/booklog/domain/library/books/service/BookImportService.java index d07bf0f..fce2f3f 100644 --- a/booklog/src/main/java/com/example/booklog/domain/library/books/service/BookImportService.java +++ b/booklog/src/main/java/com/example/booklog/domain/library/books/service/BookImportService.java @@ -14,11 +14,11 @@ import com.example.booklog.domain.library.books.service.client.KakaoBookClient; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -32,6 +32,7 @@ public class BookImportService { private final AuthorsRepository authorsRepository; private final BookSearchConverter bookSearchConverter; private final ObjectMapper objectMapper; // ✅ Spring Bean 주입 + private final EntityManager entityManager; // ✅ orphan removal 즉시 처리용 /** * 카카오 도서 검색 -> books/authors/book_authors 업서트 -> 검색 응답 반환 @@ -112,9 +113,18 @@ public BookSearchResponse searchAndUpsert(String query, int page, int size) { .build()); } + // ✅ 기존 매핑 삭제를 먼저 DB에 반영 + if (book.getId() != null) { + book.getBookAuthors().clear(); + booksRepository.save(book); + entityManager.flush(); + } + + // ✅ 새로운 매핑 추가 book.replaceBookAuthors(mappings); Books saved = booksRepository.save(book); + items.add(bookSearchConverter.toResponse(saved, doc)); } diff --git a/booklog/src/main/java/com/example/booklog/domain/search/controller/SearchController.java b/booklog/src/main/java/com/example/booklog/domain/search/controller/SearchController.java new file mode 100644 index 0000000..d37951d --- /dev/null +++ b/booklog/src/main/java/com/example/booklog/domain/search/controller/SearchController.java @@ -0,0 +1,66 @@ +package com.example.booklog.domain.search.controller; + +import com.example.booklog.domain.search.dto.BookSearchResponse; +import com.example.booklog.domain.search.service.BookSearchService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +/** + * 통합 검색 API 컨트롤러 + * - 도서 검색: /api/v1/search/books + * - 작가 검색: /api/v1/search/authors (추후 구현) + * - 통합 검색: /api/v1/search (추후 구현) + */ +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/search") +public class SearchController { + + private final BookSearchService bookSearchService; + + /** + * 도서 검색 + * GET /api/v1/search/books?query={검색어}&page={페이지}&size={크기} + */ + @GetMapping("/books") + public BookSearchResponse searchBooks( + @RequestParam String query, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size + ) { + return bookSearchService.searchBooks(query, page, size); + } + + /** + * 작가 검색 (추후 구현) + * GET /api/v1/search/authors?query={검색어}&page={페이지}&size={크기} + */ + // TODO: 작가 검색 구현 + // @GetMapping("/authors") + // public AuthorSearchResponse searchAuthors(...) + + /** + * 통합 검색 (추후 구현) + * GET /api/v1/search?query={검색어}&page={페이지}&size={크기} + */ + // TODO: 통합 검색 구현 + // @GetMapping + // public IntegratedSearchResponse search(...) + + /** + * 최근 검색어 조회 (추후 구현) + * GET /api/v1/search/recent + */ + // TODO: 최근 검색어 구현 + // @GetMapping("/recent") + // public RecentSearchResponse getRecentSearches() + + /** + * 추천 검색어 조회 (추후 구현) + * GET /api/v1/search/recommendations + */ + // TODO: 추천 검색어 구현 + // @GetMapping("/recommendations") + // public RecommendationSearchResponse getRecommendations() +} + diff --git a/booklog/src/main/java/com/example/booklog/domain/search/dto/BookSearchItemResponse.java b/booklog/src/main/java/com/example/booklog/domain/search/dto/BookSearchItemResponse.java new file mode 100644 index 0000000..1657191 --- /dev/null +++ b/booklog/src/main/java/com/example/booklog/domain/search/dto/BookSearchItemResponse.java @@ -0,0 +1,19 @@ +package com.example.booklog.domain.search.dto; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 도서 검색 결과 항목 DTO + */ +public record BookSearchItemResponse( + Long bookId, + String title, + String thumbnailUrl, + String publisherName, + String isbn13, + List authors, + List translators, + LocalDateTime publishedAt +) {} + diff --git a/booklog/src/main/java/com/example/booklog/domain/search/dto/BookSearchResponse.java b/booklog/src/main/java/com/example/booklog/domain/search/dto/BookSearchResponse.java new file mode 100644 index 0000000..5e9833a --- /dev/null +++ b/booklog/src/main/java/com/example/booklog/domain/search/dto/BookSearchResponse.java @@ -0,0 +1,15 @@ +package com.example.booklog.domain.search.dto; + +import java.util.List; + +/** + * 도서 검색 응답 DTO + */ +public record BookSearchResponse( + int page, + int size, + boolean isEnd, + int totalCount, + List items +) {} + diff --git a/booklog/src/main/java/com/example/booklog/domain/search/service/BookSearchService.java b/booklog/src/main/java/com/example/booklog/domain/search/service/BookSearchService.java new file mode 100644 index 0000000..d13f42c --- /dev/null +++ b/booklog/src/main/java/com/example/booklog/domain/search/service/BookSearchService.java @@ -0,0 +1,69 @@ +package com.example.booklog.domain.search.service; + +import com.example.booklog.domain.library.books.service.BookImportService; +import com.example.booklog.domain.search.dto.BookSearchItemResponse; +import com.example.booklog.domain.search.dto.BookSearchResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 도서 검색 서비스 + * - 카카오 API를 통한 도서 검색 및 DB 업서트 + */ +@Service +@RequiredArgsConstructor +public class BookSearchService { + + private final BookImportService bookImportService; + + /** + * 도서 검색 및 업서트 + * @param query 검색어 + * @param page 페이지 번호 + * @param size 페이지 크기 + * @return 검색 결과 + */ + public BookSearchResponse searchBooks(String query, int page, int size) { + // 기존 BookImportService의 검색 로직 활용 + com.example.booklog.domain.library.books.dto.BookSearchResponse legacyResponse = + bookImportService.searchAndUpsert(query, page, size); + + // DTO 변환 (legacy -> search domain) + return convertToSearchResponse(legacyResponse); + } + + /** + * legacy DTO를 search domain DTO로 변환 + */ + private BookSearchResponse convertToSearchResponse( + com.example.booklog.domain.library.books.dto.BookSearchResponse legacyResponse) { + + var items = legacyResponse.items().stream() + .map(this::convertToSearchItem) + .toList(); + + return new BookSearchResponse( + legacyResponse.page(), + legacyResponse.size(), + legacyResponse.isEnd(), + legacyResponse.totalCount(), + items + ); + } + + private BookSearchItemResponse convertToSearchItem( + com.example.booklog.domain.library.books.dto.BookSearchItemResponse legacyItem) { + + return new BookSearchItemResponse( + legacyItem.bookId(), + legacyItem.title(), + legacyItem.thumbnailUrl(), + legacyItem.publisherName(), + legacyItem.isbn13(), + legacyItem.authors(), + legacyItem.translators(), + legacyItem.publishedAt() + ); + } +} +