Skip to content

[LLM] Qdrant 연결 오류 해결#28

Merged
s0ooo0k merged 11 commits intoPETTY-HUB:mainfrom
s0ooo0k:feat/regionSearch3
May 21, 2025
Merged

[LLM] Qdrant 연결 오류 해결#28
s0ooo0k merged 11 commits intoPETTY-HUB:mainfrom
s0ooo0k:feat/regionSearch3

Conversation

@s0ooo0k
Copy link
Member

@s0ooo0k s0ooo0k commented May 21, 2025

📜 PR 내용 요약

  • 지속적으로 발생하던 Qdrant 연결 파트 해결
  • 리스트 형태 출력

⚒️ 작업 및 변경 내용(상세하게)

  • Spring AI + Qdrant 연결 방식 변경으로 작동되지 않던 파트 수정
  • 사진 포함 리스트 형태 출력 구축

📚 기타 참고 사항

  • 각 로컬에서 작동 되는지 확인 부탁드립니다

@PETTY-HUB PETTY-HUB deleted a comment from coderabbitai bot May 21, 2025
@s0ooo0k
Copy link
Member Author

s0ooo0k commented May 21, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented May 21, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented May 21, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Summary by CodeRabbit

  • 신규 기능

    • 추천 결과가 구조화된 카드 형태로 시각적으로 표시됩니다.
    • 추천 결과에 장소 이미지, 이름, 주소, 설명, 반려동물 동반 정보 등이 포함됩니다.
    • 벡터 DB 일괄 구성 및 관리 기능이 추가되었습니다.
  • 버그 수정

    • 추천 요청 시 필수 입력값(이름, 종, 지역, 몸무게) 검증이 강화되었습니다.
  • 리팩터

    • 추천 결과가 문자열이 아닌 구조화된 데이터로 반환되어, 더 풍부한 정보를 제공합니다.
    • 추천 로직이 개선되어 지역 필터링과 벡터 검색 정확도가 향상되었습니다.
  • 스타일

    • 추천 폼과 결과 영역이 카드형 그리드로 재디자인되어 사용자 경험이 개선되었습니다.
  • 문서화

    • 추천 결과 화면에 각 항목에 대한 설명 및 안내가 추가되었습니다.

Walkthrough

이 변경 사항은 추천 서비스의 반환 타입을 문자열에서 구조화된 DTO(RecommendResponseDTO)로 전환하며, 관련 컨트롤러, 서비스, 프론트엔드 템플릿을 모두 이에 맞게 수정합니다. 또한, 임베딩 배치 및 전처리 서비스가 새롭게 추가되고, 벡터스토어 서비스의 예외 처리 및 메타데이터 추론 로직이 강화되었습니다.

Changes

파일/경로 요약 변경 내용 요약
.../ContentType.java ENUM 설명 문자열의 구분자를 슬래시(/)에서 콤마(,)로 변경
.../controller/EmbeddingBatchController.java,
.../service/EmbeddingBatchService.java
임베딩 배치 작업을 위한 REST 컨트롤러 및 서비스 신설, 배치 저장 로직 구현, 일부 주석 처리된 재시도/배치 처리 코드 포함
.../controller/RecommendController.java,
.../service/RecommendService.java,
.../service/RecommendServiceImpl.java,
.../dto/RecommendResponseDTO.java
추천 API 반환 타입을 String에서 RecommendResponseDTO로 변경, DTO 클래스 신설, 추천 결과 생성 로직 구조화, 이미지 및 반려동물 정보 포함
.../service/ChatService.java,
.../service/ChatServiceImpl.java
기존 Chat 기반 추천 서비스 인터페이스 및 구현체 삭제
.../service/ContentService.java 콘텐츠 ID 기반 정보 조회, 이미지 URL 및 반려동물 정보 반환 서비스 신설
.../service/EmbeddingService.java 임베딩 전처리 단계에서 Gemini 전처리 서비스 연동, 메타데이터 추론 및 입력값 검증 강화
.../service/GeminiPreprocessorService.java,
.../service/GeminiPreprocessorServiceImpl.java
Gemini API를 활용한 콘텐츠 요약 전처리 서비스 인터페이스 및 구현체 신설, 예외 발생 시 기본 텍스트 반환
.../service/VectorStoreService.java 임베딩 실패 시 예외 처리 및 로깅 추가, 필터 타입 강화, 전체 콘텐츠 ID 조회 기능 추가
.../pipeline/controller/PipelineController.java 추천 결과 변수 타입을 DTO로 변경, 모델에 DTO 객체 전달
.../templates/recommend.html 추천 입력 폼 및 결과 UI 전면 개편, 입력값 검증 추가, 추천 결과를 카드 형태로 시각화, JS에서 JSON 파싱 및 동적 DOM 생성

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>
Loading
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 구성 완료"
Loading

Possibly related PRs

Poem

