diff --git a/src/main/java/com/sb/web/account/controller/BudgetController.java b/src/main/java/com/sb/web/account/controller/BudgetController.java index 8589fa5..a9c56e9 100644 --- a/src/main/java/com/sb/web/account/controller/BudgetController.java +++ b/src/main/java/com/sb/web/account/controller/BudgetController.java @@ -4,6 +4,8 @@ import com.sb.web.account.dto.BudgetPeriodStat; import com.sb.web.account.dto.BudgetRequest; import com.sb.web.account.dto.BudgetResponse; import com.sb.web.account.dto.BudgetStatus; +import com.sb.web.account.dto.ExpectedIncomeRequest; +import com.sb.web.account.service.AccountSettingService; import com.sb.web.account.service.BudgetService; import com.sb.web.auth.dto.SessionUser; import com.sb.web.auth.web.AuthInterceptor; @@ -24,6 +26,23 @@ import java.util.List; public class BudgetController { private final BudgetService budgetService; + private final AccountSettingService settingService; + + /** 월 예상 수입 조회 */ + @GetMapping("/income") + public ExpectedIncomeRequest getIncome( + @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { + return new ExpectedIncomeRequest(settingService.getExpectedIncome(current.getId())); + } + + /** 월 예상 수입 설정 */ + @PutMapping("/income") + public ExpectedIncomeRequest setIncome( + @Valid @RequestBody ExpectedIncomeRequest req, + @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { + settingService.setExpectedIncome(current.getId(), req.getExpectedIncome()); + return req; + } @GetMapping public List list( diff --git a/src/main/java/com/sb/web/account/controller/CategoryController.java b/src/main/java/com/sb/web/account/controller/CategoryController.java index 59ce75a..ded8db1 100644 --- a/src/main/java/com/sb/web/account/controller/CategoryController.java +++ b/src/main/java/com/sb/web/account/controller/CategoryController.java @@ -1,5 +1,6 @@ package com.sb.web.account.controller; +import com.sb.web.account.dto.CategoryReorderRequest; import com.sb.web.account.dto.CategoryRequest; import com.sb.web.account.dto.CategoryResponse; import com.sb.web.account.service.CategoryService; @@ -52,6 +53,14 @@ public class CategoryController { return ResponseEntity.noContent().build(); } + /** 드래그앤드랍 순서 변경 */ + @PutMapping("/reorder") + public List reorder( + @RequestBody CategoryReorderRequest req, + @RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) { + return categoryService.reorder(current.getId(), req.getType(), req.getIds()); + } + /** 기존 내역의 분류를 목록으로 가져오기 */ @PostMapping("/import") public List importFromEntries( diff --git a/src/main/java/com/sb/web/account/dto/CategoryReorderRequest.java b/src/main/java/com/sb/web/account/dto/CategoryReorderRequest.java new file mode 100644 index 0000000..4979ea2 --- /dev/null +++ b/src/main/java/com/sb/web/account/dto/CategoryReorderRequest.java @@ -0,0 +1,15 @@ +package com.sb.web.account.dto; + +import lombok.Data; + +import java.util.List; + +/** + * 분류 순서 변경 요청. ids 순서대로 sort_order 를 재설정한다. + */ +@Data +public class CategoryReorderRequest { + + private String type; // INCOME / EXPENSE + private List ids; // 새 순서의 분류 id 목록 +} diff --git a/src/main/java/com/sb/web/account/dto/ExpectedIncomeRequest.java b/src/main/java/com/sb/web/account/dto/ExpectedIncomeRequest.java new file mode 100644 index 0000000..a3ff1e5 --- /dev/null +++ b/src/main/java/com/sb/web/account/dto/ExpectedIncomeRequest.java @@ -0,0 +1,18 @@ +package com.sb.web.account.dto; + +import jakarta.validation.constraints.PositiveOrZero; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 월 예상 수입 요청/응답. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ExpectedIncomeRequest { + + @PositiveOrZero(message = "예상 수입은 0 이상이어야 합니다.") + private Long expectedIncome; +} diff --git a/src/main/java/com/sb/web/account/mapper/AccountSettingMapper.java b/src/main/java/com/sb/web/account/mapper/AccountSettingMapper.java new file mode 100644 index 0000000..b3f7094 --- /dev/null +++ b/src/main/java/com/sb/web/account/mapper/AccountSettingMapper.java @@ -0,0 +1,17 @@ +package com.sb.web.account.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 사용자별 가계부 설정 매퍼 (account_setting). + */ +@Mapper +public interface AccountSettingMapper { + + /** 월 예상 수입 (없으면 null) */ + Long findExpectedIncome(@Param("memberId") Long memberId); + + /** 월 예상 수입 upsert */ + int upsertExpectedIncome(@Param("memberId") Long memberId, @Param("expectedIncome") Long expectedIncome); +} diff --git a/src/main/java/com/sb/web/account/mapper/CategoryMapper.java b/src/main/java/com/sb/web/account/mapper/CategoryMapper.java index 682f5df..3236e92 100644 --- a/src/main/java/com/sb/web/account/mapper/CategoryMapper.java +++ b/src/main/java/com/sb/web/account/mapper/CategoryMapper.java @@ -28,4 +28,10 @@ public interface CategoryMapper { int update(AccountCategory category); 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); } diff --git a/src/main/java/com/sb/web/account/service/AccountSettingService.java b/src/main/java/com/sb/web/account/service/AccountSettingService.java new file mode 100644 index 0000000..5a3ac97 --- /dev/null +++ b/src/main/java/com/sb/web/account/service/AccountSettingService.java @@ -0,0 +1,25 @@ +package com.sb.web.account.service; + +import com.sb.web.account.mapper.AccountSettingMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 사용자별 가계부 설정 서비스 (월 예상 수입 등). + */ +@Service +@RequiredArgsConstructor +public class AccountSettingService { + + private final AccountSettingMapper settingMapper; + + public Long getExpectedIncome(Long memberId) { + return settingMapper.findExpectedIncome(memberId); + } + + @Transactional + public void setExpectedIncome(Long memberId, Long expectedIncome) { + settingMapper.upsertExpectedIncome(memberId, expectedIncome); + } +} diff --git a/src/main/java/com/sb/web/account/service/CategoryService.java b/src/main/java/com/sb/web/account/service/CategoryService.java index 4b25c3a..c311137 100644 --- a/src/main/java/com/sb/web/account/service/CategoryService.java +++ b/src/main/java/com/sb/web/account/service/CategoryService.java @@ -35,12 +35,27 @@ public class CategoryService { if (categoryMapper.countByName(memberId, req.getType(), name, null) > 0) { throw new ApiException(HttpStatus.CONFLICT, "이미 존재하는 분류입니다."); } + Integer max = categoryMapper.maxSortOrder(memberId, req.getType()); + int order = (max == null ? 0 : max + 1); AccountCategory c = AccountCategory.builder() - .memberId(memberId).type(req.getType()).name(name).sortOrder(0).build(); + .memberId(memberId).type(req.getType()).name(name).sortOrder(order).build(); categoryMapper.insert(c); return CategoryResponse.from(c); } + /** 드래그앤드랍 순서 변경: 전달된 id 순서대로 sort_order 재설정 (본인·동일 타입만) */ + @Transactional + public List reorder(Long memberId, String type, List ids) { + int i = 0; + for (Long id : ids) { + AccountCategory c = categoryMapper.findByIdAndMember(id, memberId); + if (c != null && c.getType().equals(type)) { + categoryMapper.updateSortOrder(id, memberId, i++); + } + } + return list(memberId); + } + /** 이름만 변경 가능. 변경 시 기존 내역·예산의 분류명도 함께 갱신 */ @Transactional public CategoryResponse update(Long id, CategoryRequest req, Long memberId) { diff --git a/src/main/resources/db/account.sql b/src/main/resources/db/account.sql index ba90636..a8bde3a 100644 --- a/src/main/resources/db/account.sql +++ b/src/main/resources/db/account.sql @@ -123,6 +123,14 @@ CREATE TABLE IF NOT EXISTS account_entry_tag ( KEY idx_aet_tag (tag_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- 사용자별 가계부 설정 (예: 월 예상 수입) +CREATE TABLE IF NOT EXISTS account_setting ( + member_id BIGINT NOT NULL COMMENT '소유자 member.id', + expected_income BIGINT NULL COMMENT '월 예상 수입', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (member_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + -- 투자 보유 종목 (INVEST 지갑별 · 사용자별) CREATE TABLE IF NOT EXISTS invest_holding ( id BIGINT NOT NULL AUTO_INCREMENT, diff --git a/src/main/resources/mapper/AccountSettingMapper.xml b/src/main/resources/mapper/AccountSettingMapper.xml new file mode 100644 index 0000000..9534ee0 --- /dev/null +++ b/src/main/resources/mapper/AccountSettingMapper.xml @@ -0,0 +1,16 @@ + + + + + + + + INSERT INTO account_setting (member_id, expected_income, updated_at) + VALUES (#{memberId}, #{expectedIncome}, NOW()) + ON DUPLICATE KEY UPDATE expected_income = #{expectedIncome}, updated_at = NOW() + + + diff --git a/src/main/resources/mapper/CategoryMapper.xml b/src/main/resources/mapper/CategoryMapper.xml index 182ecd1..c64d4bf 100644 --- a/src/main/resources/mapper/CategoryMapper.xml +++ b/src/main/resources/mapper/CategoryMapper.xml @@ -51,4 +51,14 @@ DELETE FROM account_category WHERE id = #{id} AND member_id = #{memberId} + + UPDATE account_category SET sort_order = #{sortOrder} + WHERE id = #{id} AND member_id = #{memberId} + + + +