From d043bded660814a2217e3a6497be9db1e74ca272 Mon Sep 17 00:00:00 2001 From: jiwonkim Date: Sun, 2 Feb 2025 18:45:05 +0900 Subject: [PATCH] =?UTF-8?q?feat=20#30=20:=20=EC=84=9C=EB=B2=84=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../buildOutputCleanup.lock | Bin 17 -> 17 bytes .gradle/file-system.probe | Bin 8 -> 8 bytes .../giftidea/configuration/GptConfig.java | 6 +- .../giftidea/controller/GptController.java | 117 +++++++++++++----- .../controller/ProductController.java | 2 +- .../repository/ProductRepository.java | 11 +- .../giftidea/service/ProductService.java | 46 +++---- 7 files changed, 105 insertions(+), 77 deletions(-) diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index e83055cc5ffac767af2aab608f0333539c9799d1..95a3759f6285f22ac38f7b8a6dc9c127ad4b4298 100644 GIT binary patch literal 17 VcmZRc_VA=6%hWql8Nh&NDgZby1qc8D literal 17 VcmZRc_VA=6%hWql8Nh&RDF8S-1r`7R diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe index 9be31cfa7cf3aeec87500e6126e9af5696533d95..7bd67292029ca3314cb3daa039e7f8185e5fe224 100644 GIT binary patch literal 8 PcmZQzV4QMv59@9K2>$}t literal 8 PcmZQzV4QMbvtud%2=W52 diff --git a/src/main/java/com/team4/giftidea/configuration/GptConfig.java b/src/main/java/com/team4/giftidea/configuration/GptConfig.java index 5e80d06..bf1f3e3 100644 --- a/src/main/java/com/team4/giftidea/configuration/GptConfig.java +++ b/src/main/java/com/team4/giftidea/configuration/GptConfig.java @@ -29,15 +29,13 @@ public class GptConfig { */ @Bean public RestTemplate restTemplate() { - log.info("Initializing RestTemplate for OpenAI API..."); - RestTemplate restTemplate = new RestTemplate(); restTemplate.getInterceptors().add((request, body, execution) -> { - request.getHeaders().add("Authorization", "Bearer " + openAiKey); + String authHeader = "Bearer " + openAiKey; + request.getHeaders().add("Authorization", authHeader); request.getHeaders().add("Content-Type", "application/json"); return execution.execute(request, body); }); - return restTemplate; } diff --git a/src/main/java/com/team4/giftidea/controller/GptController.java b/src/main/java/com/team4/giftidea/controller/GptController.java index 75f8c97..297aee0 100644 --- a/src/main/java/com/team4/giftidea/controller/GptController.java +++ b/src/main/java/com/team4/giftidea/controller/GptController.java @@ -1,11 +1,14 @@ package com.team4.giftidea.controller; +import com.fasterxml.jackson.databind.ObjectMapper; import com.team4.giftidea.configuration.GptConfig; import com.team4.giftidea.dto.GptRequestDTO; import com.team4.giftidea.dto.GptResponseDTO; import com.team4.giftidea.entity.Product; import com.team4.giftidea.service.ProductService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @@ -14,8 +17,6 @@ import java.io.*; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.*; /** @@ -37,37 +38,55 @@ public GptController(RestTemplate restTemplate, GptConfig gptConfig, ProductServ this.productService = productService; } + /** + * @param file 전송된 파일 (카카오톡 대화 내용) + * @param targetName 대상 이름 (ex: '여자친구', '남자친구') + * @param relation 관계 (ex: 'couple', 'friend', etc.) + * @param sex 대상 성별 ('male' 또는 'female') + * @param theme 선물의 주제 (ex: 'birthday', 'valentine', etc.) + * @return 추천된 상품 목록 + */ + /** + * @param file 전송된 파일 (카카오톡 대화 내용) + * @param targetName 대상 이름 (ex: '여자친구', '남자친구') + * @param relation 관계 (ex: 'couple', 'friend', etc.) + * @param sex 대상 성별 ('male' 또는 'female') + * @param theme 선물의 주제 (ex: 'birthday', 'valentine', etc.) + * @return 추천된 상품 목록 + */ @PostMapping("/process") + @Operation(description = "카카오톡 대화를 분석하여 키워드를 추출하고 그에 맞는 선물 목록을 추천합니다.") public List processFileAndRecommend( - @RequestParam("file") MultipartFile file, - @RequestParam("targetName") String targetName, - @RequestParam("relation") String relation, - @RequestParam("sex") String sex, - @RequestParam("theme") String theme) { + @RequestParam("file") @Parameter(description = "카카오톡 대화 내용이 포함된 파일", required = true) MultipartFile file, + @RequestParam("targetName") @Parameter(description = "대상 이름", required = true) String targetName, + @RequestParam("relation") @Parameter(description = "대상과의 관계", required = true) String relation, + @RequestParam("sex") @Parameter(description = "대상 성별", required = true) String sex, + @RequestParam("theme") @Parameter(description = "선물의 주제", required = true) String theme) { // 1. 파일 전처리 List processedMessages = preprocessKakaoFile(file, targetName); - // 2. GPT API 호출: 전처리된 메시지로 프롬프트 생성 - String prompt = generatePrompt(processedMessages, relation, sex, theme); - GptRequestDTO request = new GptRequestDTO(gptConfig.getModel(), prompt); - GptResponseDTO response = restTemplate.postForObject(gptConfig.getApiUrl(), request, GptResponseDTO.class); + // 2. GPT API 호출: 전처리된 메시지로 키워드 반환 + String categories = generatePrompt(processedMessages, relation, sex, theme); // 이미 키워드를 추출했음 - if (response != null && !response.getChoices().isEmpty()) { - // 3. GPT 응답에서 추천된 카테고리 추출 - String categories = response.getChoices().get(0).getMessage().getContent(); - List keywords = Arrays.asList(categories.split(",")); + // 3. 키워드 리스트로 변환된 값 그대로 사용 + List keywords = Arrays.asList(categories.split(",")); + keywords.replaceAll(String::trim); // 공백 제거 - // 4. 상품 검색 (DB에서 카테고리 기반으로 추천 상품 검색) - return productService.searchByKeywords(keywords, 0); - } + log.debug("추출된 키워드 목록: {}", keywords); + + // 4. 상품 검색 (DB에서 키워드 기반으로 추천 상품 검색) + List products = productService.searchByKeywords(keywords); - return Collections.emptyList(); // 오류 발생 시 빈 리스트 반환 + // 검색된 상품 확인 로그 + log.debug("검색된 상품: {}", products); + + return products; // 상품 목록 반환 } private List preprocessKakaoFile(MultipartFile file, String targetName) { List processedMessages = new ArrayList<>(); - int formatType = detectFormatType(file); // 양식 자동 판별 + int formatType = detectFormatType(file); File outputFile = null; try { @@ -154,13 +173,52 @@ private String generatePrompt(List processedMessages, String relation, S private String generateText(String prompt) { GptRequestDTO request = new GptRequestDTO(gptConfig.getModel(), prompt); try { + log.info("GPT 요청 시작 - 모델: {}", gptConfig.getModel()); + log.debug("요청 내용: {}", prompt); + + // HTTP 요청 전에 request 객체 로깅 + ObjectMapper mapper = new ObjectMapper(); + log.debug("전체 요청 바디: {}", mapper.writeValueAsString(request)); + GptResponseDTO response = restTemplate.postForObject(gptConfig.getApiUrl(), request, GptResponseDTO.class); - if (response != null && !response.getChoices().isEmpty()) { - return response.getChoices().get(0).getMessage().getContent(); + + // 응답 검증 + if (response != null) { + log.debug("GPT 응답 수신: {}", mapper.writeValueAsString(response)); + + // 응답에 'choices'가 있고, 그 중 첫 번째 항목이 존재하는지 확인 + if (response.getChoices() != null && !response.getChoices().isEmpty()) { + String content = response.getChoices().get(0).getMessage().getContent(); + log.debug("추출된 콘텐츠: {}", content); + + // 필요한 형태로 카테고리 추출 (예: "1. [무선이어폰, 스마트워치, 향수]" 형태) + if (content.contains("1.")) { + String categories = content.split("1.")[1].split("\n")[0]; // 첫 번째 카테고리 라인 추출 + log.debug("GPT 응답에서 추출된 카테고리: {}", categories); + + // 괄호 안의 항목들을 추출하고, 쉼표로 구분하여 키워드 리스트 만들기 + String[] categoryArray = categories.split("\\[|\\]")[1].split(","); + List keywords = new ArrayList<>(); + for (String category : categoryArray) { + keywords.add(category.trim()); + } + return String.join(", ", keywords); // 최종적으로 카테고리들을 반환 + } else { + log.warn("GPT 응답에서 카테고리 정보가 올바르지 않습니다."); + } + } else { + log.warn("GPT 응답에 'choices'가 없거나 빈 리스트입니다."); + } + } else { + log.warn("GPT 응답이 null입니다."); } return "GPT 응답 오류 발생"; } catch (Exception e) { log.error("GPT 요청 중 오류 발생: ", e); + log.error("상세 오류 메시지: {}", e.getMessage()); + if (e.getCause() != null) { + log.error("원인 예외: {}", e.getCause().getMessage()); + } return "GPT 요청 오류"; } } @@ -168,7 +226,7 @@ private String generateText(String prompt) { private String extractKeywordsAndReasonsCoupleMan(String theme, String message) { String prompt = String.format(""" 다음 텍스트를 참고하여 남자 애인이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. - 카테고리: 지갑, 신발, 백팩, 토트백, 크로스백, 벨트, 선글라스, 향수, 헬스가방, 무선이어폰, 스마트워치, 셔츠 + 카테고리: 남성 지갑, 남성 스니커즈, 백팩, 토트백, 크로스백, 벨트, 선글라스, 향수, 헬스가방, 무선이어폰, 스마트워치 텍스트: %s @@ -186,7 +244,7 @@ private String extractKeywordsAndReasonsCoupleMan(String theme, String message) private String extractKeywordsAndReasonsCoupleWoman(String theme, String message) { String prompt = String.format(""" 다음 텍스트를 참고하여 여자 애인이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. - 카테고리: 지갑, 신발, 숄더백, 토트백, 크로스백, 향수, 목걸이, 무선이어폰, 스마트워치, 가디건 + 카테고리: 여성 지갑, 여성 스니커즈, 숄더백, 토트백, 크로스백, 향수, 목걸이, 무선이어폰, 스마트워치 텍스트: %s @@ -204,7 +262,7 @@ private String extractKeywordsAndReasonsCoupleWoman(String theme, String message private String extractKeywordsAndReasonsParents(String theme, String message) { String prompt = String.format(""" 다음 텍스트를 참고하여 부모님이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. - 카테고리: 현금 박스, 안마기기, 신발, 건강식품, 여행 + 카테고리: 현금 박스, 안마기기, 남성 스니커즈, 건강식품 텍스트: %s @@ -273,13 +331,4 @@ private String extractKeywordsAndReasonsSeasonal(String theme, String message) { return generateText(prompt); // GPT 모델 호출 } - - private String readFile(String filePath) { - try { - return new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8); - } catch (IOException e) { - e.printStackTrace(); - return "파일을 읽을 수 없습니다."; - } - } } diff --git a/src/main/java/com/team4/giftidea/controller/ProductController.java b/src/main/java/com/team4/giftidea/controller/ProductController.java index 691bdca..a6d0f6f 100644 --- a/src/main/java/com/team4/giftidea/controller/ProductController.java +++ b/src/main/java/com/team4/giftidea/controller/ProductController.java @@ -80,7 +80,7 @@ public void crawlAndStoreData() { /** * 매일 20시 12분에 상품 정보를 자동으로 크롤링하는 스케줄러 */ - @Scheduled(cron = "0 12 20 * * *") + @Scheduled(cron = "0 16 17 * * *") public void scheduleCrawl() { crawlAndStoreData(); } diff --git a/src/main/java/com/team4/giftidea/repository/ProductRepository.java b/src/main/java/com/team4/giftidea/repository/ProductRepository.java index 254adf8..2e33495 100644 --- a/src/main/java/com/team4/giftidea/repository/ProductRepository.java +++ b/src/main/java/com/team4/giftidea/repository/ProductRepository.java @@ -1,8 +1,8 @@ package com.team4.giftidea.repository; +import java.util.List; + import com.team4.giftidea.entity.Product; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -21,11 +21,10 @@ public interface ProductRepository extends JpaRepository { boolean existsByProductId(String productId); /** - * 주어진 키워드로 상품을 검색하고 페이지네이션을 적용합니다. + * 주어진 키워드 목록에 해당하는 상품들을 검색합니다. * - * @param keyword 검색할 키워드 - * @param pageable 페이지네이션 정보 + * @param keywords 검색할 키워드 목록 * @return 해당 키워드에 맞는 상품 리스트 */ - Page findByKeyword(String keyword, Pageable pageable); + List findByKeywordIn(List keywords); } \ No newline at end of file diff --git a/src/main/java/com/team4/giftidea/service/ProductService.java b/src/main/java/com/team4/giftidea/service/ProductService.java index f185b8b..8536abe 100644 --- a/src/main/java/com/team4/giftidea/service/ProductService.java +++ b/src/main/java/com/team4/giftidea/service/ProductService.java @@ -3,31 +3,33 @@ import com.team4.giftidea.entity.Product; import com.team4.giftidea.repository.ProductRepository; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; -/** - * ProductService - 상품 정보를 관리하는 서비스 클래스 - */ @Service public class ProductService { private final ProductRepository productRepository; - /** - * ProductService 생성자 - * - * @param productRepository 상품 데이터 접근을 위한 리포지토리 - */ @Autowired public ProductService(ProductRepository productRepository) { this.productRepository = productRepository; } + /** + * 키워드에 해당하는 상품을 반환 + * 각 키워드 별로 상품 목록을 구분하여 반환 + * + * @param keywords 검색 키워드 리스트 + * @return 키워드 별로 구분된 상품 목록 + */ + public List searchByKeywords(List keywords) { + // 여러 키워드를 받아 해당하는 상품들을 반환 + return productRepository.findByKeywordIn(keywords); + } + /** * 상품 리스트를 저장하며, 기존 상품 ID가 존재하는 경우 중복 저장을 방지합니다. * @@ -36,32 +38,12 @@ public ProductService(ProductRepository productRepository) { */ public void saveItems(List productList, String keyword) { productList.forEach(product -> { - product.setKeyword(keyword); // 키워드 설정 + product.setKeyword(keyword); // 상품에 키워드 설정 - // 상품 ID가 존재하지 않을 경우에만 저장 + // 상품 ID가 존재하지 않으면 저장 if (!productRepository.existsByProductId(product.getProductId())) { productRepository.save(product); } }); } - - /** - * 여러 키워드를 받아서 각 키워드에 해당하는 상품들을 최대 20개씩 반환합니다. - * - * @param keywords 검색할 키워드 목록 - * @param pageNumber 페이지 번호 - * @return 해당 키워드들에 맞는 상품 리스트 - */ - public List searchByKeywords(List keywords, int pageNumber) { - List allProducts = new ArrayList<>(); - - // 각 키워드로 상품 검색 - for (String keyword : keywords) { - // 페이지네이션 처리 - Page productPage = productRepository.findByKeyword(keyword, PageRequest.of(pageNumber, 20)); - allProducts.addAll(productPage.getContent()); // getContent() 호출 - } - - return allProducts; - } } \ No newline at end of file