feat(budget): 1회성 예산 API 추가
- BudgetOnetime 도메인/DTO/Mapper/Service 신규 - BudgetController: GET/POST/PUT/DELETE /api/account/budgets/onetime 엔드포인트 추가 - account.sql: budget_onetime 테이블 CREATE TABLE IF NOT EXISTS 추가 - WalletRequest: MINUS 유형 허용 추가 - CategoryService: importDefaults, validateParent, ensureCategory 추가 ※ 최초 배포 시 서버 .env에 SQL_INIT_MODE=always 추가 후 재시작 필요 (budget_onetime 테이블 생성) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
24858f23e8
commit
a0509099f7
@@ -0,0 +1,82 @@
|
||||
package com.sb.web.account.service;
|
||||
|
||||
import com.sb.web.account.domain.BudgetOnetime;
|
||||
import com.sb.web.account.dto.BudgetOnetimeRequest;
|
||||
import com.sb.web.account.dto.BudgetOnetimeResponse;
|
||||
import com.sb.web.account.mapper.BudgetOnetimeMapper;
|
||||
import com.sb.web.common.exception.ApiException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BudgetOnetimeService {
|
||||
|
||||
private final BudgetOnetimeMapper mapper;
|
||||
|
||||
public List<BudgetOnetimeResponse> list(Long memberId) {
|
||||
return mapper.findByMember(memberId).stream()
|
||||
.map(b -> {
|
||||
long spent = mapper.sumExpenseInRange(
|
||||
memberId, b.getStartDate(), b.getEndDate(), b.getCategory());
|
||||
return BudgetOnetimeResponse.from(b, spent);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BudgetOnetimeResponse create(BudgetOnetimeRequest req, Long memberId) {
|
||||
validate(req);
|
||||
BudgetOnetime b = BudgetOnetime.builder()
|
||||
.memberId(memberId)
|
||||
.title(req.getTitle().trim())
|
||||
.category(blankToNull(req.getCategory()))
|
||||
.amount(req.getAmount())
|
||||
.startDate(req.getStartDate())
|
||||
.endDate(req.getEndDate())
|
||||
.build();
|
||||
mapper.insert(b);
|
||||
long spent = mapper.sumExpenseInRange(memberId, b.getStartDate(), b.getEndDate(), b.getCategory());
|
||||
return BudgetOnetimeResponse.from(b, spent);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BudgetOnetimeResponse update(Long id, BudgetOnetimeRequest req, Long memberId) {
|
||||
validate(req);
|
||||
BudgetOnetime b = mustFind(id, memberId);
|
||||
b.setTitle(req.getTitle().trim());
|
||||
b.setCategory(blankToNull(req.getCategory()));
|
||||
b.setAmount(req.getAmount());
|
||||
b.setStartDate(req.getStartDate());
|
||||
b.setEndDate(req.getEndDate());
|
||||
mapper.update(b);
|
||||
long spent = mapper.sumExpenseInRange(memberId, b.getStartDate(), b.getEndDate(), b.getCategory());
|
||||
return BudgetOnetimeResponse.from(b, spent);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long id, Long memberId) {
|
||||
mustFind(id, memberId);
|
||||
mapper.delete(id, memberId);
|
||||
}
|
||||
|
||||
private void validate(BudgetOnetimeRequest req) {
|
||||
if (req.getEndDate().isBefore(req.getStartDate())) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "종료일이 시작일보다 빠를 수 없습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
private BudgetOnetime mustFind(Long id, Long memberId) {
|
||||
BudgetOnetime b = mapper.findByIdAndMember(id, memberId);
|
||||
if (b == null) throw new ApiException(HttpStatus.NOT_FOUND, "1회성 예산을 찾을 수 없습니다.");
|
||||
return b;
|
||||
}
|
||||
|
||||
private String blankToNull(String s) {
|
||||
return (s == null || s.isBlank()) ? null : s.trim();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user