feat(wallet): 무료 회원 계좌 종류별 2개 한도 (유료 무제한)
Deploy / deploy (push) Failing after 11m4s

- createWallet 에 premium 여부 전달, 무료는 종류별 countByType >= 한도면 400
- 한도 계산을 freeWalletLimit 한 곳에 모음(추후 포인트 상점의 영구 보너스 슬롯 최대 5까지 확장 지점)
- 복원(BackupService)은 본인 데이터 복구라 한도 우회

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb
2026-07-06 01:40:25 +09:00
parent cc36f9e939
commit 29b14a5464
5 changed files with 32 additions and 3 deletions
@@ -56,7 +56,9 @@ public class AccountController {
public ResponseEntity<WalletResponse> createWallet( public ResponseEntity<WalletResponse> createWallet(
@Valid @RequestBody WalletRequest req, @Valid @RequestBody WalletRequest req,
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
return ResponseEntity.status(HttpStatus.CREATED).body(accountService.createWallet(req, current.getId())); boolean premium = "ADMIN".equals(current.getRole()) || "PREMIUM".equals(current.getPlan());
return ResponseEntity.status(HttpStatus.CREATED)
.body(accountService.createWallet(req, current.getId(), premium));
} }
@PutMapping("/wallets/{id}") @PutMapping("/wallets/{id}")
@@ -31,4 +31,7 @@ public interface WalletMapper {
/** 해당 타입의 현재 최대 sort_order (신규 계좌를 맨 뒤에 배치) */ /** 해당 타입의 현재 최대 sort_order (신규 계좌를 맨 뒤에 배치) */
Integer maxSortOrder(@Param("memberId") Long memberId, @Param("type") String type); Integer maxSortOrder(@Param("memberId") Long memberId, @Param("type") String type);
/** 해당 타입의 계좌 개수 (무료 회원 종류별 등록 한도 판정용) */
int countByType(@Param("memberId") Long memberId, @Param("type") String type);
} }
@@ -556,8 +556,27 @@ public class AccountService {
.build(); .build();
} }
// 무료 회원 계좌 등록 한도: 종류(은행·현금·카드·대출·증권·마이너스)별 기본 2개.
// 포인트 상점(추후)에서 영구 보너스 슬롯을 구매하면 종류별 최대 5개까지 확장 가능.
public static final int FREE_WALLET_BASE_LIMIT = 2;
public static final int FREE_WALLET_MAX_LIMIT = 5;
/** 무료 회원의 해당 타입 등록 한도 = 기본 2 + 구매한 영구 보너스 슬롯(추후 상점), 최대 5. */
private int freeWalletLimit(Long memberId, String type) {
int bonus = 0; // TODO: 포인트 상점 구현 시 회원×종류별 구매 슬롯 수를 조회해 더함
return Math.min(FREE_WALLET_BASE_LIMIT + bonus, FREE_WALLET_MAX_LIMIT);
}
@Transactional @Transactional
public WalletResponse createWallet(WalletRequest req, Long memberId) { public WalletResponse createWallet(WalletRequest req, Long memberId, boolean premium) {
// 유료(또는 관리자)는 무제한. 무료는 종류별 한도 검증.
if (!premium) {
int limit = freeWalletLimit(memberId, req.getType());
if (walletMapper.countByType(memberId, req.getType()) >= limit) {
throw new ApiException(HttpStatus.BAD_REQUEST,
"무료 회원은 종류별 계좌를 " + limit + "개까지 등록할 수 있어요. 유료로 업그레이드하면 무제한이에요.");
}
}
Wallet wallet = toWallet(req); Wallet wallet = toWallet(req);
wallet.setMemberId(memberId); wallet.setMemberId(memberId);
Integer max = walletMapper.maxSortOrder(memberId, req.getType()); Integer max = walletMapper.maxSortOrder(memberId, req.getType());
@@ -71,7 +71,8 @@ public class BackupService {
wr.setOpeningBalance(w.getOpeningBalance()); wr.setOpeningBalance(w.getOpeningBalance());
wr.setOpeningDate(w.getOpeningDate()); wr.setOpeningDate(w.getOpeningDate());
wr.setCurrentValue(w.getCurrentValue()); wr.setCurrentValue(w.getCurrentValue());
Long newId = accountService.createWallet(wr, memberId).getId(); // 복원은 사용자 본인 데이터 복구이므로 무료 한도(종류별 2개)를 적용하지 않는다.
Long newId = accountService.createWallet(wr, memberId, true).getId();
if (w.getOldId() != null) walletMap.put(w.getOldId(), newId); if (w.getOldId() != null) walletMap.put(w.getOldId(), newId);
} }
} }
@@ -97,4 +97,8 @@
SELECT MAX(sort_order) FROM wallet WHERE member_id = #{memberId} AND type = #{type} SELECT MAX(sort_order) FROM wallet WHERE member_id = #{memberId} AND type = #{type}
</select> </select>
<select id="countByType" resultType="int">
SELECT COUNT(*) FROM wallet WHERE member_id = #{memberId} AND type = #{type}
</select>
</mapper> </mapper>