diff --git a/common/src/main/java/com/ntropy/common/domain/LoanDisbursementKeywords.java b/common/src/main/java/com/ntropy/common/domain/LoanDisbursementKeywords.java new file mode 100644 index 00000000..046b4366 --- /dev/null +++ b/common/src/main/java/com/ntropy/common/domain/LoanDisbursementKeywords.java @@ -0,0 +1,43 @@ +package com.ntropy.common.domain; + +import java.util.List; +import java.util.Locale; + +/** + * LOAN 거래구분명에서 대출금 지급(신규·실행·증액) 거래를 판정하는 단일 규칙입니다. + * account-service의 월간 소비 집계·금융 납입 예정 조회(SQL)와 ai-service의 + * 일간 소비 분류 배치(Java)가 이 키워드 목록과 판정 의미를 함께 사용합니다. + * + * 판정 의미는 "공백 정규화 후 부분 문자열 포함"입니다. "실행"이 "대출실행"의 + * 부분 문자열이므로 "대출실행"은 목록에서 제외해도 판정 결과가 같습니다. + */ +public final class LoanDisbursementKeywords { + + public static final List KEYWORDS = List.of( + "신규", + "실행", + "증액" + ); + + private LoanDisbursementKeywords() { + } + + /** + * loanTransactionTypeName이 대출금 지급 거래를 나타내는 키워드를 포함하는지 판정합니다. + * null/빈 문자열은 제외 사유가 없는 것으로 보고 false(정상 상환 후보)를 반환합니다. + */ + public static boolean matches(String loanTransactionTypeName) { + String normalized = normalize(loanTransactionTypeName); + return KEYWORDS.stream().anyMatch(normalized::contains); + } + + private static String normalize(String value) { + if (value == null) { + return ""; + } + + return value + .replaceAll("\\s+", "") + .toUpperCase(Locale.ROOT); + } +} diff --git a/common/src/test/java/com/ntropy/common/domain/LoanDisbursementKeywordsTest.java b/common/src/test/java/com/ntropy/common/domain/LoanDisbursementKeywordsTest.java new file mode 100644 index 00000000..a6a7eb4e --- /dev/null +++ b/common/src/test/java/com/ntropy/common/domain/LoanDisbursementKeywordsTest.java @@ -0,0 +1,79 @@ +package com.ntropy.common.domain; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class LoanDisbursementKeywordsTest { + + @Test + void keywordListIsNeverEmpty() { + assertFalse( + LoanDisbursementKeywords.KEYWORDS.isEmpty(), + "MyBatis 가 빈 목록으로는 유효한 조건을 생성하지 못하므로 " + + "목록이 비어 있으면 안 됩니다" + ); + } + + @Test + void nullIsNotDisbursement() { + assertFalse(LoanDisbursementKeywords.matches(null)); + } + + @Test + void blankIsNotDisbursement() { + assertFalse(LoanDisbursementKeywords.matches("")); + assertFalse(LoanDisbursementKeywords.matches(" ")); + } + + @Test + void normalRepaymentIsNotDisbursement() { + assertFalse(LoanDisbursementKeywords.matches("정상상환")); + } + + @Test + void newLoanIsDisbursement() { + assertTrue(LoanDisbursementKeywords.matches("신규")); + } + + @Test + void executionIsDisbursement() { + assertTrue(LoanDisbursementKeywords.matches("실행")); + } + + @Test + void increaseIsDisbursement() { + assertTrue(LoanDisbursementKeywords.matches("증액")); + } + + @Test + void loanExecutionIsDisbursementBecauseItContainsExecution() { + assertTrue(LoanDisbursementKeywords.matches("대출실행")); + } + + @Test + void loanExecutionWithInternalSpaceIsDisbursement() { + assertTrue(LoanDisbursementKeywords.matches("대출 실행")); + } + + @Test + void newLoanWithInternalSpaceIsDisbursement() { + assertTrue(LoanDisbursementKeywords.matches("신 규")); + } + + @Test + void executionWithInternalSpaceIsDisbursement() { + assertTrue(LoanDisbursementKeywords.matches("실 행")); + } + + @Test + void increaseWithInternalSpaceIsDisbursement() { + assertTrue(LoanDisbursementKeywords.matches("증 액")); + } + + @Test + void keywordInMiddleOfStringIsDisbursement() { + assertTrue(LoanDisbursementKeywords.matches("2026년 신규 대출 실행분")); + } +} diff --git a/services/account-service/src/main/java/com/ntropy/account/mapper/FinancialCommitmentMapper.java b/services/account-service/src/main/java/com/ntropy/account/mapper/FinancialCommitmentMapper.java index 0ca2e810..2cdce597 100644 --- a/services/account-service/src/main/java/com/ntropy/account/mapper/FinancialCommitmentMapper.java +++ b/services/account-service/src/main/java/com/ntropy/account/mapper/FinancialCommitmentMapper.java @@ -16,7 +16,14 @@ public interface FinancialCommitmentMapper { List findSavingCommitmentCandidates(@Param("userId") Long userId); - List findLoanCommitmentCandidates(@Param("userId") Long userId); + /** + * loanDisbursementKeywords는 LOAN 신규·실행·증액(대출금 지급) 판정에 사용되며, + * 호출 측은 com.ntropy.common.domain.LoanDisbursementKeywords.KEYWORDS를 전달해야 합니다. + */ + List findLoanCommitmentCandidates( + @Param("userId") Long userId, + @Param("loanDisbursementKeywords") List loanDisbursementKeywords + ); List findInsuranceOutflowCandidates( @Param("userId") Long userId, diff --git a/services/account-service/src/main/java/com/ntropy/account/mapper/MonthlyExpenseMapper.java b/services/account-service/src/main/java/com/ntropy/account/mapper/MonthlyExpenseMapper.java index d3bf0eaa..51410ae1 100644 --- a/services/account-service/src/main/java/com/ntropy/account/mapper/MonthlyExpenseMapper.java +++ b/services/account-service/src/main/java/com/ntropy/account/mapper/MonthlyExpenseMapper.java @@ -16,11 +16,14 @@ public interface MonthlyExpenseMapper { /** * 특정 기간의 총소비 금액을 조회합니다. + * loanDisbursementKeywords는 LOAN 신규·실행·증액(대출금 지급) 판정에 사용되며, + * 호출 측은 com.ntropy.common.domain.LoanDisbursementKeywords.KEYWORDS를 전달해야 합니다. */ Long findTotalExpense( @Param("userId") Long userId, @Param("startDate") LocalDate startDate, - @Param("endDate") LocalDate endDate + @Param("endDate") LocalDate endDate, + @Param("loanDisbursementKeywords") List loanDisbursementKeywords ); /** @@ -29,7 +32,8 @@ Long findTotalExpense( List findCategoryExpenses( @Param("userId") Long userId, @Param("startDate") LocalDate startDate, - @Param("endDate") LocalDate endDate + @Param("endDate") LocalDate endDate, + @Param("loanDisbursementKeywords") List loanDisbursementKeywords ); /** @@ -38,6 +42,7 @@ List findCategoryExpenses( Long findFixedExpense( @Param("userId") Long userId, @Param("startDate") LocalDate startDate, - @Param("endDate") LocalDate endDate + @Param("endDate") LocalDate endDate, + @Param("loanDisbursementKeywords") List loanDisbursementKeywords ); } \ No newline at end of file diff --git a/services/account-service/src/main/java/com/ntropy/account/service/FinancialCommitmentService.java b/services/account-service/src/main/java/com/ntropy/account/service/FinancialCommitmentService.java index b460e519..31794291 100644 --- a/services/account-service/src/main/java/com/ntropy/account/service/FinancialCommitmentService.java +++ b/services/account-service/src/main/java/com/ntropy/account/service/FinancialCommitmentService.java @@ -20,6 +20,7 @@ import com.ntropy.account.mapper.projection.InsuranceOutflowRow; import com.ntropy.account.mapper.projection.LoanCommitmentCandidateRow; import com.ntropy.account.mapper.projection.SavingCommitmentCandidateRow; +import com.ntropy.common.domain.LoanDisbursementKeywords; import com.ntropy.common.dto.account.FinancialCommitmentSummary; import com.ntropy.common.exception.ServiceException; @@ -87,7 +88,8 @@ private List buildSavingCommitments(Long userId, Loc private List buildLoanCommitments(Long userId, LocalDate fromDate, LocalDate toDate) { List result = new ArrayList<>(); - for (LoanCommitmentCandidateRow row : financialCommitmentMapper.findLoanCommitmentCandidates(userId)) { + for (LoanCommitmentCandidateRow row : financialCommitmentMapper.findLoanCommitmentCandidates( + userId, LoanDisbursementKeywords.KEYWORDS)) { if (!withinRangeOrUnknown(row.getNextPaymentDate(), fromDate, toDate)) { continue; } diff --git a/services/account-service/src/main/java/com/ntropy/account/service/MonthlyExpenseService.java b/services/account-service/src/main/java/com/ntropy/account/service/MonthlyExpenseService.java index d2f62746..6bfd3d5e 100644 --- a/services/account-service/src/main/java/com/ntropy/account/service/MonthlyExpenseService.java +++ b/services/account-service/src/main/java/com/ntropy/account/service/MonthlyExpenseService.java @@ -10,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional; import com.ntropy.account.mapper.MonthlyExpenseMapper; +import com.ntropy.common.domain.LoanDisbursementKeywords; import com.ntropy.common.dto.account.CategoryExpenseAmount; import com.ntropy.common.dto.account.MonthlyExpenseSummary; @@ -58,21 +59,24 @@ public MonthlyExpenseSummary findMonthlyExpense( monthlyExpenseMapper.findTotalExpense( userId, startDate, - endDate + endDate, + LoanDisbursementKeywords.KEYWORDS ); Long fixedExpense = monthlyExpenseMapper.findFixedExpense( userId, startDate, - endDate + endDate, + LoanDisbursementKeywords.KEYWORDS ); List rows = monthlyExpenseMapper.findCategoryExpenses( userId, startDate, - endDate + endDate, + LoanDisbursementKeywords.KEYWORDS ); Map categoryExpenses = diff --git a/services/account-service/src/main/resources/mapper/account/FinancialCommitmentMapper.xml b/services/account-service/src/main/resources/mapper/account/FinancialCommitmentMapper.xml index 04a8ae45..75a3b16b 100644 --- a/services/account-service/src/main/resources/mapper/account/FinancialCommitmentMapper.xml +++ b/services/account-service/src/main/resources/mapper/account/FinancialCommitmentMapper.xml @@ -30,10 +30,24 @@ AND account_row.deposit_type_code = '12' + + + + REGEXP_REPLACE( + COALESCE(loan_row.loan_transaction_type_name, ''), + '[[:space:]]+', + '' + ) NOT LIKE CONCAT('%', #{keyword}, '%') + + +