Conversation
… feat/regionSearch2
… feat/regionSearch3
- EmbeddingService null 값 처리 - EmbeddingBatchController, EmbeddingBatchServci로 저장 로직 추가
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Summary by CodeRabbit
Walkthrough이 변경 사항은 추천 서비스의 반환 타입을 문자열에서 구조화된 DTO( Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant RecommendController
participant RecommendService
participant VectorStoreService
participant ContentService
User->>RecommendController: POST /recommend (promptMap)
RecommendController->>RecommendService: recommend(promptMap)
RecommendService->>VectorStoreService: findSimilarWithFilter(query, k, filter)
VectorStoreService-->>RecommendService: List<Document>
RecommendService->>ContentService: getImageUrl(contentId) & getPetInfo(contentId)
ContentService-->>RecommendService: 이미지 URL, 반려동물 정보
RecommendService-->>RecommendController: RecommendResponseDTO
RecommendController-->>User: ResponseEntity<RecommendResponseDTO>
sequenceDiagram
participant Admin
participant EmbeddingBatchController
participant EmbeddingBatchService
participant ContentRepository
participant VectorStoreService
Admin->>EmbeddingBatchController: POST /embedding-batch/run
EmbeddingBatchController->>EmbeddingBatchService: saveAllContentsInBatch()
EmbeddingBatchService->>ContentRepository: findTop20ByOrderByContentIdAsc()
ContentRepository-->>EmbeddingBatchService: List<Content>
EmbeddingBatchService->>VectorStoreService: saveContents(List<Content>)
VectorStoreService-->>EmbeddingBatchService: (void)
EmbeddingBatchService-->>EmbeddingBatchController: (void)
EmbeddingBatchController-->>Admin: "Vector DB 구성 완료"
Possibly related PRs
Poem
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 11
🔭 Outside diff range comments (1)
src/main/java/io/github/petty/llm/service/RecommendServiceImpl.java (1)
63-67:⚠️ Potential issue
isDanger널 체크 누락으로 NPE 가능
isDanger.equals("true")호출 전isDanger가 null인지 확인해야 합니다.- if(isDanger.equals("true")) { + if ("true".equals(isDanger)) {상수 먼저 비교하면 동시에 NPE 도 방지할 수 있습니다.
🧹 Nitpick comments (13)
src/main/java/io/github/petty/pipeline/controller/PipelineController.java (1)
40-46: 주석 처리된 코드를 정리하는 것이 좋겠습니다.40번 줄에 주석 처리된 코드가 있습니다. 이 줄은 더 이상 필요하지 않다면 제거하는 것이 코드 가독성 측면에서 좋겠습니다.
-// String prompt = togetherPromptBuilder.buildPrompt(visionReport, location);src/main/java/io/github/petty/llm/dto/RecommendResponseDTO.java (1)
5-18: 응답 DTO가 잘 설계되었습니다. 일부 필드에 대한 주석 추가를 고려해보세요.Record를 사용한 불변 데이터 구조는 응답 포맷을 명확하게 정의하는 좋은 방법입니다. 이미지 URL과 함께 장소 추천 정보를 포함하는 구조로 PR의 목표에 잘 부합합니다.
한 가지 제안하자면,
acmpyTypeCd,acmpyPsblCpam,acmpyNeedMtr와 같은 필드명은 의미가 명확하지 않을 수 있으므로, 각 필드에 Javadoc 주석을 추가하면 코드의 가독성과 유지보수성이 향상될 것입니다.// 추천 응답 반환 public record RecommendResponseDTO ( List<PlaceRecommend> recommend ) { public record PlaceRecommend ( String contentId, String title, String addr, String description, String imageUrl, + // 동반 유형 코드 String acmpyTypeCd, + // 동반 가능 용량 String acmpyPsblCpam, + // 동반 필요 물품 String acmpyNeedMtr ) {} }src/main/java/io/github/petty/llm/controller/EmbeddingBatchController.java (1)
12-23: 배치 처리를 위한 컨트롤러 구현이 적절합니다.Qdrant 연결 문제 해결을 위한 배치 처리 컨트롤러가 잘 구현되었습니다. 대량의 데이터를 한 번에 처리하는 방식은 개별 처리보다 효율적일 수 있습니다.
다만, 이 엔드포인트는 리소스를 많이 사용하는 작업을 트리거하므로 보안 측면을 고려해볼 필요가 있습니다. 인증된 관리자만 접근할 수 있도록 권한 제어를 추가하는 것이 좋을 것 같습니다.
@RestController @RequestMapping("/embedding-batch") @RequiredArgsConstructor +@PreAuthorize("hasRole('ADMIN')") // Spring Security를 사용하는 경우 public class EmbeddingBatchController { private final EmbeddingBatchService embeddingBatchService; // 기본 저장 @PostMapping("/run") public String runBatch() { embeddingBatchService.saveAllContentsInBatch(); return "Vector DB 구성 완료"; }src/main/java/io/github/petty/llm/service/EmbeddingBatchService.java (1)
32-82: 주석 처리된 고급 배치 처리 기능을 활성화하는 것을 고려해보세요.주석 처리된 코드에는 더 강력한 배치 처리 기능(전체 콘텐츠 처리, 중복 검사, 실패 항목 재시도 등)이 포함되어 있습니다. 이러한 기능들은 Qdrant 연결 문제를 더욱 효과적으로 해결하는 데 도움이 될 수 있습니다.
특히 중복 방지와 실패 항목 재시도 기능은 시스템의 안정성을 크게 향상시킬 수 있으므로, 이 기능들을 구현 완료하고 활성화하는 것을 고려해보세요. 추가적으로, 트랜잭션 관리와 배치 크기 조정 기능도 유용할 수 있습니다.
src/main/java/io/github/petty/llm/service/VectorStoreService.java (1)
5-6: 중복 import 제거 필요
lombok.extern.slf4j.Slf4j가 두 번 선언되어 있습니다. 빌드 속도와 가독성에 모두 불필요한 부담이므로 하나를 삭제해 주세요.Also applies to: 13-14
src/main/resources/templates/recommend.html (2)
1-3: 파일 주석이 실제 경로와 불일치
<!-- src/main/resources/templates/index.html -->라는 주석은 혼동을 줄 수 있습니다. 현재 파일은recommend.html이므로 주석을 수정하거나 삭제해 주세요.
168-180: HTTP 오류 처리 누락
fetch결과가!res.ok인 경우 사용자에게 오류 메시지를 보여주지 않아 원인을 파악하기 어렵습니다. 상태코드 체크 후 예외 처리 로직을 추가해 주세요.src/main/java/io/github/petty/llm/service/EmbeddingService.java (1)
131-134: 임베딩 누락 시 예외 메시지 개선 제안
IllegalArgumentException메시지에 콘텐츠 제목도 포함하면 디버깅이 쉬워집니다.- throw new IllegalArgumentException("Embedding할 text가 비어있습니다: contentId=" + content.getContentId()); + throw new IllegalArgumentException( + "Embedding할 text가 비어있습니다: contentId=" + content.getContentId() + + ", title=" + content.getTitle());src/main/java/io/github/petty/llm/service/GeminiPreprocessorServiceImpl.java (3)
21-29: 서비스 의존성 관리 개선 가능서비스 클래스가 잘 구성되어 있습니다. 다만, HttpClient를 필드로 선언하여 재사용하는 것이 성능상 더 좋을 수 있습니다. 현재는 매 요청마다 새로운 HttpClient를 생성하고 있습니다.
@Slf4j @Service @RequiredArgsConstructor public class GeminiPreprocessorServiceImpl implements GeminiPreprocessorService { @Value("${gemini.api.key}") private String apiKey; private final ObjectMapper objectMapper; + private final HttpClient httpClient = HttpClient.newHttpClient(); }
77-95: 프롬프트 구성 최적화프롬프트가 잘 구성되어 있습니다. 다만, 프롬프트 텍스트가 메소드 내에 하드코딩되어 있어 수정이 필요할 경우 코드 변경이 필요합니다. 설정 파일이나 데이터베이스에서 관리하는 것을 고려해보세요.
프롬프트 템플릿을 외부 파일로 분리하거나 설정으로 관리하면 엔지니어링 팀과 프롬프트 엔지니어링 팀의 작업을 분리할 수 있습니다.
97-105: 로깅 및 메소드 구조 개선generateFinalText 메소드에서 전체 프롬프트를 로깅하고 있는데, 프로덕션 환경에서는 불필요하게 로그 파일이 커질 수 있습니다. 또한 "prompt"라는 변수명이 실제로는 결과 텍스트를 의미하므로 변수명을 더 명확하게 하는 것이 좋습니다.
private String generateFinalText(Content content, String geminiResponse) { String ContentTypeName = ContentType.fromCode(content.getContentTypeId()).getName(); - String prompt = "%s은/는 %s 종류의 장소입니다.\n %s" + String finalText = "%s은/는 %s 종류의 장소입니다.\n %s" .formatted( content.getTitle(), ContentTypeName, geminiResponse ); - log.info(prompt); + log.debug("[전처리 완료] 콘텐츠 ID: {}", content.getContentId()); - return prompt; + return finalText; }src/main/java/io/github/petty/llm/service/ContentService.java (2)
26-35: 메소드 구현 적절함contentId를 파싱하고 예외 처리를 잘 구현했습니다. 다만 로그 메시지에 더 구체적인 정보를 추가하면 디버깅에 도움이 될 수 있습니다.
try { Long id = Long.parseLong(contentId); return contentRepository.findById(id); } catch (NumberFormatException e) { // contentid가 없을 때 - log.error("Invalid contentId format: {}", contentId, e); + log.error("컨텐츠 ID 형식이 유효하지 않습니다: {}", contentId, e); return Optional.empty(); }
73-86: 주석 처리된 코드 제거 필요주석 처리된 코드가 있습니다. 이 코드는 이제 사용되지 않는 것으로 보이며, 코드 가독성을 위해 제거하는 것이 좋습니다. 필요한 경우 Git 히스토리에서 참조할 수 있습니다.
-/* - if (pet != null) { - sb.append("반려 동물 정보: "); - if (pet.getAcmpyTypeCd() != null) - sb.append("동반 유형은 ").append(pet.getAcmpyTypeCd()).append(", "); - if (pet.getEtcAcmpyInfo() != null) - sb.append("가능 동물: ").append(pet.getEtcAcmpyInfo()).append(", "); - if (pet.getAcmpyPsblCpam() != null) - sb.append("추가 정보: ").append(pet.getAcmpyPsblCpam()).append(", "); - if (pet.getAcmpyNeedMtr() != null) - sb.append("준비물: ").append(pet.getAcmpyNeedMtr()).append(". "); - sb.append("\n"); - } -*/
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
src/main/resources/static/assets/noimg.pngis excluded by!**/*.png
📒 Files selected for processing (16)
src/main/java/io/github/petty/llm/common/ContentType.java(1 hunks)src/main/java/io/github/petty/llm/controller/EmbeddingBatchController.java(1 hunks)src/main/java/io/github/petty/llm/controller/RecommendController.java(2 hunks)src/main/java/io/github/petty/llm/dto/RecommendResponseDTO.java(1 hunks)src/main/java/io/github/petty/llm/service/ChatService.java(0 hunks)src/main/java/io/github/petty/llm/service/ChatServiceImpl.java(0 hunks)src/main/java/io/github/petty/llm/service/ContentService.java(1 hunks)src/main/java/io/github/petty/llm/service/EmbeddingBatchService.java(1 hunks)src/main/java/io/github/petty/llm/service/EmbeddingService.java(5 hunks)src/main/java/io/github/petty/llm/service/GeminiPreprocessorService.java(1 hunks)src/main/java/io/github/petty/llm/service/GeminiPreprocessorServiceImpl.java(1 hunks)src/main/java/io/github/petty/llm/service/RecommendService.java(1 hunks)src/main/java/io/github/petty/llm/service/RecommendServiceImpl.java(2 hunks)src/main/java/io/github/petty/llm/service/VectorStoreService.java(4 hunks)src/main/java/io/github/petty/pipeline/controller/PipelineController.java(2 hunks)src/main/resources/templates/recommend.html(3 hunks)
💤 Files with no reviewable changes (2)
- src/main/java/io/github/petty/llm/service/ChatService.java
- src/main/java/io/github/petty/llm/service/ChatServiceImpl.java
🔇 Additional comments (6)
src/main/java/io/github/petty/llm/common/ContentType.java (1)
11-15: enum 상수 설명에서 구분자 변경 적용이 일관되게 이루어졌습니다.slash(/) 대신 쉼표(,)를 사용하여 ContentType enum 상수의 설명을 일관되게 변경했습니다. 이러한 변경은 새로운 내용 전처리 및 추천 로직과 잘 통합되어 있습니다.
src/main/java/io/github/petty/llm/service/RecommendService.java (1)
3-4: 구조화된 응답 형식으로의 변경을 확인했습니다.'String'에서 'RecommendResponseDTO'로 반환 타입을 변경한 것은 좋은 개선입니다. 이렇게 하면 단순 텍스트가 아닌 구조화된 데이터를 클라이언트에 제공할 수 있어 더 풍부한 정보와 유연성을 제공합니다.
Also applies to: 8-8
src/main/java/io/github/petty/llm/service/GeminiPreprocessorService.java (1)
1-12: Gemini를 활용한 전처리 인터페이스 생성이 잘 설계되었습니다.인터페이스가 명확하고 목적에 맞게 잘 정의되었습니다. JavaDoc 주석이 메서드의 목적과 기능을 명확하게 설명하고 있어 유지보수성이 향상됩니다. 하나의 명확한 책임(Content 엔티티로부터 전처리된 텍스트 생성)을 가진 인터페이스를 만든 것은 단일 책임 원칙(SRP)을 잘 따른 설계입니다.
src/main/java/io/github/petty/pipeline/controller/PipelineController.java (1)
5-5: RecommendResponseDTO 사용으로 구조화된 응답 처리가 개선되었습니다.String 대신 RecommendResponseDTO를 사용하도록 변경한 것은 RecommendService의 변경 사항과 일관성이 있으며, 데이터를 더 구조화된 방식으로 처리할 수 있게 합니다. 이로써 뷰에서 추천 정보를 더 효과적으로 표시할 수 있습니다.
Also applies to: 46-46
src/main/java/io/github/petty/llm/controller/RecommendController.java (1)
4-4: 구조화된 응답 형식으로의 개선이 잘 이루어졌습니다.String 타입에서 RecommendResponseDTO로 반환 타입을 변경한 것은 좋은 개선입니다. 이를 통해 프론트엔드에서 추천 결과를 리스트 형태로 이미지와 함께 표시하기가 더 용이해질 것입니다. 구조화된 데이터 전송은 API의 확장성과 가독성을 높이는 좋은 접근법입니다.
Also applies to: 26-27
src/main/java/io/github/petty/llm/service/ContentService.java (1)
16-25: 서비스 선언 적절함서비스 클래스가 잘 구성되어 있습니다. 필요한 의존성이 적절히 주입되어 있고, 기본 이미지 URL도 상수로 정의되어 있습니다.
| // 실패 ID 재저장 | ||
| // @PostMapping("/retry") | ||
| // public String retryFailedEmbeddings(@RequestBody List<Long> failedContentIds) { | ||
| // embeddingBatchService.retryFailedContents(failedContentIds); | ||
| // return "재시도 완료!"; | ||
| // } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
재시도 기능 구현을 고려해보세요.
주석 처리된 재시도 기능은 실패한 임베딩을 복구하는 데 유용할 수 있습니다. Qdrant 연결 오류가 발생했을 때 견고성을 높이기 위해 이 기능을 활성화하는 것을 고려해보세요. 주석을 제거하고 실제 구현을 완료하면 시스템의 안정성이 향상될 것입니다.
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/llm/controller/EmbeddingBatchController.java
around lines 25 to 30, the retryFailedEmbeddings method is commented out. To
improve robustness against Qdrant connection errors, uncomment this method and
ensure it is fully implemented to accept a list of failed content IDs, call
embeddingBatchService.retryFailedContents with this list, and return a
confirmation message. This will enable retrying failed embeddings and enhance
system stability.
| // Content 50개 저장 | ||
| public void saveAllContentsInBatch() { | ||
| // 1. 상위 50개 Content 조회 | ||
| List<Content> top50Contents = contentRepository.findTop20ByOrderByContentIdAsc(); | ||
|
|
||
| log.info("Content 20개 가져오기 완료. 저장 시작!"); | ||
|
|
||
| // 2. 바로 저장 (중복 검사 없이) | ||
| vectorStoreService.saveContents(top50Contents); | ||
|
|
||
| log.info("Content 20개 저장 완료!"); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
배치 서비스 구현에 몇 가지 개선점이 있습니다.
현재 구현된 배치 서비스는 Qdrant 연결 문제 해결에 도움이 될 것으로 보입니다. 그러나 몇 가지 개선이 필요합니다:
- 메소드명
saveAllContentsInBatch()는 모든 콘텐츠를 저장한다는 의미이지만 실제로는 20개만 저장합니다. 메소드명을saveTop20ContentsInBatch()로 변경하는 것이 더 명확할 것 같습니다. - 주석은 50개 콘텐츠에 대해 언급하고 있지만, 실제 코드는 20개를 가져옵니다 (22, 24, 29줄). 주석과 코드의 일관성을 유지해주세요.
- 예외 처리가 없어 오류 발생 시 대응이 어려울 수 있습니다.
- // Content 50개 저장
+ // Content 20개 저장
- public void saveAllContentsInBatch() {
+ public void saveTop20ContentsInBatch() {
- // 1. 상위 50개 Content 조회
+ // 1. 상위 20개 Content 조회
List<Content> top50Contents = contentRepository.findTop20ByOrderByContentIdAsc();
log.info("Content 20개 가져오기 완료. 저장 시작!");
// 2. 바로 저장 (중복 검사 없이)
- vectorStoreService.saveContents(top50Contents);
+ try {
+ vectorStoreService.saveContents(top50Contents);
+ log.info("Content 20개 저장 완료!");
+ } catch (Exception e) {
+ log.error("Content 저장 중 오류 발생: {}", e.getMessage(), e);
+ throw new RuntimeException("벡터 DB 저장 실패", e);
+ }
-
- log.info("Content 20개 저장 완료!");
}📝 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.
| // Content 50개 저장 | |
| public void saveAllContentsInBatch() { | |
| // 1. 상위 50개 Content 조회 | |
| List<Content> top50Contents = contentRepository.findTop20ByOrderByContentIdAsc(); | |
| log.info("Content 20개 가져오기 완료. 저장 시작!"); | |
| // 2. 바로 저장 (중복 검사 없이) | |
| vectorStoreService.saveContents(top50Contents); | |
| log.info("Content 20개 저장 완료!"); | |
| } | |
| // Content 20개 저장 | |
| public void saveTop20ContentsInBatch() { | |
| // 1. 상위 20개 Content 조회 | |
| List<Content> top50Contents = contentRepository.findTop20ByOrderByContentIdAsc(); | |
| log.info("Content 20개 가져오기 완료. 저장 시작!"); | |
| // 2. 바로 저장 (중복 검사 없이) | |
| try { | |
| vectorStoreService.saveContents(top50Contents); | |
| log.info("Content 20개 저장 완료!"); | |
| } catch (Exception e) { | |
| log.error("Content 저장 중 오류 발생: {}", e.getMessage(), e); | |
| throw new RuntimeException("벡터 DB 저장 실패", e); | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/llm/service/EmbeddingBatchService.java lines 19
to 30, rename the method from saveAllContentsInBatch() to
saveTop20ContentsInBatch() to accurately reflect that only 20 contents are
processed. Update all comments to consistently mention 20 contents instead of
50. Add appropriate exception handling around the content retrieval and saving
logic to manage potential errors gracefully.
| // ContentID로 중복 제거 | ||
| public List<String> findAllContentIds() { | ||
| List<Document> allDocuments = vectorStore.similaritySearch(SearchRequest.builder() | ||
| .query("시") // 더미텍스트 | ||
| .topK(10000) | ||
| .build()); | ||
|
|
||
| return allDocuments.stream() | ||
| .map(doc -> doc.getMetadata().get("contentId").toString()) | ||
| .collect(Collectors.toList()); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
findAllContentIds 구현 방식은 부정확‧비효율적
- 더미 문자열
"시"로 similarity 검색을 수행하면
‑ 코사인 유사도 0.1 이상인 일부 문서만 반환돼 전체 ID를 확보하지 못할 수 있습니다. topK(10000)은 데이터 증가 시 누락 위험이 존재하며, 불필요한 대량 전송으로 Qdrant 부하를 유발합니다.
VectorStore 의 listAll()-계열 메서드가 없다면 다음과 같이 컬렉션 API를 이용한 paging-loop 로 전환을 권장합니다.
int offset = 0;
final int pageSize = 512;
List<String> ids = new ArrayList<>();
while (true) {
List<Document> page = vectorStore.similaritySearch(
SearchRequest.builder()
.query("*") // wildcard 또는 빈 쿼리 지원 여부 확인
.topK(pageSize)
.offset(offset)
.build());
if (page.isEmpty()) break;
ids.addAll(page.stream()
.map(d -> String.valueOf(d.getMetadata().get("contentId")))
.toList());
offset += pageSize;
}
return ids;🤖 Prompt for AI Agents
In src/main/java/io/github/petty/llm/service/VectorStoreService.java around
lines 92 to 102, the current findAllContentIds method uses a fixed dummy query
string and a large topK value, which can miss documents and overload the vector
store. To fix this, replace the similaritySearch call with a paging loop that
queries with a wildcard or empty query (if supported), uses a reasonable page
size (e.g., 512), and increments an offset to fetch all documents in batches
until no more results are returned. Collect contentId values from each page and
return the complete list after the loop.
| public void saveContents(List<Content> contents) { | ||
| List<Document> documents = contents.stream() | ||
| .map(content -> { | ||
| EmbeddingResult result = embeddingService.embedContent(content); | ||
| return embeddingService.toDocument(result, content); | ||
| try { | ||
| EmbeddingResult result = embeddingService.embedContent(content); | ||
| return embeddingService.toDocument(result, content); | ||
| } catch (IllegalArgumentException e) { | ||
| log.warn("Embedding 실패 - contentId: {}. 사유: {}", content.getContentId(), e.getMessage()); | ||
| return null; // 실패한 건 버림 | ||
| } | ||
| }) | ||
| .filter(Objects::nonNull) // null인 건 제외 | ||
| .collect(Collectors.toList()); | ||
|
|
||
| // Qdrant Vectorstore에 문서 추가 | ||
| vectorStore.add(documents); | ||
|
|
||
| if (!documents.isEmpty()) { | ||
| log.info("embedding 완료, documents {}개 저장 시작", documents.size()); | ||
| vectorStore.add(documents); | ||
| log.info("documents 저장 완료"); | ||
| } else { | ||
| log.info("저장할 documents 없음 (모두 실패)"); | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
예외 처리 범위가 좁아 실패 원인 누락 가능성
embedContent 내부에서는 IllegalArgumentException 외에도 네트워크 오류, 모델 호출 실패 등 다양한 예외가 발생할 수 있습니다. 현재는 IllegalArgumentException만 캐치하여 로그에 남기고 무시하고 있어 다른 예외가 발생하면 메서드 전체가 실패합니다.
- } catch (IllegalArgumentException e) {
- log.warn("Embedding 실패 - contentId: {}. 사유: {}", content.getContentId(), e.getMessage());
+ } catch (Exception e) {
+ log.warn("Embedding 실패 - contentId: {}. 사유: {}", content.getContentId(), e.getMessage());또한, 실패한 항목의 수를 집계하여 최종 로그에 함께 출력하면 배치 품질을 빠르게 파악할 수 있습니다.
📝 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.
| public void saveContents(List<Content> contents) { | |
| List<Document> documents = contents.stream() | |
| .map(content -> { | |
| EmbeddingResult result = embeddingService.embedContent(content); | |
| return embeddingService.toDocument(result, content); | |
| try { | |
| EmbeddingResult result = embeddingService.embedContent(content); | |
| return embeddingService.toDocument(result, content); | |
| } catch (IllegalArgumentException e) { | |
| log.warn("Embedding 실패 - contentId: {}. 사유: {}", content.getContentId(), e.getMessage()); | |
| return null; // 실패한 건 버림 | |
| } | |
| }) | |
| .filter(Objects::nonNull) // null인 건 제외 | |
| .collect(Collectors.toList()); | |
| // Qdrant Vectorstore에 문서 추가 | |
| vectorStore.add(documents); | |
| if (!documents.isEmpty()) { | |
| log.info("embedding 완료, documents {}개 저장 시작", documents.size()); | |
| vectorStore.add(documents); | |
| log.info("documents 저장 완료"); | |
| } else { | |
| log.info("저장할 documents 없음 (모두 실패)"); | |
| } | |
| } | |
| public void saveContents(List<Content> contents) { | |
| List<Document> documents = contents.stream() | |
| .map(content -> { | |
| try { | |
| EmbeddingResult result = embeddingService.embedContent(content); | |
| return embeddingService.toDocument(result, content); | |
| } catch (Exception e) { | |
| log.warn("Embedding 실패 - contentId: {}. 사유: {}", content.getContentId(), e.getMessage()); | |
| return null; // 실패한 건 버림 | |
| } | |
| }) | |
| .filter(Objects::nonNull) // null인 건 제외 | |
| .collect(Collectors.toList()); | |
| if (!documents.isEmpty()) { | |
| log.info("embedding 완료, documents {}개 저장 시작", documents.size()); | |
| vectorStore.add(documents); | |
| log.info("documents 저장 완료"); | |
| } else { | |
| log.info("저장할 documents 없음 (모두 실패)"); | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/llm/service/VectorStoreService.java lines 38 to
59, the current exception handling only catches IllegalArgumentException, which
risks missing other exceptions like network errors or model failures that could
cause the entire method to fail. Broaden the catch block to handle all
exceptions (e.g., catch Exception) to prevent method failure and log the error
details. Additionally, track and log the count of failed items alongside the
successful document count in the final log to provide better insight into batch
processing quality.
| const card = document.createElement('div'); | ||
| card.className = 'recommend-card'; | ||
| card.innerHTML = ` | ||
| <img src="${place.imageUrl}" alt="${place.title}" /> | ||
| <h2>${place.title}</h2> | ||
| <p><strong>주소:</strong>${place.addr}</p> | ||
| <p><strong>설명:</strong> ${place.description}</p> | ||
| <p><strong>동반 유형:</strong> ${place.acmpyTypeCd || '정보 없음'}</p> | ||
| <p><strong>가능 동물:</strong> ${place.acmpyPsblCpam || '정보 없음'}</p> | ||
| <p><strong>준비물:</strong> ${place.acmpyNeedMtr || '정보 없음'}</p> | ||
| `; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
innerHTML 사용 시 XSS 위험
추천 카드를 innerHTML 로 통째로 삽입하고 외부 이미지·텍스트를 그대로 출력하면 스크립트 삽입 공격에 취약합니다. textContent 를 활용하거나 DOM API 로 요소를 개별 생성해 주세요.
const title = document.createElement('h2');
title.textContent = place.title;
// ...
card.append(title);🤖 Prompt for AI Agents
In src/main/resources/templates/recommend.html around lines 188 to 198, the code
uses innerHTML to insert external data directly, which poses an XSS risk. To fix
this, replace the innerHTML assignment by creating each element individually
using DOM methods, setting textContent for text fields and src/alt attributes
for images, then append these elements to the card div. This approach safely
escapes content and prevents script injection.
| private Filter.Expression buildRegion(String location) { | ||
| // String이 아닌 FilterExpression API를 이용해서 QureryDSL 그대로 반환 | ||
| FilterExpressionBuilder b = new FilterExpressionBuilder(); | ||
|
|
||
| if (location == null || location.isBlank()) | ||
| return b.eq("areaCode", 0).build(); | ||
|
|
||
| String[] parts = location.trim().split(" "); | ||
| if (parts.length == 0) return "areaCode == 0"; | ||
| if (parts.length == 0) | ||
| return b.eq("areaCode", 0).build(); | ||
|
|
||
| String areaName = parts[0]; | ||
| AreaCode areaCode = AreaCode.fromName(areaName); | ||
| return "areaCode == %d".formatted(areaCode.getCode()); | ||
|
|
||
| // 기본 필터: areaCode | ||
| FilterExpressionBuilder.Op areaExpr = b.eq("areaCode", areaCode.getCode()); | ||
|
|
||
| log.info(areaExpr.toString()); | ||
| // 시군구까지 필터링 | ||
| if (parts.length > 1) { | ||
| String sigungu = parts[1]; | ||
| return b.and(areaExpr, b.eq("sigungu", sigungu)).build(); | ||
| } | ||
| return areaExpr.build(); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
지역 코드 변환 실패 시 예외 처리 필요
AreaCode.fromName(areaName) 가 정의되지 않은 지역을 받을 경우 null 을 반환할 수 있습니다. 이후 getCode() 호출 시 NPE 발생 위험이 있습니다.
AreaCode areaCode = AreaCode.fromName(areaName);
if (areaCode == null) {
log.warn("알 수 없는 지역명: {}", areaName);
return b.eq("areaCode", 0).build(); // 기본값
}🤖 Prompt for AI Agents
In src/main/java/io/github/petty/llm/service/RecommendServiceImpl.java around
lines 80 to 104, the method buildRegion calls AreaCode.fromName(areaName) which
can return null for unknown area names, leading to a NullPointerException when
getCode() is called. To fix this, add a null check after calling fromName; if
areaCode is null, log a warning with the unknown areaName and return a default
filter expression with areaCode 0 to avoid the exception.
| areaCode = AreaCode.fromName(parts[0]).getCode(); | ||
| } | ||
| } |
There was a problem hiding this comment.
알 수 없는 지역명 처리 시 NPE 위험
AreaCode.fromName(parts[0]) 가 null 을 반환할 경우 getCode() 호출에서 NPE 가 발생합니다. 안전성을 위해 null-check 를 추가해 주세요.
- areaCode = AreaCode.fromName(parts[0]).getCode();
+ AreaCode ac = AreaCode.fromName(parts[0]);
+ if (ac != null) {
+ areaCode = ac.getCode();
+ }📝 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.
| areaCode = AreaCode.fromName(parts[0]).getCode(); | |
| } | |
| } | |
| AreaCode ac = AreaCode.fromName(parts[0]); | |
| if (ac != null) { | |
| areaCode = ac.getCode(); | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/llm/service/EmbeddingService.java around lines
87 to 89, the call to AreaCode.fromName(parts[0]) may return null, causing a
NullPointerException when getCode() is called. Add a null check after
AreaCode.fromName(parts[0]) to ensure it is not null before calling getCode(),
and handle the null case appropriately to prevent NPE.
| @Override | ||
| public String preprocessContent(Content content) { | ||
| try { | ||
| String overview = content.getOverview(); | ||
| if (overview == null || overview.isBlank()) { | ||
| log.warn("[전처리] Overview 없음, 기본 텍스트 사용 (contentId: {})", content.getContentId()); | ||
| return defaultPrepareText(content); | ||
| } | ||
|
|
||
| String finalPrompt = buildPrompt(overview); | ||
|
|
||
| HttpClient httpClient = HttpClient.newHttpClient(); | ||
| String url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=%s" | ||
| .formatted(apiKey); | ||
| log.info("[Gemini 요청] 프롬프트:\n{}", finalPrompt); | ||
| HttpRequest request = HttpRequest.newBuilder() | ||
| .uri(URI.create(url)) | ||
| .header("Content-Type", "application/json") | ||
| .POST(HttpRequest.BodyPublishers.ofString( | ||
| objectMapper.writeValueAsString( | ||
| new GeminiRequestDTO(List.of( | ||
| new GeminiRequestDTO.Content(List.of( | ||
| new GeminiRequestDTO.Part(finalPrompt) | ||
| )) | ||
| )) | ||
| ) | ||
| )) | ||
| .build(); | ||
|
|
||
| HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); | ||
| GeminiResponseDTO resp = objectMapper.readValue(response.body(), GeminiResponseDTO.class); | ||
| String geminiResponseText = resp.candidates().get(0).content().parts().get(0).text(); | ||
| return generateFinalText(content, geminiResponseText); | ||
| } catch (Exception e) { | ||
| return defaultPrepareText(content); | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
API 호출 부분 개선 필요
preprocessContent 메서드에서 몇 가지 개선이 필요합니다:
- API URL에 API 키가 직접 포함되어 로그에 노출될 위험이 있습니다.
- 모든 예외를 한 번에 처리하고 있어 문제 진단이 어렵습니다.
- HTTP 요청에 타임아웃 설정이 없습니다.
@Override
public String preprocessContent(Content content) {
try {
String overview = content.getOverview();
if (overview == null || overview.isBlank()) {
log.warn("[전처리] Overview 없음, 기본 텍스트 사용 (contentId: {})", content.getContentId());
return defaultPrepareText(content);
}
String finalPrompt = buildPrompt(overview);
- HttpClient httpClient = HttpClient.newHttpClient();
String url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=%s"
.formatted(apiKey);
- log.info("[Gemini 요청] 프롬프트:\n{}", finalPrompt);
+ log.info("[Gemini 요청] 콘텐츠 ID: {}", content.getContentId());
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Content-Type", "application/json")
+ .timeout(java.time.Duration.ofSeconds(10))
.POST(HttpRequest.BodyPublishers.ofString(
objectMapper.writeValueAsString(
new GeminiRequestDTO(List.of(
new GeminiRequestDTO.Content(List.of(
new GeminiRequestDTO.Part(finalPrompt)
))
))
)
))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ if (response.statusCode() != 200) {
+ log.error("[Gemini 오류] 상태 코드: {}, 응답: {}", response.statusCode(), response.body());
+ return defaultPrepareText(content);
+ }
GeminiResponseDTO resp = objectMapper.readValue(response.body(), GeminiResponseDTO.class);
String geminiResponseText = resp.candidates().get(0).content().parts().get(0).text();
return generateFinalText(content, geminiResponseText);
- } catch (Exception e) {
+ } catch (java.io.IOException e) {
+ log.error("[Gemini 오류] JSON 처리 오류", e);
+ return defaultPrepareText(content);
+ } catch (java.net.http.HttpTimeoutException e) {
+ log.error("[Gemini 오류] 요청 타임아웃", e);
+ return defaultPrepareText(content);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ log.error("[Gemini 오류] 요청 중단", e);
+ return defaultPrepareText(content);
+ } catch (Exception e) {
+ log.error("[Gemini 오류] 예상치 못한 오류", e);
return defaultPrepareText(content);
}
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/llm/service/GeminiPreprocessorServiceImpl.java
lines 30 to 66, the preprocessContent method has three issues: the API key is
directly embedded in the URL which risks exposure in logs, all exceptions are
caught generically making diagnosis difficult, and the HTTP request lacks a
timeout setting. To fix this, remove the API key from the URL and instead add it
securely in the request headers or use environment variables without logging it;
catch specific exceptions separately to improve error handling and logging; and
configure the HttpClient or HttpRequest with appropriate timeout settings to
avoid indefinite waits.
| public Optional<DetailPetDto> getPetInfo(String contentId) { | ||
| try { | ||
| Long id = Long.parseLong(contentId); | ||
| Optional<Content> contentOpt = contentRepository.findById(id); | ||
|
|
||
| if (contentOpt.isPresent()) { | ||
| Content content = contentOpt.get(); | ||
| if (content.getPetTourInfo() != null) { | ||
| DetailPetDto dto = new DetailPetDto(); | ||
| dto.setContentId(id); | ||
| dto.setAcmpyTypeCd(content.getPetTourInfo().getAcmpyTypeCd()); | ||
| dto.setAcmpyPsblCpam(content.getPetTourInfo().getAcmpyPsblCpam()); | ||
| dto.setAcmpyNeedMtr(content.getPetTourInfo().getAcmpyNeedMtr()); | ||
| return Optional.of(dto); | ||
| } | ||
| } | ||
| return Optional.empty(); | ||
| } catch (NumberFormatException e) { | ||
| log.error("반려동물 정보 관련 오류: {}", contentId, e); | ||
| return Optional.empty(); | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
코드 중복 제거 필요
getPetInfo 메소드도 contentId 파싱 로직이 중복됩니다. 앞서 제안한 parseContentId 메소드를 활용하여 중복을 제거하는 것이 좋습니다. 또한 로그 메시지를 더 일관성 있게 작성하면 좋겠습니다.
public Optional<DetailPetDto> getPetInfo(String contentId) {
- try {
- Long id = Long.parseLong(contentId);
- Optional<Content> contentOpt = contentRepository.findById(id);
-
- if (contentOpt.isPresent()) {
- Content content = contentOpt.get();
- if (content.getPetTourInfo() != null) {
- DetailPetDto dto = new DetailPetDto();
- dto.setContentId(id);
- dto.setAcmpyTypeCd(content.getPetTourInfo().getAcmpyTypeCd());
- dto.setAcmpyPsblCpam(content.getPetTourInfo().getAcmpyPsblCpam());
- dto.setAcmpyNeedMtr(content.getPetTourInfo().getAcmpyNeedMtr());
- return Optional.of(dto);
- }
- }
- return Optional.empty();
- } catch (NumberFormatException e) {
- log.error("반려동물 정보 관련 오류: {}", contentId, e);
- return Optional.empty();
- }
+ return parseContentId(contentId)
+ .flatMap(id -> contentRepository.findById(id)
+ .filter(content -> content.getPetTourInfo() != null)
+ .map(content -> {
+ DetailPetDto dto = new DetailPetDto();
+ dto.setContentId(id);
+ dto.setAcmpyTypeCd(content.getPetTourInfo().getAcmpyTypeCd());
+ dto.setAcmpyPsblCpam(content.getPetTourInfo().getAcmpyPsblCpam());
+ dto.setAcmpyNeedMtr(content.getPetTourInfo().getAcmpyNeedMtr());
+ return dto;
+ })
+ );
}📝 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.
| public Optional<DetailPetDto> getPetInfo(String contentId) { | |
| try { | |
| Long id = Long.parseLong(contentId); | |
| Optional<Content> contentOpt = contentRepository.findById(id); | |
| if (contentOpt.isPresent()) { | |
| Content content = contentOpt.get(); | |
| if (content.getPetTourInfo() != null) { | |
| DetailPetDto dto = new DetailPetDto(); | |
| dto.setContentId(id); | |
| dto.setAcmpyTypeCd(content.getPetTourInfo().getAcmpyTypeCd()); | |
| dto.setAcmpyPsblCpam(content.getPetTourInfo().getAcmpyPsblCpam()); | |
| dto.setAcmpyNeedMtr(content.getPetTourInfo().getAcmpyNeedMtr()); | |
| return Optional.of(dto); | |
| } | |
| } | |
| return Optional.empty(); | |
| } catch (NumberFormatException e) { | |
| log.error("반려동물 정보 관련 오류: {}", contentId, e); | |
| return Optional.empty(); | |
| } | |
| } | |
| public Optional<DetailPetDto> getPetInfo(String contentId) { | |
| return parseContentId(contentId) | |
| .flatMap(id -> contentRepository.findById(id) | |
| .filter(content -> content.getPetTourInfo() != null) | |
| .map(content -> { | |
| DetailPetDto dto = new DetailPetDto(); | |
| dto.setContentId(id); | |
| dto.setAcmpyTypeCd(content.getPetTourInfo().getAcmpyTypeCd()); | |
| dto.setAcmpyPsblCpam(content.getPetTourInfo().getAcmpyPsblCpam()); | |
| dto.setAcmpyNeedMtr(content.getPetTourInfo().getAcmpyNeedMtr()); | |
| return dto; | |
| }) | |
| ); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/llm/service/ContentService.java around lines 87
to 108, the getPetInfo method duplicates the contentId parsing logic. Refactor
this method to use the existing parseContentId helper method for parsing
contentId to Long to remove redundancy. Also, update the log message format to
be consistent with other logging in the class, ensuring clear and uniform error
reporting.
| public String getImageUrl(String contentId){ | ||
| try { | ||
| Long id = Long.parseLong(contentId); | ||
|
|
||
| // 1. Content 엔티티에서 이미지 확인 | ||
| Optional<Content> contentOpt = contentRepository.findById(id); | ||
| if (contentOpt.isPresent()) { | ||
| Content content = contentOpt.get(); | ||
|
|
||
| // firstimage 필드가 있다고 가정 (필드명은 실제 엔티티에 맞게 조정 필요) | ||
| String contentImage = content.getFirstImage(); | ||
| if (contentImage != null && !contentImage.isEmpty()) { | ||
| return contentImage; | ||
| } | ||
| } | ||
|
|
||
| // 2. ContentImageRepository에서 이미지 검색 | ||
| List<ContentImage> images = contentImageRepository.findByContent_ContentId(id); | ||
| if (images != null && !images.isEmpty()) { | ||
| // 첫 번째 이미지의 URL 반환 (필드명은 실제 엔티티에 맞게 조정 필요) | ||
| return images.get(0).getOriginImgUrl(); | ||
| } | ||
|
|
||
| // 3. 이미지가 없는 경우 기본 이미지 반환 | ||
| return DEFAULT_IMAGE_URL; | ||
|
|
||
| } catch (NumberFormatException e) { | ||
| log.error("Invalid contentId format: {}", contentId, e); | ||
| return DEFAULT_IMAGE_URL; | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
코드 중복 제거 및 메소드 리팩토링 필요
이 메소드는 길이가 길고 contentId 파싱 로직이 다른 메소드와 중복됩니다. 파싱 로직을 추출하여 재사용하는 것이 좋습니다. 또한 이미지 검색 로직을 더 작은 메소드로 분리하면 가독성이 향상됩니다.
+private Optional<Long> parseContentId(String contentId) {
+ try {
+ return Optional.of(Long.parseLong(contentId));
+ } catch (NumberFormatException e) {
+ log.error("컨텐츠 ID 형식이 유효하지 않습니다: {}", contentId, e);
+ return Optional.empty();
+ }
+}
public String getImageUrl(String contentId){
- try {
- Long id = Long.parseLong(contentId);
+ return parseContentId(contentId)
+ .map(id -> {
+ // 1. Content 엔티티에서 이미지 확인
+ return contentRepository.findById(id)
+ .map(content -> {
+ // firstimage 필드가 있으면 반환
+ String contentImage = content.getFirstImage();
+ if (contentImage != null && !contentImage.isEmpty()) {
+ return contentImage;
+ }
+
+ // 2. ContentImageRepository에서 이미지 검색
+ List<ContentImage> images = contentImageRepository.findByContent_ContentId(id);
+ if (images != null && !images.isEmpty()) {
+ return images.get(0).getOriginImgUrl();
+ }
+
+ // 3. 이미지가 없는 경우 기본 이미지 반환
+ return DEFAULT_IMAGE_URL;
+ })
+ .orElse(DEFAULT_IMAGE_URL);
+ })
+ .orElse(DEFAULT_IMAGE_URL);
- // 1. Content 엔티티에서 이미지 확인
- Optional<Content> contentOpt = contentRepository.findById(id);
- if (contentOpt.isPresent()) {
- Content content = contentOpt.get();
-
- // firstimage 필드가 있다고 가정 (필드명은 실제 엔티티에 맞게 조정 필요)
- String contentImage = content.getFirstImage();
- if (contentImage != null && !contentImage.isEmpty()) {
- return contentImage;
- }
- }
-
- // 2. ContentImageRepository에서 이미지 검색
- List<ContentImage> images = contentImageRepository.findByContent_ContentId(id);
- if (images != null && !images.isEmpty()) {
- // 첫 번째 이미지의 URL 반환 (필드명은 실제 엔티티에 맞게 조정 필요)
- return images.get(0).getOriginImgUrl();
- }
-
- // 3. 이미지가 없는 경우 기본 이미지 반환
- return DEFAULT_IMAGE_URL;
-
- } catch (NumberFormatException e) {
- log.error("Invalid contentId format: {}", contentId, e);
- return DEFAULT_IMAGE_URL;
- }
}📝 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.
| public String getImageUrl(String contentId){ | |
| try { | |
| Long id = Long.parseLong(contentId); | |
| // 1. Content 엔티티에서 이미지 확인 | |
| Optional<Content> contentOpt = contentRepository.findById(id); | |
| if (contentOpt.isPresent()) { | |
| Content content = contentOpt.get(); | |
| // firstimage 필드가 있다고 가정 (필드명은 실제 엔티티에 맞게 조정 필요) | |
| String contentImage = content.getFirstImage(); | |
| if (contentImage != null && !contentImage.isEmpty()) { | |
| return contentImage; | |
| } | |
| } | |
| // 2. ContentImageRepository에서 이미지 검색 | |
| List<ContentImage> images = contentImageRepository.findByContent_ContentId(id); | |
| if (images != null && !images.isEmpty()) { | |
| // 첫 번째 이미지의 URL 반환 (필드명은 실제 엔티티에 맞게 조정 필요) | |
| return images.get(0).getOriginImgUrl(); | |
| } | |
| // 3. 이미지가 없는 경우 기본 이미지 반환 | |
| return DEFAULT_IMAGE_URL; | |
| } catch (NumberFormatException e) { | |
| log.error("Invalid contentId format: {}", contentId, e); | |
| return DEFAULT_IMAGE_URL; | |
| } | |
| } | |
| // add this helper inside ContentService | |
| private Optional<Long> parseContentId(String contentId) { | |
| try { | |
| return Optional.of(Long.parseLong(contentId)); | |
| } catch (NumberFormatException e) { | |
| log.error("컨텐츠 ID 형식이 유효하지 않습니다: {}", contentId, e); | |
| return Optional.empty(); | |
| } | |
| } | |
| public String getImageUrl(String contentId) { | |
| return parseContentId(contentId) | |
| .map(id -> { | |
| // 1. Content 엔티티에서 이미지 확인 | |
| return contentRepository.findById(id) | |
| .map(content -> { | |
| // firstImage 필드가 있으면 반환 | |
| String contentImage = content.getFirstImage(); | |
| if (contentImage != null && !contentImage.isEmpty()) { | |
| return contentImage; | |
| } | |
| // 2. ContentImageRepository에서 이미지 검색 | |
| List<ContentImage> images = contentImageRepository.findByContent_ContentId(id); | |
| if (images != null && !images.isEmpty()) { | |
| return images.get(0).getOriginImgUrl(); | |
| } | |
| // 3. 이미지가 없는 경우 기본 이미지 반환 | |
| return DEFAULT_IMAGE_URL; | |
| }) | |
| .orElse(DEFAULT_IMAGE_URL); | |
| }) | |
| .orElse(DEFAULT_IMAGE_URL); | |
| } |
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/llm/service/ContentService.java around lines 40
to 70, the getImageUrl method contains duplicated contentId parsing logic and a
long image retrieval process. Refactor by extracting the contentId parsing into
a separate reusable method that returns an Optional<Long> or handles invalid
input gracefully. Also, split the image retrieval steps into smaller private
methods, such as one for fetching the image from the Content entity and another
for fetching from ContentImageRepository, then call these from getImageUrl to
improve readability and reduce duplication.
PR #28 리뷰: Qdrant 연결 오류 해결PR 확인했습니다! Qdrant 연결 문제로 고생 많으셨는데 문제를 잘 해결하고 추가적인 개선도 함께 진행하셨네요. 주요 해결 사항들 ✅
사용자 경험 개선 부분들 🎨
기능적 확장 부분들 🛠️
코드 정리 작업들 📝
Qdrant 연결 문제로 꽤 오래 고생하셨을 텐데 이렇게 해결하면서 추가 기능들까지 구현해주셔서 감사합니다. 이제 이 기반 위에서 더 다양한 기능들 확장하기도 수월해질 것 같아요! 수고 많으셨습니다! 👍 |
📜 PR 내용 요약
⚒️ 작업 및 변경 내용(상세하게)
📚 기타 참고 사항