diff --git a/src/main/java/org/umc/valuedi/domain/asset/dto/res/AssetResDTO.java b/src/main/java/org/umc/valuedi/domain/asset/dto/res/AssetResDTO.java index ace3564..a2ab1f8 100644 --- a/src/main/java/org/umc/valuedi/domain/asset/dto/res/AssetResDTO.java +++ b/src/main/java/org/umc/valuedi/domain/asset/dto/res/AssetResDTO.java @@ -1,13 +1,12 @@ package org.umc.valuedi.domain.asset.dto.res; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import java.time.LocalDate; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Schema(description = "자산 통합 응답 DTO") public class AssetResDTO { @@ -30,6 +29,7 @@ public static class AssetSummaryCountDTO { @Getter @AllArgsConstructor + @NoArgsConstructor // Map 초기화를 위해 기본 생성자 추가 @Builder @Schema(description = "AssetFetchService의 내부 처리 결과 DTO (API 응답으로 직접 사용되지 않음)") public static class AssetSyncResult { @@ -50,5 +50,21 @@ public static class AssetSyncResult { @Schema(description = "가계부 동기화에 사용될 조회 종료일") private LocalDate toDate; + + @Builder.Default + @Schema(description = "계좌 ID별 최신 잔액 맵") + private Map latestBalances = new HashMap<>(); + + public boolean hasLatestBalanceFor(Long accountId) { + return latestBalances != null && latestBalances.containsKey(accountId); + } + + public Long getLatestBalanceFor(Long accountId) { + return latestBalances.get(accountId); + } + + public void addLatestBalance(Long accountId, Long balance) { + this.latestBalances.put(accountId, balance); + } } -} +} \ No newline at end of file diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/AssetBalanceService.java b/src/main/java/org/umc/valuedi/domain/asset/service/AssetBalanceService.java index 7f11781..588954e 100644 --- a/src/main/java/org/umc/valuedi/domain/asset/service/AssetBalanceService.java +++ b/src/main/java/org/umc/valuedi/domain/asset/service/AssetBalanceService.java @@ -28,33 +28,30 @@ public class AssetBalanceService { private final BankAccountRepository bankAccountRepository; private final BankTransactionRepository bankTransactionRepository; - /** - * 자산 동기화를 수행하고, 특정 계좌의 최신 잔액을 반환합니다. - * 동기화 실패 시 기존 잔액을 반환합니다. - */ @Transactional(propagation = Propagation.NOT_SUPPORTED) public Long syncAndGetLatestBalance(Long memberId, Long accountId) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); - // 자산 동기화 시도 try { AssetResDTO.AssetSyncResult result = assetFetchService.fetchAndSaveLatestData(member); - log.info("[AssetBalanceService] 자산 동기화 완료. MemberID: {}, NewTransactions: {}, SuccessOrgs: {}, FailedOrgs: {}", - memberId, result.getNewBankTransactionCount(), result.getSuccessOrganizations(), result.getFailureOrganizations()); + // 1. 동기화 결과 DTO에 방금 수집한 실시간 잔액이 있다면 DB 조회 없이 즉시 반환 (레이스 컨디션 방지) + if (result.hasLatestBalanceFor(accountId)) { + log.info("[AssetBalanceService] 실시간 동기화 데이터 사용. AccountID: {}", accountId); + return result.getLatestBalanceFor(accountId); + } } catch (Exception e) { - log.warn("잔액 조회 중 자산 동기화 실패 (기존 잔액 사용): {}", e.getMessage()); + log.warn("[AssetBalanceService] 잔액 조회 중 자산 동기화 실패 (기존 DB 잔액 사용): {}", e.getMessage()); } - // 계좌 조회 (영속성 컨텍스트 초기화 가능성 고려하여 재조회 권장) + // 2. Fallback: 실시간 데이터가 없거나 동기화 실패 시 DB에서 최신 데이터 조회 BankAccount account = bankAccountRepository.findByIdAndMemberId(accountId, memberId) .orElseThrow(() -> new GoalException(GoalErrorCode.ACCOUNT_NOT_FOUND)); - // 최신 거래내역 기반 잔액 조회 return bankTransactionRepository.findTopByBankAccountOrderByTrDatetimeDesc(account) .map(BankTransaction::getAfterBalance) .orElse(account.getBalanceAmount()); } -} +} \ No newline at end of file diff --git a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetFetchService.java b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetFetchService.java index 8d7d5e0..6f6e74d 100644 --- a/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetFetchService.java +++ b/src/main/java/org/umc/valuedi/domain/asset/service/command/AssetFetchService.java @@ -21,11 +21,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -59,15 +55,13 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) { .map(CompletableFuture::join) .toList(); - // 모든 거래내역을 한번에 조회하기 위한 준비 List allFetchedBankTransactions = new ArrayList<>(); List allFetchedCardApprovals = new ArrayList<>(); - + List successOrganizations = new ArrayList<>(); List failureOrganizations = new ArrayList<>(); LocalDate overallMinDate = today; - // 취합된 결과를 바탕으로 DB 저장 및 중복 제거 로직 실행 for (AssetFetchWorker.FetchResult result : fetchResults) { if (result.isSuccess()) { successOrganizations.add(result.connection().getOrganization()); @@ -82,6 +76,8 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) { } } + Map realTimeBalances = new HashMap<>(); + // 새로운 데이터 필터링 및 저장 int totalNewBankTransactions = 0; if (!allFetchedBankTransactions.isEmpty()) { @@ -90,9 +86,19 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) { bankTransactionRepository.bulkInsert(newBankTransactions); totalNewBankTransactions = newBankTransactions.size(); - // 계좌 잔액 업데이트 로직 추가 + // 계좌 잔액 업데이트 (기존 엔티티 반영) updateAccountBalances(newBankTransactions); } + + // 수집된 모든 거래내역 중 계좌별 가장 최신 잔액을 추출하여 실시간 데이터 맵에 저장 + allFetchedBankTransactions.stream() + .collect(Collectors.groupingBy(tx -> tx.getBankAccount().getId(), + Collectors.maxBy(Comparator.comparing(BankTransaction::getTrDatetime)))) + .forEach((accountId, optTx) -> optTx.ifPresent(tx -> { + if (tx.getAfterBalance() != null) { + realTimeBalances.put(accountId, tx.getAfterBalance()); + } + })); } int totalNewCardApprovals = 0; @@ -115,20 +121,20 @@ public AssetResDTO.AssetSyncResult fetchAndSaveLatestData(Member member) { .failureOrganizations(failureOrganizations) .fromDate(overallMinDate) .toDate(today) + .latestBalances(realTimeBalances) // 실시간 잔액 데이터 전달 .build(); } private void updateAccountBalances(List transactions) { - // 계좌별로 가장 최신 거래내역을 찾아서 잔액 업데이트 Map latestTransactions = transactions.stream() - .collect(Collectors.groupingBy(BankTransaction::getBankAccount, + .collect(Collectors.groupingBy(BankTransaction::getBankAccount, Collectors.collectingAndThen( - Collectors.maxBy((t1, t2) -> t1.getTrDatetime().compareTo(t2.getTrDatetime())), - java.util.Optional::get + Collectors.maxBy(Comparator.comparing(BankTransaction::getTrDatetime)), + Optional::get ))); List updatedAccounts = new ArrayList<>(); - + latestTransactions.forEach((account, latestTransaction) -> { if (latestTransaction.getAfterBalance() != null) { account.updateBalance(latestTransaction.getAfterBalance()); @@ -145,8 +151,8 @@ private List filterNewBankTransactions(List al if (allFetched.isEmpty()) return List.of(); LocalDate minDate = allFetched.stream().map(BankTransaction::getTrDate).min(LocalDate::compareTo).orElse(LocalDate.now()); - List accounts = allFetched.stream().map(BankTransaction::getBankAccount).distinct().collect(Collectors.toList()); - + List accounts = allFetched.stream().map(BankTransaction::getBankAccount).distinct().toList(); + List existingTransactions = bankTransactionRepository.findByBankAccountInAndTrDatetimeAfter(accounts, minDate.atStartOfDay()); Set existingKeys = existingTransactions.stream() @@ -155,14 +161,14 @@ private List filterNewBankTransactions(List al return allFetched.stream() .filter(tx -> !existingKeys.contains(new BankTransactionKey(tx.getTrDatetime(), tx.getInAmount(), tx.getOutAmount(), Objects.toString(tx.getDesc3(), "")))) - .collect(Collectors.toList()); + .toList(); } private List filterNewCardApprovals(List allFetched) { if (allFetched.isEmpty()) return List.of(); - List cards = allFetched.stream().map(CardApproval::getCard).distinct().collect(Collectors.toList()); - List approvalNos = allFetched.stream().map(CardApproval::getApprovalNo).distinct().collect(Collectors.toList()); + List cards = allFetched.stream().map(CardApproval::getCard).distinct().toList(); + List approvalNos = allFetched.stream().map(CardApproval::getApprovalNo).distinct().toList(); List existingApprovals = cardApprovalRepository.findByCardInAndApprovalNoIn(cards, approvalNos); @@ -172,6 +178,6 @@ private List filterNewCardApprovals(List allFetched) return allFetched.stream() .filter(ca -> !existingKeys.contains(new CardApprovalKey(ca.getCard(), ca.getApprovalNo()))) - .collect(Collectors.toList()); + .toList(); } -} +} \ No newline at end of file