From dba7126180e78b058ac8141460ccec24a685f6ef Mon Sep 17 00:00:00 2001 From: leesiyeon Date: Thu, 19 Feb 2026 03:41:41 +0900 Subject: [PATCH 1/7] =?UTF-8?q?refactor:=20=EC=A0=9C=EB=AF=B8=EB=82=98?= =?UTF-8?q?=EC=9D=B4=20=ED=98=B8=EC=B6=9C=20=EC=84=A4=EC=A0=95=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EC=A0=81=EA=B8=88=20=EC=B6=94=EC=B2=9C?= =?UTF-8?q?=20=EA=B0=9C=EC=88=98=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../savings/controller/RecommendationController.java | 2 +- .../savings/controller/RecommendationControllerDocs.java | 8 ++++---- .../domain/savings/repository/SavingsRepository.java | 4 ++++ .../domain/savings/service/RecommendationService.java | 6 +++--- .../global/external/genai/client/GeminiClient.java | 8 ++++---- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationController.java b/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationController.java index 57e1e2ef..1066ee91 100644 --- a/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationController.java +++ b/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationController.java @@ -24,7 +24,7 @@ public ApiResponse recommend( return ApiResponse.onSuccess(GeneralSuccessCode.OK, result); } - // 최신 추천 15개 조회 + // 최신 추천 10개 조회 @GetMapping public ApiResponse latest15( @RequestParam(required = false) String rsrvType, diff --git a/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationControllerDocs.java b/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationControllerDocs.java index 17d0ac2b..9b691d82 100644 --- a/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationControllerDocs.java +++ b/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationControllerDocs.java @@ -21,7 +21,7 @@ public interface RecommendationControllerDocs { 로그인 사용자(JWT)의 현재 MBTI를 바탕으로 Gemini 추천을 생성하고 DB를 갱신합니다. MBTI 검사 완료 후 이 API를 호출하여 맞춤 추천을 받을 수 있습니다. - - 응답 속도는 Gemini API 호출을 포함하므로 약 3~7초 정도 소요될 수 있습니다 + - 응답 속도는 Gemini API 호출을 포함하므로 약 10~20초 정도 소요될 수 있습니다 """, responses = { @io.swagger.v3.oas.annotations.responses.ApiResponse( @@ -84,10 +84,10 @@ ApiResponse recommend( ); @Operation( - summary = "최신 추천 15개 조회 API", + summary = "최신 추천 10개 조회 API", description = """ 로그인 사용자(JWT)의 '현재 활성 MBTI 테스트' 기준으로 - DB에 저장된 최신 추천 15개를 조회 (Gemini 호출 X) + DB에 저장된 최신 추천 10개를 조회 (Gemini 호출 X) """, responses = { @io.swagger.v3.oas.annotations.responses.ApiResponse( @@ -99,7 +99,7 @@ ApiResponse recommend( examples = { @ExampleObject( name = "success", - summary = "추천 15개 조회 성공", + summary = "추천 10개 조회 성공", value = """ { "isSuccess": true, diff --git a/src/main/java/org/umc/valuedi/domain/savings/repository/SavingsRepository.java b/src/main/java/org/umc/valuedi/domain/savings/repository/SavingsRepository.java index c74b4756..87cab834 100644 --- a/src/main/java/org/umc/valuedi/domain/savings/repository/SavingsRepository.java +++ b/src/main/java/org/umc/valuedi/domain/savings/repository/SavingsRepository.java @@ -3,9 +3,13 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.umc.valuedi.domain.savings.entity.Savings; +import java.util.List; import java.util.Optional; public interface SavingsRepository extends JpaRepository { Optional findByFinPrdtCd(String finPrdtCd); + + // 여러 finPrdtCd를 한 번에 조회) + List findAllByFinPrdtCdIn(List finPrdtCds); } diff --git a/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java b/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java index b0eeb84d..a6cc2034 100644 --- a/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java +++ b/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java @@ -36,10 +36,10 @@ @RequiredArgsConstructor public class RecommendationService { - private static final int RECOMMEND_COUNT = 15; + private static final int RECOMMEND_COUNT = 10; private static final int TOP3_COUNT = 3; - private static final int CANDIDATE_LIMIT = 40; + private static final int CANDIDATE_LIMIT = 25; private final MemberMbtiTestRepository memberMbtiTestRepository; private final FinanceMbtiProvider financeMbtiProvider; @@ -96,7 +96,7 @@ public SavingsResponseDTO.SavingsListResponse generateAndSaveRecommendations(Lon return SavingsConverter.toSavingsListResponseDTO(savingsList, savingsList.size(), 1, 1); } - // 추천 상품 15개 조회 + // 추천 상품 10개 조회 @Transactional(readOnly = true) public SavingsResponseDTO.SavingsListResponse getRecommendation(Long memberId, String rsrvType) { Long mbtiTestId = memberMbtiTestRepository.findCurrentActiveTest(memberId) diff --git a/src/main/java/org/umc/valuedi/global/external/genai/client/GeminiClient.java b/src/main/java/org/umc/valuedi/global/external/genai/client/GeminiClient.java index 16e9a41c..418ba441 100644 --- a/src/main/java/org/umc/valuedi/global/external/genai/client/GeminiClient.java +++ b/src/main/java/org/umc/valuedi/global/external/genai/client/GeminiClient.java @@ -21,13 +21,13 @@ public class GeminiClient { private final Client genaiClient; private final GeminiProperties geminiProperties; - private static final int MAX_ATTEMPTS = 4; + private static final int MAX_ATTEMPTS = 2; - private static final Duration PER_ATTEMPT_TIMEOUT = Duration.ofSeconds(30); // 시도별 제한 - private static final Duration OVERALL_DEADLINE = Duration.ofSeconds(240); // 전체 제한 + private static final Duration PER_ATTEMPT_TIMEOUT = Duration.ofSeconds(90); // 시도별 제한 + private static final Duration OVERALL_DEADLINE = Duration.ofSeconds(130); // 전체 제한 private static final long BASE_BACKOFF_MILLIS = 2_000; - private static final long MAX_BACKOFF_MILLIS = 90_000; + private static final long MAX_BACKOFF_MILLIS = 8_000; private static final ExecutorService executor = Executors.newFixedThreadPool(4, r -> { From b843ad3907a833f1ce7c6f06eab47533bead889d Mon Sep 17 00:00:00 2001 From: leesiyeon Date: Thu, 19 Feb 2026 03:48:47 +0900 Subject: [PATCH 2/7] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../valuedi/domain/savings/repository/SavingsRepository.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/org/umc/valuedi/domain/savings/repository/SavingsRepository.java b/src/main/java/org/umc/valuedi/domain/savings/repository/SavingsRepository.java index 87cab834..3901ce43 100644 --- a/src/main/java/org/umc/valuedi/domain/savings/repository/SavingsRepository.java +++ b/src/main/java/org/umc/valuedi/domain/savings/repository/SavingsRepository.java @@ -9,7 +9,4 @@ public interface SavingsRepository extends JpaRepository { Optional findByFinPrdtCd(String finPrdtCd); - - // 여러 finPrdtCd를 한 번에 조회) - List findAllByFinPrdtCdIn(List finPrdtCds); } From 29e5bbacfbc9e25d6de65e009d9cff9801580454 Mon Sep 17 00:00:00 2001 From: leesiyeon Date: Thu, 19 Feb 2026 03:49:48 +0900 Subject: [PATCH 3/7] =?UTF-8?q?refactor:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/savings/controller/RecommendationController.java | 2 +- .../domain/savings/controller/RecommendationControllerDocs.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationController.java b/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationController.java index 1066ee91..f66431e8 100644 --- a/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationController.java +++ b/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationController.java @@ -26,7 +26,7 @@ public ApiResponse recommend( // 최신 추천 10개 조회 @GetMapping - public ApiResponse latest15( + public ApiResponse latest10( @RequestParam(required = false) String rsrvType, @CurrentMember Long memberId ) { diff --git a/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationControllerDocs.java b/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationControllerDocs.java index 9b691d82..0ca7b339 100644 --- a/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationControllerDocs.java +++ b/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationControllerDocs.java @@ -139,7 +139,7 @@ ApiResponse recommend( ) } ) - ApiResponse latest15( + ApiResponse latest10( @Parameter( description = "적립유형 필터 (S=정기적금, F=자유적금). 미입력 시 전체", schema = @Schema(allowableValues = {"S", "F"}, example = "S") From eeae7092e432388ded86c95cb4bb6db0d2b9b509 Mon Sep 17 00:00:00 2001 From: leesiyeon Date: Thu, 19 Feb 2026 03:58:58 +0900 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20=EC=A0=9C=EB=AF=B8=EB=82=98?= =?UTF-8?q?=EC=9D=B4=20=ED=98=B8=EC=B6=9C=20=EC=84=A4=EC=A0=95=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EB=B0=8F=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?import=EB=AC=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../valuedi/domain/savings/repository/SavingsRepository.java | 1 - .../valuedi/domain/savings/service/RecommendationService.java | 1 - .../umc/valuedi/global/external/genai/client/GeminiClient.java | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/umc/valuedi/domain/savings/repository/SavingsRepository.java b/src/main/java/org/umc/valuedi/domain/savings/repository/SavingsRepository.java index 3901ce43..c74b4756 100644 --- a/src/main/java/org/umc/valuedi/domain/savings/repository/SavingsRepository.java +++ b/src/main/java/org/umc/valuedi/domain/savings/repository/SavingsRepository.java @@ -3,7 +3,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.umc.valuedi.domain.savings.entity.Savings; -import java.util.List; import java.util.Optional; public interface SavingsRepository extends JpaRepository { diff --git a/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java b/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java index a6cc2034..dc71375d 100644 --- a/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java +++ b/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java @@ -18,7 +18,6 @@ import org.umc.valuedi.domain.savings.entity.Recommendation; import org.umc.valuedi.domain.savings.entity.Savings; import org.umc.valuedi.domain.savings.entity.SavingsOption; -import org.umc.valuedi.domain.savings.enums.RecommendationStatus; import org.umc.valuedi.domain.savings.exception.SavingsException; import org.umc.valuedi.domain.savings.exception.code.SavingsErrorCode; import org.umc.valuedi.domain.savings.repository.RecommendationRepository; diff --git a/src/main/java/org/umc/valuedi/global/external/genai/client/GeminiClient.java b/src/main/java/org/umc/valuedi/global/external/genai/client/GeminiClient.java index 418ba441..1eea087f 100644 --- a/src/main/java/org/umc/valuedi/global/external/genai/client/GeminiClient.java +++ b/src/main/java/org/umc/valuedi/global/external/genai/client/GeminiClient.java @@ -24,7 +24,7 @@ public class GeminiClient { private static final int MAX_ATTEMPTS = 2; private static final Duration PER_ATTEMPT_TIMEOUT = Duration.ofSeconds(90); // 시도별 제한 - private static final Duration OVERALL_DEADLINE = Duration.ofSeconds(130); // 전체 제한 + private static final Duration OVERALL_DEADLINE = Duration.ofSeconds(190); // 전체 제한 private static final long BASE_BACKOFF_MILLIS = 2_000; private static final long MAX_BACKOFF_MILLIS = 8_000; From 4ac2937b4dc7394b9e6ae85edade94f7bbb1f6f4 Mon Sep 17 00:00:00 2001 From: leesiyeon Date: Thu, 19 Feb 2026 05:01:20 +0900 Subject: [PATCH 5/7] =?UTF-8?q?refactor:=20MbtiTraits=20enum=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EC=B6=94=EC=B2=9C=20=ED=94=84?= =?UTF-8?q?=EB=A1=AC=ED=94=84=ED=8A=B8=20=EA=B2=BD=EB=9F=89=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../valuedi/domain/mbti/enums/MbtiTraits.java | 34 +++++++++++++++ .../service/RecommendationService.java | 42 ++++++++----------- 2 files changed, 52 insertions(+), 24 deletions(-) create mode 100644 src/main/java/org/umc/valuedi/domain/mbti/enums/MbtiTraits.java diff --git a/src/main/java/org/umc/valuedi/domain/mbti/enums/MbtiTraits.java b/src/main/java/org/umc/valuedi/domain/mbti/enums/MbtiTraits.java new file mode 100644 index 00000000..8ef7381f --- /dev/null +++ b/src/main/java/org/umc/valuedi/domain/mbti/enums/MbtiTraits.java @@ -0,0 +1,34 @@ +package org.umc.valuedi.domain.mbti.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +public enum MbtiTraits { + + APGV(List.of("기본계획", "감정개입", "즉흥허용", "도전성", "규칙완화", "후회가능", "허용범위설정", "숙려권장")), + APGR(List.of("리스크인지", "계산형도전", "구조중시", "장단기분리", "균형추구", "현실판단", "결정피로주의", "기준기반선호")), + APCV(List.of("안정선호", "예측가능선호", "보수적", "단순구조선호", "변화회피", "결정미룸", "소액확장권장", "실행체크리스트")), + APCR(List.of("목표지향", "원칙준수", "절제소비", "장기지속", "계획으로불안관리", "성과시각화선호", "규칙강함", "유연성확보")), + AIGV(List.of("감정우선", "충동성", "즉시행동", "손실회피", "확인회피", "고위험주의", "한도설정필수", "대기규칙권장")), + AIGR(List.of("불안존재", "준비후도전", "기준설정", "정보비교", "변동감수", "분산선호", "자동화선호", "자금분리")), + AICV(List.of("타인영향", "불안높음", "결정부담", "유행민감", "확신짧음", "기준필요", "자동저축선호", "패턴기록권장")), + AICR(List.of("목표분명", "불안동반", "조심스러움", "실행지연", "점검빈번", "목표형선호", "단계저축선호", "시각화선호")), + SPGV(List.of("중립감정", "계획존재", "깊은고민회피", "충분하면실행", "사후관리약함", "방치위험", "중간점검필요", "알림장치유리")), + SPGR(List.of("전략중심", "장기목표", "계산형", "기준명확", "구조화강점", "공격가능", "결정피로주의", "자동화로부담감소")), + SPCV(List.of("안정최우선", "보수적", "확신없으면미룸", "현상유지", "단계적확장필요", "자극회피", "정기점검필요", "단순상품선호")), + SPCR(List.of("계획중심", "기준중요", "즉흥적음", "장기안정", "지속력", "여유부족주의", "도전부족가능", "상향목표제안유리")), + SIGV(List.of("기회민감", "빠른결단", "속도강점", "불안동반", "손실시자책", "브레이크필요", "한도설정", "리스크기준정리")), + SIGR(List.of("도전성", "구조분석", "흐름중시", "정보비교", "복잡감당", "판단지연피로", "전략정리필요", "단순화선호")), + SICV(List.of("관심낮음", "관리귀찮음", "안전지향", "결정미룸", "기회놓침가능", "자동이체필수", "기본값선호", "단순상품선호")), + SICR(List.of("안정지향", "목표지향", "합리적", "장기계획", "침착함", "예측가능선호", "정체주의", "정기점검필요")); + + private final List traits; + + public static List of(MbtiType type) { + return MbtiTraits.valueOf(type.name()).getTraits(); + } +} diff --git a/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java b/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java index dc71375d..c4ee93ac 100644 --- a/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java +++ b/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java @@ -6,8 +6,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.umc.valuedi.domain.mbti.dto.FinanceMbtiTypeInfoDto; import org.umc.valuedi.domain.mbti.entity.MemberMbtiTest; +import org.umc.valuedi.domain.mbti.enums.MbtiTraits; import org.umc.valuedi.domain.mbti.enums.MbtiType; import org.umc.valuedi.domain.mbti.exception.MbtiException; import org.umc.valuedi.domain.mbti.exception.code.MbtiErrorCode; @@ -64,8 +64,7 @@ public SavingsResponseDTO.SavingsListResponse generateAndSaveRecommendations(Lon // 제미나이 프롬프트 생성 MbtiType mbtiType = memberMbtiTest.getResultType(); - FinanceMbtiTypeInfoDto financeMbtiTypeInfo = financeMbtiProvider.get(mbtiType); - String prompt = buildPrompt(mbtiType, financeMbtiTypeInfo, candidates, RECOMMEND_COUNT); + String prompt = buildPrompt(mbtiType, candidates, RECOMMEND_COUNT); // 제미나이 호출 log.info("[Recommend] Gemini request. memberId={}, promptChars={}", memberId, prompt.length()); @@ -153,7 +152,6 @@ private SavingsResponseDTO.SavingsListResponse emptyResponse() { private String buildPrompt( MbtiType mbtiType, - FinanceMbtiTypeInfoDto typeInfo, List candidates, int recommendCount ) { @@ -182,18 +180,22 @@ private String buildPrompt( candidateText = "[]"; } - // JSON만 출력 강제 + 키 이름 DTO와 일치(GeminiSavingsResponseDTO.Result가 recommendations를 가지도록) + List traits = MbtiTraits.of(mbtiType); // enum에서 가져오기 + String traitsText; + try { + traitsText = objectMapper.writeValueAsString(traits); // ["...", "..."] + } catch (Exception e) { + traitsText = "[]"; + } + + // JSON만 출력 강제 return """ 당신은 금융 추천 엔진입니다. - 아래 "사용자 금융 MBTI"와 "후보 적금 옵션 목록"을 기반으로, 사용자에게 가장 적합한 적금 옵션 %d개를 추천하세요. + 아래 "사용자 성향(traits)"과 "후보 적금 옵션 목록"을 기반으로, 가장 적합한 적금 옵션 %d개를 추천하세요. - [사용자 금융 MBTI] - - type: %s - - title: %s - - tagline: %s - - detail: %s - - warning: %s - - recommend: %s + [사용자] + - mbtiType: %s + - traits: %s [후보 적금 옵션 목록] %s @@ -203,11 +205,11 @@ private String buildPrompt( - 반드시 아래 스키마를 정확히 지키세요. - optionId는 후보 목록에 있는 값만 사용하세요. - score는 0~1 사이 숫자(소수)로, 높을수록 추천 우선순위입니다. - - reasons는 1~3개. reasonCode는 대문자 스네이크로 작성하세요(예: HIGH_RATE, MATCH_TERM, MBTI_FIT). + - reasons는 1~3개. reasonCode는 대문자 스네이크로 작성하세요(예: HIGH_RATE, MATCH_TERM). [JSON 스키마] { - "rationale": "전체 추천 요약(한 문단)", + "rationale": "전체 추천 요약(한 문장)", "recommendations": [ { "finPrdtCd": "string", @@ -222,11 +224,7 @@ private String buildPrompt( """.formatted( recommendCount, mbtiType.name(), - safeStr(typeInfo.title()), - safeStr(typeInfo.tagline()), - safeStr(typeInfo.detail()), - safeStr(String.join(" / ", safeList(typeInfo.cautions()))), - safeStr(String.join(" / ", safeList(typeInfo.recommendedActions()))), + traitsText, candidateText ); } @@ -262,10 +260,6 @@ private static List safeList(List list) { return list == null ? List.of() : list; } - private static String safeStr(String s) { - return s == null ? "" : s; - } - private static BigDecimal nullSafe(BigDecimal bd) { return bd == null ? BigDecimal.ZERO : bd; } From 4ac0cd46e1e358e1dd9b1f68246b342ef623df2a Mon Sep 17 00:00:00 2001 From: Kwon-DoHee <152317074+seamooll@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:53:02 +0900 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20Gemini=202.5-flash=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20=EB=B0=8F=20=EC=B6=94=EC=B2=9C=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=20=EC=9D=B4=EC=8A=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../savings/repository/RecommendationRepository.java | 11 +++++++++++ .../domain/savings/service/RecommendationService.java | 2 ++ .../savings/service/RecommendationTxService.java | 6 ++++++ .../global/external/genai/client/GeminiClient.java | 8 +++++++- src/main/resources/application.yml | 2 +- 5 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/umc/valuedi/domain/savings/repository/RecommendationRepository.java b/src/main/java/org/umc/valuedi/domain/savings/repository/RecommendationRepository.java index 6235333d..dacb34fa 100644 --- a/src/main/java/org/umc/valuedi/domain/savings/repository/RecommendationRepository.java +++ b/src/main/java/org/umc/valuedi/domain/savings/repository/RecommendationRepository.java @@ -35,4 +35,15 @@ boolean existsByMemberIdAndMemberMbtiTestId( @Param("memberId") Long memberId, @Param("memberMbtiTestId") Long memberMbtiTestId ); + + @Query(""" + select r + from Recommendation r + where r.member.id = :memberId + and r.memberMbtiTestId = :memberMbtiTestId + """) + List findAllByMemberIdAndMemberMbtiTestId( + @Param("memberId") Long memberId, + @Param("memberMbtiTestId") Long memberMbtiTestId + ); } diff --git a/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java b/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java index c4ee93ac..7111e999 100644 --- a/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java +++ b/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java @@ -89,6 +89,7 @@ public SavingsResponseDTO.SavingsListResponse generateAndSaveRecommendations(Lon List savingsList = items.stream() .map(i -> savingsRepository.findByFinPrdtCd(i.finPrdtCd()).orElse(null)) .filter(Objects::nonNull) + .filter(new LinkedHashSet<>()::add) .toList(); return SavingsConverter.toSavingsListResponseDTO(savingsList, savingsList.size(), 1, 1); @@ -204,6 +205,7 @@ private String buildPrompt( - 반드시 JSON만 출력하세요. (설명 문장, 마크다운 금지) - 반드시 아래 스키마를 정확히 지키세요. - optionId는 후보 목록에 있는 값만 사용하세요. + - 동일한 finPrdtCd를 가진 옵션은 하나만 선택하세요. (같은 상품의 중복 추천 금지) - score는 0~1 사이 숫자(소수)로, 높을수록 추천 우선순위입니다. - reasons는 1~3개. reasonCode는 대문자 스네이크로 작성하세요(예: HIGH_RATE, MATCH_TERM). diff --git a/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationTxService.java b/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationTxService.java index b7b60c5c..f278485d 100644 --- a/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationTxService.java +++ b/src/main/java/org/umc/valuedi/domain/savings/service/RecommendationTxService.java @@ -53,6 +53,12 @@ public SavingsResponseDTO.RecommendResponse saveRecommendations( Map optionById = pickedOptions.stream() .collect(Collectors.toMap(SavingsOption::getId, Function.identity())); + // 기존 추천 삭제 (중복 방지) + List existing = recommendationRepository.findAllByMemberIdAndMemberMbtiTestId(memberId, memberMbtiTest.getId()); + if (!existing.isEmpty()) { + recommendationRepository.deleteAll(existing); + } + // 추천 상품 저장 LocalDateTime now = LocalDateTime.now(); List toSave = new ArrayList<>(); diff --git a/src/main/java/org/umc/valuedi/global/external/genai/client/GeminiClient.java b/src/main/java/org/umc/valuedi/global/external/genai/client/GeminiClient.java index 1eea087f..9c30642e 100644 --- a/src/main/java/org/umc/valuedi/global/external/genai/client/GeminiClient.java +++ b/src/main/java/org/umc/valuedi/global/external/genai/client/GeminiClient.java @@ -1,7 +1,9 @@ package org.umc.valuedi.global.external.genai.client; import com.google.genai.Client; +import com.google.genai.types.GenerateContentConfig; import com.google.genai.types.GenerateContentResponse; +import com.google.genai.types.ThinkingConfig; import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -109,9 +111,13 @@ public String generateText(String prompt) { throw new GeminiException(GeminiErrorCode.GEMINI_CALL_FAILED, lastCause); } + private static final GenerateContentConfig NO_THINKING_CONFIG = GenerateContentConfig.builder() + .thinkingConfig(ThinkingConfig.builder().thinkingBudget(0).build()) + .build(); + private String callWithTimeout(String prompt, Duration timeout) throws TimeoutException, ExecutionException, InterruptedException { Future future = executor.submit(() -> { - GenerateContentResponse response = genaiClient.models.generateContent(geminiProperties.getModel(), prompt, null); + GenerateContentResponse response = genaiClient.models.generateContent(geminiProperties.getModel(), prompt, NO_THINKING_CONFIG); return response.text(); }); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c92d7105..f0ffc561 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -69,7 +69,7 @@ fss: genai: gemini: api-key: ${GEMINI_API_KEY} - model: gemini-3-flash-preview + model: gemini-2.5-flash springdoc: swagger-ui: From 6d8aec0218735cd1a27a7d4bb11e0082f06e55ea Mon Sep 17 00:00:00 2001 From: leesiyeon Date: Thu, 19 Feb 2026 17:17:59 +0900 Subject: [PATCH 7/7] =?UTF-8?q?docs:=20=EC=B6=94=EC=B2=9C=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/savings/controller/RecommendationControllerDocs.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationControllerDocs.java b/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationControllerDocs.java index 0ca7b339..5b6ead3e 100644 --- a/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationControllerDocs.java +++ b/src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationControllerDocs.java @@ -21,7 +21,7 @@ public interface RecommendationControllerDocs { 로그인 사용자(JWT)의 현재 MBTI를 바탕으로 Gemini 추천을 생성하고 DB를 갱신합니다. MBTI 검사 완료 후 이 API를 호출하여 맞춤 추천을 받을 수 있습니다. - - 응답 속도는 Gemini API 호출을 포함하므로 약 10~20초 정도 소요될 수 있습니다 + - 응답 속도는 Gemini API 호출을 포함하므로 약 10초 정도 소요될 수 있습니다 """, responses = { @io.swagger.v3.oas.annotations.responses.ApiResponse(