Skip to content

feat: 목차#160

Merged
millkk04 merged 1 commit into
devfrom
feat/156/book
Feb 10, 2026
Merged

feat: 목차#160
millkk04 merged 1 commit into
devfrom
feat/156/book

Conversation

@millkk04
Copy link
Copy Markdown
Collaborator

@millkk04 millkk04 commented Feb 10, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Books now include AI-generated tables of contents
    • Book recommendations reorganized into three separate sections: author-based, genre-based, and mood-based recommendations
  • Improvements

    • Enhanced detection of complete Korean sentences in book descriptions for better text processing

@millkk04 millkk04 self-assigned this Feb 10, 2026
@millkk04 millkk04 added the enhancement New feature or request label Feb 10, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
GPT-Based TOC Generation
GptService.java
Adds new method generateTableOfContents() with helper methods to build GPT prompts and parse TOC responses into structured lists, with error handling that returns empty results on failure.
Book Enrichment Integration
BookEnrichmentService.java, BookQueryServiceImpl.java
Integrates GptService into enrichment workflow; replaces monolithic skip logic with field-by-field update tracking; adds TOC extraction with fallback generation; introduces EntityManager refresh for consistency.
Korean Sentence Detection
BookDescriptionEnhancer.java
Expands set of Korean sentence-ending patterns (없다, 같다, 나다, etc.) to improve incomplete sentence detection.
Recommendation Restructuring
OnboardingBasedRecommendationResponse.java, OnboardingRecommendationService.java
Refactors recommendation response from flat list structure to three-section model (authorSection, genreSection, moodSection); updates service to generate distinct section-based recommendations for library, onboarding, and popular books flows.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • jaehyeon4406
  • icarus0616

Poem

🐰 A rabbit hops through tables of contents bright,
GPT whispers wisely through the digital night,
Recommendations bloom in three section flowers,
Korean sentences now complete with fuller powers! 📚✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title '목차' (Table of Contents) is vague and lacks specificity about what implementation or changes were made. Consider a more descriptive title such as 'feat: Add GPT-based table of contents generation' to better communicate the main change and implementation details.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/156/book

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@millkk04 millkk04 merged commit 7fba89d into dev Feb 10, 2026
1 check was pending
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

findLastKoreanSentenceEnding is out of sync with isIncompleteSentence.

The endings array here is used by truncateToLastCompleteSentence to find valid cut points, but it doesn't include the newly added patterns (같다, 나다, 뜻하다, 받다, 가다, 오다, 하다, 되다, 이다) nor several original ones (, 세요). This means completeIncompleteSentence may fail to truncate at a sentence that isIncompleteSentence would 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_ENDINGS in both isIncompleteSentence and findLastKoreanSentenceEnding instead of maintaining two separate lists.

booklog/src/main/java/com/example/booklog/domain/onboarding/service/OnboardingRecommendationService.java (1)

486-552: ⚠️ Potential issue | 🟡 Minor

Popular books path: sections are labeled by author/genre/mood but the split is arbitrary.

The 6 popular books are fetched by publishedDate DESC 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, genreBooks and moodBooks will 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 | 🟠 Major

Excessive 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 info level. 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 at info.

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 defaulting books to an empty list to guard against null.

If any section is built without explicitly setting books, downstream consumers could encounter a NullPointerException when 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 existing trimmed.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), then endsWith("다") 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 unused extractRecommendationCriteria method and MAX_RECOMMENDATIONS constant.

extractRecommendationCriteria is never called and MAX_RECOMMENDATIONS is 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: Redundant flush()enrichBookInfo already flushes internally.

BookEnrichmentService.enrichBookInfo (line 104–105 of that file) already calls save + flush. The booksRepository.flush() here on line 83 is a no-op in the same transaction. It's harmless but unnecessary.

Also, logging the full tableOfContents JSON at info level on every book detail request will be noisy in production — consider debug.

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:

  1. The hardcoded TOC for one specific book won't scale and will become stale if the codebase grows.
  2. 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 at info level.

Same concern as in GptService — lines 78–79, 82, 84, 90, 92, 96, 100, 102, 107 all log at info with detailed diagnostic content. This will be very noisy on every book detail view. Consider debug for the step-by-step diagnostics and keep only the final outcome at info.

Comment on lines +309 to +353
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();
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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:

  1. Parameterizing max_tokens in callGptApiForSimpleText, or
  2. 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.

Comment on lines +358 to +380
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();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +51 to +97
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("목차 이미 존재 - 생성 스킵");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 f

Repository: 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 10

Repository: 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.

Comment on lines 282 to 291
/**
* 목차 정보 추출
* - rawData(JSON)에서 목차 정보가 있으면 추출
* - 없으면 null 반환 (실패해도 괜찮음)
* - GPT API를 사용하여 책 제목과 저자 정보로 목차 생성
* - 실패하면 빈 배열 반환
*/
/**
* 목차 정보 추출
* - GPT API를 사용하여 책 제목과 저자 정보로 목차 생성
* - GPT 실패 시 기본 템플릿 제공
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
/**
* 목차 정보 추출
* - 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.

Comment on lines +157 to +263
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();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Comment on lines +294 to +337
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();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

This was referenced Feb 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant