- 분류: sort_order reorder 엔드포인트, 생성 시 맨 뒤 배치 - 예산: account_setting 테이블 + 월 예상 수입 GET/PUT Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ import com.sb.web.account.dto.BudgetPeriodStat;
|
|||||||
import com.sb.web.account.dto.BudgetRequest;
|
import com.sb.web.account.dto.BudgetRequest;
|
||||||
import com.sb.web.account.dto.BudgetResponse;
|
import com.sb.web.account.dto.BudgetResponse;
|
||||||
import com.sb.web.account.dto.BudgetStatus;
|
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.account.service.BudgetService;
|
||||||
import com.sb.web.auth.dto.SessionUser;
|
import com.sb.web.auth.dto.SessionUser;
|
||||||
import com.sb.web.auth.web.AuthInterceptor;
|
import com.sb.web.auth.web.AuthInterceptor;
|
||||||
@@ -24,6 +26,23 @@ import java.util.List;
|
|||||||
public class BudgetController {
|
public class BudgetController {
|
||||||
|
|
||||||
private final BudgetService budgetService;
|
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
|
@GetMapping
|
||||||
public List<BudgetResponse> list(
|
public List<BudgetResponse> list(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.sb.web.account.controller;
|
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.CategoryRequest;
|
||||||
import com.sb.web.account.dto.CategoryResponse;
|
import com.sb.web.account.dto.CategoryResponse;
|
||||||
import com.sb.web.account.service.CategoryService;
|
import com.sb.web.account.service.CategoryService;
|
||||||
@@ -52,6 +53,14 @@ public class CategoryController {
|
|||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 드래그앤드랍 순서 변경 */
|
||||||
|
@PutMapping("/reorder")
|
||||||
|
public List<CategoryResponse> reorder(
|
||||||
|
@RequestBody CategoryReorderRequest req,
|
||||||
|
@RequestAttribute(AuthInterceptor.CURRENT_MEMBER) SessionUser current) {
|
||||||
|
return categoryService.reorder(current.getId(), req.getType(), req.getIds());
|
||||||
|
}
|
||||||
|
|
||||||
/** 기존 내역의 분류를 목록으로 가져오기 */
|
/** 기존 내역의 분류를 목록으로 가져오기 */
|
||||||
@PostMapping("/import")
|
@PostMapping("/import")
|
||||||
public List<CategoryResponse> importFromEntries(
|
public List<CategoryResponse> importFromEntries(
|
||||||
|
|||||||
@@ -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<Long> ids; // 새 순서의 분류 id 목록
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -28,4 +28,10 @@ public interface CategoryMapper {
|
|||||||
int update(AccountCategory category);
|
int update(AccountCategory category);
|
||||||
|
|
||||||
int delete(@Param("id") Long id, @Param("memberId") Long memberId);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,12 +35,27 @@ public class CategoryService {
|
|||||||
if (categoryMapper.countByName(memberId, req.getType(), name, null) > 0) {
|
if (categoryMapper.countByName(memberId, req.getType(), name, null) > 0) {
|
||||||
throw new ApiException(HttpStatus.CONFLICT, "이미 존재하는 분류입니다.");
|
throw new ApiException(HttpStatus.CONFLICT, "이미 존재하는 분류입니다.");
|
||||||
}
|
}
|
||||||
|
Integer max = categoryMapper.maxSortOrder(memberId, req.getType());
|
||||||
|
int order = (max == null ? 0 : max + 1);
|
||||||
AccountCategory c = AccountCategory.builder()
|
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);
|
categoryMapper.insert(c);
|
||||||
return CategoryResponse.from(c);
|
return CategoryResponse.from(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 드래그앤드랍 순서 변경: 전달된 id 순서대로 sort_order 재설정 (본인·동일 타입만) */
|
||||||
|
@Transactional
|
||||||
|
public List<CategoryResponse> reorder(Long memberId, String type, List<Long> 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
|
@Transactional
|
||||||
public CategoryResponse update(Long id, CategoryRequest req, Long memberId) {
|
public CategoryResponse update(Long id, CategoryRequest req, Long memberId) {
|
||||||
|
|||||||
@@ -123,6 +123,14 @@ CREATE TABLE IF NOT EXISTS account_entry_tag (
|
|||||||
KEY idx_aet_tag (tag_id)
|
KEY idx_aet_tag (tag_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) 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 지갑별 · 사용자별)
|
-- 투자 보유 종목 (INVEST 지갑별 · 사용자별)
|
||||||
CREATE TABLE IF NOT EXISTS invest_holding (
|
CREATE TABLE IF NOT EXISTS invest_holding (
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.sb.web.account.mapper.AccountSettingMapper">
|
||||||
|
|
||||||
|
<select id="findExpectedIncome" resultType="java.lang.Long">
|
||||||
|
SELECT expected_income FROM account_setting WHERE member_id = #{memberId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<update id="upsertExpectedIncome">
|
||||||
|
INSERT INTO account_setting (member_id, expected_income, updated_at)
|
||||||
|
VALUES (#{memberId}, #{expectedIncome}, NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE expected_income = #{expectedIncome}, updated_at = NOW()
|
||||||
|
</update>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -51,4 +51,14 @@
|
|||||||
DELETE FROM account_category WHERE id = #{id} AND member_id = #{memberId}
|
DELETE FROM account_category WHERE id = #{id} AND member_id = #{memberId}
|
||||||
</delete>
|
</delete>
|
||||||
|
|
||||||
|
<update id="updateSortOrder">
|
||||||
|
UPDATE account_category SET sort_order = #{sortOrder}
|
||||||
|
WHERE id = #{id} AND member_id = #{memberId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<select id="maxSortOrder" resultType="java.lang.Integer">
|
||||||
|
SELECT MAX(sort_order) FROM account_category
|
||||||
|
WHERE member_id = #{memberId} AND type = #{type}
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
Reference in New Issue
Block a user