feat: 예상 수입 월별 저장 + 계좌 순서변경 API
CI / build (push) Failing after 12m2s

- 예상 수입: budget_income(연·월별) 로 변경, GET/PUT 에 year·month
- 계좌: wallet.sort_order + reorder 엔드포인트, 생성 시 맨 뒤 배치

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-06-01 22:54:23 +09:00
parent cac9919cc5
commit f5e9b78a14
11 changed files with 93 additions and 24 deletions
@@ -10,6 +10,7 @@ import com.sb.web.account.dto.NetWorthPoint;
import com.sb.web.account.dto.NetWorthResponse;
import com.sb.web.account.dto.PeriodStat;
import com.sb.web.account.dto.RepaymentRequest;
import com.sb.web.account.dto.WalletReorderRequest;
import com.sb.web.account.dto.WalletRequest;
import com.sb.web.account.dto.WalletResponse;
import com.sb.web.account.service.AccountService;
@@ -69,6 +70,14 @@ public class AccountController {
return ResponseEntity.noContent().build();
}
/** 계좌/카드 순서 변경 (드래그앤드랍) */
@PutMapping("/wallets/reorder")
public List<WalletResponse> reorderWallets(
@RequestBody WalletReorderRequest req,
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
return accountService.reorderWallets(current.getId(), req.getType(), req.getIds());
}
@GetMapping("/networth")
public NetWorthResponse netWorth(
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
@@ -28,19 +28,23 @@ public class BudgetController {
private final BudgetService budgetService;
private final AccountSettingService settingService;
/** 월 예상 수입 조회 */
/** 월 예상 수입 조회 (연·월별) */
@GetMapping("/income")
public ExpectedIncomeRequest getIncome(
@RequestParam int year,
@RequestParam int month,
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
return new ExpectedIncomeRequest(settingService.getExpectedIncome(current.getId()));
return new ExpectedIncomeRequest(settingService.getExpectedIncome(current.getId(), year, month));
}
/** 월 예상 수입 설정 */
/** 월 예상 수입 설정 (연·월별) */
@PutMapping("/income")
public ExpectedIncomeRequest setIncome(
@RequestParam int year,
@RequestParam int month,
@Valid @RequestBody ExpectedIncomeRequest req,
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
settingService.setExpectedIncome(current.getId(), req.getExpectedIncome());
settingService.setExpectedIncome(current.getId(), year, month, req.getExpectedIncome());
return req;
}
@@ -29,6 +29,7 @@ public class Wallet implements Serializable {
private Long openingBalance; // 초기 잔액(부호있음: 자산+, 부채-)
private LocalDate openingDate;
private Long currentValue; // 현재 평가금액 (INVEST 전용, 수동 갱신)
private Integer sortOrder; // 표시 순서 (타입 내 정렬)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,15 @@
package com.sb.web.account.dto;
import lombok.Data;
import java.util.List;
/**
* 계좌/카드 순서 변경 요청. ids 순서대로 sort_order 를 재설정한다.
*/
@Data
public class WalletReorderRequest {
private String type; // BANK / CARD / LOAN / INVEST
private List<Long> ids; // 새 순서의 wallet id 목록
}
@@ -4,14 +4,19 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 사용자별 가계부 설정 매퍼 (account_setting).
* 월 예상 수입 매퍼 (budget_income, 월별).
*/
@Mapper
public interface AccountSettingMapper {
/** 예상 수입 (없으면 null) */
Long findExpectedIncome(@Param("memberId") Long memberId);
/** 해당 연·월의 예상 수입 (없으면 null) */
Long findExpectedIncome(@Param("memberId") Long memberId,
@Param("year") int year,
@Param("month") int month);
/** 월 예상 수입 upsert */
int upsertExpectedIncome(@Param("memberId") Long memberId, @Param("expectedIncome") Long expectedIncome);
/** 해당 연·월 예상 수입 upsert */
int upsertExpectedIncome(@Param("memberId") Long memberId,
@Param("year") int year,
@Param("month") int month,
@Param("expectedIncome") Long expectedIncome);
}
@@ -25,4 +25,10 @@ public interface WalletMapper {
int update(Wallet wallet);
int delete(@Param("id") Long id, @Param("memberId") Long memberId);
/** 순서(sort_order) 변경 */
int updateSortOrder(@Param("id") Long id, @Param("memberId") Long memberId, @Param("sortOrder") int sortOrder);
/** 해당 타입의 현재 최대 sort_order (신규 계좌를 맨 뒤에 배치) */
Integer maxSortOrder(@Param("memberId") Long memberId, @Param("type") String type);
}
@@ -301,10 +301,25 @@ public class AccountService {
public WalletResponse createWallet(WalletRequest req, Long memberId) {
Wallet wallet = toWallet(req);
wallet.setMemberId(memberId);
Integer max = walletMapper.maxSortOrder(memberId, req.getType());
wallet.setSortOrder(max == null ? 0 : max + 1); // 같은 타입의 맨 뒤
walletMapper.insert(wallet);
return WalletResponse.from(wallet, wallet.getOpeningBalance()); // 신규 → 잔액=초기잔액
}
/** 드래그앤드랍 순서 변경: 전달된 id 순서대로 sort_order 재설정 (본인·동일 타입만) */
@Transactional
public List<WalletResponse> reorderWallets(Long memberId, String type, List<Long> ids) {
int i = 0;
for (Long id : ids) {
Wallet w = walletMapper.findByIdAndMember(id, memberId);
if (w != null && w.getType().equals(type)) {
walletMapper.updateSortOrder(id, memberId, i++);
}
}
return listWallets(memberId);
}
@Transactional
public WalletResponse updateWallet(Long id, WalletRequest req, Long memberId) {
Wallet wallet = mustFindWallet(id, memberId);
@@ -6,7 +6,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 사용자별 가계부 설정 서비스 (월 예상 수입 등).
* 월 예상 수입 설정 서비스 (월).
*/
@Service
@RequiredArgsConstructor
@@ -14,12 +14,12 @@ public class AccountSettingService {
private final AccountSettingMapper settingMapper;
public Long getExpectedIncome(Long memberId) {
return settingMapper.findExpectedIncome(memberId);
public Long getExpectedIncome(Long memberId, int year, int month) {
return settingMapper.findExpectedIncome(memberId, year, month);
}
@Transactional
public void setExpectedIncome(Long memberId, Long expectedIncome) {
settingMapper.upsertExpectedIncome(memberId, expectedIncome);
public void setExpectedIncome(Long memberId, int year, int month, Long expectedIncome) {
settingMapper.upsertExpectedIncome(memberId, year, month, expectedIncome);
}
}