From 871d413515d64f9fdb40b34d4a13cad044b14574 Mon Sep 17 00:00:00 2001 From: jiwonkim Date: Wed, 26 Feb 2025 12:41:30 +0900 Subject: [PATCH 1/8] =?UTF-8?q?hotfix=20:=20=EC=A4=91=EB=B3=B5=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 33 ++-- .../controller/CoupangPartnersController.java | 25 +++ .../controller/ProductController.java | 40 ++--- .../repository/ProductRepository.java | 2 + .../service/CoupangPartnersService.java | 170 ++++++++++++++++++ .../giftidea/service/NaverApiService.java | 2 +- src/main/resources/application-prod.yml | 7 + 7 files changed, 239 insertions(+), 40 deletions(-) create mode 100644 src/main/java/com/team4/giftidea/controller/CoupangPartnersController.java create mode 100644 src/main/java/com/team4/giftidea/service/CoupangPartnersService.java diff --git a/build.gradle b/build.gradle index 5c13890..dfd0dc7 100644 --- a/build.gradle +++ b/build.gradle @@ -24,36 +24,45 @@ repositories { } dependencies { - // Spring Boot Starters - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + // ✅ Spring Boot 기본 스타터 패키지 implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-logging' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-aop' - implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.boot:spring-boot-starter-webflux' // ✅ WebClient 추가 + + // ✅ 데이터베이스 관련 + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'com.mysql:mysql-connector-j' + + // ✅ 보안 관련 + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'io.jsonwebtoken:jjwt:0.12.3' // ✅ JWT 토큰 생성을 위한 라이브러리 + implementation 'com.fasterxml.jackson.core:jackson-databind' - // Selenium for web scraping + // ✅ Selenium 웹 스크래핑 implementation 'org.seleniumhq.selenium:selenium-java:4.26.0' - // OpenAPI documentation + // ✅ OpenAPI 문서 자동화 (Swagger UI) implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' - // MySQL Driver - implementation 'com.mysql:mysql-connector-j' - - // Lombok for reducing boilerplate code + // ✅ Lombok (코드 간소화) compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' - implementation 'org.springframework.boot:spring-boot-starter-security' + // ✅ 로깅 및 JSON 처리 + implementation 'com.fasterxml.jackson.core:jackson-databind' - // Testing + // ✅ 쿠팡 파트너스 API 관련 + implementation 'org.apache.httpcomponents.client5:httpclient5' // HTTP 요청을 위한 라이브러리 + + // ✅ 테스트 관련 testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' } tasks.named('test') { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/src/main/java/com/team4/giftidea/controller/CoupangPartnersController.java b/src/main/java/com/team4/giftidea/controller/CoupangPartnersController.java new file mode 100644 index 0000000..122a87b --- /dev/null +++ b/src/main/java/com/team4/giftidea/controller/CoupangPartnersController.java @@ -0,0 +1,25 @@ +package com.team4.giftidea.controller; + +import com.team4.giftidea.service.CoupangPartnersService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/coupang") +public class CoupangPartnersController { + + private final CoupangPartnersService coupangPartnersService; + + public CoupangPartnersController(CoupangPartnersService coupangPartnersService) { + this.coupangPartnersService = coupangPartnersService; + } + + /** + * DB의 모든 쿠팡 상품을 조회하여 파트너스 링크로 업데이트하는 API + */ + @PostMapping("/update-all") + public ResponseEntity updateAllCoupangProductLinks() { + int updatedCount = coupangPartnersService.updateAllCoupangProductLinks(); + return ResponseEntity.ok(updatedCount + "개의 쿠팡 상품이 업데이트되었습니다."); + } +} \ No newline at end of file diff --git a/src/main/java/com/team4/giftidea/controller/ProductController.java b/src/main/java/com/team4/giftidea/controller/ProductController.java index 89c7477..81d0d5f 100644 --- a/src/main/java/com/team4/giftidea/controller/ProductController.java +++ b/src/main/java/com/team4/giftidea/controller/ProductController.java @@ -3,30 +3,26 @@ import com.team4.giftidea.entity.Product; import com.team4.giftidea.service.CoupangApiService; import com.team4.giftidea.service.KreamApiService; -import com.team4.giftidea.service.NaverApiService; import com.team4.giftidea.service.ProductService; import lombok.extern.slf4j.Slf4j; + import org.springframework.scheduling.annotation.Scheduled; import org.springframework.web.bind.annotation.*; import java.util.List; -@Slf4j // 로깅 추가 +@Slf4j @RestController -@RequestMapping("/api/products") +@RequestMapping("/api/ products") public class ProductController { - - private final NaverApiService naverApiService; private final CoupangApiService coupangApiService; private final ProductService productService; private final KreamApiService kreamApiService; public ProductController( - NaverApiService naverApiService, CoupangApiService coupangApiService, KreamApiService kreamApiService, ProductService productService) { - this.naverApiService = naverApiService; this.coupangApiService = coupangApiService; this.kreamApiService = kreamApiService; this.productService = productService; @@ -39,17 +35,12 @@ public ProductController( public void crawlAndStoreData() { log.info("🔍 크롤링 시작..."); - // 네이버 키워드 목록 - List naverKeywords = List.of( - "현금 박스", "아버지 신발", "어머니 신발", "건강식품", "헬스가방", "핸드크림", "디퓨저", - "오설록 티세트", "휴지", "초콜릿", "수제 초콜릿 키트", "파자마세트", "남자 화장품", "에어랩", - "무드등", "수건", "전기포트", "에어프라이기", "비타민", "입욕제", "블루투스 스피커", "와인" - ); - // 쿠팡 키워드 목록 List coupangKeywords = List.of( "안마기기", "무선이어폰", "스마트워치", "등산용품", "스마트폰", "맨투맨", "마우스", - "키보드", "게임기", "전기면도기" + "키보드", "게임기", "전기면도기", "현금 박스", "아버지 신발", "어머니 신발", "건강식품", "헬스가방", "핸드크림", "디퓨저", + "오설록 티세트", "휴지", "초콜릿", "수제 초콜릿 키트", "파자마세트", "남자 화장품", "에어랩", + "무드등", "수건", "전기포트", "에어프라이기", "비타민", "입욕제", "블루투스 스피커", "와인" ); // Kream 키워드 목록 @@ -60,18 +51,6 @@ public void crawlAndStoreData() { ); log.info("📢 네이버 크롤링 시작..."); - naverKeywords.forEach(keyword -> { - log.debug("🔎 네이버 검색 키워드: {}", keyword); - List naverProducts = naverApiService.searchItems(List.of(keyword)); - log.info("✅ 네이버 크롤링 완료 (키워드: {}, 검색 결과: {} 개)", keyword, naverProducts.size()); - - if (!naverProducts.isEmpty()) { - productService.saveItems(naverProducts, keyword); - log.info("✅ 네이버 상품 저장 완료 (키워드: {}, 저장된 개수: {})", keyword, naverProducts.size()); - } else { - log.warn("⚠️ 네이버 크롤링 실패 또는 검색 결과 없음 (키워드: {})", keyword); - } - }); log.info("📢 쿠팡 크롤링 시작..."); coupangKeywords.forEach(keyword -> { @@ -103,4 +82,11 @@ public void crawlAndStoreData() { log.info("🎯 크롤링 및 저장 작업 완료!"); } + + @Scheduled(cron = "0 5 1 * * *") + public void autoCrawlAndStoreData() { + log.info("🕐 자동 크롤링 시작 (새벽 1시)..."); + crawlAndStoreData(); + log.info("✅ 자동 크롤링 완료!"); + } } \ No newline at end of file diff --git a/src/main/java/com/team4/giftidea/repository/ProductRepository.java b/src/main/java/com/team4/giftidea/repository/ProductRepository.java index 51f2fc5..d6c0025 100644 --- a/src/main/java/com/team4/giftidea/repository/ProductRepository.java +++ b/src/main/java/com/team4/giftidea/repository/ProductRepository.java @@ -27,4 +27,6 @@ public interface ProductRepository extends JpaRepository { * @return 상품 엔티티 (없으면 Optional.empty()) */ Optional findByProductId(String productId); + + List findByMallName(String mallName); } \ No newline at end of file diff --git a/src/main/java/com/team4/giftidea/service/CoupangPartnersService.java b/src/main/java/com/team4/giftidea/service/CoupangPartnersService.java new file mode 100644 index 0000000..11d2fac --- /dev/null +++ b/src/main/java/com/team4/giftidea/service/CoupangPartnersService.java @@ -0,0 +1,170 @@ +package com.team4.giftidea.service; + +import com.team4.giftidea.entity.Product; +import com.team4.giftidea.repository.ProductRepository; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.utils.Hex; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.*; + +@Slf4j +@Service +public class CoupangPartnersService { + + private final RestTemplate restTemplate = new RestTemplate(); + private final ProductRepository productRepository; + + @Value("${coupang.api.base-url}") + private String baseUrl; + + @Value("${coupang.api.access-key}") + private String accessKey; + + @Value("${coupang.api.secret-key}") + private String secretKey; + + @Value("${coupang.api.partner-id}") + private String partnerId; + + public CoupangPartnersService(ProductRepository productRepository) { + this.productRepository = productRepository; + } + + /** + * DB에서 모든 쿠팡 상품을 조회하고, 파트너스 링크로 업데이트 + */ + @Transactional + public int updateAllCoupangProductLinks() { + log.info("🔍 [START] 쿠팡 상품의 파트너스 링크 업데이트 시작"); + + List coupangProducts = productRepository.findByMallName("Coupang"); + log.info("📦 총 {}개의 쿠팡 상품을 찾음", coupangProducts.size()); + + int updatedCount = 0; + for (Product product : coupangProducts) { + String originalUrl = product.getLink(); + log.info("🔗 상품 ID {}의 기존 URL: {}", product.getProductId(), originalUrl); + + String partnerLink = generatePartnerLink(originalUrl); + if (partnerLink != null) { + log.info("✅ 상품 ID {}의 변환된 파트너스 링크: {}", product.getProductId(), partnerLink); + product.setLink(partnerLink); + productRepository.save(product); + updatedCount++; + } else { + log.warn("⚠️ 파트너스 링크 생성 실패 (상품 ID: {})", product.getProductId()); + } + } + log.info("🎯 [END] 총 {}개의 쿠팡 상품이 업데이트됨", updatedCount); + return updatedCount; + } + + /** + * 기존 쿠팡 상품 URL을 파트너스 트래킹 URL로 변환하기 위한 API 호출 + */ + private String generatePartnerLink(String originalUrl) { + try { + // 엔드포인트 URI (baseUrl과 결합) + String endpoint = "/v2/providers/affiliate_open_api/apis/openapi/v1/deeplink"; + String apiUrl = baseUrl + endpoint; + log.info("📡 쿠팡 파트너스 API 호출: {}", apiUrl); + + // HMAC 기반 Authorization 헤더 생성 + String authorization = generateAuthorizationHeader("POST", endpoint); + log.info("🔑 생성된 Authorization 헤더: {}", authorization); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", authorization); + String requestId = UUID.randomUUID().toString(); + headers.set("X-Request-Id", requestId); + log.info("🆔 X-Request-Id: {}", requestId); + + // 요청 바디 구성 (문서 예시에 맞게 coupangUrls와 subId 포함) + Map requestBody = new HashMap<>(); + requestBody.put("coupangUrls", Collections.singletonList(originalUrl)); + requestBody.put("subId", partnerId); + log.info("🔍 요청 바디: {}", requestBody); + log.debug("🔍 요청 헤더: {}", headers); + + long startTime = System.currentTimeMillis(); + HttpEntity> entity = new HttpEntity<>(requestBody, headers); + ResponseEntity response = restTemplate.exchange(apiUrl, HttpMethod.POST, entity, Map.class); + long duration = System.currentTimeMillis() - startTime; + log.info("⏱️ API 호출 소요 시간: {}ms", duration); + + log.info("🔍 API 응답 상태 코드: {}", response.getStatusCode()); + log.debug("🔍 API 응답 헤더: {}", response.getHeaders()); + log.info("📦 API 응답 바디: {}", response.getBody()); + + if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { + String rCode = (String) response.getBody().get("rCode"); + if ("0".equals(rCode)) { + List> data = (List>) response.getBody().get("data"); + if (data != null && !data.isEmpty()) { + return data.get(0).get("shortenUrl").toString(); + } else { + log.warn("⚠️ 응답에 data 없음, API 응답: {}", response.getBody()); + } + } else { + log.warn("⚠️ API 호출 실패, rCode: {}, rMessage: {}", rCode, response.getBody().get("rMessage")); + } + } else { + log.warn("⚠️ API 호출 실패, 상태 코드: {}", response.getStatusCode()); + } + } catch (Exception e) { + log.error("❌ 쿠팡 파트너스 링크 생성 중 오류 발생: {}", e.getMessage(), e); + } + return null; + } + + /** + * HMAC 서명 기반의 Authorization 헤더 생성 + * + * 메시지 형식: signedDate + method + path + query + * signedDate 포맷: "yyMMdd'T'HHmmss'Z'" (GMT 기준) + * + * 최종 형식: + * "CEA algorithm=HmacSHA256, access-key=ACCESS_KEY, signed-date=SIGNED_DATE, signature=SIGNATURE" + */ + private String generateAuthorizationHeader(String method, String uri) { + // GMT 기준 날짜/시간 생성 (형식: yyMMdd'T'HHmmss'Z') + SimpleDateFormat dateFormatGmt = new SimpleDateFormat("yyMMdd'T'HHmmss'Z'"); + dateFormatGmt.setTimeZone(TimeZone.getTimeZone("GMT")); + String signedDate = dateFormatGmt.format(new Date()); + + // uri에서 path와 query 분리 (query가 없는 경우 빈 문자열 사용) + String[] parts = uri.split("\\?", 2); + String path = parts[0]; + String query = (parts.length == 2) ? parts[1] : ""; + + // 메시지 생성 + String message = signedDate + method + path + query; + log.debug("🔐 서명할 메시지: {}", message); + + String signature; + try { + SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(signingKey); + byte[] rawHmac = mac.doFinal(message.getBytes(StandardCharsets.UTF_8)); + signature = Hex.encodeHexString(rawHmac); + log.debug("🔐 생성된 서명: {}", signature); + } catch (Exception e) { + throw new RuntimeException("HMAC 서명 생성 오류: " + e.getMessage(), e); + } + + // 최종 Authorization 헤더 생성 + return String.format("CEA algorithm=%s, access-key=%s, signed-date=%s, signature=%s", + "HmacSHA256", accessKey, signedDate, signature); + } +} \ No newline at end of file diff --git a/src/main/java/com/team4/giftidea/service/NaverApiService.java b/src/main/java/com/team4/giftidea/service/NaverApiService.java index 7e2bad0..ebe117a 100644 --- a/src/main/java/com/team4/giftidea/service/NaverApiService.java +++ b/src/main/java/com/team4/giftidea/service/NaverApiService.java @@ -13,7 +13,7 @@ import java.util.List; /** - * 네이버 쇼핑 API를 호출하여 상품 정보를 가져오는 서비스 클래스 + * 네이버 쇼핑 API를 호출하여 상품 정보를 가져오는 서비스 클래스(더 이상 사용하지 않음) */ @Service @Slf4j diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 6527078..5a24c01 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -36,3 +36,10 @@ openai: api: key: ${OPENAI_API_KEY} url: "https://api.openai.com/v1/chat/completions" + +coupang: + api: + base-url: https://api-gateway.coupang.com + access-key: "mock" + secret-key: "mock" + partner-id: "mock" \ No newline at end of file From 9efe470b7f688d8a8a19d2d9062dac6beed4b087 Mon Sep 17 00:00:00 2001 From: jiwonkim Date: Tue, 27 Feb 2024 12:00:00 +0900 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20CoupangPartnersService=20=EC=B0=A8?= =?UTF-8?q?=EB=8B=A8=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CoupangPartnersService.java | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/team4/giftidea/service/CoupangPartnersService.java b/src/main/java/com/team4/giftidea/service/CoupangPartnersService.java index 11d2fac..bed5f08 100644 --- a/src/main/java/com/team4/giftidea/service/CoupangPartnersService.java +++ b/src/main/java/com/team4/giftidea/service/CoupangPartnersService.java @@ -40,7 +40,7 @@ public CoupangPartnersService(ProductRepository productRepository) { } /** - * DB에서 모든 쿠팡 상품을 조회하고, 파트너스 링크로 업데이트 + * DB에서 모든 쿠팡 상품을 조회하고, 파트너스 링크로 업데이트 (배치+대기 적용) */ @Transactional public int updateAllCoupangProductLinks() { @@ -49,21 +49,45 @@ public int updateAllCoupangProductLinks() { List coupangProducts = productRepository.findByMallName("Coupang"); log.info("📦 총 {}개의 쿠팡 상품을 찾음", coupangProducts.size()); + // 한 번에 처리할 상품 수 (필요에 따라 조정) + final int BATCH_SIZE = 50; + // 각 배치 처리 후 대기 시간 (밀리초) (필요에 따라 조정) + final long SLEEP_MS = 60000L; + int updatedCount = 0; - for (Product product : coupangProducts) { - String originalUrl = product.getLink(); - log.info("🔗 상품 ID {}의 기존 URL: {}", product.getProductId(), originalUrl); - - String partnerLink = generatePartnerLink(originalUrl); - if (partnerLink != null) { - log.info("✅ 상품 ID {}의 변환된 파트너스 링크: {}", product.getProductId(), partnerLink); - product.setLink(partnerLink); - productRepository.save(product); - updatedCount++; - } else { - log.warn("⚠️ 파트너스 링크 생성 실패 (상품 ID: {})", product.getProductId()); + + // 배치(Chunk) 단위로 상품을 나눠 처리 + for (int i = 0; i < coupangProducts.size(); i += BATCH_SIZE) { + List batch = coupangProducts.subList(i, Math.min(i + BATCH_SIZE, coupangProducts.size())); + log.info("🔸 Batch 처리: index {} ~ {} (총 {}개)", i, i + batch.size() - 1, batch.size()); + + for (Product product : batch) { + String originalUrl = product.getLink(); + log.info("🔗 상품 ID {}의 기존 URL: {}", product.getProductId(), originalUrl); + + String partnerLink = generatePartnerLink(originalUrl); + if (partnerLink != null) { + log.info("✅ 상품 ID {}의 변환된 파트너스 링크: {}", product.getProductId(), partnerLink); + product.setLink(partnerLink); + productRepository.save(product); + updatedCount++; + } else { + log.warn("⚠️ 파트너스 링크 생성 실패 (상품 ID: {})", product.getProductId()); + } + } + + // 한 배치를 끝냈으므로 일정 시간 대기 (과도 호출 방지) + if (i + BATCH_SIZE < coupangProducts.size()) { + log.info("🔸 Batch 처리 완료: {}개 상품 업데이트, 다음 배치 전 {}ms 대기", batch.size(), SLEEP_MS); + try { + Thread.sleep(SLEEP_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("스레드 대기 중 인터럽트 발생: {}", e.getMessage()); + } } } + log.info("🎯 [END] 총 {}개의 쿠팡 상품이 업데이트됨", updatedCount); return updatedCount; } @@ -73,7 +97,6 @@ public int updateAllCoupangProductLinks() { */ private String generatePartnerLink(String originalUrl) { try { - // 엔드포인트 URI (baseUrl과 결합) String endpoint = "/v2/providers/affiliate_open_api/apis/openapi/v1/deeplink"; String apiUrl = baseUrl + endpoint; log.info("📡 쿠팡 파트너스 API 호출: {}", apiUrl); @@ -89,7 +112,6 @@ private String generatePartnerLink(String originalUrl) { headers.set("X-Request-Id", requestId); log.info("🆔 X-Request-Id: {}", requestId); - // 요청 바디 구성 (문서 예시에 맞게 coupangUrls와 subId 포함) Map requestBody = new HashMap<>(); requestBody.put("coupangUrls", Collections.singletonList(originalUrl)); requestBody.put("subId", partnerId); @@ -137,17 +159,14 @@ private String generatePartnerLink(String originalUrl) { * "CEA algorithm=HmacSHA256, access-key=ACCESS_KEY, signed-date=SIGNED_DATE, signature=SIGNATURE" */ private String generateAuthorizationHeader(String method, String uri) { - // GMT 기준 날짜/시간 생성 (형식: yyMMdd'T'HHmmss'Z') SimpleDateFormat dateFormatGmt = new SimpleDateFormat("yyMMdd'T'HHmmss'Z'"); dateFormatGmt.setTimeZone(TimeZone.getTimeZone("GMT")); String signedDate = dateFormatGmt.format(new Date()); - // uri에서 path와 query 분리 (query가 없는 경우 빈 문자열 사용) String[] parts = uri.split("\\?", 2); String path = parts[0]; String query = (parts.length == 2) ? parts[1] : ""; - // 메시지 생성 String message = signedDate + method + path + query; log.debug("🔐 서명할 메시지: {}", message); @@ -163,7 +182,6 @@ private String generateAuthorizationHeader(String method, String uri) { throw new RuntimeException("HMAC 서명 생성 오류: " + e.getMessage(), e); } - // 최종 Authorization 헤더 생성 return String.format("CEA algorithm=%s, access-key=%s, signed-date=%s, signature=%s", "HmacSHA256", accessKey, signedDate, signature); } From ddc622ca2ba826aebef11e02d9c31c3898bc321e Mon Sep 17 00:00:00 2001 From: yeoEun Date: Sat, 15 Feb 2025 02:25:32 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat=20#78=20:=20=EA=B7=BC=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../giftidea/controller/GptController.java | 45 +++++++++++++++---- .../com/team4/giftidea/entity/Product.java | 7 +++ 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/team4/giftidea/controller/GptController.java b/src/main/java/com/team4/giftidea/controller/GptController.java index 079b9f9..7c3b241 100644 --- a/src/main/java/com/team4/giftidea/controller/GptController.java +++ b/src/main/java/com/team4/giftidea/controller/GptController.java @@ -72,13 +72,22 @@ public List processFileAndRecommend( List processedMessages = preprocessKakaoFile(file, targetName); // 2. GPT API 호출: 전처리된 메시지로 키워드 반환 - String categories = generatePrompt(processedMessages, relation, sex, theme); + String gptResponse = generatePrompt(processedMessages, relation, sex, theme); - // 3. 키워드 리스트 변환 및 상품 검색 - List keywords = Arrays.asList(categories.split(",")); + // 3. 키워드, 근거 리스트 변환 및 상품 검색 + String[] responseLines = gptResponse.split("\n"); + String categories = responseLines[0].replace("Categories: ", "").trim(); + String reasons = responseLines.length > 1 ? responseLines[1].trim() : ""; + + List keywords = Arrays.asList(categories.split(", ")); keywords.replaceAll(String::trim); + List reasonList = Arrays.asList(reasons.split("\n")); + List products = productService.searchByKeywords(keywords); + for (int i = 0; i < products.size() && i < reasonList.size(); i++) { + products.get(i).setReason(reasonList.get(i)); + } return products; } @@ -207,7 +216,6 @@ private String generatePrompt(List processedMessages, String relation, S private String generateText(String prompt) { GptRequestDTO request = new GptRequestDTO(gptConfig.getModel(), prompt); try { - // HTTP 요청 전에 request 객체 로깅 ObjectMapper mapper = new ObjectMapper(); @@ -217,21 +225,39 @@ private String generateText(String prompt) { 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(); - // 필요한 형태로 카테고리 추출 (예: "1. [무선이어폰, 스마트워치, 향수]" 형태) if (content.contains("1.")) { - String categories = content.split("1.")[1].split("\n")[0]; // 첫 번째 카테고리 라인 추출 + // 첫 번째 줄: 카테고리 리스트 추출 + String categories = content.split("1.")[1].split("\n")[0]; - // 괄호 안의 항목들을 추출하고, 쉼표로 구분하여 키워드 리스트 만들기 + // 카테고리 리스트 (괄호 안의 항목들) String[] categoryArray = categories.split("\\[|\\]")[1].split(","); + List keywords = new ArrayList<>(); for (String category : categoryArray) { keywords.add(category.trim()); } - return String.join(", ", keywords); // 최종적으로 카테고리들을 반환 + + // 두 번째 줄 이후: 카테고리별 설명(reason) 추출 + List reasons = new ArrayList<>(); + String[] lines = content.split("\n"); + + for (String line : lines) { + line = line.trim(); + if (line.startsWith("- ")) { // 설명 부분인지 확인 + int startIndex = line.indexOf(": ["); + if (startIndex != -1) { + String reason = line.substring(startIndex + 3, line.length() - 1).trim(); + reasons.add(reason); + } + } + } + + // 카테고리와 설명을 조합하여 반환 + return "Categories: " + String.join(", ", keywords) + "\n" + + "Reasons: " + String.join("\n", reasons); } else { log.warn("GPT 응답에서 카테고리 정보가 올바르지 않습니다."); } @@ -251,6 +277,7 @@ private String generateText(String prompt) { } } + private String extractKeywordsAndReasonsCoupleMan(String theme, String message) { String prompt = String.format(""" 다음 텍스트를 참고하여 남자 애인이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. diff --git a/src/main/java/com/team4/giftidea/entity/Product.java b/src/main/java/com/team4/giftidea/entity/Product.java index b6aa81d..9c5ce76 100644 --- a/src/main/java/com/team4/giftidea/entity/Product.java +++ b/src/main/java/com/team4/giftidea/entity/Product.java @@ -72,4 +72,11 @@ public class Product { */ @Column(nullable = false) private String keyword; + + /** + * 추천 근거 (GPT가 제공하는 추천 이유) + */ + @Column(length = 1000) // 추천 근거가 길어질 수 있으므로 적절한 길이 설정 + private String reason; + } \ No newline at end of file From 89f2a218fc71b9665e845f4b7113024211c82046 Mon Sep 17 00:00:00 2001 From: yeoEun Date: Sat, 15 Feb 2025 03:46:12 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat=20#78=20:=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=ED=98=95=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gradle/buildOutputCleanup/cache.properties | 2 +- .gradle/buildOutputCleanup/outputFiles.bin | Bin 19505 -> 19001 bytes .../giftidea/controller/GptController.java | 9 ++++----- .../com/team4/giftidea/entity/Product.java | 6 ------ 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties index 8bfd2aa..b12c514 100644 --- a/.gradle/buildOutputCleanup/cache.properties +++ b/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Fri Feb 07 20:37:40 KST 2025 +#Sat Feb 15 02:43:43 KST 2025 gradle.version=8.10 diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin index 1f7b7a32928e640828c392024bb279892e9365cf..ed5e392c7c13bab99779a179e55becdc656f3eff 100644 GIT binary patch delta 588 zcmdlugK_5+#tkMC^)l^hYfon_c_739243p_p`e5NPS!o|R9A?&M=DIbr(BvvV|Qg1 zRQxMUym#e``+Kt=?t+R3!^G!SykGliOWs6?xaV}3_>yC4?hEH~#XW&Yc!^GaD<-hJQQpvGy##P|GP>y^lC!wwZ+2@~IUW#6Ar2JJqG zxQ8Q5{B&gNljoi82chckL&PU1J^8MrI3xSyxl-bi`(f&TUHF=wyy=_;RDI&) zzf$6f7h$rJJTvCr{?xJpD*F*8E|uqdZ>KnS2-Nm+n7B^$?~^P?Ssy~xx5C8j4eSqf z)~23-s_&dEDjzpB0%U3;E<2>Y)2HMcdlvUUnrE@RiN`k-$-9lM>p6Z_$tzOqc+ ze3S6dZtQuws-C^$lRnb^Z1%Is5gWD}r31oMW{W>69PDjb zqW9s(1+-tyelPijXU-uv1$h(u{n9s2n&Y*%Y5xj~o3uORJ1>{e{+{d)Le{yDwh!A) zp2uGC)KH#z?2JF{uV;T2(DFRHi*qydb zd|T2O@|EVD*j+s8dg-%cR?_@lc2`Z&$hjkC9ucm(!QTC4=*fV%L7#;yv)KE)?$%W{ zZahl!uh~6fCsm#6>oP;QVlum*UEtV?hpEG9ez?WW*QYkEsVt)XT6X_Lv(e#(V=e9Z z#2!53UfqI7uQb{tV;>VR=4#4XHSLj4XIBJuZrD_(Z>9Nd7QYqfY{>ZPP1kK;4=vd- zD=A{R9bLDYJxnJrGt29{(4J`aX-m|m&h&q;e`OQ0|LAa>1^h&}n^qR8b5>@ki< zm%MVmrl~Y1;IfgGSo?>3EXJ3`u5}fLt z;U!$PmYsh~lH*kI9Dbqz3ZMWApa2S>01BW03ZMWApa2S>01BW03ZMWApa2S>01BW0 z3ZMWApa2S>01BW03ZMWApa2S>01BYMzbP=Z?K16O0B-+s@UJfjcmG4$>0e3PrEIvC t^;v%-w%wQYX502ypJm(Tjyub?&DQtTanf;rp>3Nx?kwxgw#l#m#ZTr1{aye7 diff --git a/src/main/java/com/team4/giftidea/controller/GptController.java b/src/main/java/com/team4/giftidea/controller/GptController.java index 3787962..4d16661 100644 --- a/src/main/java/com/team4/giftidea/controller/GptController.java +++ b/src/main/java/com/team4/giftidea/controller/GptController.java @@ -64,7 +64,7 @@ public GptController(RestTemplate restTemplate, GptConfig gptConfig, ProductServ @ApiResponse(responseCode = "500", description = "서버 내부 오류 발생") }) @PostMapping(value = "/process", consumes = "multipart/form-data", produces = "application/json") - public List processFileAndRecommend( + public List processFileAndRecommend( @RequestParam("file") @Parameter(description = "카카오톡 대화 파일 (.txt)", required = true) MultipartFile file, @RequestParam("targetName") @Parameter(description = "분석 대상 이름 (예: '여자친구')", required = true) String targetName, @RequestParam("relation") @Parameter(description = "대상과의 관계 (couple, friend, parent 등)", required = true) String relation, @@ -128,10 +128,9 @@ public List processFileAndRecommend( List reasonList = Arrays.asList(reasons.split("\n")); - List products = productService.searchByKeywords(keywords); - for (int i = 0; i < products.size() && i < reasonList.size(); i++) { - products.get(i).setReason(reasonList.get(i)); - } + List products_No_reason = productService.searchByKeywords(keywords); + List products = new ArrayList<>(products_No_reason); + products.add(reasonList); return products; } diff --git a/src/main/java/com/team4/giftidea/entity/Product.java b/src/main/java/com/team4/giftidea/entity/Product.java index 9c5ce76..656c83e 100644 --- a/src/main/java/com/team4/giftidea/entity/Product.java +++ b/src/main/java/com/team4/giftidea/entity/Product.java @@ -73,10 +73,4 @@ public class Product { @Column(nullable = false) private String keyword; - /** - * 추천 근거 (GPT가 제공하는 추천 이유) - */ - @Column(length = 1000) // 추천 근거가 길어질 수 있으므로 적절한 길이 설정 - private String reason; - } \ No newline at end of file From d3fc018e3f1f905087626b81949d9aaa7edc4f36 Mon Sep 17 00:00:00 2001 From: jiwonkim Date: Sat, 15 Feb 2025 09:23:12 +0900 Subject: [PATCH 5/8] =?UTF-8?q?release=20:=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=EC=9A=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gradle/buildOutputCleanup/cache.properties | 2 +- .gradle/buildOutputCleanup/outputFiles.bin | Bin 19001 -> 18893 bytes .../giftidea/controller/GptController.java | 366 +++++++++--------- 3 files changed, 175 insertions(+), 193 deletions(-) diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties index b12c514..c9f936c 100644 --- a/.gradle/buildOutputCleanup/cache.properties +++ b/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Sat Feb 15 02:43:43 KST 2025 +#Sat Feb 15 09:08:54 KST 2025 gradle.version=8.10 diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin index ed5e392c7c13bab99779a179e55becdc656f3eff..8744ab6e7c86ff5fc73faec7a5d36535b5ab2a05 100644 GIT binary patch literal 18893 zcmeI&Pe>GD7=ZC{(b5tPK`}3_cu3I0f+mU@3L>)*D`9yEF+)<&6x4&FAb3!WRS#us z5Jertg0{?4A*5j7AHuW}1C>e*4GJMO5fR;G-dE`w3L?+I?tVP;?zc0)>uio;M4#D_ z!`MF}_m`{?KmY**5I_I{1Q0*~0R#|0009ILKmY**5J2F#2we5u$SLb)q7!ZYMR=WI zI6{4kDEIR>e_wpPR$`rYdi43hy`pbp<-^@wRyS{I|Fn4bj@dQ%Ugl3~|D4+tdC}&& zFa4SJS;xlQlMnGPrRQn?9+=!Xn)^$hO_=N2e{@&opX+I8li$tP{-@({S&+RcLiRbU zJ+yozy)dnyM|!{Zu%%1Aj+*nUvd;tU@c}7W(^KvXGM}*5x87Y|Z2=zxCbCwVYx>rtXKSzi*+2Pu@zH7NliKV1HZt6C z9=p|@wc7R5Fl@2bIo=UK009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1df_OlJA22e^3vQml5_)@#mvde3x@b*YkfnALzGs0c&nH A1poj5 literal 19001 zcmeI&Pe_w-9LMor)0#CYQzW&hODsVTf*>9Wk^XGWLaD~8Ln5|>ClS=e3h5G!TxeiB z6zW3+3hwYAwMxobnJCVgD0Yct^UokDuo|h5fdSsR`o)5Xcv=~ zt&1J(ate`?s2=RQV^o4t+eEVVvgHu}eo${1mqJDM<61Y{xi>iX#%Fflkf_gbZ}iTD z-&~!1Mg1&y`TBPy`mCx+>n9&@Z|gUj9?O>^TDPQf_sOB%DGt{U+8^QmqTY6^DIgxv zK8}0W(awfO|H28{XK{bMtXRt84(q9Vx%)r*m#iY|p4QEU+=CZfkuPpj5ADO;M|7cq zS!;p;*JwY= zJw5TE{5g8NgZ4kUXH=aIz1(9sM|&st?7GE3eBahd+Pkt$W%>503dRQva~lTlJpjck_(YySYmx zw&t$F3hHKsdrdH*)BkJXfVMYJbLVFvbWgO`@CX42KmY;|fB*y_009U<00Izz00bZa z0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*y_009U<00Oxx!FKf?`&0PpAH#S4 cQDW)eF6s$x(iv{-lQ-{Oe}D4E%IbHz0i_@% diff --git a/src/main/java/com/team4/giftidea/controller/GptController.java b/src/main/java/com/team4/giftidea/controller/GptController.java index b2a0bf7..a91d151 100644 --- a/src/main/java/com/team4/giftidea/controller/GptController.java +++ b/src/main/java/com/team4/giftidea/controller/GptController.java @@ -1,11 +1,17 @@ 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 io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; @@ -13,19 +19,11 @@ import java.io.*; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -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 java.util.*; +import java.util.stream.Collectors; @Slf4j +@Tag(name = "🎁 GPT 추천 API", description = "카카오톡 대화를 분석하여 GPT를 통해 추천 선물을 제공하는 API") @RestController @RequestMapping("/api/gpt") public class GptController { @@ -41,17 +39,19 @@ public GptController(RestTemplate restTemplate, GptConfig gptConfig, ProductServ this.productService = productService; } - - // GPT 모델의 입력 토큰 제한 (예: 출력 토큰 고려 후 설정, 여기서는 예시로 25000) + // GPT 모델의 입력 토큰 제한 (예: 출력 토큰 고려 후 설정, 여기서는 예시로 11000) private static final int GPT_INPUT_LIMIT = 11000; /** - * 파일의 아랫부분부터 토큰을 센 후, 총 토큰 수가 GPT_INPUT_LIMIT 이하인 내용만 - * 선택하여 로컬에 저장하고, 그 청크를 반환합니다. + * 파일의 아랫부분부터 토큰을 누적하여, GPT 입력 제한 이하인 내용만 선택한 후, + * 해당 청크를 GPT API로 보내 키워드를 추출하고, 최종적으로 관련 상품과 Reasons를 반환합니다. * * @param file 업로드된 카카오톡 대화 파일 (.txt) * @param targetName 대상 이름 (예: "여자친구") - * @return 전처리된 청크 (아랫부분부터 토큰 누적하여 GPT_INPUT_LIMIT 이하) + * @param relation 대상과의 관계 (예: "couple", "friend", "parent") + * @param sex 대상 성별 ("male" 또는 "female") + * @param theme 선물 주제 (예: "birthday", "valentine") + * @return 상품 목록과 Reasons 리스트를 포함한 JSON 배열 */ @Operation( summary = "카톡 대화 분석 후 선물 추천", @@ -69,14 +69,11 @@ public List processFileAndRecommend( @RequestParam("targetName") @Parameter(description = "분석 대상 이름 (예: '여자친구')", required = true) String targetName, @RequestParam("relation") @Parameter(description = "대상과의 관계 (couple, friend, parent 등)", required = true) String relation, @RequestParam("sex") @Parameter(description = "대상 성별 (male 또는 female)", required = true) String sex, - @RequestParam("theme") @Parameter(description = "선물 주제 (birthday, valentine 등)", required = true) String theme - ) { + @RequestParam("theme") @Parameter(description = "선물 주제 (birthday, valentine 등)", required = true) String theme) { - List processedMessages = new ArrayList<>(); - int formatType = detectFormatType(file); - - // 1. 파일의 모든 줄을 읽고, targetName이 포함된 줄만 필터링하여 리스트에 저장 + // 1. 파일의 모든 줄 중, targetName이 포함된 줄만 필터링 List allTargetLines = new ArrayList<>(); + int formatType = detectFormatType(file); try (BufferedReader reader = new BufferedReader( new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) { String line; @@ -90,49 +87,61 @@ public List processFileAndRecommend( log.error("파일 읽기 오류: ", e); } - // 2. 파일의 아랫부분부터 토큰을 누적 (역순으로 처리) + // 2. 파일의 아랫부분부터 토큰을 역순으로 누적하여 GPT_INPUT_LIMIT 이하인 내용만 선택 int currentTokenCount = 0; List selectedLines = new ArrayList<>(); - // reverse 순회 for (int i = allTargetLines.size() - 1; i >= 0; i--) { String currentLine = allTargetLines.get(i); int tokenCount = countTokens(currentLine); if (currentTokenCount + tokenCount > GPT_INPUT_LIMIT) { - // 토큰 제한을 초과하면 중단 break; } - // 아랫부분부터 선택하므로, 먼저 선택된 줄이 마지막에 온다. selectedLines.add(currentLine); currentTokenCount += tokenCount; } - // 원래 순서대로 복원 (파일에서 아랫부분이 우선이므로, 리스트를 reverse) + // 원래 순서대로 복원 Collections.reverse(selectedLines); - - // 3. 선택된 줄들을 하나의 청크로 합침 StringBuilder finalChunk = new StringBuilder(); for (String s : selectedLines) { finalChunk.append(s).append("\n"); } + List processedMessages = new ArrayList<>(); processedMessages.add(finalChunk.toString()); - // 2. GPT API 호출: 전처리된 메시지로 키워드 반환 + // (선택 사항) - 로컬에 저장 (여기서는 임시 파일로 저장 후 삭제하지 않음) + try { + File outputFile = new File(System.getProperty("user.home"), "processed_kakaochat.txt"); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile, false))) { + writer.write(finalChunk.toString()); + writer.flush(); + } + log.info("전처리 완료. 결과 파일 저장 위치: " + outputFile.getAbsolutePath()); + } catch (IOException e) { + log.error("파일 저장 오류: ", e); + } + + // 3. GPT API 호출: 전처리된 메시지(청크)를 기반으로 키워드 및 근거 추출 String gptResponse = generatePrompt(processedMessages, relation, sex, theme); - // 3. 키워드, 근거 리스트 변환 및 상품 검색 + // 4. GPT 응답 파싱 + // 예를 들어, GPT 응답이 아래와 같은 형식이라 가정: + // "Categories: 향수, 무선이어폰, 목걸이\n- 향수: [근거 내용...]\n- 무선이어폰: [근거 내용...]\n- 목걸이: [근거 내용...]" String[] responseLines = gptResponse.split("\n"); String categories = responseLines[0].replace("Categories: ", "").trim(); - String reasons = responseLines.length > 1 ? responseLines[1].trim() : ""; - - List keywords = Arrays.asList(categories.split(", ")); - keywords.replaceAll(String::trim); + String reasons = responseLines.length > 1 ? gptResponse.substring(gptResponse.indexOf("\n") + 1).trim() : ""; + List keywords = Arrays.stream(categories.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); List reasonList = Arrays.asList(reasons.split("\n")); + // 5. 데이터베이스에서 검색 (키워드를 기반으로 상품 조회) List products_No_reason = productService.searchByKeywords(keywords); - List products = new ArrayList<>(products_No_reason); - products.add(reasonList); + List finalResponse = new ArrayList<>(products_No_reason); + finalResponse.add(reasonList); - return products; + return finalResponse; } private int detectFormatType(MultipartFile file) { @@ -169,8 +178,7 @@ private int countTokens(String text) { } private String generatePrompt(List processedMessages, String relation, String sex, String theme) { - String combinedMessages = String.join("\n", processedMessages); // List을 하나의 String으로 합침 - + String combinedMessages = String.join("\n", processedMessages); if ("couple".equals(relation)) { if ("male".equals(sex)) { return extractKeywordsAndReasonsCoupleMan(theme, combinedMessages); @@ -194,63 +202,46 @@ private String generatePrompt(List processedMessages, String relation, S return extractKeywordsAndReasonsSeasonalWoman(theme, combinedMessages); } } - return "조건에 맞는 선물 추천 기능이 없습니다."; } private String generateText(String prompt) { GptRequestDTO request = new GptRequestDTO(gptConfig.getModel(), prompt); try { - // HTTP 요청 전에 request 객체 로깅 ObjectMapper mapper = new ObjectMapper(); - GptResponseDTO response = restTemplate.postForObject(gptConfig.getApiUrl(), request, GptResponseDTO.class); - // 응답 검증 - if (response != null) { - log.debug("GPT 응답 수신: {}", mapper.writeValueAsString(response)); - - if (response.getChoices() != null && !response.getChoices().isEmpty()) { - String content = response.getChoices().get(0).getMessage().getContent(); - - if (content.contains("1.")) { - // 첫 번째 줄: 카테고리 리스트 추출 - String categories = content.split("1.")[1].split("\n")[0]; - - // 카테고리 리스트 (괄호 안의 항목들) - String[] categoryArray = categories.split("\\[|\\]")[1].split(","); - - List keywords = new ArrayList<>(); - for (String category : categoryArray) { - keywords.add(category.trim()); - } - - // 두 번째 줄 이후: 카테고리별 설명(reason) 추출 - List reasons = new ArrayList<>(); - String[] lines = content.split("\n"); - - for (String line : lines) { - line = line.trim(); - if (line.startsWith("- ")) { // 설명 부분인지 확인 - int startIndex = line.indexOf(": ["); - if (startIndex != -1) { - String reason = line.substring(startIndex + 3, line.length() - 1).trim(); - reasons.add(reason); - } - } - } - - // 카테고리와 설명을 조합하여 반환 - return "Categories: " + String.join(", ", keywords) + "\n" + - "Reasons: " + String.join("\n", reasons); + if (response != null && response.getChoices() != null && !response.getChoices().isEmpty()) { + String content = response.getChoices().get(0).getMessage().getContent(); + log.debug("GPT 전체 응답: {}", content); + + // "1."과 "2."를 기준으로 파싱 (응답 포맷이 아래와 같다고 가정) + // 예: "Categories: 향수, 무선이어폰, 목걸이\n- 향수: [...]\n- 무선이어폰: [...]\n- 목걸이: [...]" + if (content.contains("1.") && content.contains("2.")) { + String[] parts = content.split("2\\."); + String part1 = parts[0].trim(); + String reasonsPart = parts[1].trim(); + + if (part1.startsWith("1.")) { + part1 = part1.substring(2).trim(); + } + int startIdx = part1.indexOf("["); + int endIdx = part1.indexOf("]"); + String categories = ""; + if (startIdx != -1 && endIdx != -1 && endIdx > startIdx) { + categories = part1.substring(startIdx + 1, endIdx).trim(); } else { - log.warn("GPT 응답에서 카테고리 정보가 올바르지 않습니다."); + log.warn("카테고리 부분 추출 실패, 전체 내용: {}", part1); } + log.debug("추출된 카테고리: {}", categories); + log.debug("추출된 Reasons: {}", reasonsPart); + + return "Categories: " + categories + "\n" + reasonsPart; } else { - log.warn("GPT 응답에 'choices'가 없거나 빈 리스트입니다."); + log.warn("응답 포맷이 예상과 다릅니다: {}", content); } } else { - log.warn("GPT 응답이 null입니다."); + log.warn("GPT 응답이 null이거나 choices가 비어 있습니다."); } return "GPT 응답 오류 발생"; } catch (Exception e) { @@ -262,149 +253,140 @@ private String generateText(String prompt) { } } - private String extractKeywordsAndReasonsCoupleMan(String theme, String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 남자 애인이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. - 카테고리: 남성 지갑, 남성 스니커즈, 백팩, 토트백, 크로스백, 벨트, 선글라스, 향수, 헬스가방, 무선이어폰, 스마트워치, 맨투맨, 마우스, 키보드, 전기면도기, 게임기 - - 텍스트: %s - - 출력 형식: - 1. [카테고리1,카테고리2,카테고리3] - 2. - - 카테고리1: [근거1] - - 카테고리2: [근거2] - - 카테고리3: [근거3] - """, theme, message); - + 다음 텍스트를 참고하여 남자 애인이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 카테고리: 남성 지갑, 남성 스니커즈, 백팩, 토트백, 크로스백, 벨트, 선글라스, 향수, 헬스가방, 무선이어폰, 스마트워치, 맨투맨, 마우스, 키보드, 전기면도기, 게임기 + + 텍스트: %s + + 출력 형식: + 1. [카테고리1,카테고리2,카테고리3] + 2. + - 카테고리1: [근거1] + - 카테고리2: [근거2] + - 카테고리3: [근거3] + """, theme, message); return generateText(prompt); } private String extractKeywordsAndReasonsCoupleWoman(String theme, String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 여자 애인이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. - 카테고리: 여성 지갑, 여성 스니커즈, 숄더백, 토트백, 크로스백, 향수, 목걸이, 무선이어폰, 스마트워치, 에어랩 - - 텍스트: %s - - 출력 형식: - 1. [카테고리1,카테고리2,카테고리3] - 2. - - 카테고리1: [근거1] - - 카테고리2: [근거2] - - 카테고리3: [근거3] - """, theme, message); - - return generateText(prompt); // GPT 모델 호출 + 다음 텍스트를 참고하여 여자 애인이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 카테고리: 여성 지갑, 여성 스니커즈, 숄더백, 토트백, 크로스백, 향수, 목걸이, 무선이어폰, 스마트워치, 에어랩 + + 텍스트: %s + + 출력 형식: + 1. [카테고리1,카테고리2,카테고리3] + 2. + - 카테고리1: [근거1] + - 카테고리2: [근거2] + - 카테고리3: [근거3] + """, theme, message); + return generateText(prompt); } private String extractKeywordsAndReasonsDad(String theme, String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 부모님이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. - 카테고리: 현금 박스, 안마기기, 아버지 신발, 시계 - - 텍스트: %s - - 출력 형식: - 1. [카테고리1,카테고리2,카테고리3] - 2. - - 카테고리1: [근거1] - - 카테고리2: [근거2] - - 카테고리3: [근거3] - """, theme, message); - + 다음 텍스트를 참고하여 부모님이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 카테고리: 현금 박스, 안마기기, 아버지 신발, 시계 + + 텍스트: %s + + 출력 형식: + 1. [카테고리1,카테고리2,카테고리3] + 2. + - 카테고리1: [근거1] + - 카테고리2: [근거2] + - 카테고리3: [근거3] + """, theme, message); return generateText(prompt); } private String extractKeywordsAndReasonsMom(String theme, String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 부모님이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. - 카테고리: 현금 박스, 안마기기, 어머니 신발, 건강식품, 스카프 - - 텍스트: %s - - 출력 형식: - 1. [카테고리1,카테고리2,카테고리3] - 2. - - 카테고리1: [근거1] - - 카테고리2: [근거2] - - 카테고리3: [근거3] - """, theme, message); - + 다음 텍스트를 참고하여 부모님이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 카테고리: 현금 박스, 안마기기, 어머니 신발, 건강식품, 스카프 + + 텍스트: %s + + 출력 형식: + 1. [카테고리1,카테고리2,카테고리3] + 2. + - 카테고리1: [근거1] + - 카테고리2: [근거2] + - 카테고리3: [근거3] + """, theme, message); return generateText(prompt); } private String extractKeywordsAndReasonsFriend(String theme, String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 친구가 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. - 제시된 카테고리에 없는 추천 선물이 있다면 3개에 포함해주세요. - 카테고리: 핸드크림, 텀블러, 립밤, 머플러, 비타민, 입욕제, 블루투스 스피커 - - 텍스트: %s - - 출력 형식: - 1. [카테고리1,카테고리2,카테고리3] - 2. - - 카테고리1: [근거1] - - 카테고리2: [근거2] - - 카테고리3: [근거3] - """, theme, message); - + 다음 텍스트를 참고하여 친구가 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 제시된 카테고리에 없는 추천 선물이 있다면 3개에 포함해주세요. + 카테고리: 핸드크림, 텀블러, 립밤, 머플러, 비타민, 입욕제, 블루투스 스피커 + + 텍스트: %s + + 출력 형식: + 1. [카테고리1,카테고리2,카테고리3] + 2. + - 카테고리1: [근거1] + - 카테고리2: [근거2] + - 카테고리3: [근거3] + """, theme, message); return generateText(prompt); } private String extractKeywordsAndReasonsHousewarming(String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 집들이에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. - 카테고리: 조명, 핸드워시, 식기, 디퓨저, 오설록 티세트, 휴지, 파자마세트, 무드등, 디퓨저, 수건, 전기포트, 에어프라이기 - - 텍스트: %s - - 출력 형식: - 1. [카테고리1,카테고리2,카테고리3] - 2. - - 카테고리1: [근거1] - - 카테고리2: [근거2] - - 카테고리3: [근거3] - """, message); - + 다음 텍스트를 참고하여 집들이에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 카테고리: 조명, 핸드워시, 식기, 디퓨저, 오설록 티세트, 휴지, 파자마세트, 무드등, 디퓨저, 수건, 전기포트, 에어프라이기 + + 텍스트: %s + + 출력 형식: + 1. [카테고리1,카테고리2,카테고리3] + 2. + - 카테고리1: [근거1] + - 카테고리2: [근거2] + - 카테고리3: [근거3] + """, message); return generateText(prompt); } private String extractKeywordsAndReasonsSeasonalMan(String theme, String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. - 카테고리: 초콜릿, 수제 초콜릿 키트, 파자마세트, 남자 화장품 - - 텍스트: %s - - 출력 형식: - 1. [카테고리1,카테고리2,카테고리3] - 2. - - 카테고리1: [근거1] - - 카테고리2: [근거2] - - 카테고리3: [근거3] - """, theme, message); - + 다음 텍스트를 참고하여 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 카테고리: 초콜릿, 수제 초콜릿 키트, 파자마세트, 남자 화장품 + + 텍스트: %s + + 출력 형식: + 1. [카테고리1,카테고리2,카테고리3] + 2. + - 카테고리1: [근거1] + - 카테고리2: [근거2] + - 카테고리3: [근거3] + """, theme, message); return generateText(prompt); } private String extractKeywordsAndReasonsSeasonalWoman(String theme, String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. - 카테고리: 초콜릿, 수제 초콜릿 키트, 립밤, 파자마세트, 립스틱 - - 텍스트: %s - - 출력 형식: - 1. [카테고리1,카테고리2,카테고리3] - 2. - - 카테고리1: [근거1] - - 카테고리2: [근거2] - - 카테고리3: [근거3] - """, theme, message); - + 다음 텍스트를 참고하여 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 카테고리: 초콜릿, 수제 초콜릿 키트, 립밤, 파자마세트, 립스틱 + + 텍스트: %s + + 출력 형식: + 1. [카테고리1,카테고리2,카테고리3] + 2. + - 카테고리1: [근거1] + - 카테고리2: [근거2] + - 카테고리3: [근거3] + """, theme, message); return generateText(prompt); } -} +} \ No newline at end of file From df8e85ba3871f531a9adff7f69fc691ab5440f4f Mon Sep 17 00:00:00 2001 From: jiwonkim Date: Sat, 15 Feb 2025 09:27:46 +0900 Subject: [PATCH 6/8] =?UTF-8?q?release=20#1=20:=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=EC=9A=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../giftidea/controller/GptController.java | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/team4/giftidea/controller/GptController.java b/src/main/java/com/team4/giftidea/controller/GptController.java index a91d151..c47457b 100644 --- a/src/main/java/com/team4/giftidea/controller/GptController.java +++ b/src/main/java/com/team4/giftidea/controller/GptController.java @@ -64,14 +64,15 @@ public GptController(RestTemplate restTemplate, GptConfig gptConfig, ProductServ @ApiResponse(responseCode = "500", description = "서버 내부 오류 발생") }) @PostMapping(value = "/process", consumes = "multipart/form-data", produces = "application/json") - public List processFileAndRecommend( + public Map processFileAndRecommend( @RequestParam("file") @Parameter(description = "카카오톡 대화 파일 (.txt)", required = true) MultipartFile file, @RequestParam("targetName") @Parameter(description = "분석 대상 이름 (예: '여자친구')", required = true) String targetName, @RequestParam("relation") @Parameter(description = "대상과의 관계 (couple, friend, parent 등)", required = true) String relation, @RequestParam("sex") @Parameter(description = "대상 성별 (male 또는 female)", required = true) String sex, - @RequestParam("theme") @Parameter(description = "선물 주제 (birthday, valentine 등)", required = true) String theme) { + @RequestParam("theme") @Parameter(description = "선물 주제 (birthday, valentine 등)", required = true) String theme + ) { - // 1. 파일의 모든 줄 중, targetName이 포함된 줄만 필터링 + // 1. 파일 전처리 (아랫부분부터 토큰 누적) List allTargetLines = new ArrayList<>(); int formatType = detectFormatType(file); try (BufferedReader reader = new BufferedReader( @@ -87,7 +88,6 @@ public List processFileAndRecommend( log.error("파일 읽기 오류: ", e); } - // 2. 파일의 아랫부분부터 토큰을 역순으로 누적하여 GPT_INPUT_LIMIT 이하인 내용만 선택 int currentTokenCount = 0; List selectedLines = new ArrayList<>(); for (int i = allTargetLines.size() - 1; i >= 0; i--) { @@ -99,7 +99,6 @@ public List processFileAndRecommend( selectedLines.add(currentLine); currentTokenCount += tokenCount; } - // 원래 순서대로 복원 Collections.reverse(selectedLines); StringBuilder finalChunk = new StringBuilder(); for (String s : selectedLines) { @@ -108,7 +107,7 @@ public List processFileAndRecommend( List processedMessages = new ArrayList<>(); processedMessages.add(finalChunk.toString()); - // (선택 사항) - 로컬에 저장 (여기서는 임시 파일로 저장 후 삭제하지 않음) + // (옵션) 로컬 파일 저장 (필요시) try { File outputFile = new File(System.getProperty("user.home"), "processed_kakaochat.txt"); try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile, false))) { @@ -120,12 +119,8 @@ public List processFileAndRecommend( log.error("파일 저장 오류: ", e); } - // 3. GPT API 호출: 전처리된 메시지(청크)를 기반으로 키워드 및 근거 추출 + // 2. GPT API 호출 및 응답 파싱 String gptResponse = generatePrompt(processedMessages, relation, sex, theme); - - // 4. GPT 응답 파싱 - // 예를 들어, GPT 응답이 아래와 같은 형식이라 가정: - // "Categories: 향수, 무선이어폰, 목걸이\n- 향수: [근거 내용...]\n- 무선이어폰: [근거 내용...]\n- 목걸이: [근거 내용...]" String[] responseLines = gptResponse.split("\n"); String categories = responseLines[0].replace("Categories: ", "").trim(); String reasons = responseLines.length > 1 ? gptResponse.substring(gptResponse.indexOf("\n") + 1).trim() : ""; @@ -136,12 +131,14 @@ public List processFileAndRecommend( .collect(Collectors.toList()); List reasonList = Arrays.asList(reasons.split("\n")); - // 5. 데이터베이스에서 검색 (키워드를 기반으로 상품 조회) - List products_No_reason = productService.searchByKeywords(keywords); - List finalResponse = new ArrayList<>(products_No_reason); - finalResponse.add(reasonList); + // 3. 데이터베이스 검색 + List productsNoReason = productService.searchByKeywords(keywords); - return finalResponse; + // 4. 최종 응답 구성 (JSON 객체로) + Map result = new HashMap<>(); + result.put("product", productsNoReason); + result.put("reason", reasonList); + return result; } private int detectFormatType(MultipartFile file) { From 826d688fb51068db69b8637def6cee2e499f00f1 Mon Sep 17 00:00:00 2001 From: jiwonkim Date: Sat, 15 Feb 2025 10:11:36 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat=20:=20=EC=B6=94=EC=B2=9C=20=EC=9D=B4?= =?UTF-8?q?=EC=9C=A0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../giftidea/controller/GptController.java | 70 ++++++++++++------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/team4/giftidea/controller/GptController.java b/src/main/java/com/team4/giftidea/controller/GptController.java index c47457b..b1a3728 100644 --- a/src/main/java/com/team4/giftidea/controller/GptController.java +++ b/src/main/java/com/team4/giftidea/controller/GptController.java @@ -39,20 +39,9 @@ public GptController(RestTemplate restTemplate, GptConfig gptConfig, ProductServ this.productService = productService; } - // GPT 모델의 입력 토큰 제한 (예: 출력 토큰 고려 후 설정, 여기서는 예시로 11000) + // GPT 모델의 입력 토큰 제한 (예: 11000) private static final int GPT_INPUT_LIMIT = 11000; - /** - * 파일의 아랫부분부터 토큰을 누적하여, GPT 입력 제한 이하인 내용만 선택한 후, - * 해당 청크를 GPT API로 보내 키워드를 추출하고, 최종적으로 관련 상품과 Reasons를 반환합니다. - * - * @param file 업로드된 카카오톡 대화 파일 (.txt) - * @param targetName 대상 이름 (예: "여자친구") - * @param relation 대상과의 관계 (예: "couple", "friend", "parent") - * @param sex 대상 성별 ("male" 또는 "female") - * @param theme 선물 주제 (예: "birthday", "valentine") - * @return 상품 목록과 Reasons 리스트를 포함한 JSON 배열 - */ @Operation( summary = "카톡 대화 분석 후 선물 추천", description = "카카오톡 대화 파일을 분석하여 GPT API를 이용해 키워드를 추출하고, 이에 맞는 추천 상품을 반환합니다." @@ -71,8 +60,7 @@ public Map processFileAndRecommend( @RequestParam("sex") @Parameter(description = "대상 성별 (male 또는 female)", required = true) String sex, @RequestParam("theme") @Parameter(description = "선물 주제 (birthday, valentine 등)", required = true) String theme ) { - - // 1. 파일 전처리 (아랫부분부터 토큰 누적) + // 1. 파일의 모든 줄 중 targetName이 포함된 줄을 수집 List allTargetLines = new ArrayList<>(); int formatType = detectFormatType(file); try (BufferedReader reader = new BufferedReader( @@ -88,6 +76,7 @@ public Map processFileAndRecommend( log.error("파일 읽기 오류: ", e); } + // 2. 파일의 아랫부분부터 역순으로 토큰을 누적하여 GPT_INPUT_LIMIT 이하 내용 선택 int currentTokenCount = 0; List selectedLines = new ArrayList<>(); for (int i = allTargetLines.size() - 1; i >= 0; i--) { @@ -107,7 +96,7 @@ public Map processFileAndRecommend( List processedMessages = new ArrayList<>(); processedMessages.add(finalChunk.toString()); - // (옵션) 로컬 파일 저장 (필요시) + // (옵션) 로컬 파일에 저장 try { File outputFile = new File(System.getProperty("user.home"), "processed_kakaochat.txt"); try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile, false))) { @@ -119,22 +108,51 @@ public Map processFileAndRecommend( log.error("파일 저장 오류: ", e); } - // 2. GPT API 호출 및 응답 파싱 + // 3. GPT API 호출: 전처리된 메시지(청크)로부터 키워드 및 근거 추출 String gptResponse = generatePrompt(processedMessages, relation, sex, theme); - String[] responseLines = gptResponse.split("\n"); - String categories = responseLines[0].replace("Categories: ", "").trim(); - String reasons = responseLines.length > 1 ? gptResponse.substring(gptResponse.indexOf("\n") + 1).trim() : ""; - List keywords = Arrays.stream(categories.split(",")) + // 4. GPT 응답 파싱 + // 예상 응답 예시: + // "Categories: 향수, 무선이어폰, 목걸이\n- 향수: [첫번째 근거]\n- 향수: [마지막 근거]\n- 무선이어폰: [근거]\n- 목걸이: [근거]" + String[] responseParts = gptResponse.split("\n", 2); + String categoriesPart = responseParts[0].replace("Categories: ", "").trim(); + String reasonsPart = responseParts.length > 1 ? responseParts[1].trim() : ""; + + List keywords = Arrays.stream(categoriesPart.split(",")) .map(String::trim) .filter(s -> !s.isEmpty()) .collect(Collectors.toList()); - List reasonList = Arrays.asList(reasons.split("\n")); - // 3. 데이터베이스 검색 + // 파싱: 각 reason 줄에서 키워드와 설명 추출하고, 같은 키워드는 마지막 설명으로 덮어씀. + Map reasonMap = new HashMap<>(); + String[] reasonLines = reasonsPart.split("\n"); + for (String line : reasonLines) { + line = line.trim(); + if (line.startsWith("- ")) { + int colonIdx = line.indexOf(":"); + if (colonIdx != -1) { + String key = line.substring(2, colonIdx).trim(); // 예: "향수" + String value = line.substring(colonIdx + 1).trim(); + reasonMap.put(key, value); // 마지막에 나온 설명이 덮어쓰기 됨 + } + } + } + + // 최종 reason 객체 리스트 생성: 각 키워드에 대해 reasonMap에서 설명 가져오기 + List> reasonList = new ArrayList<>(); + for (String keyword : keywords) { + if (reasonMap.containsKey(keyword)) { + Map entry = new HashMap<>(); + entry.put("keyword", keyword); + entry.put("reason", reasonMap.get(keyword)); + reasonList.add(entry); + } + } + + // 5. 데이터베이스에서 상품 검색 (키워드를 이용) List productsNoReason = productService.searchByKeywords(keywords); - // 4. 최종 응답 구성 (JSON 객체로) + // 6. 최종 응답 구성 (JSON 객체) Map result = new HashMap<>(); result.put("product", productsNoReason); result.put("reason", reasonList); @@ -207,15 +225,13 @@ private String generateText(String prompt) { try { ObjectMapper mapper = new ObjectMapper(); GptResponseDTO response = restTemplate.postForObject(gptConfig.getApiUrl(), request, GptResponseDTO.class); - if (response != null && response.getChoices() != null && !response.getChoices().isEmpty()) { String content = response.getChoices().get(0).getMessage().getContent(); log.debug("GPT 전체 응답: {}", content); - // "1."과 "2."를 기준으로 파싱 (응답 포맷이 아래와 같다고 가정) - // 예: "Categories: 향수, 무선이어폰, 목걸이\n- 향수: [...]\n- 무선이어폰: [...]\n- 목걸이: [...]" + // 응답 포맷: "1. [카테고리1,카테고리2,카테고리3]\n2.\n(이후 Reasons 내용)" if (content.contains("1.") && content.contains("2.")) { - String[] parts = content.split("2\\."); + String[] parts = content.split("2\\.", 2); String part1 = parts[0].trim(); String reasonsPart = parts[1].trim(); From 9d031059f3f4c875ce5e8ae8721a9b65838a1e85 Mon Sep 17 00:00:00 2001 From: yeoEun Date: Sun, 23 Feb 2025 02:06:40 +0900 Subject: [PATCH 8/8] =?UTF-8?q?feat=20:=20=EC=9D=91=EB=8B=B5=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=88=98=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gradle/buildOutputCleanup/outputFiles.bin | Bin 18893 -> 19379 bytes .../giftidea/controller/GptController.java | 18 +++++++++--------- .../com/team4/giftidea/dto/GptRequestDTO.java | 9 ++++++++- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin index 8744ab6e7c86ff5fc73faec7a5d36535b5ab2a05..4ea7a140279fc5e2272a70fd6b365e0eabb91f89 100644 GIT binary patch delta 717 zcmX>*nQ`-U#tkMCk%k@Ice3tzr@AtLfmh>yDCl0Z^V_=nQOOW-&*?Dno^ojxjop=5 zQ1SaP@!pj$?(fZhxC?4d{$y7Ph5ES_@7I3Xk~a||=v@m_yyTdg`@*?#PoRopVd9(T z-V+oqY5f3I?+z2+))C$`@g&21sCsXh_;%;@J-z$ByFt~P!o>IdU+a~~Y?H|jmFR~_ z?7On>PbhS2w?vOg;rQ zUvP4)#0-Z|hpQj_7TD|#ReT$!_}7K6>B*bUSwIy>!o<0y9to{onNtc?|90|Q2@4HL zo*8p*e`;9)6|{sYmdf+Jw^N)u1S&pxvaFMy z&yO$%q)c9VQRVXHD5&CvaPc|plec!6sX@fOL}B7t0pYW@JEp_LPforo86Z$}nq&RG xFW=>%ivLemmC|BjS-Da1hxkT=0^!Y$9zBec_j~Y8{@@`*07p$u@R$I^@c>sXO+x?x delta 71 zcmdlyo$>5s#tkMCjAE00B_t+K1>(7q6DF5R8BD$k#In*7lYONPCQk*@M}c&xjK}1y XGD=KLiW?Pwh;KCbA-dVoqlXaycD5Lx diff --git a/src/main/java/com/team4/giftidea/controller/GptController.java b/src/main/java/com/team4/giftidea/controller/GptController.java index b1a3728..fa3ba64 100644 --- a/src/main/java/com/team4/giftidea/controller/GptController.java +++ b/src/main/java/com/team4/giftidea/controller/GptController.java @@ -221,7 +221,7 @@ private String generatePrompt(List processedMessages, String relation, S } private String generateText(String prompt) { - GptRequestDTO request = new GptRequestDTO(gptConfig.getModel(), prompt); + GptRequestDTO request = new GptRequestDTO(gptConfig.getModel(), prompt, 110); try { ObjectMapper mapper = new ObjectMapper(); GptResponseDTO response = restTemplate.postForObject(gptConfig.getApiUrl(), request, GptResponseDTO.class); @@ -268,7 +268,7 @@ private String generateText(String prompt) { private String extractKeywordsAndReasonsCoupleMan(String theme, String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 남자 애인이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 다음 텍스트를 참고하여 남자 애인이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 100토큰 내로 제공해주세요. 카테고리: 남성 지갑, 남성 스니커즈, 백팩, 토트백, 크로스백, 벨트, 선글라스, 향수, 헬스가방, 무선이어폰, 스마트워치, 맨투맨, 마우스, 키보드, 전기면도기, 게임기 텍스트: %s @@ -285,7 +285,7 @@ private String extractKeywordsAndReasonsCoupleMan(String theme, String message) private String extractKeywordsAndReasonsCoupleWoman(String theme, String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 여자 애인이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 다음 텍스트를 참고하여 여자 애인이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 100토큰 내로 제공해주세요. 카테고리: 여성 지갑, 여성 스니커즈, 숄더백, 토트백, 크로스백, 향수, 목걸이, 무선이어폰, 스마트워치, 에어랩 텍스트: %s @@ -302,7 +302,7 @@ private String extractKeywordsAndReasonsCoupleWoman(String theme, String message private String extractKeywordsAndReasonsDad(String theme, String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 부모님이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 다음 텍스트를 참고하여 부모님이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 100토큰 내로 제공해주세요. 카테고리: 현금 박스, 안마기기, 아버지 신발, 시계 텍스트: %s @@ -319,7 +319,7 @@ private String extractKeywordsAndReasonsDad(String theme, String message) { private String extractKeywordsAndReasonsMom(String theme, String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 부모님이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 다음 텍스트를 참고하여 부모님이 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 100토큰 내로 제공해주세요. 카테고리: 현금 박스, 안마기기, 어머니 신발, 건강식품, 스카프 텍스트: %s @@ -336,7 +336,7 @@ private String extractKeywordsAndReasonsMom(String theme, String message) { private String extractKeywordsAndReasonsFriend(String theme, String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 친구가 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 다음 텍스트를 참고하여 친구가 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 100토큰 내로 제공해주세요. 제시된 카테고리에 없는 추천 선물이 있다면 3개에 포함해주세요. 카테고리: 핸드크림, 텀블러, 립밤, 머플러, 비타민, 입욕제, 블루투스 스피커 @@ -354,7 +354,7 @@ private String extractKeywordsAndReasonsFriend(String theme, String message) { private String extractKeywordsAndReasonsHousewarming(String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 집들이에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 다음 텍스트를 참고하여 집들이에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 100토큰 내로 제공해주세요. 카테고리: 조명, 핸드워시, 식기, 디퓨저, 오설록 티세트, 휴지, 파자마세트, 무드등, 디퓨저, 수건, 전기포트, 에어프라이기 텍스트: %s @@ -371,7 +371,7 @@ private String extractKeywordsAndReasonsHousewarming(String message) { private String extractKeywordsAndReasonsSeasonalMan(String theme, String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 다음 텍스트를 참고하여 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 100토큰 내로 제공해주세요. 카테고리: 초콜릿, 수제 초콜릿 키트, 파자마세트, 남자 화장품 텍스트: %s @@ -388,7 +388,7 @@ private String extractKeywordsAndReasonsSeasonalMan(String theme, String message private String extractKeywordsAndReasonsSeasonalWoman(String theme, String message) { String prompt = String.format(""" - 다음 텍스트를 참고하여 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 제공해주세요. + 다음 텍스트를 참고하여 %s에 선물로 받으면 좋아할 카테고리 3개와 판단에 참고한 대화를 100토큰 내로 제공해주세요. 카테고리: 초콜릿, 수제 초콜릿 키트, 립밤, 파자마세트, 립스틱 텍스트: %s diff --git a/src/main/java/com/team4/giftidea/dto/GptRequestDTO.java b/src/main/java/com/team4/giftidea/dto/GptRequestDTO.java index 2be77b7..20cf324 100644 --- a/src/main/java/com/team4/giftidea/dto/GptRequestDTO.java +++ b/src/main/java/com/team4/giftidea/dto/GptRequestDTO.java @@ -21,15 +21,22 @@ public class GptRequestDTO { */ private List messages; + /** + * 최대 토큰 수 제한 (응답 길이 제한) + */ + private int max_tokens; // 🔥 추가된 필드 + /** * 생성자 - 주어진 모델과 프롬프트를 기반으로 GPT 요청을 생성합니다. * * @param model 사용할 GPT 모델 * @param prompt 사용자 입력 프롬프트 + * @param max_tokens 최대 토큰 제한 */ - public GptRequestDTO(String model, String prompt) { + public GptRequestDTO(String model, String prompt, int max_tokens) { this.model = model; this.messages = new ArrayList<>(); this.messages.add(new MessageDTO("user", prompt)); + this.max_tokens = max_tokens; } } \ No newline at end of file