From 912d68af2e5b52c44f3232f55599f8f8c6b0fa72 Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Tue, 6 Jan 2026 21:47:26 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[Feat][Recommendation]=20gemini=20=ED=86=B5?= =?UTF-8?q?=ED=95=B4=EC=84=9C=20=EC=B1=85=20=EC=B6=94=EC=B2=9C=EB=B0=9B?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NaverBlogPostCrawlingStrategy.java | 25 +----- .../api/RecommendationController.java | 32 ++++++++ .../entity/MemberBookRecommendation.java | 30 ++++++++ .../MemberBookRecommendationRepository.java | 18 +++++ .../GeminiBookRecommendationProvider.java | 77 +++++++++++++++++++ .../service/RecommendationService.java | 76 ++++++++++++++++++ .../book/search/service/AladinService.java | 32 ++++++++ 7 files changed, 266 insertions(+), 24 deletions(-) create mode 100644 src/main/java/book/book/recommendation/api/RecommendationController.java create mode 100644 src/main/java/book/book/recommendation/entity/MemberBookRecommendation.java create mode 100644 src/main/java/book/book/recommendation/repository/MemberBookRecommendationRepository.java create mode 100644 src/main/java/book/book/recommendation/service/GeminiBookRecommendationProvider.java create mode 100644 src/main/java/book/book/recommendation/service/RecommendationService.java diff --git a/src/main/java/book/book/crawler/strategy/NaverBlogPostCrawlingStrategy.java b/src/main/java/book/book/crawler/strategy/NaverBlogPostCrawlingStrategy.java index fb1687dd..08f8c0db 100644 --- a/src/main/java/book/book/crawler/strategy/NaverBlogPostCrawlingStrategy.java +++ b/src/main/java/book/book/crawler/strategy/NaverBlogPostCrawlingStrategy.java @@ -33,10 +33,8 @@ public class NaverBlogPostCrawlingStrategy implements CrawlingStrategy { private final CrawlPersistenceService crawlPersistenceService; private final NaverBlogPostCrawlResultRepository naverBlogPostCrawlResultRepository; private final BookCategoryRepository bookCategoryRepository; - private final BookRepository bookRepository; private final AladinService aladinService; private final SkipBookCategoryRepository skipBookCategoryRepository; - private final BookSaveService bookSaveService; private String[] extractBlogId(String urlString) throws MalformedURLException { // TODO: 유닛테스트 @@ -138,7 +136,7 @@ private NaverBlogPostCrawlResult.SaveResult savePost(CrawledData crawledData, Na } String bookTitle = bookTitleOptional.get(); - Optional bookOptional = findOrCreateBook(bookTitle); + Optional bookOptional = aladinService.getOrSearchBook(bookTitle); if (bookOptional.isEmpty()) { log.warn("검색 결과가 없는 책 {}/{}", blogPost.getBlogId(), blogPost.getPostId()); return NaverBlogPostCrawlResult.SaveResult.NO_BOOK; @@ -159,27 +157,6 @@ private NaverBlogPostCrawlResult.SaveResult savePost(CrawledData crawledData, Na return NaverBlogPostCrawlResult.SaveResult.SUCCESS; } - private Optional findOrCreateBook(String bookTitle) { - Optional existingBook = bookRepository.findByTitle(bookTitle); - if (existingBook.isPresent()) { - return existingBook; - } - - try { - AladinSearchResponse searchResponse = aladinService.search(bookTitle, 1, 1); - if (searchResponse.getItem().isEmpty()) { - log.error("책 찾을 수 없음: {}", bookTitle); - return Optional.empty(); - } - - Book book = bookSaveService.getOrCreateBookFromAladin(searchResponse.getItem().get(0)); - return Optional.of(book); - } catch (Exception e) { - log.error("책 검색 중 오류 발생: {}, {}", e, bookTitle); - return Optional.empty(); - } - } - private boolean isQualifiedPost(NaverBlogPost post) { // TODO: 저품질 글을 거를 수 있는 조건 찾기 return post.getPostCommentCount() >= 0 && post.getPostLikeCount() >= 0; diff --git a/src/main/java/book/book/recommendation/api/RecommendationController.java b/src/main/java/book/book/recommendation/api/RecommendationController.java new file mode 100644 index 00000000..cc03f4a7 --- /dev/null +++ b/src/main/java/book/book/recommendation/api/RecommendationController.java @@ -0,0 +1,32 @@ +package book.book.recommendation.api; + +import book.book.book.entity.Book; +import book.book.member.entity.Member; +import book.book.member.repository.MemberRepository; +import book.book.recommendation.service.RecommendationService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/recommendation") +@RequiredArgsConstructor +public class RecommendationController { + private final RecommendationService recommendationService; + private final MemberRepository memberRepository; + + @GetMapping("/members/{memberId}/books/refresh") + public List refreshBooksForMember(@PathVariable Long memberId) { + Member member = memberRepository.findByIdOrElseThrow(memberId); + return recommendationService.generateBookRecommendationForMember(member); + } + + @GetMapping("/members/{memberId}/books") + public List getBooksForMember(@PathVariable Long memberId) { + Member member = memberRepository.findByIdOrElseThrow(memberId); + return recommendationService.getLastRecommendedBooksForMember(member); + } +} diff --git a/src/main/java/book/book/recommendation/entity/MemberBookRecommendation.java b/src/main/java/book/book/recommendation/entity/MemberBookRecommendation.java new file mode 100644 index 00000000..d76b88f5 --- /dev/null +++ b/src/main/java/book/book/recommendation/entity/MemberBookRecommendation.java @@ -0,0 +1,30 @@ +package book.book.recommendation.entity; + +import book.book.book.entity.Book; +import book.book.common.BaseTimeEntity; +import book.book.member.entity.Member; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MemberBookRecommendation extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column + private Long collectionId; + + @ManyToOne + @JoinColumn(name = "book_id") + private Book book; + + @ManyToOne + @JoinColumn(name = "member_id") + private Member member; +} diff --git a/src/main/java/book/book/recommendation/repository/MemberBookRecommendationRepository.java b/src/main/java/book/book/recommendation/repository/MemberBookRecommendationRepository.java new file mode 100644 index 00000000..15bda05e --- /dev/null +++ b/src/main/java/book/book/recommendation/repository/MemberBookRecommendationRepository.java @@ -0,0 +1,18 @@ +package book.book.recommendation.repository; + +import book.book.member.entity.Member; +import book.book.recommendation.entity.MemberBookRecommendation; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface MemberBookRecommendationRepository extends JpaRepository { + @Query(value = "SELECT MAX(mbr.collectionId) FROM MemberBookRecommendation mbr WHERE mbr.member = :member") + Optional findLastCollectionIdByMember(Member member); + + List findByMemberAndCollectionId(Member member, Long collectionId); +} diff --git a/src/main/java/book/book/recommendation/service/GeminiBookRecommendationProvider.java b/src/main/java/book/book/recommendation/service/GeminiBookRecommendationProvider.java new file mode 100644 index 00000000..46d8829c --- /dev/null +++ b/src/main/java/book/book/recommendation/service/GeminiBookRecommendationProvider.java @@ -0,0 +1,77 @@ +package book.book.recommendation.service; + +import book.book.onboarding.dto.OnboardingKeywordDto; +import book.book.onboarding.dto.OnboardingResultResponse; +import book.book.quiz.dto.external.GeminiRequest; +import book.book.quiz.external.gemini.GeminiSdkClient; +import book.book.recommendation.dto.BookRecommendationResponses; +import com.google.genai.types.Content; +import com.google.genai.types.Part; +import com.google.genai.types.Schema; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class GeminiBookRecommendationProvider { + private final GeminiSdkClient geminiSdkClient; + + public BookRecommendationResponses generateRecommendation(OnboardingResultResponse onboarding) { + GeminiRequest request = createRequest(onboarding); + return geminiSdkClient.generateContent(request); + } + + private GeminiRequest createRequest(OnboardingResultResponse onboarding) { + String category = onboarding.category() != null ? onboarding.category().name() : "unspecified"; + String keywords = onboarding.keywords() != null + ? onboarding.keywords().stream().map(OnboardingKeywordDto::name).collect(Collectors.joining(", ")) + : ""; + String prompt = String.format(""" + You are a book recommendation assistant. + + Recommend books that fit the member's preferences: + - Preferred category: %s + - Keywords of interest: %s + - Favorite author: %s + - Favorite book: %s + + Return only JSON that matches the provided schema and include 5 strong recommendations with diverse authors. + """, + category, + keywords.isBlank() ? "none provided" : keywords, + onboarding.favoriteAuthor(), + onboarding.favoriteBook()); + + Content systemInstruction = Content.fromParts(Part.fromText(prompt)); + + Schema bookSchema = Schema.builder() + .type("object") + .properties(Map.of( + "title", Schema.builder().type("string").build(), + "author", Schema.builder().type("string").build())) + .required(List.of("title", "author")) + .build(); + + Schema schema = Schema.builder() + .type("object") + .properties(Map.of( + "books", Schema.builder() + .type("array") + .items(bookSchema) + .build())) + .required(List.of("books")) + .build(); + + return GeminiRequest.builder() + .prompt(prompt) + .systemInstruction(systemInstruction) + .schema(schema) + .temperature(0.7f) + .responseType(BookRecommendationResponses.class) + .contextForLogging("[도서추천] category: " + category + ", keywords: " + keywords) + .build(); + } +} diff --git a/src/main/java/book/book/recommendation/service/RecommendationService.java b/src/main/java/book/book/recommendation/service/RecommendationService.java new file mode 100644 index 00000000..c2fbfe2c --- /dev/null +++ b/src/main/java/book/book/recommendation/service/RecommendationService.java @@ -0,0 +1,76 @@ +package book.book.recommendation.service; + +import book.book.book.entity.Book; +import book.book.common.lock.DistributedLock; +import book.book.member.entity.Member; +import book.book.onboarding.dto.OnboardingResultResponse; +import book.book.onboarding.service.OnboardingService; +import book.book.recommendation.dto.BookRecommendationResponses; +import book.book.recommendation.entity.MemberBookRecommendation; +import book.book.recommendation.repository.MemberBookRecommendationRepository; +import book.book.search.service.AladinService; +import jakarta.transaction.Transactional; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class RecommendationService { + private final MemberBookRecommendationRepository memberBookRecommendationRepository; + private final OnboardingService onboardingService; + private final GeminiBookRecommendationProvider geminiBookRecommendationProvider; + private final AladinService aladinService; + + @DistributedLock(key = "'recommendation_lock:' + #member.id", waitTime = 0) + @Transactional + public List generateBookRecommendationForMember(Member member) { + OnboardingResultResponse memberPreferences = onboardingService.getMemberPreferences(member.getId()); + BookRecommendationResponses bookRecommendations = geminiBookRecommendationProvider.generateRecommendation(memberPreferences); + List books = bookRecommendations.books() + .stream() + .map(BookRecommendationResponses.BookRecommendationResponse::title) + .map(aladinService::getOrSearchBook) + .flatMap(Optional::stream) + .toList(); + addCollectionWithoutLock(member, books); + return books; + } + + public List getLastRecommendedBooksForMember(Member member) { + Long lastCollectionId = memberBookRecommendationRepository.findLastCollectionIdByMember(member) + .orElse(0L); + + if (lastCollectionId == 0) { + return Collections.emptyList(); + } + + return memberBookRecommendationRepository.findByMemberAndCollectionId(member, lastCollectionId) + .stream() + .map(MemberBookRecommendation::getBook) + .toList(); + } + + @DistributedLock(key = "'recommendation_lock:' + #member.id", waitTime = 0) + @Transactional + public void addCollection(Member member, List books) { + addCollectionWithoutLock(member, books); + } + + private void addCollectionWithoutLock(Member member, List books) { + Long lastCollectionId = memberBookRecommendationRepository.findLastCollectionIdByMember(member) + .orElse(0L); + List recommendations = books.stream() + .map(book -> MemberBookRecommendation.builder() + .book(book) + .member(member) + .collectionId(lastCollectionId + 1) + .build()) + .toList(); + memberBookRecommendationRepository.saveAll(recommendations); + } +} \ No newline at end of file diff --git a/src/main/java/book/book/search/service/AladinService.java b/src/main/java/book/book/search/service/AladinService.java index e8a59135..66ae2c6a 100644 --- a/src/main/java/book/book/search/service/AladinService.java +++ b/src/main/java/book/book/search/service/AladinService.java @@ -1,5 +1,8 @@ package book.book.search.service; +import book.book.book.entity.Book; +import book.book.book.repository.BookRepository; +import book.book.book.service.BookSaveService; import book.book.common.CustomException; import book.book.search.dto.aladin.AladinItemLookUpResponse; import book.book.search.dto.aladin.AladinSearchResponse; @@ -8,13 +11,17 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.reactive.function.client.WebClient; +import java.util.Optional; + @RequiredArgsConstructor @Service +@Slf4j public class AladinService { private static final int PAGE_SIZE = 20; @@ -45,6 +52,31 @@ public class AladinService { // 기본적으로 JSON은 큰따옴표(")만 허용하지만, 이 설정으로 작은따옴표도 허용 .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); + private final BookRepository bookRepository; + private final BookSaveService bookSaveService; + + @Transactional + public Optional getOrSearchBook(String bookTitle) { + Optional existingBook = bookRepository.findByTitle(bookTitle); + if (existingBook.isPresent()) { + return existingBook; + } + + try { + AladinSearchResponse searchResponse = search(bookTitle, 1, 1); + if (searchResponse.getItem().isEmpty()) { + log.error("책 찾을 수 없음: {}", bookTitle); + return Optional.empty(); + } + + Book book = bookSaveService.getOrCreateBookFromAladin(searchResponse.getItem().get(0)); + return Optional.of(book); + } catch (Exception e) { + log.error("책 검색 중 오류 발생: {}, {}", e, bookTitle); + return Optional.empty(); + } + } + @Transactional public AladinSearchResponse search(String query, Integer start, int pageSize) { try { From a47214408cbc80867e07f83196b8e65e816fb475 Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Mon, 12 Jan 2026 19:04:33 +0900 Subject: [PATCH 2/4] merge --- .../service/BookRecommendationResponses.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/book/book/recommendation/service/BookRecommendationResponses.java diff --git a/src/main/java/book/book/recommendation/service/BookRecommendationResponses.java b/src/main/java/book/book/recommendation/service/BookRecommendationResponses.java new file mode 100644 index 00000000..9fecb050 --- /dev/null +++ b/src/main/java/book/book/recommendation/service/BookRecommendationResponses.java @@ -0,0 +1,32 @@ +package book.book.recommendation.service; + +import java.util.List; +import java.util.Objects; + +public record BookRecommendationResponses(List books) { + + public BookRecommendationResponses { + books = books == null + ? List.of() + : books.stream() + .filter(Objects::nonNull) + .filter(BookRecommendationResponse::isValid) + .toList(); + } + + public boolean isValid() { + return !books.isEmpty(); + } + + public record BookRecommendationResponse(String title, String author) { + public BookRecommendationResponse { + title = title == null ? null : title.trim(); + author = author == null ? null : author.trim(); + } + + public boolean isValid() { + return title != null && !title.isBlank() + && author != null && !author.isBlank(); + } + } +} From 2c92eae385d79cdc58403a75c24a28cd2ffa0a0d Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Mon, 12 Jan 2026 23:57:41 +0900 Subject: [PATCH 3/4] refactor --- .../entity/MemberBookRecommendation.java | 1 - .../GeminiBookRecommendationProvider.java | 22 ++++++++++--------- .../service/RecommendationService.java | 18 ++++++--------- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/main/java/book/book/recommendation/entity/MemberBookRecommendation.java b/src/main/java/book/book/recommendation/entity/MemberBookRecommendation.java index d76b88f5..27e72de0 100644 --- a/src/main/java/book/book/recommendation/entity/MemberBookRecommendation.java +++ b/src/main/java/book/book/recommendation/entity/MemberBookRecommendation.java @@ -8,7 +8,6 @@ @Entity @Getter -@Setter @Builder @AllArgsConstructor @NoArgsConstructor diff --git a/src/main/java/book/book/recommendation/service/GeminiBookRecommendationProvider.java b/src/main/java/book/book/recommendation/service/GeminiBookRecommendationProvider.java index 6664841a..f667ad72 100644 --- a/src/main/java/book/book/recommendation/service/GeminiBookRecommendationProvider.java +++ b/src/main/java/book/book/recommendation/service/GeminiBookRecommendationProvider.java @@ -28,23 +28,25 @@ private GeminiRequest createRequest(OnboardingResul String keywords = onboarding.keywords() != null ? onboarding.keywords().stream().map(OnboardingKeywordDto::name).collect(Collectors.joining(", ")) : ""; - String prompt = String.format(""" - You are a book recommendation assistant. - - Recommend books that fit the member's preferences: - - Preferred category: %s - - Keywords of interest: %s - - Favorite author: %s - - Favorite book: %s + String systemPrompt = """ + 당신은 도서 추천 어시스턴트입니다. + 제공된 스키마에 맞는 JSON만 반환하고, 저자가 다양하도록 5권의 강력한 추천을 포함하세요. + 모든 응답은 한국어로 작성하세요. + """; - Return only JSON that matches the provided schema and include 5 strong recommendations with diverse authors. + String prompt = String.format(""" + 회원 선호 정보: + - 선호 카테고리: %s + - 관심 키워드: %s + - 좋아하는 작가: %s + - 좋아하는 책: %s """, category, keywords.isBlank() ? "none provided" : keywords, onboarding.favoriteAuthor(), onboarding.favoriteBook()); - Content systemInstruction = Content.fromParts(Part.fromText(prompt)); + Content systemInstruction = Content.fromParts(Part.fromText(systemPrompt)); Schema bookSchema = Schema.builder() .type("object") diff --git a/src/main/java/book/book/recommendation/service/RecommendationService.java b/src/main/java/book/book/recommendation/service/RecommendationService.java index 8f9cae24..3829fbf1 100644 --- a/src/main/java/book/book/recommendation/service/RecommendationService.java +++ b/src/main/java/book/book/recommendation/service/RecommendationService.java @@ -41,17 +41,13 @@ public List generateBookRecommendationForMember(Member member) { } public List getLastRecommendedBooksForMember(Member member) { - Long lastCollectionId = memberBookRecommendationRepository.findLastCollectionIdByMember(member) - .orElse(0L); - - if (lastCollectionId == 0) { - return Collections.emptyList(); - } - - return memberBookRecommendationRepository.findByMemberAndCollectionId(member, lastCollectionId) - .stream() - .map(MemberBookRecommendation::getBook) - .toList(); + return memberBookRecommendationRepository.findLastCollectionIdByMember(member) + .map(lastCollectionId -> memberBookRecommendationRepository + .findByMemberAndCollectionId(member, lastCollectionId) + .stream() + .map(MemberBookRecommendation::getBook) + .toList()) + .orElse(Collections.emptyList()); } @DistributedLock(key = "'recommendation_lock:' + #member.id", waitTime = 0) From bcb510944127cdf63bbd56772b2641f4ed87b2e3 Mon Sep 17 00:00:00 2001 From: AlphaBs Date: Sun, 18 Jan 2026 22:24:53 +0900 Subject: [PATCH 4/4] fix --- .../api/RecommendationController.java | 12 ++++++++---- .../dto/BookRecommendationResponse.java | 13 +++++++++++++ .../service/GeminiBookRecommendationProvider.java | 10 ++++++---- .../service/RecommendationService.java | 2 +- .../book/book/search/service/AladinService.java | 2 +- 5 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 src/main/java/book/book/recommendation/dto/BookRecommendationResponse.java diff --git a/src/main/java/book/book/recommendation/api/RecommendationController.java b/src/main/java/book/book/recommendation/api/RecommendationController.java index cc03f4a7..d9e96833 100644 --- a/src/main/java/book/book/recommendation/api/RecommendationController.java +++ b/src/main/java/book/book/recommendation/api/RecommendationController.java @@ -1,8 +1,10 @@ package book.book.recommendation.api; +import book.book.book.dto.BookOverviewResponse; import book.book.book.entity.Book; import book.book.member.entity.Member; import book.book.member.repository.MemberRepository; +import book.book.recommendation.dto.BookRecommendationResponse; import book.book.recommendation.service.RecommendationService; import java.util.List; import lombok.RequiredArgsConstructor; @@ -19,14 +21,16 @@ public class RecommendationController { private final MemberRepository memberRepository; @GetMapping("/members/{memberId}/books/refresh") - public List refreshBooksForMember(@PathVariable Long memberId) { + public List refreshBooksForMember(@PathVariable Long memberId) { Member member = memberRepository.findByIdOrElseThrow(memberId); - return recommendationService.generateBookRecommendationForMember(member); + List books = recommendationService.generateBookRecommendationForMember(member); + return books.stream().map(BookRecommendationResponse::from).toList(); } @GetMapping("/members/{memberId}/books") - public List getBooksForMember(@PathVariable Long memberId) { + public List getBooksForMember(@PathVariable Long memberId) { Member member = memberRepository.findByIdOrElseThrow(memberId); - return recommendationService.getLastRecommendedBooksForMember(member); + List books = recommendationService.getLastRecommendedBooksForMember(member); + return books.stream().map(BookRecommendationResponse::from).toList(); } } diff --git a/src/main/java/book/book/recommendation/dto/BookRecommendationResponse.java b/src/main/java/book/book/recommendation/dto/BookRecommendationResponse.java new file mode 100644 index 00000000..2a5e469d --- /dev/null +++ b/src/main/java/book/book/recommendation/dto/BookRecommendationResponse.java @@ -0,0 +1,13 @@ +package book.book.recommendation.dto; + +import book.book.book.entity.Book; + +public record BookRecommendationResponse( + Long id, + String title, + String author +) { + public static BookRecommendationResponse from(Book book) { + return new BookRecommendationResponse(book.getId(), book.getTitle(), book.getAuthor()); + } +} diff --git a/src/main/java/book/book/recommendation/service/GeminiBookRecommendationProvider.java b/src/main/java/book/book/recommendation/service/GeminiBookRecommendationProvider.java index f667ad72..c1289360 100644 --- a/src/main/java/book/book/recommendation/service/GeminiBookRecommendationProvider.java +++ b/src/main/java/book/book/recommendation/service/GeminiBookRecommendationProvider.java @@ -27,7 +27,9 @@ private GeminiRequest createRequest(OnboardingResul String category = onboarding.category() != null ? onboarding.category().name() : "unspecified"; String keywords = onboarding.keywords() != null ? onboarding.keywords().stream().map(OnboardingKeywordDto::name).collect(Collectors.joining(", ")) - : ""; + : "none provided"; + String favoriteAuthor = onboarding.favoriteAuthor() != null ? onboarding.favoriteAuthor() : "none provided"; + String favoriteBook = onboarding.favoriteBook() != null ? onboarding.favoriteBook() : "none provided"; String systemPrompt = """ 당신은 도서 추천 어시스턴트입니다. 제공된 스키마에 맞는 JSON만 반환하고, 저자가 다양하도록 5권의 강력한 추천을 포함하세요. @@ -42,9 +44,9 @@ private GeminiRequest createRequest(OnboardingResul - 좋아하는 책: %s """, category, - keywords.isBlank() ? "none provided" : keywords, - onboarding.favoriteAuthor(), - onboarding.favoriteBook()); + keywords, + favoriteAuthor, + favoriteBook); Content systemInstruction = Content.fromParts(Part.fromText(systemPrompt)); diff --git a/src/main/java/book/book/recommendation/service/RecommendationService.java b/src/main/java/book/book/recommendation/service/RecommendationService.java index 3829fbf1..50f8e555 100644 --- a/src/main/java/book/book/recommendation/service/RecommendationService.java +++ b/src/main/java/book/book/recommendation/service/RecommendationService.java @@ -25,7 +25,7 @@ public class RecommendationService { private final GeminiBookRecommendationProvider geminiBookRecommendationProvider; private final AladinService aladinService; - @DistributedLock(key = "'recommendation_lock:' + #member.id", waitTime = 0) + @DistributedLock(key = "'recommendation_lock:' + #member.id", waitTime = 0, leaseTime = 30) @Transactional public List generateBookRecommendationForMember(Member member) { OnboardingResultResponse memberPreferences = onboardingService.getMemberPreferences(member.getId()); diff --git a/src/main/java/book/book/search/service/AladinService.java b/src/main/java/book/book/search/service/AladinService.java index 100e1a60..4028cd2a 100644 --- a/src/main/java/book/book/search/service/AladinService.java +++ b/src/main/java/book/book/search/service/AladinService.java @@ -74,7 +74,7 @@ public Optional getOrSearchBook(String bookTitle) { Book book = bookService.saveBooksParallel(searchResponse.getItem()).get(0); return Optional.of(book); } catch (Exception e) { - log.error("책 검색 중 오류 발생: {}, {}", e, bookTitle); + log.error("책 검색 중 오류 발생: {}", bookTitle, e); return Optional.empty(); } }