Skip to content
8 changes: 4 additions & 4 deletions src/main/java/io/github/petty/llm/common/ContentType.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
public enum ContentType {
TOUR(12, "관광지"),
CULTURE(14, "문화시설"),
FESTIVAL(15, "축제/공연/행사"),
SPORTS(28, "레포츠/레저"),
STAY(32, "숙박/숙소/호텔/모텔/펜션"),
FESTIVAL(15, "축제, 공연, 행사"),
SPORTS(28, "레포츠, 레저"),
STAY(32, "숙박, 숙소, 호텔, 모텔, 펜션"),
SHOPPING(38, "쇼핑"),
FOOD(39, "음식점/식당"),
FOOD(39, "음식점, 식당"),
ETC(0, "기타");

private final int code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.github.petty.llm.controller;

import io.github.petty.llm.service.EmbeddingBatchService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/embedding-batch")
@RequiredArgsConstructor
public class EmbeddingBatchController {
private final EmbeddingBatchService embeddingBatchService;

// 기본 저장
@PostMapping("/run")
public String runBatch() {
embeddingBatchService.saveAllContentsInBatch();
return "Vector DB 구성 완료";
}

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

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.github.petty.llm.controller;

import io.github.petty.llm.common.AreaCode;
import io.github.petty.llm.service.ChatService;
import io.github.petty.llm.dto.RecommendResponseDTO;
import io.github.petty.llm.service.RecommendService;
import io.github.petty.llm.service.VectorStoreService;
import lombok.RequiredArgsConstructor;
Expand All @@ -23,9 +23,9 @@ public class RecommendController {
private final RecommendService recommendService;

@PostMapping
public ResponseEntity<String> recommend(@RequestBody Map<String, String> promptMap) {
String result = recommendService.recommend(promptMap);
public ResponseEntity<RecommendResponseDTO> recommend(@RequestBody Map<String, String> promptMap) {
RecommendResponseDTO result = recommendService.recommend(promptMap);
return ResponseEntity.ok(result);
}

}
}
18 changes: 18 additions & 0 deletions src/main/java/io/github/petty/llm/dto/RecommendResponseDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.petty.llm.dto;

import java.util.List;

// 추천 응답 반환
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 ) {}
}
9 changes: 0 additions & 9 deletions src/main/java/io/github/petty/llm/service/ChatService.java

This file was deleted.

70 changes: 0 additions & 70 deletions src/main/java/io/github/petty/llm/service/ChatServiceImpl.java

This file was deleted.

109 changes: 109 additions & 0 deletions src/main/java/io/github/petty/llm/service/ContentService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package io.github.petty.llm.service;


import io.github.petty.tour.dto.DetailPetDto;
import io.github.petty.tour.entity.Content;
import io.github.petty.tour.entity.ContentImage;
import io.github.petty.tour.repository.ContentImageRepository;
import io.github.petty.tour.repository.ContentRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
public class ContentService {
// content ID 받아서 추가 정보 받기
private final ContentRepository contentRepository;
private final ContentImageRepository contentImageRepository;

private static final String DEFAULT_IMAGE_URL = "/assets/noimg.png";

public Optional<Content> findByContentId(String contentId){
try {
Long id = Long.parseLong(contentId);
return contentRepository.findById(id);
} catch (NumberFormatException e) {
// contentid가 없을 때
log.error("Invalid contentId format: {}", contentId, e);
return Optional.empty();
}
}




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



/*
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");
}
*/
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();
}
}
Comment on lines +87 to +108
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.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package io.github.petty.llm.service;

import io.github.petty.tour.entity.Content;
import io.github.petty.tour.repository.ContentRepository;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
@Slf4j
public class EmbeddingBatchService {
private final ContentRepository contentRepository;
private final VectorStoreService vectorStoreService;

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


// 기본 리스트 저장
// public void saveAllContentsInBatch() {
// List<Content> allContents = contentRepository.findAll();
// int batchSize = 50;
// int totalSize = allContents.size();
//
// // 중간에 끊긴 번호부터 시작
//// int startIndex = 450;
////
//// // 중복 검사 = 현재 VectorStore에 저장된 contentId 리스트 가져오기
//// List<String> existingContentIds = vectorStoreService.findAllContentIds();
////
//// log.info("총 {}개 데이터, {}번부터 배치 크기 {}로 저장 시작", totalSize, startIndex, batchSize);
////
//// for (int i = startIndex; i < totalSize; i += batchSize) {
//// int end = Math.min(i + batchSize, totalSize);
//// List<Content> batchList = allContents.subList(i, end);
////
//// log.info("{}번째 batch 시작 ({} ~ {})", (i / batchSize) + 1, i, end);
////
//// // 중복이면 제거하기
//// List<Content> filteredBatch = batchList.stream()
//// .filter(content -> {
//// String contentId = content.getContentId().toString();
//// return !existingContentIds.contains(contentId);
//// })
//// .toList();
////
//// if (filteredBatch.isEmpty()) {
//// log.info("{}번째 batch: 저장할 새 데이터 없음 (모두 중복)", (i / batchSize) + 1);
//// continue;
//// }
////
//// // 3. 저장
//// vectorStoreService.saveContents(filteredBatch);
////
//// log.info("{}번째 batch 저장 완료 ({}개 저장)", (i / batchSize) + 1, filteredBatch.size());
//// }
//// }
//
// // 실패한 리스트 저장
//// public void retryFailedContents(List<Long> failedContentIds) {
//// List<Content> failedContents = contentRepository.findAllById(failedContentIds);
////
//// log.info("재시도할 Content 수: {}", failedContents.size());
////
//// vectorStoreService.saveContents(failedContents);
////
//// log.info("재시도 저장 완료");
//// }
}

Loading
Loading