Merge branch 'dev'
Deploy / deploy (push) Failing after 11m25s
CI / build (push) Failing after 13m19s

This commit is contained in:
ByungCheol
2026-06-22 22:38:06 +09:00
4 changed files with 81 additions and 1 deletions
@@ -167,11 +167,16 @@ public class AccountController {
return accountService.categoryStats(current.getId(), type, year, month); return accountService.categoryStats(current.getId(), type, year, month);
} }
/** 월별 순자산 추이 */ /** 순자산 추이 (unit=WEEK 면 주별, weeks 개수 / 그 외 월별, months 개수) */
@GetMapping("/networth/trend") @GetMapping("/networth/trend")
public List<NetWorthPoint> netWorthTrend( public List<NetWorthPoint> netWorthTrend(
@RequestParam(required = false) Integer months, @RequestParam(required = false) Integer months,
@RequestParam(required = false) Integer weeks,
@RequestParam(defaultValue = "MONTH") String unit,
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
if ("WEEK".equalsIgnoreCase(unit)) {
return accountService.netWorthTrendWeekly(current.getId(), weeks);
}
return accountService.netWorthTrend(current.getId(), months); return accountService.netWorthTrend(current.getId(), months);
} }
@@ -47,6 +47,9 @@ public interface AccountEntryMapper {
/** 월별 순현금흐름(수입−지출, 계좌 지정분) — {ym, net} 행 반환 (순자산 추이용) */ /** 월별 순현금흐름(수입−지출, 계좌 지정분) — {ym, net} 행 반환 (순자산 추이용) */
List<Map<String, Object>> monthlyWalletFlow(@Param("memberId") Long memberId); List<Map<String, Object>> monthlyWalletFlow(@Param("memberId") Long memberId);
/** 주별 순현금흐름(수입−지출, 계좌 지정분) — {wk(=주 월요일 yyyy-MM-dd), net} (주간 순자산 추이용) */
List<Map<String, Object>> weeklyWalletFlow(@Param("memberId") Long memberId);
/** 기간 버킷(일/주/월/년)별 수입·지출 — {bucket, income, expense} (이체 제외) */ /** 기간 버킷(일/주/월/년)별 수입·지출 — {bucket, income, expense} (이체 제외) */
List<Map<String, Object>> statsByPeriod(@Param("memberId") Long memberId, List<Map<String, Object>> statsByPeriod(@Param("memberId") Long memberId,
@Param("unit") String unit, @Param("unit") String unit,
@@ -26,6 +26,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.YearMonth; import java.time.YearMonth;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@@ -128,6 +129,66 @@ public class AccountService {
return result; return result;
} }
/** 주별 순자산 추이 (월요일 기준 버킷). 기본 13주(최근 약 3개월). */
public List<NetWorthPoint> 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<String, Long> flow = new HashMap<>();
for (Map<String, Object> row : mapper.weeklyWalletFlow(memberId)) {
Object net = row.get("net");
flow.put((String) row.get("wk"), net == null ? 0L : ((Number) net).longValue());
}
// 개시잔액: 개시주가 시작주 이전(또는 미상)이면 기준선, 범위 내면 해당 주 버킷
List<Wallet> ws = walletMapper.findByMember(memberId);
Map<String, Long> 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<String, Long> e : flow.entrySet()) {
if (e.getKey().compareTo(startKey) < 0) {
baseline += e.getValue();
}
}
List<NetWorthPoint> 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<PeriodStat> stats(Long memberId, String unit, Integer year, Integer month) { public List<PeriodStat> stats(Long memberId, String unit, Integer year, Integer month) {
String u = switch (unit == null ? "" : unit.toUpperCase()) { String u = switch (unit == null ? "" : unit.toUpperCase()) {
@@ -108,6 +108,17 @@
ORDER BY ym ORDER BY ym
</select> </select>
<select id="weeklyWalletFlow" resultType="map">
SELECT DATE_FORMAT(DATE_SUB(entry_date, INTERVAL WEEKDAY(entry_date) DAY), '%Y-%m-%d') AS wk,
COALESCE(SUM(CASE WHEN type = 'INCOME' THEN amount
WHEN type = 'EXPENSE' THEN -amount
ELSE 0 END), 0) AS net
FROM account_entry
WHERE member_id = #{memberId} AND wallet_id IS NOT NULL AND type IN ('INCOME', 'EXPENSE')
GROUP BY wk
ORDER BY wk
</select>
<select id="statsByPeriod" resultType="map"> <select id="statsByPeriod" resultType="map">
SELECT SELECT
<choose> <choose>