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
@@ -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<String> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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 <foreach>가 빈 목록으로는 유효한 조건을 생성하지 못하므로 " +
"목록이 비어 있으면 안 됩니다"
);
}

@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년 신규 대출 실행분"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ public interface FinancialCommitmentMapper {

List<SavingCommitmentCandidateRow> findSavingCommitmentCandidates(@Param("userId") Long userId);

List<LoanCommitmentCandidateRow> findLoanCommitmentCandidates(@Param("userId") Long userId);
/**
* loanDisbursementKeywords는 LOAN 신규·실행·증액(대출금 지급) 판정에 사용되며,
* 호출 측은 com.ntropy.common.domain.LoanDisbursementKeywords.KEYWORDS를 전달해야 합니다.
*/
List<LoanCommitmentCandidateRow> findLoanCommitmentCandidates(
@Param("userId") Long userId,
@Param("loanDisbursementKeywords") List<String> loanDisbursementKeywords
);

List<InsuranceOutflowRow> findInsuranceOutflowCandidates(
@Param("userId") Long userId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> loanDisbursementKeywords
);

/**
Expand All @@ -29,7 +32,8 @@ Long findTotalExpense(
List<CategoryExpenseAmount> findCategoryExpenses(
@Param("userId") Long userId,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate
@Param("endDate") LocalDate endDate,
@Param("loanDisbursementKeywords") List<String> loanDisbursementKeywords
);

/**
Expand All @@ -38,6 +42,7 @@ List<CategoryExpenseAmount> findCategoryExpenses(
Long findFixedExpense(
@Param("userId") Long userId,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate
@Param("endDate") LocalDate endDate,
@Param("loanDisbursementKeywords") List<String> loanDisbursementKeywords
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -87,7 +88,8 @@ private List<FinancialCommitmentSummary> buildSavingCommitments(Long userId, Loc

private List<FinancialCommitmentSummary> buildLoanCommitments(Long userId, LocalDate fromDate, LocalDate toDate) {
List<FinancialCommitmentSummary> result = new ArrayList<>();
for (LoanCommitmentCandidateRow row : financialCommitmentMapper.findLoanCommitmentCandidates(userId)) {
for (LoanCommitmentCandidateRow row : financialCommitmentMapper.findLoanCommitmentCandidates(
userId, LoanDisbursementKeywords.KEYWORDS)) {
if (!withinRangeOrUnknown(row.getNextPaymentDate(), fromDate, toDate)) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<CategoryExpenseAmount> rows =
monthlyExpenseMapper.findCategoryExpenses(
userId,
startDate,
endDate
endDate,
LoanDisbursementKeywords.KEYWORDS
);

Map<String, Long> categoryExpenses =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,24 @@
AND account_row.deposit_type_code = '12'
</select>

<!--
LOAN 신규·실행·증액(대출금 지급) 거래를 제외한다. MonthlyExpenseMapper의
상환 후보 판정과 동일하게 common 모듈 LoanDisbursementKeywords와 키워드 목록을
공유하며, 판정 의미는 "공백 정규화 후 부분 문자열 포함"이다. 거래구분명이 NULL이면
COALESCE로 빈 문자열이 되어 어떤 키워드와도 매치되지 않으므로 후보로 인정된다.
-->
<sql id="loanDisbursementExclusion">
<foreach collection="loanDisbursementKeywords" item="keyword" separator=" AND ">
REGEXP_REPLACE(
COALESCE(loan_row.loan_transaction_type_name, ''),
'[[:space:]]+',
''
) NOT LIKE CONCAT('%', #{keyword}, '%')
</foreach>
</sql>

<!--
대출 계좌(deposit_type_code=40, account_group=LOAN)와, 있다면 가장 최근 "정상 상환" LOAN 거래 1건을 함께 반환한다.
신규·실행·대출실행·증액 거래는 정상 상환 후보에서 제외한다(부분 문자열 포함 기준).
거래구분명이 null/blank이면 제외 사유가 없는 것으로 보고 후보로 인정한다.
-->
<select id="findLoanCommitmentCandidates"
resultType="com.ntropy.account.mapper.projection.LoanCommitmentCandidateRow">
Expand All @@ -52,15 +66,7 @@
WHERE loan_row.account_id = account_row.account_id
AND loan_row.transaction_category = 'LOAN'
AND loan_row.out_amount > 0
AND (
loan_row.loan_transaction_type_name IS NULL
OR (
loan_row.loan_transaction_type_name NOT LIKE '%신규%'
AND loan_row.loan_transaction_type_name NOT LIKE '%실행%'
AND loan_row.loan_transaction_type_name NOT LIKE '%대출실행%'
AND loan_row.loan_transaction_type_name NOT LIKE '%증액%'
)
)
AND (<include refid="loanDisbursementExclusion"/>)
ORDER BY loan_row.tran_date DESC,
loan_row.tran_time DESC,
loan_row.account_transaction_id DESC
Expand Down
Loading
Loading