feat(recurring): 고정지출 무료 2건 허용(써보고 업셀) — 유료전용 해제
Deploy / deploy (push) Failing after 13m57s

WebConfig에서 /recurrings 유료 게이트 제거, 무료는 생성 시 종류무관 2건 한도(초과 400).
유료·관리자 무제한, 복원은 한도 미적용. RecurringMapper.countByMember 추가.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb
2026-07-07 21:22:31 +09:00
parent 489c3aacb7
commit 0780546d4f
6 changed files with 20 additions and 4 deletions
@@ -34,7 +34,9 @@ public class RecurringController {
public ResponseEntity<RecurringResponse> create( public ResponseEntity<RecurringResponse> create(
@Valid @RequestBody RecurringRequest req, @Valid @RequestBody RecurringRequest req,
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
return ResponseEntity.status(HttpStatus.CREATED).body(recurringService.create(req, current.getId())); boolean premium = "ADMIN".equals(current.getRole()) || "PREMIUM".equals(current.getPlan());
return ResponseEntity.status(HttpStatus.CREATED)
.body(recurringService.create(req, current.getId(), premium));
} }
@PutMapping("/{id}") @PutMapping("/{id}")
@@ -15,6 +15,9 @@ public interface RecurringMapper {
List<Recurring> findByMember(@Param("memberId") Long memberId); List<Recurring> findByMember(@Param("memberId") Long memberId);
/** 회원의 정기 거래 개수 (무료 회원 등록 한도 판정용) */
int countByMember(@Param("memberId") Long memberId);
/** 활성 규칙 (자동 생성용) */ /** 활성 규칙 (자동 생성용) */
List<Recurring> findActiveByMember(@Param("memberId") Long memberId); List<Recurring> findActiveByMember(@Param("memberId") Long memberId);
@@ -153,7 +153,8 @@ public class BackupService {
rr.setStartDate(r.getStartDate()); rr.setStartDate(r.getStartDate());
rr.setEndDate(r.getEndDate()); rr.setEndDate(r.getEndDate());
rr.setActive(r.getActive() == null || r.getActive()); rr.setActive(r.getActive() == null || r.getActive());
recurringService.create(rr, memberId); // 복원은 본인 데이터 복구이므로 무료 한도 미적용
recurringService.create(rr, memberId, true);
} }
} }
@@ -30,8 +30,15 @@ public class RecurringService {
return recurringMapper.findByMember(memberId).stream().map(this::toResponse).toList(); return recurringMapper.findByMember(memberId).stream().map(this::toResponse).toList();
} }
// 무료 회원 고정지출 등록 한도(써보고 → 결제 유도). 유료·관리자는 무제한.
public static final int FREE_RECURRING_LIMIT = 2;
@Transactional @Transactional
public RecurringResponse create(RecurringRequest req, Long memberId) { public RecurringResponse create(RecurringRequest req, Long memberId, boolean premium) {
if (!premium && recurringMapper.countByMember(memberId) >= FREE_RECURRING_LIMIT) {
throw new ApiException(HttpStatus.BAD_REQUEST,
"무료 회원은 고정 지출을 " + FREE_RECURRING_LIMIT + "개까지 등록할 수 있어요. 유료로 업그레이드하면 무제한이에요.");
}
Recurring r = toEntity(req); Recurring r = toEntity(req);
r.setMemberId(memberId); r.setMemberId(memberId);
recurringMapper.insert(r); recurringMapper.insert(r);
@@ -42,7 +42,6 @@ public class WebConfig implements WebMvcConfigurer {
"/api/account/ai-comment", "/api/account/ai-comment",
"/api/account/networth/trend", "/api/account/networth/trend",
"/api/account/budgets", "/api/account/budgets/**", "/api/account/budgets", "/api/account/budgets/**",
"/api/account/recurrings", "/api/account/recurrings/**",
"/api/account/ocr", "/api/account/ocr/**", "/api/account/ocr", "/api/account/ocr/**",
"/api/account/invest", "/api/account/invest/**", "/api/account/invest", "/api/account/invest/**",
"/api/account/tags", "/api/account/tags/**", "/api/account/tags", "/api/account/tags/**",
@@ -78,4 +78,8 @@
UPDATE recurring SET last_run_date = #{lastRunDate} WHERE id = #{id} UPDATE recurring SET last_run_date = #{lastRunDate} WHERE id = #{id}
</update> </update>
<select id="countByMember" parameterType="long" resultType="int">
SELECT COUNT(*) FROM recurring WHERE member_id = #{memberId}
</select>
</mapper> </mapper>