feat: 목차#160
Conversation
📝 WalkthroughWalkthroughThis PR introduces GPT-powered table of contents generation for book enrichment, restructures the onboarding recommendation system from flat recommendations to three-section groupings (author, genre, mood), enhances Korean sentence completion detection, and adds EntityManager refresh logic for database consistency. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
booklog/src/main/java/com/example/booklog/domain/library/books/service/BookDescriptionEnhancer.java (1)
196-201:⚠️ Potential issue | 🟠 Major
findLastKoreanSentenceEndingis out of sync withisIncompleteSentence.The endings array here is used by
truncateToLastCompleteSentenceto find valid cut points, but it doesn't include the newly added patterns (같다,나다,뜻하다,받다,가다,오다,하다,되다,이다) nor several original ones (까,세요). This meanscompleteIncompleteSentencemay fail to truncate at a sentence thatisIncompleteSentencewould consider complete.Extract a shared constant for Korean endings to keep both methods consistent.
Proposed refactor: shared endings constant
+ private static final String[] KOREAN_SENTENCE_ENDINGS = { + "습니다", "입니다", "였습니다", "있습니다", "했습니다", + "한다", "된다", "있다", "였다", "없다", + "같다", "나다", "뜻하다", "받다", "가다", "오다", "하다", "되다", "이다", + "요", "네요", "어요", "세요", "죠", "까" + };Then use
KOREAN_SENTENCE_ENDINGSin bothisIncompleteSentenceandfindLastKoreanSentenceEndinginstead of maintaining two separate lists.booklog/src/main/java/com/example/booklog/domain/onboarding/service/OnboardingRecommendationService.java (1)
486-552:⚠️ Potential issue | 🟡 MinorPopular books path: sections are labeled by author/genre/mood but the split is arbitrary.
The 6 popular books are fetched by
publishedDateDESC and simply chunked positionally (first 2 → "author", next 2 → "genre", last 2 → "mood"). The section labels imply semantic grouping, but the assignment is purely positional. If fewer than 6 books are returned,genreBooksandmoodBookswill be empty lists. This isn't a crash, but the UX is misleading — consider either genuinely grouping by attribute or using neutral section titles for this fallback path.booklog/src/main/java/com/example/booklog/domain/ai/service/GptService.java (1)
481-534: 🛠️ Refactor suggestion | 🟠 MajorExcessive info-level logging leaks prompt content and full API responses.
Lines 491, 507, 520, and 524 log the system message, full request body, raw API response body, and extracted content — all at
infolevel. In production this will:
- Generate very high log volume on every GPT call
- Expose prompt-engineering details and potentially user-related data in logs
Downgrade these to
debug, keeping only concise status messages atinfo.Suggested approach
- log.info("모델: {}", gptConfig.getModel()); - log.info("System Message: {}", systemMessage); - log.info("User Prompt 길이: {} 자", userPrompt.length()); + log.debug("모델: {}, System Message 길이: {}, User Prompt 길이: {} 자", + gptConfig.getModel(), systemMessage.length(), userPrompt.length()); ... - log.info("Request Body: {}", requestBody); + log.debug("Request Body: {}", requestBody); ... - log.info("응답 Body: {}", response.getBody()); + log.debug("응답 Body: {}", response.getBody());
🤖 Fix all issues with AI agents
In `@booklog/src/main/java/com/example/booklog/domain/ai/service/GptService.java`:
- Around line 358-380: The prompt built by buildTableOfContentsPrompt currently
instructs the model to fabricate a TOC when unavailable, causing hallucinations;
update buildTableOfContentsPrompt to remove the instruction "4) 목차 정보를 찾을 수 없는
경우, 해당 책에 대한 정보를 바탕으로 추정 가능한 목차를 제공해주세요." or replace it with a clear directive
that any non-verified/generated TOC must be explicitly labeled (e.g., prepend
"AI 추정:") so downstream code/UI can treat it as estimated; ensure the fallback
logic that shows an empty result or uses the existing fallback template still
triggers when no verified TOC is found.
- Around line 309-353: generateTableOfContents delegates to
callGptApiForSimpleText which currently caps responses at max_tokens=200 causing
TOC truncation; update the code so TOC generation uses a larger token budget by
either (A) adding a maxTokens parameter to callGptApiForSimpleText and passing a
higher value (e.g., 500–800) from generateTableOfContents, or (B) creating a new
method (e.g., callGptApiForToc or callGptApiWithMaxTokens) that sets max_tokens
to 500–800 and invoking that from generateTableOfContents; adjust the method
signature(s) and all call sites accordingly so other callers keep the 200
default while generateTableOfContents receives the increased limit.
In
`@booklog/src/main/java/com/example/booklog/domain/library/books/service/BookEnrichmentService.java`:
- Around line 282-291: In BookEnrichmentService remove the duplicated Javadoc
for extractTableOfContents so only a single docblock remains; keep the intended
description that mentions using the GPT API and the fallback behavior ("GPT 실패 시
기본 템플릿 제공") and delete the redundant comment immediately adjacent to it to avoid
duplicate JavaDoc entries for the extractTableOfContents method.
- Around line 51-97: The enrichment logic only treats null as missing; update
the checks for shortIntro, aiTasteComment, and tasteAnalysis to also treat empty
or blank strings as missing (e.g., trim and check isEmpty) before calling
generateShortIntro, generateAiTasteComment, or generateTasteAnalysis, and only
assign/mark needsUpdate when the generated value is non-blank; keep the existing
robust handling for tableOfContents
(isTableOfContentsEmpty/extractTableOfContents) and ensure values passed to the
entity method updateEnrichedInfo() are non-null and non-empty so empty strings
won't persist and block future regeneration.
In
`@booklog/src/main/java/com/example/booklog/domain/onboarding/service/OnboardingRecommendationService.java`:
- Around line 294-337: The loop in generateAuthorSection repeatedly calls
generateOnboardingBasedCard with identical inputs (criteria, recentSearches,
profile) which can return duplicate books; modify generateOnboardingBasedCard
(and mirror changes for generateGenreSection and generateMoodSection) to accept
an additional parameter (e.g., excludeIds or resultOffset) or return unique
suggestions when given an exclusion list, then in the for-loop pass a different
offset or accumulated exclude list each iteration so each call requests a
different Kakao result (or omits already-selected book IDs) to ensure distinct
recommendations; update RecommendationCriteria usage only if needed to carry the
new parameter and ensure generateOnboardingBasedCard checks the exclude list
before returning a card.
- Around line 157-263: The three methods generateLibraryAuthorSection,
generateLibraryGenreSection, and generateLibraryMoodSection currently extract
identical authorInfo and call generateLibraryBasedCard the same way, producing
duplicate recommendations; fix by varying the search criterion per section
(e.g., pass a sectionType or fieldName into generateLibraryBasedCard) and
extract a single helper to remove duplication. Specifically, modify
generateLibraryGenreSection to extract genre info from book (e.g.,
book.getGenres() or the appropriate genre relationship) and pass a distinct
identifier like "genre" to generateLibraryBasedCard; modify
generateLibraryMoodSection to extract mood/tone metadata (e.g., book.getMood()
or tags) and pass "mood"; update generateLibraryBasedCard signature to accept
the section type/field name and use it when building the GPT/search prompt;
finally, consolidate the looping/limit logic into a private helper method (e.g.,
buildLibrarySection(List<UserBooks>, String recentSearches, String sectionType,
String title, String description)) and have each of the three section methods
call that helper with the correct extractor/sectionType, title, and description.
🧹 Nitpick comments (7)
booklog/src/main/java/com/example/booklog/domain/onboarding/dto/OnboardingBasedRecommendationResponse.java (1)
43-67: Consider defaultingbooksto an empty list to guard against null.If any section is built without explicitly setting
books, downstream consumers could encounter aNullPointerExceptionwhen iterating over it. A safe default avoids that.Proposed fix
/** * 추천 도서 목록 */ `@Schema`(description = "추천 도서 목록") - private List<BookRecommendationCardResponse> books; + `@Builder.Default` + private List<BookRecommendationCardResponse> books = List.of();booklog/src/main/java/com/example/booklog/domain/library/books/service/BookDescriptionEnhancer.java (1)
76-88: All newly added endings are redundant —endsWith("다")on line 77 already covers them.Every new entry (
없다,같다,나다,뜻하다,받다,가다,오다,하다,되다,이다) ends with다, so the existingtrimmed.endsWith("다")check already matches all of them. These additions have no behavioral effect.If the intent is to be more precise (i.e., avoid treating any text ending in
다as complete), thenendsWith("다")should be removed and replaced with the explicit list. Otherwise, these lines are dead conditions.booklog/src/main/java/com/example/booklog/domain/onboarding/service/OnboardingRecommendationService.java (2)
66-106: Multiple sequential GPT + Kakao API calls inside a@Transactional(readOnly = true)method.The library-based path makes up to 6 GPT calls + 6 Kakao API calls + 6 keyword-analysis GPT calls (18 external HTTP calls total), all synchronous and sequential, while holding a read-only transaction/DB connection. This can cause long response times and connection pool exhaustion under load. Consider:
- Moving external API calls outside the transaction boundary, or
- Parallelizing independent section generation with
CompletableFuture/ virtual threads.
49-49: Remove unusedextractRecommendationCriteriamethod andMAX_RECOMMENDATIONSconstant.
extractRecommendationCriteriais never called andMAX_RECOMMENDATIONSis only referenced within it. Both are dead code leftover from the per-section generation refactor.booklog/src/main/java/com/example/booklog/domain/library/books/service/BookQueryServiceImpl.java (1)
82-86: Redundantflush()—enrichBookInfoalready flushes internally.
BookEnrichmentService.enrichBookInfo(line 104–105 of that file) already callssave+flush. ThebooksRepository.flush()here on line 83 is a no-op in the same transaction. It's harmless but unnecessary.Also, logging the full
tableOfContentsJSON atinfolevel on every book detail request will be noisy in production — considerdebug.Suggested change
- // ✅ 4. DB 즉시 반영 및 엔티티 최신화 - booksRepository.flush(); entityManager.refresh(book); - log.info("엔티티 refresh 완료 - bookId: {}, tableOfContents: {}", - bookId, book.getTableOfContents()); + log.debug("엔티티 refresh 완료 - bookId: {}, tableOfContents: {}", + bookId, book.getTableOfContents());booklog/src/main/java/com/example/booklog/domain/library/books/service/BookEnrichmentService.java (2)
342-381: Hardcoded fallback for a single book title is fragile and misleading.The fallback generates specific chapter titles for "소년이 온다" (lines 349–356) and generic placeholders like "제1장", "제2장" for everything else. Both are problematic:
- The hardcoded TOC for one specific book won't scale and will become stale if the codebase grows.
- Generic placeholders ("제1장", "제2장", "에필로그") provide no real value to users and may be confusing — they look like real data but aren't.
Consider returning
"[]"(empty) when GPT fails, and handling the "no TOC available" case in the UI instead of fabricating data. If fallback content is truly needed, at least mark it as AI-generated/estimated.
76-97: Verbose debug logging atinfolevel.Same concern as in
GptService— lines 78–79, 82, 84, 90, 92, 96, 100, 102, 107 all log atinfowith detailed diagnostic content. This will be very noisy on every book detail view. Considerdebugfor the step-by-step diagnostics and keep only the final outcome atinfo.
| public List<String> generateTableOfContents(String bookTitle, String author, String publisher) { | ||
| try { | ||
| log.info(">>> GPT 목차 생성 요청 시작"); | ||
| log.info(">>> 책 제목: '{}'", bookTitle); | ||
| log.info(">>> 저자: '{}'", author); | ||
| log.info(">>> 출판사: '{}'", publisher); | ||
|
|
||
| String prompt = buildTableOfContentsPrompt(bookTitle, author, publisher); | ||
| log.info(">>> 생성된 프롬프트:\n{}", prompt); | ||
|
|
||
| // GPT API 호출 | ||
| log.info(">>> GPT API 호출 중..."); | ||
| String gptResponse = callGptApiForSimpleText( | ||
| "너는 도서 정보 전문가입니다. 책 제목과 저자 정보를 바탕으로 해당 책의 목차를 정확하게 제공해주세요.", | ||
| prompt | ||
| ); | ||
|
|
||
| log.info(">>> GPT API 응답 수신:"); | ||
| log.info(">>> 응답 내용:\n{}", gptResponse); | ||
|
|
||
| // 응답을 목차 리스트로 파싱 | ||
| List<String> tableOfContents = parseTableOfContentsResponse(gptResponse); | ||
|
|
||
| log.info(">>> 파싱 완료 - 목차 개수: {}", tableOfContents.size()); | ||
| if (!tableOfContents.isEmpty()) { | ||
| log.info(">>> 목차 내용:"); | ||
| for (int i = 0; i < tableOfContents.size(); i++) { | ||
| log.info(">>> [{}] {}", i, tableOfContents.get(i)); | ||
| } | ||
| } else { | ||
| log.warn(">>> 파싱 결과: 빈 목차 (GPT가 목차를 찾지 못함)"); | ||
| } | ||
|
|
||
| return tableOfContents; | ||
|
|
||
| } catch (Exception e) { | ||
| log.error(">>> GPT 책 목차 생성 예외 발생"); | ||
| log.error(">>> 책 제목: {}", bookTitle); | ||
| log.error(">>> 예외 타입: {}", e.getClass().getName()); | ||
| log.error(">>> 예외 메시지: {}", e.getMessage()); | ||
| log.error(">>> 스택 트레이스:", e); | ||
| // 실패 시 빈 리스트 반환 | ||
| return List.of(); | ||
| } | ||
| } |
There was a problem hiding this comment.
max_tokens=200 in callGptApiForSimpleText is likely too low for TOC generation.
This method delegates to callGptApiForSimpleText (line 505), which caps output at 200 tokens. A table of contents for a book with 15–20+ chapters can easily exceed that, resulting in truncated output and lost entries. The existing callers (monthly summary) are fine with 200, but TOC is a different beast.
Consider either:
- Parameterizing
max_tokensincallGptApiForSimpleText, or - Creating a dedicated GPT call method for TOC with a higher token limit (e.g., 500–800).
🤖 Prompt for AI Agents
In `@booklog/src/main/java/com/example/booklog/domain/ai/service/GptService.java`
around lines 309 - 353, generateTableOfContents delegates to
callGptApiForSimpleText which currently caps responses at max_tokens=200 causing
TOC truncation; update the code so TOC generation uses a larger token budget by
either (A) adding a maxTokens parameter to callGptApiForSimpleText and passing a
higher value (e.g., 500–800) from generateTableOfContents, or (B) creating a new
method (e.g., callGptApiForToc or callGptApiWithMaxTokens) that sets max_tokens
to 500–800 and invoking that from generateTableOfContents; adjust the method
signature(s) and all call sites accordingly so other callers keep the 200
default while generateTableOfContents receives the increased limit.
| private String buildTableOfContentsPrompt(String bookTitle, String author, String publisher) { | ||
| StringBuilder prompt = new StringBuilder(); | ||
| prompt.append("다음 책의 목차를 정확하게 제공해주세요.\n\n"); | ||
| prompt.append("도서 정보:\n"); | ||
| prompt.append("- 제목: ").append(bookTitle).append("\n"); | ||
| prompt.append("- 저자: ").append(author).append("\n"); | ||
| if (publisher != null && !publisher.isEmpty()) { | ||
| prompt.append("- 출판사: ").append(publisher).append("\n"); | ||
| } | ||
|
|
||
| prompt.append("\n다음 형식으로 응답해주세요:\n"); | ||
| prompt.append("1. [첫 번째 장 제목]\n"); | ||
| prompt.append("2. [두 번째 장 제목]\n"); | ||
| prompt.append("3. [세 번째 장 제목]\n"); | ||
| prompt.append("...\n"); | ||
| prompt.append("\n※ 중요:\n"); | ||
| prompt.append("1) 실제 책의 목차를 정확하게 제공해주세요.\n"); | ||
| prompt.append("2) 각 항목은 '번호. 제목' 형식으로 작성하세요.\n"); | ||
| prompt.append("3) 다른 설명 없이 목차만 나열하세요.\n"); | ||
| prompt.append("4) 목차 정보를 찾을 수 없는 경우, 해당 책에 대한 정보를 바탕으로 추정 가능한 목차를 제공해주세요."); | ||
|
|
||
| return prompt.toString(); | ||
| } |
There was a problem hiding this comment.
Prompt instructs GPT to fabricate TOC when unknown — this will produce hallucinated content.
Line 377: the prompt explicitly tells GPT to guess the table of contents if it can't find it. GPT will confidently return plausible-looking but fictional chapter titles, which could mislead users who expect accurate book metadata.
Consider either removing instruction 4) and returning empty (triggering the fallback template), or clearly labeling GPT-generated TOC as "AI 추정" in the UI/data model.
🤖 Prompt for AI Agents
In `@booklog/src/main/java/com/example/booklog/domain/ai/service/GptService.java`
around lines 358 - 380, The prompt built by buildTableOfContentsPrompt currently
instructs the model to fabricate a TOC when unavailable, causing hallucinations;
update buildTableOfContentsPrompt to remove the instruction "4) 목차 정보를 찾을 수 없는
경우, 해당 책에 대한 정보를 바탕으로 추정 가능한 목차를 제공해주세요." or replace it with a clear directive
that any non-verified/generated TOC must be explicitly labeled (e.g., prepend
"AI 추정:") so downstream code/UI can treat it as estimated; ensure the fallback
logic that shows an empty result or uses the existing fallback template still
triggers when no verified TOC is found.
| boolean needsUpdate = false; | ||
| String shortIntro = book.getShortIntro(); | ||
| String aiTasteComment = book.getAiTasteComment(); | ||
| String tasteAnalysis = book.getTasteAnalysis(); | ||
| String tableOfContents = book.getTableOfContents(); | ||
|
|
||
| try { | ||
| // 1. 간략 소개 생성 (description에서 첫 문장 추출 또는 AI 생성) | ||
| String shortIntro = generateShortIntro(book); | ||
|
|
||
| // 2. AI 취향 코멘트 생성 | ||
| String aiTasteComment = generateAiTasteComment(book); | ||
| // 1. 간략 소개가 없으면 생성 | ||
| if (shortIntro == null) { | ||
| shortIntro = generateShortIntro(book); | ||
| needsUpdate = true; | ||
| } | ||
|
|
||
| // 3. 상세 취향 분석 생성 | ||
| String tasteAnalysis = generateTasteAnalysis(book); | ||
| // 2. AI 취향 코멘트가 없으면 생성 | ||
| if (aiTasteComment == null) { | ||
| aiTasteComment = generateAiTasteComment(book); | ||
| needsUpdate = true; | ||
| } | ||
|
|
||
| // 4. 목차 정보는 외부 API에서 가져온 rawData에서 추출 시도 | ||
| String tableOfContents = extractTableOfContents(book); | ||
| // 3. 상세 취향 분석이 없으면 생성 | ||
| if (tasteAnalysis == null) { | ||
| tasteAnalysis = generateTasteAnalysis(book); | ||
| needsUpdate = true; | ||
| } | ||
|
|
||
| // 5. 엔티티 업데이트 | ||
| book.updateEnrichedInfo(shortIntro, aiTasteComment, tasteAnalysis, tableOfContents); | ||
| // 4. 목차가 없거나 빈 배열이면 생성 | ||
| boolean needsTableOfContents = isTableOfContentsEmpty(tableOfContents); | ||
| log.info("=== 목차 체크 === bookId: {}, 빈 배열 여부: {}, 현재값: '{}'", | ||
| bookId, needsTableOfContents, tableOfContents); | ||
|
|
||
| if (needsTableOfContents) { | ||
| log.info(">>> 목차 생성 시작"); | ||
| String generatedToc = extractTableOfContents(book); | ||
| log.info(">>> GPT 생성 결과: '{}'", generatedToc); | ||
|
|
||
| // GPT가 실제로 목차를 생성했는지 확인 | ||
| if (generatedToc != null && !generatedToc.equals("[]")) { | ||
| tableOfContents = generatedToc; | ||
| needsUpdate = true; | ||
| log.info(">>> 목차 생성 성공! DB 업데이트할 내용: {}", tableOfContents); | ||
| } else { | ||
| log.warn(">>> 목차 생성 실패 (빈 배열 반환) - DB 업데이트 스킵"); | ||
| // needsUpdate = false 유지, DB에 빈 배열 저장 안 함 | ||
| } | ||
| } else { | ||
| log.info("목차 이미 존재 - 생성 스킵"); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n 'shortIntro|aiTasteComment|tasteAnalysis|tableOfContents' --type java -C 2 -g '**/entity/Books.java'Repository: Project-BookLog/BookLog-BackEnd
Length of output: 5402
🏁 Script executed:
# Also find the Books entity file to inspect its structure
fd -name 'Books.java' --type fRepository: Project-BookLog/BookLog-BackEnd
Length of output: 305
🏁 Script executed:
# Check for updateEnrichedInfo method to see how it processes these fields
rg -n 'updateEnrichedInfo' --type java -A 10Repository: Project-BookLog/BookLog-BackEnd
Length of output: 3257
Defensive check needed for empty strings on all enrichment fields.
The enrichment logic only checks for null when deciding to regenerate (lines 59, 65, 71), but the entity's updateEnrichedInfo() method accepts any non-null value without validation. If generation methods return empty strings "" instead of null on failure, those empty values persist in the database and prevent future regeneration attempts. Note that tableOfContents already has protection against this (checking !generatedToc.equals("[]")), but shortIntro, aiTasteComment, and tasteAnalysis do not. Either enforce that generation methods always return null on failure, or add empty-string checks for consistency across all four fields.
🤖 Prompt for AI Agents
In
`@booklog/src/main/java/com/example/booklog/domain/library/books/service/BookEnrichmentService.java`
around lines 51 - 97, The enrichment logic only treats null as missing; update
the checks for shortIntro, aiTasteComment, and tasteAnalysis to also treat empty
or blank strings as missing (e.g., trim and check isEmpty) before calling
generateShortIntro, generateAiTasteComment, or generateTasteAnalysis, and only
assign/mark needsUpdate when the generated value is non-blank; keep the existing
robust handling for tableOfContents
(isTableOfContentsEmpty/extractTableOfContents) and ensure values passed to the
entity method updateEnrichedInfo() are non-null and non-empty so empty strings
won't persist and block future regeneration.
| /** | ||
| * 목차 정보 추출 | ||
| * - rawData(JSON)에서 목차 정보가 있으면 추출 | ||
| * - 없으면 null 반환 (실패해도 괜찮음) | ||
| * - GPT API를 사용하여 책 제목과 저자 정보로 목차 생성 | ||
| * - 실패하면 빈 배열 반환 | ||
| */ | ||
| /** | ||
| * 목차 정보 추출 | ||
| * - GPT API를 사용하여 책 제목과 저자 정보로 목차 생성 | ||
| * - GPT 실패 시 기본 템플릿 제공 | ||
| */ |
There was a problem hiding this comment.
Duplicate Javadoc block.
There are two consecutive Javadoc comments for extractTableOfContents (lines 282–286 and 287–291). Remove one.
Suggested fix
- /**
- * 목차 정보 추출
- * - GPT API를 사용하여 책 제목과 저자 정보로 목차 생성
- * - 실패하면 빈 배열 반환
- */
/**
* 목차 정보 추출
* - GPT API를 사용하여 책 제목과 저자 정보로 목차 생성
* - GPT 실패 시 기본 템플릿 제공
*/📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * 목차 정보 추출 | |
| * - rawData(JSON)에서 목차 정보가 있으면 추출 | |
| * - 없으면 null 반환 (실패해도 괜찮음) | |
| * - GPT API를 사용하여 책 제목과 저자 정보로 목차 생성 | |
| * - 실패하면 빈 배열 반환 | |
| */ | |
| /** | |
| * 목차 정보 추출 | |
| * - GPT API를 사용하여 책 제목과 저자 정보로 목차 생성 | |
| * - GPT 실패 시 기본 템플릿 제공 | |
| */ | |
| /** | |
| * 목차 정보 추출 | |
| * - GPT API를 사용하여 책 제목과 저자 정보로 목차 생성 | |
| * - GPT 실패 시 기본 템플릿 제공 | |
| */ |
🤖 Prompt for AI Agents
In
`@booklog/src/main/java/com/example/booklog/domain/library/books/service/BookEnrichmentService.java`
around lines 282 - 291, In BookEnrichmentService remove the duplicated Javadoc
for extractTableOfContents so only a single docblock remains; keep the intended
description that mentions using the GPT API and the fallback behavior ("GPT 실패 시
기본 템플릿 제공") and delete the redundant comment immediately adjacent to it to avoid
duplicate JavaDoc entries for the extractTableOfContents method.
| private OnboardingBasedRecommendationResponse.RecommendationSection generateLibraryAuthorSection( | ||
| List<UserBooks> userBooks, String recentSearches) { | ||
|
|
||
| List<BookRecommendationCardResponse> books = new ArrayList<>(); | ||
|
|
||
| for (UserBooks userBook : userBooks) { | ||
| if (books.size() >= 2) break; | ||
|
|
||
| try { | ||
| Books book = userBook.getBook(); | ||
| String authorInfo = book.getBookAuthors().stream() | ||
| .filter(ba -> ba.getRole() == com.example.booklog.domain.library.books.entity.AuthorRole.AUTHOR) | ||
| .map(ba -> ba.getAuthor().getName()) | ||
| .collect(Collectors.joining(", ")); | ||
|
|
||
| if (!authorInfo.isEmpty()) { | ||
| BookRecommendationCardResponse card = generateLibraryBasedCard( | ||
| book.getTitle(), authorInfo, recentSearches); | ||
| if (card != null) { | ||
| books.add(card); | ||
| } | ||
| } | ||
| } catch (Exception e) { | ||
| log.error("서재 작가 섹션 카드 생성 실패", e); | ||
| } | ||
| } | ||
|
|
||
| return OnboardingBasedRecommendationResponse.RecommendationSection.builder() | ||
| .title("서재의 작가와 비슷한 작가") | ||
| .description("서재에 담긴 책의 작가와 유사한 작품을 추천드립니다") | ||
| .books(books) | ||
| .build(); | ||
| } | ||
|
|
||
| /** | ||
| * 서재 기반 장르 섹션 생성 | ||
| */ | ||
| private OnboardingBasedRecommendationResponse.RecommendationSection generateLibraryGenreSection( | ||
| List<UserBooks> userBooks, String recentSearches) { | ||
|
|
||
| List<BookRecommendationCardResponse> books = new ArrayList<>(); | ||
|
|
||
| for (UserBooks userBook : userBooks) { | ||
| if (books.size() >= 2) break; | ||
|
|
||
| try { | ||
| Books book = userBook.getBook(); | ||
| String authorInfo = book.getBookAuthors().stream() | ||
| .filter(ba -> ba.getRole() == com.example.booklog.domain.library.books.entity.AuthorRole.AUTHOR) | ||
| .map(ba -> ba.getAuthor().getName()) | ||
| .collect(Collectors.joining(", ")); | ||
|
|
||
| if (!authorInfo.isEmpty()) { | ||
| BookRecommendationCardResponse card = generateLibraryBasedCard( | ||
| book.getTitle(), authorInfo, recentSearches); | ||
| if (card != null) { | ||
| books.add(card); | ||
| } | ||
| } | ||
| } catch (Exception e) { | ||
| log.error("서재 장르 섹션 카드 생성 실패", e); | ||
| } | ||
| } | ||
|
|
||
| // 작가 정보 추출 | ||
| return OnboardingBasedRecommendationResponse.RecommendationSection.builder() | ||
| .title("서재의 책과 비슷한 장르") | ||
| .description("서재에 담긴 책과 유사한 장르의 작품을 추천드립니다") | ||
| .books(books) | ||
| .build(); | ||
| } | ||
|
|
||
| /** | ||
| * 서재 기반 분위기 섹션 생성 | ||
| */ | ||
| private OnboardingBasedRecommendationResponse.RecommendationSection generateLibraryMoodSection( | ||
| List<UserBooks> userBooks, String recentSearches) { | ||
|
|
||
| List<BookRecommendationCardResponse> books = new ArrayList<>(); | ||
|
|
||
| for (UserBooks userBook : userBooks) { | ||
| if (books.size() >= 2) break; | ||
|
|
||
| try { | ||
| Books book = userBook.getBook(); | ||
| String authorInfo = book.getBookAuthors().stream() | ||
| .filter(ba -> ba.getRole() == com.example.booklog.domain.library.books.entity.AuthorRole.AUTHOR) | ||
| .map(ba -> ba.getAuthor().getName()) | ||
| .collect(Collectors.joining(", ")); | ||
|
|
||
| if (authorInfo.isEmpty()) { | ||
| continue; | ||
| if (!authorInfo.isEmpty()) { | ||
| BookRecommendationCardResponse card = generateLibraryBasedCard( | ||
| book.getTitle(), authorInfo, recentSearches); | ||
| if (card != null) { | ||
| books.add(card); | ||
| } | ||
| } | ||
| } catch (Exception e) { | ||
| log.error("서재 분위기 섹션 카드 생성 실패", e); | ||
| } | ||
| } | ||
|
|
||
| return OnboardingBasedRecommendationResponse.RecommendationSection.builder() | ||
| .title("서재의 책과 비슷한 분위기") | ||
| .description("서재에 담긴 책과 유사한 분위기의 작품을 추천드립니다") | ||
| .books(books) | ||
| .build(); | ||
| } |
There was a problem hiding this comment.
All three library-based sections produce identical recommendations — likely a logic bug.
generateLibraryAuthorSection, generateLibraryGenreSection, and generateLibraryMoodSection iterate the same userBooks list, extract the same author info, and call generateLibraryBasedCard with the same arguments. The only difference is the section title/description. This means the "genre" and "mood" sections will return the exact same book recommendations as the "author" section.
If the intent is to differentiate sections, the GPT prompt or search criteria must vary per section (e.g., passing a section type or different fieldName to generateLibraryBasedCard). Additionally, the three methods are ~95% duplicated code — once the logic is fixed, extract a shared helper parameterized by section type, title, and description.
🤖 Prompt for AI Agents
In
`@booklog/src/main/java/com/example/booklog/domain/onboarding/service/OnboardingRecommendationService.java`
around lines 157 - 263, The three methods generateLibraryAuthorSection,
generateLibraryGenreSection, and generateLibraryMoodSection currently extract
identical authorInfo and call generateLibraryBasedCard the same way, producing
duplicate recommendations; fix by varying the search criterion per section
(e.g., pass a sectionType or fieldName into generateLibraryBasedCard) and
extract a single helper to remove duplication. Specifically, modify
generateLibraryGenreSection to extract genre info from book (e.g.,
book.getGenres() or the appropriate genre relationship) and pass a distinct
identifier like "genre" to generateLibraryBasedCard; modify
generateLibraryMoodSection to extract mood/tone metadata (e.g., book.getMood()
or tags) and pass "mood"; update generateLibraryBasedCard signature to accept
the section type/field name and use it when building the GPT/search prompt;
finally, consolidate the looping/limit logic into a private helper method (e.g.,
buildLibrarySection(List<UserBooks>, String recentSearches, String sectionType,
String title, String description)) and have each of the three section methods
call that helper with the correct extractor/sectionType, title, and description.
| private OnboardingBasedRecommendationResponse.RecommendationSection generateAuthorSection( | ||
| UserReadingProfile profile, String recentSearches) { | ||
|
|
||
| List<BookRecommendationCardResponse> books = new ArrayList<>(); | ||
|
|
||
| try { | ||
| // 문체/표현 방향 기반으로 작가 스타일 추천 | ||
| RecommendationCriteria criteria = null; | ||
|
|
||
| if (profile.getExpressionTexture() != null) { | ||
| criteria = new RecommendationCriteria( | ||
| "expressionTexture", | ||
| profile.getExpressionTexture().name(), | ||
| profile.getExpressionTexture().getLabel() | ||
| ); | ||
| } else if (profile.getSentenceBreath() != null) { | ||
| criteria = new RecommendationCriteria( | ||
| "sentenceBreath", | ||
| profile.getSentenceBreath().name(), | ||
| profile.getSentenceBreath().getLabel() | ||
| ); | ||
| } | ||
|
|
||
| if (card != null) { | ||
| recommendations.add(card); | ||
| processedCount++; | ||
| if (criteria != null) { | ||
| // 2개의 도서 추천 | ||
| for (int i = 0; i < 2; i++) { | ||
| BookRecommendationCardResponse card = generateOnboardingBasedCard( | ||
| criteria, recentSearches, profile); | ||
| if (card != null) { | ||
| books.add(card); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| } catch (Exception e) { | ||
| log.error("서재 기반 추천 카드 생성 실패 - bookId: {}, error: {}", | ||
| userBook.getBook().getId(), e.getMessage()); | ||
| // 개별 카드 실패는 무시하고 계속 진행 | ||
| } catch (Exception e) { | ||
| log.error("작가 섹션 생성 실패", e); | ||
| } | ||
|
|
||
| return OnboardingBasedRecommendationResponse.RecommendationSection.builder() | ||
| .title("이런 작가는 어떠세요?") | ||
| .description("선택하신 문체 취향을 바탕으로 추천드립니다") | ||
| .books(books) | ||
| .build(); | ||
| } |
There was a problem hiding this comment.
Onboarding author section: the loop may call GPT with the same criteria twice, yielding duplicate books.
Lines 319–325 call generateOnboardingBasedCard twice in a loop with the exact same criteria, recentSearches, and profile. Since the GPT prompt inputs are identical, you're likely to get the same search keyword back and thus the same book from Kakao (first result). Consider varying the request (e.g., requesting a different index from Kakao results, or passing an exclusion list) to ensure distinct recommendations.
The same pattern applies to generateGenreSection (line 367) and generateMoodSection (line 409).
🤖 Prompt for AI Agents
In
`@booklog/src/main/java/com/example/booklog/domain/onboarding/service/OnboardingRecommendationService.java`
around lines 294 - 337, The loop in generateAuthorSection repeatedly calls
generateOnboardingBasedCard with identical inputs (criteria, recentSearches,
profile) which can return duplicate books; modify generateOnboardingBasedCard
(and mirror changes for generateGenreSection and generateMoodSection) to accept
an additional parameter (e.g., excludeIds or resultOffset) or return unique
suggestions when given an exclusion list, then in the for-loop pass a different
offset or accumulated exclude list each iteration so each call requests a
different Kakao result (or omits already-selected book IDs) to ensure distinct
recommendations; update RecommendationCriteria usage only if needed to carry the
new parameter and ensure generateOnboardingBasedCard checks the exclude list
before returning a card.
Summary by CodeRabbit
Release Notes
New Features
Improvements