feat: 분류 대/소분류(2단계) — account_category.parent_id
- parent_id 추가(CREATE + ALTER IF NOT EXISTS 멱등 마이그레이션) - 도메인/DTO/매퍼에 parentId 노출, 2단계 검증(대분류 아래만 소분류) - 자식 있는 대분류는 소분류화·삭제 차단(countChildren) - 통계/예산/내역은 소분류 이름 기준 그대로(무변경) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3fc233d697
commit
5f21cc4474
@@ -35,14 +35,30 @@ public class CategoryService {
|
||||
if (categoryMapper.countByName(memberId, req.getType(), name, null) > 0) {
|
||||
throw new ApiException(HttpStatus.CONFLICT, "이미 존재하는 분류입니다.");
|
||||
}
|
||||
validateParent(req.getParentId(), req.getType(), memberId);
|
||||
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(order).build();
|
||||
.memberId(memberId).type(req.getType()).name(name)
|
||||
.parentId(req.getParentId()).sortOrder(order).build();
|
||||
categoryMapper.insert(c);
|
||||
return CategoryResponse.from(c);
|
||||
}
|
||||
|
||||
/** 대분류 지정 검증: null=대분류 / 값=대분류여야(2단계 제한·동일타입·본인) */
|
||||
private void validateParent(Long parentId, String type, Long memberId) {
|
||||
if (parentId == null) {
|
||||
return;
|
||||
}
|
||||
AccountCategory parent = categoryMapper.findByIdAndMember(parentId, memberId);
|
||||
if (parent == null || !parent.getType().equals(type)) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "대분류가 올바르지 않습니다.");
|
||||
}
|
||||
if (parent.getParentId() != null) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "대분류 아래에만 소분류를 둘 수 있습니다(2단계).");
|
||||
}
|
||||
}
|
||||
|
||||
/** 드래그앤드랍 순서 변경: 전달된 id 순서대로 sort_order 재설정 (본인·동일 타입만) */
|
||||
@Transactional
|
||||
public List<CategoryResponse> reorder(Long memberId, String type, List<Long> ids) {
|
||||
@@ -56,7 +72,8 @@ public class CategoryService {
|
||||
return list(memberId);
|
||||
}
|
||||
|
||||
/** 이름만 변경 가능. 변경 시 기존 내역·예산의 분류명도 함께 갱신 */
|
||||
/** 이름 변경 + 대분류 이동. 이름 변경 시 기존 내역·예산의 분류명도 함께 갱신.
|
||||
* 주의: parentId 는 항상 현재값(또는 새 대분류)을 보내야 한다(미전송 시 대분류로 풀림). */
|
||||
@Transactional
|
||||
public CategoryResponse update(Long id, CategoryRequest req, Long memberId) {
|
||||
AccountCategory c = mustFind(id, memberId);
|
||||
@@ -64,8 +81,20 @@ public class CategoryService {
|
||||
if (categoryMapper.countByName(memberId, c.getType(), newName, id) > 0) {
|
||||
throw new ApiException(HttpStatus.CONFLICT, "이미 존재하는 분류입니다.");
|
||||
}
|
||||
Long newParent = req.getParentId();
|
||||
if (newParent != null) {
|
||||
if (newParent.equals(id)) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "자기 자신을 대분류로 지정할 수 없습니다.");
|
||||
}
|
||||
// 소분류를 가진 대분류는 소분류로 바꿀 수 없음(3단계 방지)
|
||||
if (categoryMapper.countChildren(memberId, id) > 0) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "소분류가 있는 대분류는 소분류로 바꿀 수 없습니다.");
|
||||
}
|
||||
}
|
||||
validateParent(newParent, c.getType(), memberId);
|
||||
String oldName = c.getName();
|
||||
c.setName(newName);
|
||||
c.setParentId(newParent);
|
||||
categoryMapper.update(c);
|
||||
if (!oldName.equals(newName)) {
|
||||
entryMapper.renameCategory(memberId, c.getType(), oldName, newName);
|
||||
@@ -79,6 +108,9 @@ public class CategoryService {
|
||||
@Transactional
|
||||
public void delete(Long id, Long memberId) {
|
||||
mustFind(id, memberId);
|
||||
if (categoryMapper.countChildren(memberId, id) > 0) {
|
||||
throw new ApiException(HttpStatus.BAD_REQUEST, "소분류가 있는 대분류는 삭제할 수 없습니다. 소분류를 먼저 옮기거나 삭제하세요.");
|
||||
}
|
||||
categoryMapper.delete(id, memberId);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user