From e3e37285c27cb5422a70bc13f68a6feaa58db863 Mon Sep 17 00:00:00 2001 From: shinhyung <23102009@seoultech.ac.kr> Date: Mon, 24 Aug 2026 09:50:46 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EB=B0=A9=EC=96=B4=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EB=B3=B4=ED=97=98=EB=A3=8C=EB=A5=BC=20=EC=83=81?= =?UTF-8?q?=ED=92=88=EB=B3=84=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FinancialCommitmentQueryClient.java | 2 +- .../service/FinancialCommitmentService.java | 46 +++++++++++-------- .../account/FinancialCommitmentMapper.xml | 2 +- .../FinancialCommitmentServiceTest.java | 30 ++++++------ 4 files changed, 44 insertions(+), 36 deletions(-) diff --git a/common/src/main/java/com/ntropy/common/client/FinancialCommitmentQueryClient.java b/common/src/main/java/com/ntropy/common/client/FinancialCommitmentQueryClient.java index 2791dc2d..1f9b27c0 100644 --- a/common/src/main/java/com/ntropy/common/client/FinancialCommitmentQueryClient.java +++ b/common/src/main/java/com/ntropy/common/client/FinancialCommitmentQueryClient.java @@ -21,7 +21,7 @@ public interface FinancialCommitmentQueryClient { /** * 조회 기간에 납입 예정인 활성 금융상품을 반환한다. * 납입일을 알 수 없는 활성 대출·적금도 목록에서 제외하지 않는다. - * 보험은 반복 납입이 감지된 항목만 생성하므로 납입일 미상 상태 자체가 없다. + * 보험은 실제 거래 설명의 상품명별로 분리하고 최신 출금일과 금액으로 다음 납입을 추정한다. */ List findFinancialCommitments( Long userId, 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 31794291..58af7a1d 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 @@ -4,7 +4,6 @@ import java.math.RoundingMode; import java.time.Clock; import java.time.LocalDate; -import java.time.YearMonth; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; @@ -107,10 +106,7 @@ private List buildLoanCommitments(Long userId, Local return result; } - /** - * 보험사 법인명·축약명 registry로 판정한 후보를 사용자별 표준 보험사명 하나로 묶는다. - * 출금계좌·계약·금액이 달라도 같은 보험사면 하나의 항목으로 합산하며, 반복 횟수나 동일 금액은 요구하지 않는다. - */ + /** 보험사 registry로 판정한 출금 거래를 실제 상품명별로 분리하고, 상품별 최신 납입액을 반환한다. */ private List buildInsuranceCommitments(Long userId, LocalDate fromDate, LocalDate toDate) { LocalDate today = LocalDate.now(clock); LocalDate observationEnd = toDate.isBefore(today) ? toDate : today; @@ -119,36 +115,32 @@ private List buildInsuranceCommitments(Long userId, List rows = financialCommitmentMapper.findInsuranceOutflowCandidates( userId, observationStart, observationEnd); - Map> occurrencesByInsurer = new LinkedHashMap<>(); + Map> occurrencesByProduct = new LinkedHashMap<>(); for (InsuranceOutflowRow row : rows) { String combinedDescription = combineDescriptions(row); if (combinedDescription == null || row.getOutAmount() == null) { continue; } - InsuranceCompany.matchStandardName(combinedDescription).ifPresent(standardName -> - occurrencesByInsurer.computeIfAbsent(standardName, unused -> new ArrayList<>()).add(row)); + InsuranceCompany.matchStandardName(combinedDescription).ifPresent(standardName -> { + String productName = findInsuranceProductName(row, standardName); + occurrencesByProduct.computeIfAbsent(productName, unused -> new ArrayList<>()).add(row); + }); } List result = new ArrayList<>(); - for (Map.Entry> entry : occurrencesByInsurer.entrySet()) { + for (Map.Entry> entry : occurrencesByProduct.entrySet()) { List occurrences = entry.getValue(); - LocalDate latestTranDate = occurrences.stream() - .map(InsuranceOutflowRow::getTranDate) - .max(Comparator.naturalOrder()) + InsuranceOutflowRow latestOccurrence = occurrences.stream() + .max(Comparator.comparing(InsuranceOutflowRow::getTranDate)) .orElseThrow(); + LocalDate latestTranDate = latestOccurrence.getTranDate(); LocalDate nextPaymentDate = latestTranDate.plusMonths(1); if (!withinRangeOrUnknown(nextPaymentDate, fromDate, toDate)) { continue; } - YearMonth latestMonth = YearMonth.from(latestTranDate); - BigDecimal monthlyTotal = occurrences.stream() - .filter(row -> YearMonth.from(row.getTranDate()).equals(latestMonth)) - .map(InsuranceOutflowRow::getOutAmount) - .reduce(BigDecimal.ZERO, BigDecimal::add); - - AmountResolution amount = resolveExpectedAmount(monthlyTotal); + AmountResolution amount = resolveExpectedAmount(latestOccurrence.getOutAmount()); result.add(new FinancialCommitmentSummary( null, null, EXPENSE_TYPE_INSURANCE, entry.getKey(), null, amount.amount(), null, null, nextPaymentDate, amount.status(), STATUS_ESTIMATED @@ -157,6 +149,22 @@ private List buildInsuranceCommitments(Long userId, return result; } + /** desc1~desc4 중 판정된 보험사가 포함된 원문 필드를 상품명으로 사용한다. */ + private static String findInsuranceProductName(InsuranceOutflowRow row, String standardInsurerName) { + for (String value : new String[]{row.getDesc1(), row.getDesc2(), row.getDesc3(), row.getDesc4()}) { + if (value == null || value.trim().isEmpty()) { + continue; + } + String candidate = value.trim(); + if (InsuranceCompany.matchStandardName(candidate) + .filter(standardInsurerName::equals) + .isPresent()) { + return candidate; + } + } + return standardInsurerName; + } + /** desc1→desc2→desc3→desc4 순서로 trim한 비어 있지 않은 값을 |로 결합한다. 전부 비어 있으면 null. */ private static String combineDescriptions(InsuranceOutflowRow row) { List parts = new ArrayList<>(); 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 75a3b16b..96d9d8b0 100644 --- a/services/account-service/src/main/resources/mapper/account/FinancialCommitmentMapper.xml +++ b/services/account-service/src/main/resources/mapper/account/FinancialCommitmentMapper.xml @@ -81,7 +81,7 @@