feat: 사용자 도서 추천 기능(홈)#108
Conversation
📝 WalkthroughWalkthroughAdds an AI-powered recommendation feature: OpenAI GPT integration, DTOs, a GptService, onboarding recommendation orchestration, controller endpoint, utility matcher, config changes, and application.yaml defaults for OpenAI settings. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Controller as OnboardingController
participant Service as OnboardingRecommendationService
participant GptService as GptService
participant KakaoAPI as Kakao Book API
participant OpenAI as OpenAI GPT API
participant DB as Database
User->>Controller: GET /api/v1/onboarding/recommendations
Controller->>Service: generateRecommendations(userId)
Service->>DB: fetch user library / onboarding profile / popular books
alt library path
Service->>GptService: generateRecommendationKeyword(request)
GptService->>OpenAI: POST chat/completions
OpenAI-->>GptService: response
GptService-->>Service: GptRecommendationResponse
Service->>KakaoAPI: search books with keyword
KakaoAPI-->>Service: book results
Service->>GptService: analyzeBookKeywords(title, author, publisher)
GptService->>OpenAI: POST chat/completions
OpenAI-->>GptService: response
GptService-->>Service: mood/style/immersion
else onboarding path
loop per criterion
Service->>GptService: generateRecommendationKeyword(request)
GptService->>OpenAI: POST chat/completions
OpenAI-->>GptService: response
GptService-->>Service: GptRecommendationResponse
Service->>KakaoAPI: search books with keyword
KakaoAPI-->>Service: book results
end
else popular path
loop per book
Service->>GptService: analyzeBookKeywords(title, author, publisher)
GptService->>OpenAI: POST chat/completions
OpenAI-->>GptService: response
GptService-->>Service: mood/style/immersion
end
end
Service-->>Controller: OnboardingBasedRecommendationResponse
Controller-->>User: 200 OK + recommendations
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
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: 4
🤖 Fix all issues with AI agents
In `@booklog/src/main/java/com/example/booklog/domain/ai/service/GptService.java`:
- Around line 52-55: When returning the parsed GPT result in GptService (after
calling parseGptResponse and before the log/return), guard against an empty
response.getSearchKeyword(): if it's null/blank, construct a safe fallback
(e.g., combine non-empty genre and authorType, or use a default token like
"general" or the original prompt-derived hint) and set it on the
GptRecommendationResponse so downstream code never receives an empty query;
apply the same defensive check and fallback at the other occurrence referenced
(around lines 280–283) where GptRecommendationResponse is returned.
In
`@booklog/src/main/java/com/example/booklog/domain/onboarding/service/KeywordSimilarityMatcher.java`:
- Around line 9-12: The class JavaDoc and any inline comments claiming "cosine
similarity" and "jamo decomposition" are inaccurate; update the documentation on
KeywordSimilarityMatcher and any related comment blocks (including the other
comment span around lines 45-47) to state that the implementation uses
character-set Jaccard similarity (describe that it decomposes strings into
character sets and computes |A∩B|/|A∪B|), replace references to "cosine" and
"jamo" with "character-set Jaccard" or "character set decomposition", and ensure
the class-level and method-level docs (e.g., the KeywordSimilarityMatcher
JavaDoc and any compute/score method comments) correctly describe the actual
algorithm and any parameters/threshold semantics.
In `@booklog/src/main/java/com/example/booklog/global/config/GptConfig.java`:
- Around line 3-20: The RestTemplate bean in GptConfig.restTemplate() lacks
connect/read timeouts; update the bean to use RestTemplateBuilder (inject it
into the method or constructor) and configure setConnectTimeout and
setReadTimeout (e.g., 2000ms) before building the RestTemplate so external GPT
API calls can't hang indefinitely; ensure the bean signature still returns
RestTemplate and remove the direct new RestTemplate() usage.
In `@booklog/src/main/resources/application.yaml`:
- Around line 58-59: The secret-key property currently defaults to
"dummy-key-for-development", letting the app start without GPT_SECRET_API_KEY;
change the application.yaml entry for secret-key to make the environment
variable required (fail fast) by using the environment variable syntax that
throws on missing values for GPT_SECRET_API_KEY so the application fails at
startup when the key is not set; update the documentation/comment near
model/secret-key to reflect the new required behavior.
🧹 Nitpick comments (5)
booklog/src/main/java/com/example/booklog/domain/onboarding/service/OnboardingRecommendationService.java (4)
100-105: Broad exception handling may hide bugs.The
catch (Exception e)swallows all exceptions including potential programming errors (e.g.,NullPointerException,IllegalStateException). While returning an empty response provides resilience, it may mask underlying bugs that should surface during development/testing.Consider catching more specific exceptions or at minimum logging at
errorlevel with stack trace (which you do), but also consider whether certain exceptions should propagate (e.g., database connectivity issues might warrant a 503 response).
288-338: Misleading method name and potential performance bottleneck.Two concerns:
Misleading naming: The method
generatePopularBooksRecommendationsand variablepopularBookssuggest popularity-based ranking, but the implementation usesSort.by("publishedDate")- fetching the most recently published books, not the most popular ones. Consider renaming togenerateLatestBooksRecommendationsor implementing actual popularity metrics.Sequential GPT calls in stream: Line 306 calls
gptService.analyzeBookKeywords()insidestream.map(), resulting in 6 sequential blocking API calls. This could significantly slow down response times.♻️ Suggested fix for naming clarity
- private OnboardingBasedRecommendationResponse generatePopularBooksRecommendations() { + private OnboardingBasedRecommendationResponse generateLatestBooksRecommendations() { try { - // 최근 출판된 인기 도서 조회 (publishedDate 기준 최신순) + // 최근 출판된 도서 조회 (publishedDate 기준 최신순) PageRequest pageRequest = PageRequest.of(0, MAX_RECOMMENDATIONS, Sort.by(Sort.Direction.DESC, "publishedDate").and(Sort.by(Sort.Direction.DESC, "id"))); - List<Books> popularBooks = booksRepository.findAll(pageRequest).getContent(); + List<Books> latestBooks = booksRepository.findAll(pageRequest).getContent(); - List<BookRecommendationCardResponse> recommendations = popularBooks.stream() + List<BookRecommendationCardResponse> recommendations = latestBooks.stream()
519-541: Consider using a Java record for this immutable data carrier.Since the codebase appears to use Java 17+ (evidenced by the switch expression on line 416), this inner class could be replaced with a more concise record:
♻️ Suggested refactor to record
- private static class RecommendationCriteria { - private final String fieldName; - private final String fieldValue; - private final String fieldDescription; - - public RecommendationCriteria(String fieldName, String fieldValue, String fieldDescription) { - this.fieldName = fieldName; - this.fieldValue = fieldValue; - this.fieldDescription = fieldDescription; - } - - public String getFieldName() { - return fieldName; - } - - public String getFieldValue() { - return fieldValue; - } - - public String getFieldDescription() { - return fieldDescription; - } - } + private record RecommendationCriteria(String fieldName, String fieldValue, String fieldDescription) {}
185-235: Performance: Up to 12 sequential GPT API calls per recommendation card generation.Each library-based card makes 2 blocking GPT calls:
generateRecommendationKeyword()(line 198)analyzeBookKeywords()(line 218)With
MAX_RECOMMENDATIONS = 6, this results in up to 12 sequential blocking API calls, significantly impacting response latency. The codebase has Redis caching infrastructure in place (RedisCacheConfig), butGptServicemethods currently lack@Cacheableannotations.Consider:
- Adding
@Cacheableto both methods (infrastructure is already configured)- Batching GPT requests where possible
- Making keyword analysis optional or lazy-loaded
booklog/src/main/java/com/example/booklog/domain/onboarding/controller/OnboardingController.java (1)
133-149: API documentation is slightly inconsistent with other endpoints.Two minor observations:
Missing 404 response: Other endpoints in this controller document a 404 response for "사용자를 찾을 수 없음", but this endpoint only documents 200 and 401. While the service returns an empty response on errors (making 404 technically unreachable), consider documenting consistently for API consumers.
Description accuracy: The description states "온보딩 키워드와 최근 검색어를 바탕으로" but the service also uses library data (priority 1) and falls back to popular books. Consider updating the description to reflect all recommendation strategies:
📝 Suggested documentation update
`@Operation`( summary = "온보딩 기반 도서 추천", description = """ - 사용자의 온보딩 키워드와 최근 검색어를 바탕으로 개인화된 도서를 추천합니다. + 사용자의 서재 도서, 온보딩 키워드, 최근 검색어를 바탕으로 개인화된 도서를 추천합니다. + - 서재에 도서가 있으면 서재 기반 추천 + - 서재가 없고 온보딩이 있으면 온보딩 기반 추천 + - 둘 다 없으면 최신 도서 추천 - 최대 6개의 추천 카드를 반환합니다. - 추천 결과가 0개여도 200 OK를 반환합니다. - - 각 추천 카드는 온보딩 필드 1개를 기준으로 생성됩니다. """ )
| GptRecommendationResponse response = parseGptResponse(gptResponse); | ||
|
|
||
| log.info("GPT 추천 키워드 생성 완료 - keyword: {}", response.getSearchKeyword()); | ||
| return response; |
There was a problem hiding this comment.
Guard against empty GPT keywords before returning
If GPT responds off‑format, searchKeyword can end up blank (especially if genre and authorType are also empty). Add a fallback to avoid empty queries downstream.
✅ Example fallback
GptRecommendationResponse response = parseGptResponse(gptResponse);
+String keyword = response.getSearchKeyword();
+if (keyword == null || keyword.isBlank()) {
+ String fallback = request.getFieldDescription();
+ if (fallback == null || fallback.isBlank()) {
+ fallback = request.getFieldValue();
+ }
+ keyword = (fallback == null || fallback.isBlank())
+ ? "추천 소설"
+ : fallback + " 소설";
+ response = GptRecommendationResponse.builder()
+ .authorType(response.getAuthorType())
+ .genre(response.getGenre())
+ .searchKeyword(keyword)
+ .build();
+}
log.info("GPT 추천 키워드 생성 완료 - keyword: {}", response.getSearchKeyword());
return response;Also applies to: 280-283
🤖 Prompt for AI Agents
In `@booklog/src/main/java/com/example/booklog/domain/ai/service/GptService.java`
around lines 52 - 55, When returning the parsed GPT result in GptService (after
calling parseGptResponse and before the log/return), guard against an empty
response.getSearchKeyword(): if it's null/blank, construct a safe fallback
(e.g., combine non-empty genre and authorType, or use a default token like
"general" or the original prompt-derived hint) and set it on the
GptRecommendationResponse so downstream code never receives an empty query;
apply the same defensive check and fallback at the other occurrence referenced
(around lines 280–283) where GptRecommendationResponse is returned.
| /** | ||
| * 키워드 유사도 매칭 유틸리티 | ||
| * - 코사인 유사도 기반 키워드 매칭 | ||
| * - 사전 정의된 키워드 집합 관리 |
There was a problem hiding this comment.
Align the docs with the actual similarity algorithm
The comments say “cosine similarity” and “jamo decomposition,” but the implementation is character‑set Jaccard. Please update the docs to reflect what’s implemented.
📝 Suggested doc update
- * - 코사인 유사도 기반 키워드 매칭
+ * - 문자 집합 기반 Jaccard 유사도 매칭
...
- * 한글 자모 분해 기반 간단한 유사도 계산
+ * 문자 집합 기반 간단한 유사도 계산 (Jaccard)
* - 실제 프로덕션에서는 Word2Vec, BERT 등 사용 권장
- * - 여기서는 문자열 유사도 기반 간이 구현
+ * - 여기서는 문자 집합 유사도 기반 간이 구현Also applies to: 45-47
🤖 Prompt for AI Agents
In
`@booklog/src/main/java/com/example/booklog/domain/onboarding/service/KeywordSimilarityMatcher.java`
around lines 9 - 12, The class JavaDoc and any inline comments claiming "cosine
similarity" and "jamo decomposition" are inaccurate; update the documentation on
KeywordSimilarityMatcher and any related comment blocks (including the other
comment span around lines 45-47) to state that the implementation uses
character-set Jaccard similarity (describe that it decomposes strings into
character sets and computes |A∩B|/|A∪B|), replace references to "cosine" and
"jamo" with "character-set Jaccard" or "character set decomposition", and ensure
the class-level and method-level docs (e.g., the KeywordSimilarityMatcher
JavaDoc and any compute/score method comments) correctly describe the actual
algorithm and any parameters/threshold semantics.
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.web.client.RestTemplate; | ||
|
|
||
| @Configuration | ||
| public class GptConfig { | ||
|
|
||
| @Value("${spring.openai.secret-key}") | ||
| private String secretKey; | ||
|
|
||
| @Value("${spring.openai.model}") | ||
| private String model; | ||
|
|
||
| @Bean | ||
| public RestTemplate restTemplate() { | ||
| return new RestTemplate(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Verify whether timeouts are configured elsewhere for RestTemplate usage
rg -n --type=java "RestTemplateBuilder|RestTemplateCustomizer|setConnectTimeout|setReadTimeout"Repository: Project-BookLog/BookLog-BackEnd
Length of output: 57
🏁 Script executed:
# Search for RestTemplate usage and any timeout configurations
rg -n --type=java "new RestTemplate\(\)|restTemplate()" -A 3 -B 1Repository: Project-BookLog/BookLog-BackEnd
Length of output: 1848
🏁 Script executed:
# Check for timeout configurations in properties or YAML files
rg -n "timeout|connect-timeout|read-timeout|socket-timeout" --type=yaml --type=propertiesRepository: Project-BookLog/BookLog-BackEnd
Length of output: 109
🏁 Script executed:
# Look for all RestTemplate bean definitions in the codebase
rg -n "@Bean" --type=java -A 5 | grep -A 5 "RestTemplate"Repository: Project-BookLog/BookLog-BackEnd
Length of output: 652
🏁 Script executed:
# Find configuration files
fd -e yml -e yaml -e properties | head -20Repository: Project-BookLog/BookLog-BackEnd
Length of output: 167
🏁 Script executed:
# Read application configuration
fd application -e yml -e yaml -e properties | xargs cat 2>/dev/nullRepository: Project-BookLog/BookLog-BackEnd
Length of output: 3036
🏁 Script executed:
# Check GptService for the actual RestTemplate usage context
rg -n "restTemplate\.exchange|restTemplate\." --type=java -B 2 -A 5 booklog/src/main/java/com/example/booklog/domain/ai/service/GptService.javaRepository: Project-BookLog/BookLog-BackEnd
Length of output: 439
Add HTTP timeouts to the RestTemplate bean
The RestTemplate bean lacks connect and read timeouts, which can cause request threads to hang indefinitely on external GPT API calls. While Redis is configured with a 2000ms timeout, the RestTemplate has no such protection. Use RestTemplateBuilder to set appropriate timeouts.
🔧 Suggested fix
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
+import java.time.Duration;
`@Bean`
-public RestTemplate restTemplate() {
- return new RestTemplate();
+public RestTemplate restTemplate(RestTemplateBuilder builder) {
+ return builder
+ .setConnectTimeout(Duration.ofSeconds(3))
+ .setReadTimeout(Duration.ofSeconds(10))
+ .build();
}🤖 Prompt for AI Agents
In `@booklog/src/main/java/com/example/booklog/global/config/GptConfig.java`
around lines 3 - 20, The RestTemplate bean in GptConfig.restTemplate() lacks
connect/read timeouts; update the bean to use RestTemplateBuilder (inject it
into the method or constructor) and configure setConnectTimeout and
setReadTimeout (e.g., 2000ms) before building the RestTemplate so external GPT
API calls can't hang indefinitely; ensure the bean signature still returns
RestTemplate and remove the direct new RestTemplate() usage.
| model: ${OPENAI_MODEL:gpt-4o-mini} | ||
| secret-key: ${GPT_SECRET_API_KEY:dummy-key-for-development} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, find all application config files
echo "=== All application config files ==="
fd -a 'application.*ya?ml' booklog/
echo -e "\n=== Current application.yaml content (lines 50-70) ==="
if [ -f "booklog/src/main/resources/application.yaml" ]; then
sed -n '50,70p' booklog/src/main/resources/application.yaml
else
echo "File not found"
fi
echo -e "\n=== Search for secret-key in all yaml files ==="
rg -n 'secret-key' -g '*.ya?ml' booklog/src/main/resources/
echo -e "\n=== Check for profile-specific files ==="
ls -la booklog/src/main/resources/application-*.yaml 2>/dev/null || echo "No profile-specific files found"Repository: Project-BookLog/BookLog-BackEnd
Length of output: 913
🏁 Script executed:
#!/bin/bash
echo "=== Search for README or setup docs ==="
fd -i readme -t f booklog/ | head -5
echo -e "\n=== Check pom.xml for Spring Boot version ==="
if [ -f "booklog/pom.xml" ]; then
grep -A 2 "spring-boot-starter" booklog/pom.xml | head -10
fi
echo -e "\n=== Search for environment variable documentation ==="
rg -i 'environment|openai.*key|gpt.*secret' -t md -t txt booklog/ | head -10
echo -e "\n=== Check for deployment/docker configs ==="
fd -a 'Dockerfile|docker-compose|\.env' booklog/
echo -e "\n=== Check for other references to OpenAI config ==="
rg 'OPENAI_MODEL|GPT_SECRET_API_KEY' booklog/ --type yaml --type java --type properties | head -20Repository: Project-BookLog/BookLog-BackEnd
Length of output: 366
🏁 Script executed:
#!/bin/bash
echo "=== Check entire pom.xml for Spring Boot ==="
find booklog -name "pom.xml" -exec head -50 {} \;
echo -e "\n=== Look for any setup or configuration documentation ==="
find booklog -type f \( -name "*.md" -o -name "*.txt" -o -name "*.adoc" \) | head -10
echo -e "\n=== Check application.yaml structure to understand full config ==="
head -100 booklog/src/main/resources/application.yamlRepository: Project-BookLog/BookLog-BackEnd
Length of output: 2858
Make OpenAI secret-key fail fast on missing configuration
The current default dummy-key-for-development allows the application to start even when GPT_SECRET_API_KEY is not provided, surfacing the error only at runtime when the API is called. This is inconsistent with how other required API keys are configured (database, Kakao OAuth, Kakao REST API) which have no defaults and fail immediately at startup. Use a fail-fast placeholder instead:
Recommended fix
- secret-key: ${GPT_SECRET_API_KEY:dummy-key-for-development}
+ secret-key: ${GPT_SECRET_API_KEY:?missing_gpt_key}📝 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.
| model: ${OPENAI_MODEL:gpt-4o-mini} | |
| secret-key: ${GPT_SECRET_API_KEY:dummy-key-for-development} | |
| model: ${OPENAI_MODEL:gpt-4o-mini} | |
| secret-key: ${GPT_SECRET_API_KEY:?missing_gpt_key} |
🤖 Prompt for AI Agents
In `@booklog/src/main/resources/application.yaml` around lines 58 - 59, The
secret-key property currently defaults to "dummy-key-for-development", letting
the app start without GPT_SECRET_API_KEY; change the application.yaml entry for
secret-key to make the environment variable required (fail fast) by using the
environment variable syntax that throws on missing values for GPT_SECRET_API_KEY
so the application fails at startup when the key is not set; update the
documentation/comment near model/secret-key to reflect the new required
behavior.
|
check |
홈 화면 상단에 있는 내가 좋아하는 ~ 책 추천 기능입니다.
사용한 AI 모델은 gpt-4o-mini이고, 책 정보는 카카오 API KEY를 이용하였습니다.
Summary by CodeRabbit
New Features
Bug Fixes
✏️ Tip: You can customize this high-level summary in your review settings.