diff --git a/src/main/java/com/sb/web/account/controller/AccountController.java b/src/main/java/com/sb/web/account/controller/AccountController.java index 2f3c788..ac81001 100644 --- a/src/main/java/com/sb/web/account/controller/AccountController.java +++ b/src/main/java/com/sb/web/account/controller/AccountController.java @@ -167,11 +167,16 @@ public class AccountController { return accountService.categoryStats(current.getId(), type, year, month); } - /** 월별 순자산 추이 */ + /** 순자산 추이 (unit=WEEK 면 주별, weeks 개수 / 그 외 월별, months 개수) */ @GetMapping("/networth/trend") public List netWorthTrend( @RequestParam(required = false) Integer months, + @RequestParam(required = false) Integer weeks, + @RequestParam(defaultValue = "MONTH") String unit, @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { + if ("WEEK".equalsIgnoreCase(unit)) { + return accountService.netWorthTrendWeekly(current.getId(), weeks); + } return accountService.netWorthTrend(current.getId(), months); } diff --git a/src/main/java/com/sb/web/account/mapper/AccountEntryMapper.java b/src/main/java/com/sb/web/account/mapper/AccountEntryMapper.java index 426a4ea..107e633 100644 --- a/src/main/java/com/sb/web/account/mapper/AccountEntryMapper.java +++ b/src/main/java/com/sb/web/account/mapper/AccountEntryMapper.java @@ -47,6 +47,9 @@ public interface AccountEntryMapper { /** 월별 순현금흐름(수입−지출, 계좌 지정분) — {ym, net} 행 반환 (순자산 추이용) */ List> monthlyWalletFlow(@Param("memberId") Long memberId); + /** 주별 순현금흐름(수입−지출, 계좌 지정분) — {wk(=주 월요일 yyyy-MM-dd), net} (주간 순자산 추이용) */ + List> weeklyWalletFlow(@Param("memberId") Long memberId); + /** 기간 버킷(일/주/월/년)별 수입·지출 — {bucket, income, expense} (이체 제외) */ List> statsByPeriod(@Param("memberId") Long memberId, @Param("unit") String unit, diff --git a/src/main/java/com/sb/web/account/service/AccountService.java b/src/main/java/com/sb/web/account/service/AccountService.java index 780a2d5..68a761f 100644 --- a/src/main/java/com/sb/web/account/service/AccountService.java +++ b/src/main/java/com/sb/web/account/service/AccountService.java @@ -26,6 +26,7 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; import java.time.YearMonth; import java.util.ArrayList; import java.util.HashMap; @@ -128,6 +129,66 @@ public class AccountService { return result; } + /** 주별 순자산 추이 (월요일 기준 버킷). 기본 13주(최근 약 3개월). */ + public List netWorthTrendWeekly(Long memberId, Integer weeks) { + int n = (weeks == null || weeks <= 0 || weeks > 53) ? 13 : weeks; + LocalDate today = LocalDate.now(); + LocalDate currentMon = today.minusDays(today.getDayOfWeek().getValue() - 1L); // 이번 주 월요일 + LocalDate startMon = currentMon.minusWeeks(n - 1L); + + Map flow = new HashMap<>(); + for (Map row : mapper.weeklyWalletFlow(memberId)) { + Object net = row.get("net"); + flow.put((String) row.get("wk"), net == null ? 0L : ((Number) net).longValue()); + } + + // 개시잔액: 개시주가 시작주 이전(또는 미상)이면 기준선, 범위 내면 해당 주 버킷 + List ws = walletMapper.findByMember(memberId); + Map openingByWeek = new HashMap<>(); + long baseline = 0; + for (Wallet w : ws) { + long ob = w.getOpeningBalance() == null ? 0L : w.getOpeningBalance(); + if (ob == 0) { + continue; + } + LocalDate od = w.getOpeningDate(); + LocalDate om = od == null ? null : od.minusDays(od.getDayOfWeek().getValue() - 1L); + if (om == null || om.isBefore(startMon)) { + baseline += ob; + } else if (!om.isAfter(currentMon)) { + openingByWeek.merge(om.toString(), ob, Long::sum); + } + } + String startKey = startMon.toString(); + for (Map.Entry e : flow.entrySet()) { + if (e.getKey().compareTo(startKey) < 0) { + baseline += e.getValue(); + } + } + + List result = new ArrayList<>(); + long running = baseline; + for (LocalDate m = startMon; !m.isAfter(currentMon); m = m.plusWeeks(1)) { + String key = m.toString(); + running += flow.getOrDefault(key, 0L); + running += openingByWeek.getOrDefault(key, 0L); + result.add(NetWorthPoint.builder().month(key).netWorth(running).build()); + } + + // 투자 손익(실현+평가)은 과거 주별 이력이 없으므로 마지막 점에만 가산 (월간과 동일) + if (!result.isEmpty()) { + long investAdjust = 0; + for (InvestService.WalletInvest wi : investService.valuationByWallet(memberId).values()) { + investAdjust += wi.cashDelta + wi.stockEval; + } + if (investAdjust != 0) { + NetWorthPoint last = result.get(result.size() - 1); + last.setNetWorth(last.getNetWorth() + investAdjust); + } + } + return result; + } + /** 기간 버킷(일/주/월/년)별 수입·지출 통계 */ public List stats(Long memberId, String unit, Integer year, Integer month) { String u = switch (unit == null ? "" : unit.toUpperCase()) { diff --git a/src/main/resources/mapper/AccountEntryMapper.xml b/src/main/resources/mapper/AccountEntryMapper.xml index 459488a..8f5fca5 100644 --- a/src/main/resources/mapper/AccountEntryMapper.xml +++ b/src/main/resources/mapper/AccountEntryMapper.xml @@ -108,6 +108,17 @@ ORDER BY ym + +