- budget 테이블에 year/month 추가, (member,category)→(member,category,year,month) 유니크 교체 기존 상시 예산은 현재 월로 1회 이관 - 예산 조회/생성/현황/기간차트를 월별로(findByMember에 year/month) - copyMonth: 다른 월 예산을 대상 월로 복사(같은 분류 덮어씀) + POST /budgets/copy - 백업 복구 시 예산은 현재 월로 등록 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -50,8 +50,22 @@ public class BudgetController {
|
||||
|
||||
@GetMapping
|
||||
public List<BudgetResponse> list(
|
||||
@RequestParam int year,
|
||||
@RequestParam int month,
|
||||
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||
return budgetService.list(current.getId());
|
||||
return budgetService.list(current.getId(), year, month);
|
||||
}
|
||||
|
||||
/** 다른 월의 예산을 이 달로 복사(전월 복사 등). 같은 분류는 덮어씀 */
|
||||
@PostMapping("/copy")
|
||||
public List<BudgetResponse> copy(
|
||||
@RequestParam int fromYear,
|
||||
@RequestParam int fromMonth,
|
||||
@RequestParam int toYear,
|
||||
@RequestParam int toMonth,
|
||||
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||
budgetService.copy(current.getId(), fromYear, fromMonth, toYear, toMonth);
|
||||
return budgetService.list(current.getId(), toYear, toMonth);
|
||||
}
|
||||
|
||||
@GetMapping("/status")
|
||||
@@ -74,9 +88,11 @@ public class BudgetController {
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<BudgetResponse> create(
|
||||
@RequestParam int year,
|
||||
@RequestParam int month,
|
||||
@Valid @RequestBody BudgetRequest req,
|
||||
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(budgetService.create(req, current.getId()));
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(budgetService.create(req, current.getId(), year, month));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
|
||||
@@ -28,6 +28,8 @@ public class Budget implements Serializable {
|
||||
private Long weekly;
|
||||
private Long monthly;
|
||||
private Long yearly;
|
||||
private Integer year; // 예산 연도(월별 예산)
|
||||
private Integer month; // 예산 월(1~12)
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
@@ -12,13 +12,24 @@ import java.util.List;
|
||||
@Mapper
|
||||
public interface BudgetMapper {
|
||||
|
||||
List<Budget> findByMember(@Param("memberId") Long memberId);
|
||||
List<Budget> findByMember(@Param("memberId") Long memberId,
|
||||
@Param("year") Integer year,
|
||||
@Param("month") Integer month);
|
||||
|
||||
Budget findByIdAndMember(@Param("id") Long id, @Param("memberId") Long memberId);
|
||||
|
||||
int countByCategory(@Param("memberId") Long memberId,
|
||||
@Param("category") String category,
|
||||
@Param("excludeId") Long excludeId);
|
||||
@Param("excludeId") Long excludeId,
|
||||
@Param("year") Integer year,
|
||||
@Param("month") Integer month);
|
||||
|
||||
/** fromYear/fromMonth 예산을 toYear/toMonth 로 복사(같은 분류는 덮어씀). 복사된 건수 반환 */
|
||||
int copyMonth(@Param("memberId") Long memberId,
|
||||
@Param("fromYear") Integer fromYear,
|
||||
@Param("fromMonth") Integer fromMonth,
|
||||
@Param("toYear") Integer toYear,
|
||||
@Param("toMonth") Integer toMonth);
|
||||
|
||||
int insert(Budget budget);
|
||||
|
||||
|
||||
@@ -156,11 +156,12 @@ public class BackupService {
|
||||
}
|
||||
}
|
||||
|
||||
// 7) 예산
|
||||
// 7) 예산 — 복구 시 현재 월 예산으로 등록(백업엔 월 정보 없음)
|
||||
if (req.getBudgets() != null) {
|
||||
java.time.LocalDate today = java.time.LocalDate.now();
|
||||
for (BudgetRequest b : req.getBudgets()) {
|
||||
if (b == null || b.getCategory() == null) continue;
|
||||
budgetService.create(b, memberId);
|
||||
budgetService.create(b, memberId, today.getYear(), today.getMonthValue());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,36 +31,46 @@ public class BudgetService {
|
||||
private final BudgetMapper budgetMapper;
|
||||
private final AccountEntryMapper entryMapper;
|
||||
|
||||
public List<BudgetResponse> list(Long memberId) {
|
||||
return budgetMapper.findByMember(memberId).stream().map(BudgetResponse::from).toList();
|
||||
public List<BudgetResponse> list(Long memberId, int year, int month) {
|
||||
return budgetMapper.findByMember(memberId, year, month).stream().map(BudgetResponse::from).toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BudgetResponse create(BudgetRequest req, Long memberId) {
|
||||
public BudgetResponse create(BudgetRequest req, Long memberId, int year, int month) {
|
||||
String category = req.getCategory().trim();
|
||||
if (budgetMapper.countByCategory(memberId, category, null) > 0) {
|
||||
if (budgetMapper.countByCategory(memberId, category, null, year, month) > 0) {
|
||||
throw new ApiException(HttpStatus.CONFLICT, "이미 예산이 설정된 카테고리입니다.");
|
||||
}
|
||||
Budget budget = toBudget(req, category);
|
||||
budget.setMemberId(memberId);
|
||||
budget.setYear(year);
|
||||
budget.setMonth(month);
|
||||
budgetMapper.insert(budget);
|
||||
return BudgetResponse.from(budget);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BudgetResponse update(Long id, BudgetRequest req, Long memberId) {
|
||||
mustFind(id, memberId);
|
||||
Budget existing = mustFind(id, memberId);
|
||||
String category = req.getCategory().trim();
|
||||
if (budgetMapper.countByCategory(memberId, category, id) > 0) {
|
||||
if (budgetMapper.countByCategory(memberId, category, id, existing.getYear(), existing.getMonth()) > 0) {
|
||||
throw new ApiException(HttpStatus.CONFLICT, "이미 예산이 설정된 카테고리입니다.");
|
||||
}
|
||||
Budget budget = toBudget(req, category);
|
||||
budget.setId(id);
|
||||
budget.setMemberId(memberId);
|
||||
budget.setYear(existing.getYear());
|
||||
budget.setMonth(existing.getMonth());
|
||||
budgetMapper.update(budget);
|
||||
return BudgetResponse.from(budget);
|
||||
}
|
||||
|
||||
/** fromYear/fromMonth 예산을 toYear/toMonth 로 복사(같은 분류 덮어씀). 복사된 건수 반환 */
|
||||
@Transactional
|
||||
public int copy(Long memberId, int fromYear, int fromMonth, int toYear, int toMonth) {
|
||||
return budgetMapper.copyMonth(memberId, fromYear, fromMonth, toYear, toMonth);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long id, Long memberId) {
|
||||
mustFind(id, memberId);
|
||||
@@ -79,7 +89,7 @@ public class BudgetService {
|
||||
}
|
||||
|
||||
List<BudgetStatus> result = new ArrayList<>();
|
||||
for (Budget b : budgetMapper.findByMember(memberId)) {
|
||||
for (Budget b : budgetMapper.findByMember(memberId, year, month)) {
|
||||
long monthlyBudget = monthlyBudget(b, daysInMonth);
|
||||
long spent = spentByCategory.getOrDefault(b.getCategory(), 0L);
|
||||
result.add(BudgetStatus.builder()
|
||||
@@ -103,7 +113,8 @@ public class BudgetService {
|
||||
int y = year != null ? year : Year.now().getValue();
|
||||
int m = month != null ? month : 1;
|
||||
|
||||
List<Budget> budgets = budgetMapper.findByMember(memberId);
|
||||
// 기간 차트는 선택 월의 예산을 기준으로 각 버킷에 적용(월별 예산 도입 전 동작과 동일)
|
||||
List<Budget> budgets = budgetMapper.findByMember(memberId, y, m);
|
||||
Map<String, Long> expense = new HashMap<>();
|
||||
for (Map<String, Object> row : entryMapper.statsByPeriod(memberId, unit, year, month)) {
|
||||
expense.put(String.valueOf(((Number) row.get("bucket")).longValue()),
|
||||
|
||||
@@ -165,6 +165,15 @@ CREATE TABLE IF NOT EXISTS budget (
|
||||
UNIQUE KEY uk_budget_member_category (member_id, category)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 월별 예산: 예산을 연/월별로 분리(전월 복사로 다음 달에 동일 예산 저장 가능).
|
||||
ALTER TABLE budget ADD COLUMN IF NOT EXISTS `year` INT NULL COMMENT '예산 연도';
|
||||
ALTER TABLE budget ADD COLUMN IF NOT EXISTS `month` INT NULL COMMENT '예산 월(1~12)';
|
||||
-- 기존 상시 예산을 현재 연/월로 1회 이관
|
||||
UPDATE budget SET `year` = YEAR(NOW()), `month` = MONTH(NOW()) WHERE `year` IS NULL OR `month` IS NULL;
|
||||
-- 분류 유니크를 (member,category) → (member,category,year,month) 로 교체
|
||||
ALTER TABLE budget DROP INDEX IF EXISTS uk_budget_member_category;
|
||||
ALTER TABLE budget ADD UNIQUE KEY IF NOT EXISTS uk_budget_member_cat_ym (member_id, category, `year`, `month`);
|
||||
|
||||
-- 가계부 항목 - 태그 (다대다, tag_id 는 account_tag.id 참조)
|
||||
CREATE TABLE IF NOT EXISTS account_entry_tag (
|
||||
entry_id BIGINT NOT NULL,
|
||||
|
||||
@@ -14,15 +14,19 @@
|
||||
<result property="weekly" column="weekly"/>
|
||||
<result property="monthly" column="monthly"/>
|
||||
<result property="yearly" column="yearly"/>
|
||||
<result property="year" column="year"/>
|
||||
<result property="month" column="month"/>
|
||||
<result property="createdAt" column="created_at"/>
|
||||
<result property="updatedAt" column="updated_at"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="cols">id, member_id, category, fixed, base_unit, base_amount,
|
||||
daily, weekly, monthly, yearly, created_at, updated_at</sql>
|
||||
daily, weekly, monthly, yearly, `year`, `month`, created_at, updated_at</sql>
|
||||
|
||||
<select id="findByMember" parameterType="long" resultMap="budgetResultMap">
|
||||
SELECT <include refid="cols"/> FROM budget WHERE member_id = #{memberId} ORDER BY category
|
||||
<select id="findByMember" resultMap="budgetResultMap">
|
||||
SELECT <include refid="cols"/> FROM budget
|
||||
WHERE member_id = #{memberId} AND `year` = #{year} AND `month` = #{month}
|
||||
ORDER BY category
|
||||
</select>
|
||||
|
||||
<select id="findByIdAndMember" resultMap="budgetResultMap">
|
||||
@@ -32,15 +36,30 @@
|
||||
<select id="countByCategory" resultType="int">
|
||||
SELECT COUNT(*) FROM budget
|
||||
WHERE member_id = #{memberId} AND category = #{category}
|
||||
AND `year` = #{year} AND `month` = #{month}
|
||||
<if test="excludeId != null">AND id != #{excludeId}</if>
|
||||
</select>
|
||||
|
||||
<insert id="insert" parameterType="com.sb.web.account.domain.Budget"
|
||||
useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO budget (member_id, category, fixed, base_unit, base_amount,
|
||||
daily, weekly, monthly, yearly, created_at, updated_at)
|
||||
daily, weekly, monthly, yearly, `year`, `month`, created_at, updated_at)
|
||||
VALUES (#{memberId}, #{category}, #{fixed}, #{baseUnit}, #{baseAmount},
|
||||
#{daily}, #{weekly}, #{monthly}, #{yearly}, NOW(), NOW())
|
||||
#{daily}, #{weekly}, #{monthly}, #{yearly}, #{year}, #{month}, NOW(), NOW())
|
||||
</insert>
|
||||
|
||||
<!-- 전월(또는 임의 월) 예산을 다른 월로 복사. 같은 분류는 덮어씀(ON DUPLICATE) -->
|
||||
<insert id="copyMonth">
|
||||
INSERT INTO budget (member_id, category, fixed, base_unit, base_amount,
|
||||
daily, weekly, monthly, yearly, `year`, `month`, created_at, updated_at)
|
||||
SELECT member_id, category, fixed, base_unit, base_amount,
|
||||
daily, weekly, monthly, yearly, #{toYear}, #{toMonth}, NOW(), NOW()
|
||||
FROM budget
|
||||
WHERE member_id = #{memberId} AND `year` = #{fromYear} AND `month` = #{fromMonth}
|
||||
ON DUPLICATE KEY UPDATE
|
||||
fixed = VALUES(fixed), base_unit = VALUES(base_unit), base_amount = VALUES(base_amount),
|
||||
daily = VALUES(daily), weekly = VALUES(weekly), monthly = VALUES(monthly),
|
||||
yearly = VALUES(yearly), updated_at = NOW()
|
||||
</insert>
|
||||
|
||||
<update id="update" parameterType="com.sb.web.account.domain.Budget">
|
||||
|
||||
Reference in New Issue
Block a user