From b77a0a5798ad49e71acc751ad17586059a995ad8 Mon Sep 17 00:00:00 2001 From: kkeunii Date: Thu, 19 Feb 2026 00:53:05 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=EB=AA=A9=ED=91=9C=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EB=8F=99=EA=B8=B0=ED=99=94=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asset/service/AssetBalanceService.java | 28 +---- .../service/command/AssetFetchService.java | 2 +- .../goal/controller/GoalController.java | 6 +- .../domain/goal/converter/GoalConverter.java | 12 +- .../dto/response/GoalCreateResponseDto.java | 3 - .../dto/response/GoalDetailResponseDto.java | 4 +- .../dto/response/GoalListResponseDto.java | 4 +- .../umc/valuedi/domain/goal/entity/Goal.java | 4 - .../service/GoalAchievementRateService.java | 9 +- .../domain/goal/service/GoalLedgerFacade.java | 69 ------------ .../goal/service/GoalStatusChangeService.java | 13 +-- .../service/command/GoalCommandService.java | 7 +- .../service/query/GoalLedgerQueryService.java | 10 +- .../service/query/GoalListQueryService.java | 104 ++++++++---------- .../goal/service/query/GoalQueryService.java | 24 ++-- 15 files changed, 82 insertions(+), 217 deletions(-) delete mode 100644 src/main/java/org/umc/valuedi/domain/goal/service/GoalLedgerFacade.java 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 588954e4..717759a5 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 @@ -5,48 +5,24 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import org.umc.valuedi.domain.asset.dto.res.AssetResDTO; import org.umc.valuedi.domain.asset.entity.BankAccount; import org.umc.valuedi.domain.asset.entity.BankTransaction; import org.umc.valuedi.domain.asset.repository.bank.bankAccount.BankAccountRepository; import org.umc.valuedi.domain.asset.repository.bank.bankTransaction.BankTransactionRepository; -import org.umc.valuedi.domain.asset.service.command.AssetFetchService; import org.umc.valuedi.domain.goal.exception.GoalException; import org.umc.valuedi.domain.goal.exception.code.GoalErrorCode; -import org.umc.valuedi.domain.member.entity.Member; -import org.umc.valuedi.domain.member.exception.MemberException; -import org.umc.valuedi.domain.member.exception.code.MemberErrorCode; -import org.umc.valuedi.domain.member.repository.MemberRepository; @Slf4j @Service @RequiredArgsConstructor public class AssetBalanceService { - private final AssetFetchService assetFetchService; - private final MemberRepository memberRepository; 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); - - // 1. 동기화 결과 DTO에 방금 수집한 실시간 잔액이 있다면 DB 조회 없이 즉시 반환 (레이스 컨디션 방지) - if (result.hasLatestBalanceFor(accountId)) { - log.info("[AssetBalanceService] 실시간 동기화 데이터 사용. AccountID: {}", accountId); - return result.getLatestBalanceFor(accountId); - } - - } catch (Exception e) { - log.warn("[AssetBalanceService] 잔액 조회 중 자산 동기화 실패 (기존 DB 잔액 사용): {}", e.getMessage()); - } - - // 2. Fallback: 실시간 데이터가 없거나 동기화 실패 시 DB에서 최신 데이터 조회 + BankAccount account = bankAccountRepository.findByIdAndMemberId(accountId, memberId) .orElseThrow(() -> new GoalException(GoalErrorCode.ACCOUNT_NOT_FOUND)); @@ -54,4 +30,4 @@ public Long syncAndGetLatestBalance(Long memberId, Long accountId) { .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 6f6e74db..4b234336 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 @@ -180,4 +180,4 @@ private List filterNewCardApprovals(List allFetched) .filter(ca -> !existingKeys.contains(new CardApprovalKey(ca.getCard(), ca.getApprovalNo()))) .toList(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/umc/valuedi/domain/goal/controller/GoalController.java b/src/main/java/org/umc/valuedi/domain/goal/controller/GoalController.java index 868ffe22..685a18db 100644 --- a/src/main/java/org/umc/valuedi/domain/goal/controller/GoalController.java +++ b/src/main/java/org/umc/valuedi/domain/goal/controller/GoalController.java @@ -11,10 +11,10 @@ import org.umc.valuedi.domain.goal.enums.GoalStatus; import org.umc.valuedi.domain.goal.enums.GoalSort; import org.umc.valuedi.domain.goal.exception.code.GoalSuccessCode; -import org.umc.valuedi.domain.goal.service.GoalLedgerFacade; import org.umc.valuedi.domain.goal.service.command.GoalAccountCommandService; import org.umc.valuedi.domain.goal.service.command.GoalCommandService; import org.umc.valuedi.domain.goal.service.query.GoalAccountQueryService; +import org.umc.valuedi.domain.goal.service.query.GoalLedgerQueryService; import org.umc.valuedi.domain.goal.service.query.GoalListQueryService; import org.umc.valuedi.domain.goal.service.query.GoalQueryService; import org.umc.valuedi.domain.ledger.dto.response.LedgerListResponse; @@ -31,7 +31,7 @@ public class GoalController implements GoalControllerDocs{ private final GoalListQueryService goalListQueryService; private final GoalAccountQueryService goalAccountQueryService; private final GoalAccountCommandService goalAccountCommandService; - private final GoalLedgerFacade goalLedgerFacade; + private final GoalLedgerQueryService goalLedgerQueryService; // 목표 추가 @PostMapping @@ -158,7 +158,7 @@ public ApiResponse getGoalLedgers( ) { return ApiResponse.onSuccess( GoalSuccessCode.GOAL_LEDGER_LIST_FETCHED, - goalLedgerFacade.getGoalLedgerTransactions(memberId, goalId, page, size) + goalLedgerQueryService.getGoalLedgerTransactions(memberId, goalId, page, size) ); } } diff --git a/src/main/java/org/umc/valuedi/domain/goal/converter/GoalConverter.java b/src/main/java/org/umc/valuedi/domain/goal/converter/GoalConverter.java index 51e61751..621deadd 100644 --- a/src/main/java/org/umc/valuedi/domain/goal/converter/GoalConverter.java +++ b/src/main/java/org/umc/valuedi/domain/goal/converter/GoalConverter.java @@ -18,7 +18,7 @@ public class GoalConverter { - public static Goal toEntity(Member member,BankAccount bankAccount, GoalCreateRequestDto req, Long startAmount) { + public static Goal toEntity(Member member,BankAccount bankAccount, GoalCreateRequestDto req) { return Goal.builder() .member(member) .bankAccount(bankAccount) @@ -26,7 +26,6 @@ public static Goal toEntity(Member member,BankAccount bankAccount, GoalCreateReq .startDate(req.startDate()) .endDate(req.endDate()) .targetAmount(req.targetAmount()) - .startAmount(startAmount) .status(GoalStatus.ACTIVE) .completedAt(null) .color(GoalStyleCatalog.normalizeColor(req.colorCode())) @@ -82,7 +81,6 @@ public static GoalCreateResponseDto toCreateDto(Goal goal) { goal.getId(), goal.getTitle(), goal.getTargetAmount(), - goal.getStartAmount(), goal.getStartDate(), goal.getEndDate(), remainingDays, @@ -93,7 +91,7 @@ public static GoalCreateResponseDto toCreateDto(Goal goal) { public static GoalListResponseDto.GoalSummaryDto toSummaryDto( Goal goal, - Long savedAmount, + Long currentBalance, int achievementRate ) { Long remainingDays = calcRemainingDays(goal.getEndDate()); @@ -101,7 +99,7 @@ public static GoalListResponseDto.GoalSummaryDto toSummaryDto( return new GoalListResponseDto.GoalSummaryDto( goal.getId(), goal.getTitle(), - savedAmount, + currentBalance, remainingDays, achievementRate, goal.getStatus(), @@ -110,7 +108,7 @@ public static GoalListResponseDto.GoalSummaryDto toSummaryDto( ); } - public static GoalDetailResponseDto toDetailDto(Goal goal, Long savedAmount, int achievementRate) { + public static GoalDetailResponseDto toDetailDto(Goal goal, Long currentBalance, int achievementRate) { long remainingDays = calcRemainingDays(goal.getEndDate()); GoalDetailResponseDto.AccountDto accountDto = null; @@ -122,7 +120,7 @@ public static GoalDetailResponseDto toDetailDto(Goal goal, Long savedAmount, int return new GoalDetailResponseDto( goal.getId(), goal.getTitle(), - savedAmount, + currentBalance, goal.getTargetAmount(), goal.getStartDate(), goal.getEndDate(), diff --git a/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalCreateResponseDto.java b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalCreateResponseDto.java index d25040b6..238389d2 100644 --- a/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalCreateResponseDto.java +++ b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalCreateResponseDto.java @@ -16,9 +16,6 @@ public record GoalCreateResponseDto( @Schema(description = "목표 금액", example = "1000000") Long targetAmount, - @Schema(description = "시작 금액", example = "1000") - Long startAmount, - @Schema(description = "시작일", example = "2026-01-01") LocalDate startDate, diff --git a/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalDetailResponseDto.java b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalDetailResponseDto.java index 76442257..2c66914c 100644 --- a/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalDetailResponseDto.java +++ b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalDetailResponseDto.java @@ -14,8 +14,8 @@ public record GoalDetailResponseDto( @Schema(description = "목표 제목", example = "여행 자금 모으기") String title, - @Schema(description = "현재까지 모은 금액", example = "300000") - Long savedAmount, + @Schema(description = "현재 계좌 잔액", example = "300000") + Long currentBalance, @Schema(description = "목표 금액", example = "1000000") Long targetAmount, diff --git a/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalListResponseDto.java b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalListResponseDto.java index 8bcefa5d..8a4cbc24 100644 --- a/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalListResponseDto.java +++ b/src/main/java/org/umc/valuedi/domain/goal/dto/response/GoalListResponseDto.java @@ -21,8 +21,8 @@ public record GoalSummaryDto( @Schema(description = "목표 제목", example = "여행 자금 모으기") String title, - @Schema(description = "모은 금액", example = "700000") - Long savedAmount, + @Schema(description = "현재 계좌 잔액", example = "700000") + Long currentBalance, @Schema(description = "남은 일수", example = "30") Long remainingDays, diff --git a/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java b/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java index 0d6c5696..4de34f0c 100644 --- a/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java +++ b/src/main/java/org/umc/valuedi/domain/goal/entity/Goal.java @@ -46,9 +46,6 @@ public class Goal extends BaseEntity { @Column(name = "target_amount", nullable = false) private Long targetAmount; - @Column(name = "start_amount", nullable = false) - private Long startAmount; - @Enumerated(EnumType.STRING) @Column(name = "status", nullable = false, length = 20) private GoalStatus status; @@ -80,7 +77,6 @@ public void changeEndDate(LocalDate endDate) { public void changeTargetAmount(Long targetAmount) { this.targetAmount = targetAmount; } - public void changeStartAmount(Long startAmount) { this.startAmount = startAmount; } public void changeColor(String color) { this.color = color; } diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/GoalAchievementRateService.java b/src/main/java/org/umc/valuedi/domain/goal/service/GoalAchievementRateService.java index 3ae01feb..5302f284 100644 --- a/src/main/java/org/umc/valuedi/domain/goal/service/GoalAchievementRateService.java +++ b/src/main/java/org/umc/valuedi/domain/goal/service/GoalAchievementRateService.java @@ -5,14 +5,11 @@ @Service public class GoalAchievementRateService { - /** - * @return 0~100 (정수) - */ - public int calculateRate(Long savedAmount, Long targetAmount) { + public int calculateRate(Long currentBalance, Long targetAmount) { if (targetAmount <= 0) return 0; - if (savedAmount <= 0) return 0; + if (currentBalance <= 0) return 0; - double rate = (savedAmount * 100.0) / targetAmount; + double rate = (currentBalance * 100.0) / targetAmount; if (rate < 0) rate = 0; if (rate > 100) rate = 100; return (int) Math.floor(rate); diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/GoalLedgerFacade.java b/src/main/java/org/umc/valuedi/domain/goal/service/GoalLedgerFacade.java deleted file mode 100644 index 09611c72..00000000 --- a/src/main/java/org/umc/valuedi/domain/goal/service/GoalLedgerFacade.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.umc.valuedi.domain.goal.service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.umc.valuedi.domain.asset.dto.res.AssetResDTO; -import org.umc.valuedi.domain.asset.service.command.AssetFetchService; -import org.umc.valuedi.domain.goal.entity.Goal; -import org.umc.valuedi.domain.goal.exception.GoalException; -import org.umc.valuedi.domain.goal.exception.code.GoalErrorCode; -import org.umc.valuedi.domain.goal.repository.GoalRepository; -import org.umc.valuedi.domain.goal.service.query.GoalLedgerQueryService; -import org.umc.valuedi.domain.ledger.dto.response.LedgerListResponse; -import org.umc.valuedi.domain.ledger.service.command.LedgerSyncService; - -import java.time.LocalDate; - -@Slf4j -@Service -@RequiredArgsConstructor -public class GoalLedgerFacade { - - private final GoalRepository goalRepository; - private final AssetFetchService assetFetchService; - private final LedgerSyncService ledgerSyncService; - private final GoalLedgerQueryService goalLedgerQueryService; - - /** - * 목표 거래내역 조회 (동기화 포함) - * 트랜잭션을 분리하여 최신 데이터를 조회할 수 있도록 함 - */ - public LedgerListResponse getGoalLedgerTransactions(Long memberId, Long goalId, int page, int size) { - // 1. 목표 정보 조회 (검증용) - Goal goal = goalRepository.findById(goalId) - .orElseThrow(() -> new GoalException(GoalErrorCode.GOAL_NOT_FOUND)); - - if (!goal.getMember().getId().equals(memberId)) { - throw new GoalException(GoalErrorCode.GOAL_FORBIDDEN); - } - - // 2. 자산 및 가계부 동기화 (각각 별도의 트랜잭션으로 실행됨) - try { - // 자산 동기화 (REQUIRES_NEW) - // 여기서 최신 거래내역을 DB에 저장함 - assetFetchService.fetchAndSaveLatestData(goal.getMember()); - - // 가계부 동기화 (REQUIRES_NEW) - // 목표 기간 내의 데이터가 누락되지 않도록, 목표 시작일 ~ 오늘(또는 종료일)까지 동기화 수행 - LocalDate fromDate = goal.getStartDate(); - LocalDate toDate = LocalDate.now(); - - // 목표 종료일이 오늘보다 과거라면 종료일까지, 아니면 오늘까지 - if (goal.getEndDate().isBefore(toDate)) { - toDate = goal.getEndDate(); - } - - // 시작일이 종료일보다 늦으면(미래 목표 등) 동기화 수행 안 함 - if (!fromDate.isAfter(toDate)) { - ledgerSyncService.syncTransactionsAndUpdateMember(memberId, fromDate, toDate); - } - - } catch (Exception e) { - log.warn("목표 거래내역 조회 중 동기화 실패 (기존 데이터로 조회): {}", e.getMessage()); - } - - // 3. 최종 조회 (새로운 트랜잭션) - return goalLedgerQueryService.getGoalLedgerTransactions(memberId, goalId, page, size); - } -} diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/GoalStatusChangeService.java b/src/main/java/org/umc/valuedi/domain/goal/service/GoalStatusChangeService.java index 94b4a2f7..e7346d6b 100644 --- a/src/main/java/org/umc/valuedi/domain/goal/service/GoalStatusChangeService.java +++ b/src/main/java/org/umc/valuedi/domain/goal/service/GoalStatusChangeService.java @@ -5,7 +5,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.umc.valuedi.domain.asset.entity.BankAccount; -import org.umc.valuedi.domain.asset.service.AssetBalanceService; import org.umc.valuedi.domain.goal.entity.Goal; import org.umc.valuedi.domain.goal.enums.GoalStatus; import org.umc.valuedi.domain.goal.repository.GoalRepository; @@ -20,7 +19,6 @@ public class GoalStatusChangeService { private final GoalRepository goalRepository; - private final AssetBalanceService assetBalanceService; // 전체 목표 상태 갱신 (스케줄러용) public void refreshGoalStatuses() { @@ -32,11 +30,10 @@ public void refreshGoalStatuses() { if (account == null || !account.getIsActive()) { continue; } - // 최신 잔액 조회 (동기화 포함) - Long currentBalance = assetBalanceService.syncAndGetLatestBalance(goal.getMember().getId(), account.getId()); - long savedAmount = currentBalance - goal.getStartAmount(); + // DB에 저장된 계좌 잔액 사용 + Long currentBalance = account.getBalanceAmount(); - checkAndUpdateStatus(goal, savedAmount); + checkAndUpdateStatus(goal, currentBalance); } catch (Exception e) { log.error("목표 상태 갱신 중 오류 발생. Goal ID: {}", goal.getId(), e); } @@ -44,12 +41,12 @@ public void refreshGoalStatuses() { } // 단일 목표 상태 갱신 (조회 시 호출용) - public void checkAndUpdateStatus(Goal goal, long savedAmount) { + public void checkAndUpdateStatus(Goal goal, long currentBalance) { if (goal.getStatus() != GoalStatus.ACTIVE) { return; } - boolean isTargetReached = savedAmount >= goal.getTargetAmount(); + boolean isTargetReached = currentBalance >= goal.getTargetAmount(); // 목표 종료일이 오늘보다 이전이면 만료된 것으로 판단 (종료일 당일까지는 진행 중) boolean isExpired = goal.getEndDate().isBefore(LocalDate.now()); diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/command/GoalCommandService.java b/src/main/java/org/umc/valuedi/domain/goal/service/command/GoalCommandService.java index 7c229717..0ef5698d 100644 --- a/src/main/java/org/umc/valuedi/domain/goal/service/command/GoalCommandService.java +++ b/src/main/java/org/umc/valuedi/domain/goal/service/command/GoalCommandService.java @@ -6,7 +6,6 @@ import org.springframework.transaction.annotation.Transactional; import org.umc.valuedi.domain.asset.entity.BankAccount; import org.umc.valuedi.domain.asset.repository.bank.bankAccount.BankAccountRepository; -import org.umc.valuedi.domain.asset.service.AssetBalanceService; import org.umc.valuedi.domain.goal.converter.GoalConverter; import org.umc.valuedi.domain.goal.dto.request.GoalCreateRequestDto; import org.umc.valuedi.domain.goal.dto.request.GoalUpdateRequestDto; @@ -31,7 +30,6 @@ public class GoalCommandService { private final GoalRepository goalRepository; private final MemberRepository memberRepository; private final BankAccountRepository bankAccountRepository; - private final AssetBalanceService assetBalanceService; // 목표 생성 public GoalCreateResponseDto createGoal(Long memberId, GoalCreateRequestDto req) { @@ -51,11 +49,8 @@ public GoalCreateResponseDto createGoal(Long memberId, GoalCreateRequestDto req) throw new GoalException(GoalErrorCode.ACCOUNT_ALREADY_LINKED_TO_GOAL); } - // 동기화 후 최신 잔액 가져오기 - Long startAmount = assetBalanceService.syncAndGetLatestBalance(memberId, req.bankAccountId()); - // Goal 엔티티 생성 시 bankAccount 포함 - Goal goal = GoalConverter.toEntity(member, account, req, startAmount); + Goal goal = GoalConverter.toEntity(member, account, req); Goal saved = goalRepository.save(goal); return GoalConverter.toCreateDto(saved); diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java index e71d3ccd..04e53bc9 100644 --- a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java +++ b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java @@ -33,12 +33,10 @@ public LedgerListResponse getGoalLedgerTransactions(Long memberId, Long goalId, throw new GoalException(GoalErrorCode.GOAL_FORBIDDEN); } - // 시작 시간: 목표 생성 시각 (createdAt) - LocalDateTime from = goal.getCreatedAt(); - if (goal.getStartDate().atStartOfDay().isAfter(from)) { - from = goal.getStartDate().atStartOfDay(); - } - + // 시작 시간: 목표의 시작일(startDate) + LocalDateTime from = goal.getStartDate().atStartOfDay(); + + // 종료 시간: 목표의 종료일(endDate) LocalDateTime to = goal.getEndDate().atTime(LocalTime.MAX); Page result = ledgerQueryRepository.searchByPeriodLatest( diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalListQueryService.java b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalListQueryService.java index f1ac959d..f3af4c93 100644 --- a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalListQueryService.java +++ b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalListQueryService.java @@ -7,7 +7,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.umc.valuedi.domain.asset.entity.BankAccount; -import org.umc.valuedi.domain.asset.service.AssetBalanceService; import org.umc.valuedi.domain.goal.converter.GoalConverter; import org.umc.valuedi.domain.goal.dto.response.GoalListResponseDto; import org.umc.valuedi.domain.goal.entity.Goal; @@ -24,6 +23,7 @@ import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -33,7 +33,6 @@ public class GoalListQueryService { private final GoalRepository goalRepository; private final MemberRepository memberRepository; private final GoalAchievementRateService achievementRateService; - private final AssetBalanceService assetBalanceService; private final GoalStatusChangeService goalStatusChangeService; private void validateListStatus(GoalStatus status) { @@ -46,77 +45,63 @@ private void validateListStatus(GoalStatus status) { private List findGoals(Long memberId, GoalStatus status, Pageable pageable) { validateListStatus(status); - return switch (status) { - case ACTIVE -> - goalRepository.findAllByMember_IdAndStatus(memberId, GoalStatus.ACTIVE, pageable); - case COMPLETE -> - goalRepository.findAllByMember_IdAndStatusIn( - memberId, - List.of(GoalStatus.COMPLETE, GoalStatus.FAILED), - pageable - ); - default -> throw new GoalException(GoalErrorCode.INVALID_GOAL_LIST_STATUS); - }; + if (status == GoalStatus.ACTIVE) { + return goalRepository.findAllByMember_IdAndStatus(memberId, GoalStatus.ACTIVE, pageable); + } else { + // COMPLETE + return goalRepository.findAllByMember_IdAndStatusIn( + memberId, + List.of(GoalStatus.COMPLETE, GoalStatus.FAILED), + pageable + ); + } } // limit 없을 때 private List findGoals(Long memberId, GoalStatus status) { validateListStatus(status); - return switch (status) { - case ACTIVE -> - goalRepository.findAllByMember_IdAndStatus(memberId, GoalStatus.ACTIVE); - case COMPLETE -> - goalRepository.findAllByMember_IdAndStatusIn( - memberId, - List.of(GoalStatus.COMPLETE, GoalStatus.FAILED) - ); - default -> throw new GoalException(GoalErrorCode.INVALID_GOAL_LIST_STATUS); - }; + if (status == GoalStatus.ACTIVE) { + return goalRepository.findAllByMember_IdAndStatus(memberId, GoalStatus.ACTIVE); + } else { + // COMPLETE + return goalRepository.findAllByMember_IdAndStatusIn( + memberId, + List.of(GoalStatus.COMPLETE, GoalStatus.FAILED) + ); + } } private List toSummaryDtos(List goals, Long memberId) { return goals.stream() .map(g -> { - BankAccount account = g.getBankAccount(); - long savedAmount = 0L; + long currentBalance = 0L; - // ACTIVE 상태일 때만 계좌가 연결되어 있고, 잔액 계산 및 상태 갱신이 필요함 if (g.getStatus() == GoalStatus.ACTIVE) { + BankAccount account = g.getBankAccount(); if (account == null || !account.getIsActive()) { throw new GoalException(GoalErrorCode.GOAL_ACCOUNT_INACTIVE); } - // 동기화 후 최신 잔액 가져오기 - Long currentBalance = assetBalanceService.syncAndGetLatestBalance(memberId, account.getId()); - - // 현재 잔액 - 시작 잔액 - savedAmount = currentBalance - g.getStartAmount(); - - // 음수일 경우 0으로 처리 - if (savedAmount < 0) { - savedAmount = 0; - } + // DB에 저장된 현재 계좌 잔액 사용 + currentBalance = account.getBalanceAmount(); // 목표 달성 여부 체크 및 상태 업데이트 (공통 로직 사용) - goalStatusChangeService.checkAndUpdateStatus(g, savedAmount); + goalStatusChangeService.checkAndUpdateStatus(g, currentBalance); + } else if (g.getStatus() == GoalStatus.COMPLETE) { + currentBalance = g.getTargetAmount(); // 성공했으므로 목표 달성으로 간주 } else { - - if (g.getStatus() == GoalStatus.COMPLETE) { - savedAmount = g.getTargetAmount(); // 성공했으므로 목표 달성으로 간주 - } else { - // FAILED인 경우 달성률을 저장하지 않으므로 그냥 0으로 설정 - savedAmount = 0L; - } + // FAILED인 경우 달성률을 저장하지 않으므로 그냥 0으로 설정 + currentBalance = 0L; } - int rate = achievementRateService.calculateRate(savedAmount, g.getTargetAmount()); + int rate = achievementRateService.calculateRate(currentBalance, g.getTargetAmount()); - return GoalConverter.toSummaryDto(g, savedAmount, rate); + return GoalConverter.toSummaryDto(g, currentBalance, rate); }) - .toList(); + .collect(Collectors.toList()); } private String resolveTimeSortField(GoalStatus status) { @@ -143,20 +128,22 @@ public GoalListResponseDto getGoals(Long memberId, GoalStatus status, GoalSort s } GoalStatus listStatus = (status == null) ? GoalStatus.ACTIVE : status; - validateListStatus(listStatus); - + + // sort가 null이면 기본값 설정 GoalSort sortType = (sort == null) ? GoalSort.TIME_DESC : sort; + Integer size = (limit == null) ? null : Math.max(3, limit); // COMPLETE + PROGRESS_DESC => 성공한 목표만 + 달성(완료)된 순 if (listStatus == GoalStatus.COMPLETE && sortType == GoalSort.PROGRESS_DESC) { + // COMPLETE 상태인 것만 가져와서 정렬 List goals = goalRepository.findAllByMember_IdAndStatus(memberId, GoalStatus.COMPLETE).stream() .sorted(Comparator.comparing( Goal::getCompletedAt, Comparator.nullsLast(Comparator.naturalOrder()) ) .reversed()) - .toList(); + .collect(Collectors.toList()); if (size != null && goals.size() > size) { goals = goals.subList(0, size); @@ -176,7 +163,7 @@ public GoalListResponseDto getGoals(Long memberId, GoalStatus status, GoalSort s } else { goals = findGoals(memberId, listStatus).stream() .sorted(resolveTimeComparator(listStatus)) - .toList(); + .collect(Collectors.toList()); } return new GoalListResponseDto(toSummaryDtos(goals, memberId)); @@ -184,13 +171,16 @@ public GoalListResponseDto getGoals(Long memberId, GoalStatus status, GoalSort s // ACTIVE + PROGRESS_DESC => 달성률 높은 진행 중인 목표만 List goals = findGoals(memberId, listStatus); - var dtos = toSummaryDtos(goals, memberId).stream() - .sorted(Comparator.comparingInt(GoalListResponseDto.GoalSummaryDto::achievementRate).reversed()); - - if (size != null) { - dtos = dtos.limit(size); + + // DTO 변환 후 정렬 + List dtos = toSummaryDtos(goals, memberId).stream() + .sorted(Comparator.comparingInt(GoalListResponseDto.GoalSummaryDto::achievementRate).reversed()) + .collect(Collectors.toList()); + + if (size != null && dtos.size() > size) { + dtos = dtos.subList(0, size); } - return new GoalListResponseDto(dtos.toList()); + return new GoalListResponseDto(dtos); } } diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalQueryService.java b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalQueryService.java index 9670bc2c..dcccd340 100644 --- a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalQueryService.java +++ b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalQueryService.java @@ -4,7 +4,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.umc.valuedi.domain.asset.entity.BankAccount; -import org.umc.valuedi.domain.asset.service.AssetBalanceService; import org.umc.valuedi.domain.goal.converter.GoalConverter; import org.umc.valuedi.domain.goal.dto.response.GoalActiveCountResponseDto; import org.umc.valuedi.domain.goal.dto.response.GoalDetailResponseDto; @@ -30,7 +29,6 @@ public class GoalQueryService { private final GoalRepository goalRepository; private final MemberRepository memberRepository; private final GoalAchievementRateService achievementRateService; - private final AssetBalanceService assetBalanceService; private final GoalStatusChangeService goalStatusChangeService; // 목표 상세 조회 @@ -41,27 +39,19 @@ public GoalDetailResponseDto getGoalDetail(Long memberId, Long goalId) { BankAccount account = goal.getBankAccount(); - if (!account.getIsActive()) { + if (account == null || !account.getIsActive()) { throw new GoalException(GoalErrorCode.GOAL_ACCOUNT_INACTIVE); } - // 동기화 후 최신 잔액 가져오기 - Long currentBalance = assetBalanceService.syncAndGetLatestBalance(memberId, account.getId()); + // DB에 저장된 현재 계좌 잔액 사용 + Long currentBalance = account.getBalanceAmount(); - // 현재 잔액 - 시작 잔액 - long savedAmount = currentBalance - goal.getStartAmount(); + // 목표 달성 여부 체크 및 상태 업데이트 + goalStatusChangeService.checkAndUpdateStatus(goal, currentBalance); - // 음수일 경우 0으로 처리 - if (savedAmount < 0) { - savedAmount = 0; - } - - // 목표 달성 여부 체크 및 상태 업데이트 (공통 로직 사용) - goalStatusChangeService.checkAndUpdateStatus(goal, savedAmount); - - int rate = achievementRateService.calculateRate(savedAmount, goal.getTargetAmount()); + int rate = achievementRateService.calculateRate(currentBalance, goal.getTargetAmount()); - return GoalConverter.toDetailDto(goal, savedAmount, rate); + return GoalConverter.toDetailDto(goal, currentBalance, rate); } // 목표 개수 조회 From 4231a34cad2080339e3e357e5c5bab98cdf34b14 Mon Sep 17 00:00:00 2001 From: kkeunii Date: Thu, 19 Feb 2026 00:56:36 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20startdate=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asset/service/AssetBalanceService.java | 33 ------------------- .../service/query/GoalLedgerQueryService.java | 2 +- 2 files changed, 1 insertion(+), 34 deletions(-) delete mode 100644 src/main/java/org/umc/valuedi/domain/asset/service/AssetBalanceService.java 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 deleted file mode 100644 index 717759a5..00000000 --- a/src/main/java/org/umc/valuedi/domain/asset/service/AssetBalanceService.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.umc.valuedi.domain.asset.service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; -import org.umc.valuedi.domain.asset.entity.BankAccount; -import org.umc.valuedi.domain.asset.entity.BankTransaction; -import org.umc.valuedi.domain.asset.repository.bank.bankAccount.BankAccountRepository; -import org.umc.valuedi.domain.asset.repository.bank.bankTransaction.BankTransactionRepository; -import org.umc.valuedi.domain.goal.exception.GoalException; -import org.umc.valuedi.domain.goal.exception.code.GoalErrorCode; - -@Slf4j -@Service -@RequiredArgsConstructor -public class AssetBalanceService { - - private final BankAccountRepository bankAccountRepository; - private final BankTransactionRepository bankTransactionRepository; - - @Transactional(propagation = Propagation.NOT_SUPPORTED) - public Long syncAndGetLatestBalance(Long memberId, Long accountId) { - - BankAccount account = bankAccountRepository.findByIdAndMemberId(accountId, memberId) - .orElseThrow(() -> new GoalException(GoalErrorCode.ACCOUNT_NOT_FOUND)); - - return bankTransactionRepository.findTopByBankAccountOrderByTrDatetimeDesc(account) - .map(BankTransaction::getAfterBalance) - .orElse(account.getBalanceAmount()); - } -} diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java index 04e53bc9..0288d312 100644 --- a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java +++ b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java @@ -34,7 +34,7 @@ public LedgerListResponse getGoalLedgerTransactions(Long memberId, Long goalId, } // 시작 시간: 목표의 시작일(startDate) - LocalDateTime from = goal.getStartDate().atStartOfDay(); + LocalDateTime from = goal.getStartDate().atTime(LocalTime.MAX); // 종료 시간: 목표의 종료일(endDate) LocalDateTime to = goal.getEndDate().atTime(LocalTime.MAX); From dc32ebb0b577d7d35dd244c76646fbb3e23d4a3f Mon Sep 17 00:00:00 2001 From: kkeunii Date: Thu, 19 Feb 2026 00:59:33 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20Datetime=20=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/goal/service/query/GoalLedgerQueryService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java index 0288d312..04e53bc9 100644 --- a/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java +++ b/src/main/java/org/umc/valuedi/domain/goal/service/query/GoalLedgerQueryService.java @@ -34,7 +34,7 @@ public LedgerListResponse getGoalLedgerTransactions(Long memberId, Long goalId, } // 시작 시간: 목표의 시작일(startDate) - LocalDateTime from = goal.getStartDate().atTime(LocalTime.MAX); + LocalDateTime from = goal.getStartDate().atStartOfDay(); // 종료 시간: 목표의 종료일(endDate) LocalDateTime to = goal.getEndDate().atTime(LocalTime.MAX); From cc53b54f362d724a6f77d1b09030951aeedadcc1 Mon Sep 17 00:00:00 2001 From: kkeunii Date: Thu, 19 Feb 2026 14:08:31 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20pr=20=EC=BD=94=EB=A9=98?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../valuedi/domain/goal/service/GoalStatusChangeService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/umc/valuedi/domain/goal/service/GoalStatusChangeService.java b/src/main/java/org/umc/valuedi/domain/goal/service/GoalStatusChangeService.java index e7346d6b..cca24680 100644 --- a/src/main/java/org/umc/valuedi/domain/goal/service/GoalStatusChangeService.java +++ b/src/main/java/org/umc/valuedi/domain/goal/service/GoalStatusChangeService.java @@ -1,6 +1,6 @@ package org.umc.valuedi.domain.goal.service; -import jakarta.transaction.Transactional; +import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service;