🐇
새로운 DTO가 찾아왔어요,
추천 카드가 반짝이며 줄을 서요.
이미지와 정보, 펫 친구까지,
한눈에 쏙 들어오는 카드의 잔치!
벡터도 정리, 배치도 척척,
토끼는 오늘도 코드를 톡톡!
🥕✨


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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 353fa36 and a592fcb.

⛔ Files ignored due to path filters (1)
  • src/main/resources/static/assets/noimg.png is 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도 상수로 정의되어 있습니다.

Comment on lines +25 to +30
// 실패 ID 재저장
// @PostMapping("/retry")
// public String retryFailedEmbeddings(@RequestBody List<Long> failedContentIds) {
// embeddingBatchService.retryFailedContents(failedContentIds);
// return "재시도 완료!";
// }
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Comment on lines +19 to +30
// 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개 저장 완료!");
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

배치 서비스 구현에 몇 가지 개선점이 있습니다.

현재 구현된 배치 서비스는 Qdrant 연결 문제 해결에 도움이 될 것으로 보입니다. 그러나 몇 가지 개선이 필요합니다:

  1. 메소드명 saveAllContentsInBatch()는 모든 콘텐츠를 저장한다는 의미이지만 실제로는 20개만 저장합니다. 메소드명을 saveTop20ContentsInBatch()로 변경하는 것이 더 명확할 것 같습니다.
  2. 주석은 50개 콘텐츠에 대해 언급하고 있지만, 실제 코드는 20개를 가져옵니다 (22, 24, 29줄). 주석과 코드의 일관성을 유지해주세요.
  3. 예외 처리가 없어 오류 발생 시 대응이 어려울 수 있습니다.
-    // 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.

Suggested change
// 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.

Comment on lines +92 to +102
// 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());
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

findAllContentIds 구현 방식은 부정확‧비효율적

  1. 더미 문자열 "시" 로 similarity 검색을 수행하면
    ‑ 코사인 유사도 0.1 이상인 일부 문서만 반환돼 전체 ID를 확보하지 못할 수 있습니다.
  2. 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.

Comment on lines 38 to 59
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 없음 (모두 실패)");
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

예외 처리 범위가 좁아 실패 원인 누락 가능성

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.

Suggested change
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.

Comment on lines +188 to +198
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>
`;
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Comment on lines +80 to +104
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();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

지역 코드 변환 실패 시 예외 처리 필요

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.

Comment on lines +87 to +89
areaCode = AreaCode.fromName(parts[0]).getCode();
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

알 수 없는 지역명 처리 시 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.

Suggested change
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.

Comment on lines +30 to +66
@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);
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

API 호출 부분 개선 필요

preprocessContent 메서드에서 몇 가지 개선이 필요합니다:

  1. API URL에 API 키가 직접 포함되어 로그에 노출될 위험이 있습니다.
  2. 모든 예외를 한 번에 처리하고 있어 문제 진단이 어렵습니다.
  3. 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.

Comment on lines +87 to +108
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();
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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.

Comment on lines +40 to +70
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;
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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.

@s0ooo0k s0ooo0k merged commit e954508 into PETTY-HUB:main May 21, 2025
2 checks passed
@taehyun32
Copy link
Member

taehyun32 commented May 25, 2025

PR #28 리뷰: Qdrant 연결 오류 해결

PR 확인했습니다! Qdrant 연결 문제로 고생 많으셨는데 문제를 잘 해결하고 추가적인 개선도 함께 진행하셨네요.
변경 사항들 정리해보았습니다:

주요 해결 사항들 ✅

  • Qdrant 연결 문제 완전 해결: Spring AI 연결 방식 변경으로 지속되던 오류 해결
  • 예외 처리 강화: embedding 실패 시 전체 중단 없이 개별 처리

사용자 경험 개선 부분들 🎨

  • 카드형 UI로 변경: 기존 텍스트 나열에서 이미지 포함한 카드 형태로 변경
  • 구조화된 응답: 문자열 대신 RecommendResponseDTO 사용으로 프론트 활용도 향상
  • 입력값 검증 추가: 필수 필드에 required 설정으로 사용자 입력 실수 방지

기능적 확장 부분들 🛠️

  • 지역 필터링 고도화: 시군구 단위까지 검색 가능해져 추천 정확도 향상
  • 이미지 관리 체계화: ContentService에서 이미지 URL 관리 및 기본 이미지 대응 추가
  • Gemini 전처리 연동: 콘텐츠 요약 전처리로 임베딩 품질 개선 기대

코드 정리 작업들 📝

  • 서비스 구조 정리: 기존 ChatService 정리하고 RecommendService 중심으로 재구성
  • null 처리 강화: areaCode, sigungu 등의 null 값 처리 강화
  • 로깅 개선: 각 처리 단계별 로그 추가로 디버깅 용이

Qdrant 연결 문제로 꽤 오래 고생하셨을 텐데 이렇게 해결하면서 추가 기능들까지 구현해주셔서 감사합니다. 이제 이 기반 위에서 더 다양한 기능들 확장하기도 수월해질 것 같아요!

수고 많으셨습니다! 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants