Skip to content

Commit

Permalink
feat: 카테고리 순서 설정 기능 추가 (#137)
Browse files Browse the repository at this point in the history
* [#131] feat: 계층구조 수정을 위한 Validator 로직 작성

* [#131] feat: Category에 계층구조 변경 로직 작성

* [#131] feat: Service에서 변경된 Category 로직을 사용

* [#131] test: ObjectsUtils 테스트코드 작성

* [#131] test: CategoryValidator 테스트코드 개선

* [#131] test: add additional assertions in ObjectsUtilsTest

(cherry picked from commit db36561)
  • Loading branch information
shin-mallang committed Dec 10, 2023
1 parent 4969824 commit 1530e8c
Show file tree
Hide file tree
Showing 34 changed files with 2,107 additions and 423 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import com.mallang.blog.domain.BlogRepository;
import com.mallang.category.application.command.CreateCategoryCommand;
import com.mallang.category.application.command.DeleteCategoryCommand;
import com.mallang.category.application.command.UpdateCategoryCommand;
import com.mallang.category.application.command.UpdateCategoryHierarchyCommand;
import com.mallang.category.application.command.UpdateCategoryNameCommand;
import com.mallang.category.domain.Category;
import com.mallang.category.domain.CategoryRepository;
import com.mallang.category.domain.CategoryValidator;
Expand All @@ -27,17 +28,30 @@ public class CategoryService {
public Long create(CreateCategoryCommand command) {
Member member = memberRepository.getById(command.memberId());
Blog blog = blogRepository.getByName(command.blogName());
Category parentCategory = categoryRepository.getParentById(command.parentCategoryId());
Category category = Category.create(command.name(), member, blog, parentCategory, categoryValidator);
Category category = new Category(command.name(), member, blog);
updateHierarchy(category, command.parentId(), command.prevId(), command.nextId());
return categoryRepository.save(category).getId();
}

public void update(UpdateCategoryCommand command) {
public void updateHierarchy(UpdateCategoryHierarchyCommand command) {
Member member = memberRepository.getById(command.memberId());
Category target = categoryRepository.getById(command.categoryId());
target.validateOwner(member);
updateHierarchy(target, command.parentId(), command.prevId(), command.nextId());
}

private void updateHierarchy(Category target, Long parentId, Long prevId, Long nextId) {
Category parent = categoryRepository.getByNullableId(parentId);
Category prev = categoryRepository.getByNullableId(prevId);
Category next = categoryRepository.getByNullableId(nextId);
target.updateHierarchy(parent, prev, next, categoryValidator);
}

public void updateName(UpdateCategoryNameCommand command) {
Member member = memberRepository.getById(command.memberId());
Category category = categoryRepository.getById(command.categoryId());
category.validateOwner(member);
Category parentCategory = categoryRepository.getParentById(command.parentCategoryId());
category.update(command.name(), parentCategory, categoryValidator);
category.updateName(command.name(), categoryValidator);
}

public void delete(DeleteCategoryCommand command) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public record CreateCategoryCommand(
Long memberId,
String blogName,
String name,
@Nullable Long parentCategoryId
@Nullable Long parentId,
@Nullable Long prevId,
@Nullable Long nextId
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import lombok.Builder;

@Builder
public record UpdateCategoryCommand(
public record UpdateCategoryHierarchyCommand(
Long categoryId,
Long memberId,
String name,
@Nullable Long parentCategoryId
@Nullable Long parentId,
@Nullable Long prevId,
@Nullable Long nextId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.mallang.category.application.command;

import lombok.Builder;

@Builder
public record UpdateCategoryNameCommand(
Long categoryId,
Long memberId,
String name
) {
}
154 changes: 90 additions & 64 deletions src/main/java/com/mallang/category/domain/Category.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@
import com.mallang.auth.domain.Member;
import com.mallang.blog.domain.Blog;
import com.mallang.category.domain.event.CategoryDeletedEvent;
import com.mallang.category.exception.CategoryHierarchyViolationException;
import com.mallang.category.exception.ChildCategoryExistException;
import com.mallang.category.exception.DuplicateCategoryNameException;
import com.mallang.category.exception.NoAuthorityCategoryException;
import com.mallang.common.domain.CommonRootEntity;
import jakarta.annotation.Nullable;
Expand All @@ -21,6 +19,7 @@
import jakarta.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import lombok.Getter;
import lombok.NoArgsConstructor;

Expand Down Expand Up @@ -50,91 +49,88 @@ public class Category extends CommonRootEntity<Long> {
@OneToMany(fetch = LAZY, mappedBy = "parent")
private List<Category> children = new ArrayList<>();

private Category(String name, Member owner, Blog blog) {
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "previous_sibling_id")
private Category previousSibling;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "next_sibling_id")
private Category nextSibling;

public Category(String name, Member owner, Blog blog) {
this.name = name;
this.owner = owner;
this.blog = blog;
blog.validateOwner(owner);
}

public static Category create(
String name,
Member member,
Blog blog,
public void validateOwner(Member member) {
if (!owner.equals(member)) {
throw new NoAuthorityCategoryException();
}
}

public void updateHierarchy(
@Nullable Category parent,
@Nullable Category prevSibling,
@Nullable Category nextSibling,
CategoryValidator validator
) {
Category category = new Category(name, member, blog);
category.setParent(parent, validator);
return category;
}

public void update(String name, Category parent, CategoryValidator validator) {
this.name = name;
setParent(parent, validator);
}

public void delete() {
validateNoChildren();
unlinkFromParent();
registerEvent(new CategoryDeletedEvent(getId()));
validator.validateUpdateHierarchy(this, parent, prevSibling, nextSibling);
withdrawCurrentHierarchy();
participateHierarchy(parent, prevSibling, nextSibling);
}

private void setParent(@Nullable Category parent, CategoryValidator validator) {
if (parent == null) {
beRoot(validator);
return;
private void withdrawCurrentHierarchy() {
if (previousSibling != null) {
previousSibling.setNextSibling(nextSibling);
}
beChild(parent);
}

private void beRoot(CategoryValidator validator) {
validator.validateDuplicateRootName(owner.getId(), name);
unlinkFromParent();
}

private void unlinkFromParent() {
if (getParent() != null) {
getParent().getChildren().remove(this);
parent = null;
if (nextSibling != null) {
nextSibling.setPreviousSibling(previousSibling);
}
}

private void beChild(Category parent) {
parent.validateOwner(owner);
validateHierarchy(parent);
link(parent);
}

public void validateOwner(Member member) {
if (!owner.equals(member)) {
throw new NoAuthorityCategoryException();
if (parent != null) {
parent.getChildren().remove(this);
}
previousSibling = null;
nextSibling = null;
parent = null;
}

private void validateHierarchy(Category parent) {
if (this.equals(parent)) {
throw new CategoryHierarchyViolationException();
private void participateHierarchy(
@Nullable Category parent,
@Nullable Category prevSibling,
@Nullable Category nextSibling
) {
if (prevSibling != null) {
prevSibling.setNextSibling(this);
}
if (getDescendants().contains(parent)) {
throw new CategoryHierarchyViolationException();
if (nextSibling != null) {
nextSibling.setPreviousSibling(this);
}
if (parent != null) {
parent.getChildren().add(this);
}
this.previousSibling = prevSibling;
this.nextSibling = nextSibling;
this.parent = parent;
}

private void link(Category parent) {
public void updateName(String name, CategoryValidator validator) {
validator.validateDuplicateNameInSibling(this, name);
this.name = name;
}

public void delete() {
validateNoChildren();
unlinkFromParent();
validateDuplicatedNameInSameHierarchy(parent);
this.parent = parent;
getParent().getChildren().add(this);
registerEvent(new CategoryDeletedEvent(getId()));
}

private void validateDuplicatedNameInSameHierarchy(Category parent) {
parent.getChildren().stream()
.filter(it -> it.getName().equals(name))
.findAny()
.ifPresent(it -> {
throw new DuplicateCategoryNameException();
});
private void unlinkFromParent() {
if (parent != null) {
parent.children.remove(this);
parent = null;
}
}

private void validateNoChildren() {
Expand All @@ -154,4 +150,34 @@ public List<Category> getDescendants() {
}
return children;
}

public List<Category> getSortedChildren() {
Optional<Category> first = getChildren()
.stream()
.filter(it -> it.getPreviousSibling() == null)
.findAny();
if (first.isEmpty()) {
return new ArrayList<>();
}
List<Category> categories = new ArrayList<>();
Category current = first.get();
categories.add(current);
while (current.getNextSibling() != null) {
current = current.getNextSibling();
categories.add(current);
}
return categories;
}

// For lazy loading issue
// parent, prev, next 가 지연로딩되어 프록시로 조회되므로, 그냥 사용 시 update 가 동작하지 않음
// 이를 해결하기 위해 메서드를 통해 접근해야 하는데, private 혹은 package-private 인 경우 여전히 동작하지 않음
// 따라서 protected 로 설정한
protected void setPreviousSibling(Category previousSibling) {
this.previousSibling = previousSibling;
}

protected void setNextSibling(Category nextSibling) {
this.nextSibling = nextSibling;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mallang.category.domain;

import com.mallang.blog.domain.Blog;
import com.mallang.category.exception.NotFoundCategoryException;
import jakarta.annotation.Nullable;
import java.util.List;
Expand All @@ -14,14 +15,16 @@ default Category getById(Long id) {
return findById(id).orElseThrow(NotFoundCategoryException::new);
}

@Query("SELECT c FROM Category c WHERE c.owner.id = :memberId AND c.parent IS NULL")
List<Category> findAllRootByMemberId(@Param("memberId") Long memberId);
boolean existsByBlog(Blog blog);

@Query("SELECT c FROM Category c WHERE c.blog = :blog AND c.parent IS NULL")
List<Category> findAllRootByBlog(@Param("blog") Blog blog);

@Nullable
default Category getParentById(@Nullable Long parentCategoryId) {
if (parentCategoryId == null) {
default Category getByNullableId(@Nullable Long categoryId) {
if (categoryId == null) {
return null;
}
return getById(parentCategoryId);
return getById(categoryId);
}
}
Loading

0 comments on commit 1530e8c

Please sign in to comment.