Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.umc.valuedi.domain.mbti.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.umc.valuedi.domain.mbti.converter.FinanceMbtiTestConverter;
Expand All @@ -12,11 +13,13 @@
import org.umc.valuedi.domain.mbti.validator.FinanceMbtiTestValidator;
import org.umc.valuedi.domain.member.entity.Member;
import org.umc.valuedi.domain.member.repository.MemberRepository;
import org.umc.valuedi.domain.savings.service.RecommendationService;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
Expand All @@ -28,6 +31,7 @@ public class FinanceMbtiService {
private final FinanceMbtiScoringService scoringService;
private final FinanceMbtiTestValidator financeMbtiTestValidator;
private final FinanceMbtiTestConverter financeMbtiTestConverter;
private final RecommendationService recommendationService;

public MemberMbtiTest submitTest(Long memberId, FinanceMbtiTestRequestDto req) {

Expand All @@ -46,9 +50,17 @@ public MemberMbtiTest submitTest(Long memberId, FinanceMbtiTestRequestDto req) {
));

FinanceMbtiScoringService.ScoreResult score = scoringService.score(activeQuestions, answersByQuestionId);

MemberMbtiTest test = financeMbtiTestConverter.toEntity(member, req, score, activeQuestionMap);

return memberMbtiTestRepository.save(test);
MemberMbtiTest savedTest = memberMbtiTestRepository.save(test);

try {
recommendationService.generateAndSaveRecommendations(memberId);
log.info("[Recommend] MBTI 검사 후 자동 추천 생성 성공. memberId={}", memberId);
} catch (Exception e) {
// 제미나이 호출이 실패해도 MBTI 저장은 유지되도록 예외를 삼키고 로그만 남김
log.error("[Recommend] MBTI 저장에는 성공했으나, 자동 추천 생성 중 오류 발생. memberId={}: {}", memberId, e.getMessage());

Choose a reason for hiding this comment

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

medium

예외를 로깅할 때 e.getMessage()만 사용하면 원인 파악에 필요한 정보가 부족할 수 있습니다. 특히 getMessage()null을 반환하는 경우 디버깅이 어려워집니다. 전체 스택 트레이스를 포함하도록 로거를 사용하는 것이 좋습니다.

Suggested change
log.error("[Recommend] MBTI 저장에는 성공했으나, 자동 추천 생성 중 오류 발생. memberId={}: {}", memberId, e.getMessage());
log.error("[Recommend] MBTI 저장에는 성공했으나, 자동 추천 생성 중 오류 발생. memberId={}", memberId, e);

}
return savedTest;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.umc.valuedi.domain.savings.dto.response.SavingsResponseDTO;
import org.umc.valuedi.domain.savings.entity.RecommendationBatch;
import org.umc.valuedi.domain.savings.enums.RecommendationStatus;
import org.umc.valuedi.domain.savings.service.RecommendationAsyncWorker;
import org.umc.valuedi.domain.savings.service.RecommendationService;
import org.umc.valuedi.domain.savings.service.RecommendationTxService;
import org.umc.valuedi.global.apiPayload.ApiResponse;
import org.umc.valuedi.global.apiPayload.code.GeneralSuccessCode;
import org.umc.valuedi.global.security.annotation.CurrentMember;
Expand All @@ -18,28 +14,14 @@
public class RecommendationController implements RecommendationControllerDocs {

private final RecommendationService recommendationService;
private final RecommendationTxService recommendationTxService;
private final RecommendationAsyncWorker recommendationAsyncWorker;

// 추천 생성 트리거(비동기)
// 추천 생성
@PostMapping
public ApiResponse<SavingsResponseDTO.TriggerResponse> recommend(
public ApiResponse<SavingsResponseDTO.SavingsListResponse> recommend(
@CurrentMember Long memberId
) {
SavingsResponseDTO.TriggerDecision triggerDecision = recommendationTxService.triggerRecommendation(memberId);

// 진행 중이면 새로 실행하지 않음
if (triggerDecision.shouldStartAsync()) {
recommendationAsyncWorker.generateAndSaveAsync(memberId, triggerDecision.batchId());
}

return ApiResponse.onSuccess(GeneralSuccessCode.ACCEPTED,
SavingsResponseDTO.TriggerResponse.builder()
.batchId(triggerDecision.batchId())
.status(triggerDecision.status())
.message(triggerDecision.message())
.build()
);
SavingsResponseDTO.SavingsListResponse result = recommendationService.generateAndSaveRecommendations(memberId);
return ApiResponse.onSuccess(GeneralSuccessCode.OK, result);
}

// 최신 추천 15개 조회
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ public static SavingsResponseDTO.SavingsListResponse toSavingsListResponseDTO(Li

return SavingsResponseDTO.SavingsListResponse.builder()
.totalCount(totalCount)
.maxPageNo(maxPageNo)
.nowPageNo(nowPageNo)
.hasNext(nowPageNo < maxPageNo)
.products(products)
.build();
}
Expand All @@ -109,22 +109,31 @@ public static SavingsResponseDTO.SavingsDetailResponse toSavingsDetailResponseDT
))
.toList();

SavingsResponseDTO.SavingsDetailResponse.SavingProductDetail product = new SavingsResponseDTO.SavingsDetailResponse.SavingProductDetail(
savings.getKorCoNm(),
savings.getFinPrdtCd(),
savings.getFinPrdtNm(),
savings.getJoinWay(),
savings.getMtrtInt(),
savings.getSpclCnd(),
savings.getJoinDeny(),
savings.getJoinMember(),
savings.getEtcNote(),
savings.getMaxLimit() == null ? null : String.valueOf(savings.getMaxLimit()),
options
);
SavingsOption representativeOption = savings.getSavingsOptionList().stream()
.filter(o -> Integer.valueOf(12).equals(o.getSaveTrm()))

Choose a reason for hiding this comment

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

medium

대표 옵션을 찾을 때 12라는 매직 넘버(magic number)가 사용되었습니다. 코드의 가독성과 유지보수성을 높이기 위해 클래스 레벨에 private static final Integer REPRESENTATIVE_TERM_MONTHS = 12; 와 같이 상수로 정의하여 사용하는 것을 권장합니다.

Suggested change
.filter(o -> Integer.valueOf(12).equals(o.getSaveTrm()))
.filter(o -> REPRESENTATIVE_TERM_MONTHS.equals(o.getSaveTrm()))

.findFirst()
.orElseGet(() -> savings.getSavingsOptionList().stream()
.filter(o -> o.getIntrRate2() != null)
.max(java.util.Comparator.comparing(SavingsOption::getIntrRate2))
.orElse(null));

Double basicRate = (representativeOption != null) ? representativeOption.getIntrRate() : null;
Double maxRate = (representativeOption != null) ? representativeOption.getIntrRate2() : null;

return SavingsResponseDTO.SavingsDetailResponse.builder()
.product(product)
.korCoNm(savings.getKorCoNm())
.finPrdtCd(savings.getFinPrdtCd())
.finPrdtNm(savings.getFinPrdtNm())
.basicRate(basicRate)
.maxRate(maxRate)
.joinWay(savings.getJoinWay())
.mtrtInt(savings.getMtrtInt())
.spclCnd(savings.getSpclCnd())
.joinDeny(savings.getJoinDeny())
.joinMember(savings.getJoinMember())
.etcNote(savings.getEtcNote())
.maxLimit(savings.getMaxLimit() == null ? null : String.valueOf(savings.getMaxLimit()))
.options(options)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ public class SavingsResponseDTO {
@Builder
public record SavingsListResponse(
int totalCount, // 총 상품건수
int maxPageNo, // 총 페이지 건수
int nowPageNo, // 현재 조회 페이지 번호
List<RecommendedSavingProduct> products, // 상품 목록
RecommendationStatus status, // 상품 추천 상태 (PENDING | SUCCESS | FAILED)
String message // 상품 추천 상태 메시지
int nowPageNo, // 현재 페이지
boolean hasNext, // 다음 페이지 존재 여부
List<RecommendedSavingProduct> products // 상품 목록
) {
// 상품 목록 조회
public record RecommendedSavingProduct(
Expand All @@ -29,23 +27,20 @@ public record RecommendedSavingProduct(

@Builder
public record SavingsDetailResponse(
SavingProductDetail product // 상품
String korCoNm,
String finPrdtCd,
String finPrdtNm,
Double basicRate,
Double maxRate,
String joinWay,
String mtrtInt,
String spclCnd,
String joinDeny,
String joinMember,
String etcNote,
String maxLimit,
List<Option> options
) {
// 적금 상품 상세 조회
public record SavingProductDetail (
String korCoNm, // 금융회사 명
String finPrdtCd, // 금융상품 코드
String finPrdtNm, // 금융 상품명
String joinWay, // 가입 방법
String mtrtInt, // 만기 후 이자율
String spclCnd, // 우대조건
String joinDeny, // 가입 제한
String joinMember, // 가입대상
String etcNote, // 기타 유의사항
String maxLimit, // 최고한도
List<Option> options
) {}

public record Option (
String intrRateType, // 저축 금리 유형
String intrRateTypeNm, // 저축 금리 유형명
Expand All @@ -72,19 +67,4 @@ public record RecommendedProduct(
String rsrvTypeNm,
BigDecimal score
) {}

@Builder
public record TriggerResponse(
Long batchId,
RecommendationStatus status, // PENDING | SUCCESS | FAILED
String message
) {}

@Builder
public record TriggerDecision(
Long batchId,
RecommendationStatus status,
String message,
boolean shouldStartAsync
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
@AllArgsConstructor
public enum SavingsErrorCode implements BaseErrorCode {

SAVINGS_NOT_FOUND(HttpStatus.NOT_FOUND, "SAVINGS404_1", "해당 적금 상품을 찾을 수 없습니다.")
SAVINGS_NOT_FOUND(HttpStatus.NOT_FOUND, "SAVINGS404_1", "해당 적금 상품을 찾을 수 없습니다."),
RECOMMENDATION_NOT_FOUND(HttpStatus.NOT_FOUND, "SAVINGS404_2", "아직 추천받은 내역이 없습니다. 먼저 상품 추천을 진행해 주세요."),
FILTERED_RECOMMENDATION_NOT_FOUND(HttpStatus.NOT_FOUND, "SAVINGS400_1", "해당 필터 조건에 맞는 추천 상품이 없습니다."),

Choose a reason for hiding this comment

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

medium

FILTERED_RECOMMENDATION_NOT_FOUND 에러 코드의 HTTP 상태는 NOT_FOUND(404)이지만, 코드명은 SAVINGS400_1로 불일치합니다. API 응답의 일관성을 위해 HTTP 상태와 코드명을 맞추는 것이 좋습니다. 필터 조건에 맞는 결과가 없는 것은 클라이언트의 요청(필터)에 따른 결과로 볼 수 있으므로, HttpStatus.BAD_REQUEST(400)로 변경하는 것을 제안합니다.

Suggested change
FILTERED_RECOMMENDATION_NOT_FOUND(HttpStatus.NOT_FOUND, "SAVINGS400_1", "해당 필터 조건에 맞는 추천 상품이 없습니다."),
FILTERED_RECOMMENDATION_NOT_FOUND(HttpStatus.BAD_REQUEST, "SAVINGS400_1", "해당 필터 조건에 맞는 추천 상품이 없습니다."),

RECOMMENDATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "SAVINGS500_1", "AI 추천 생성에 실패했습니다. 다시 시도해 주세요.")
;

private final HttpStatus status;
Expand Down

This file was deleted.

Loading