diff --git a/src/main/java/com/mallang/category/TieredCategory.java b/src/main/java/com/mallang/category/TieredCategory.java index a2882e83..e57843c0 100644 --- a/src/main/java/com/mallang/category/TieredCategory.java +++ b/src/main/java/com/mallang/category/TieredCategory.java @@ -42,6 +42,19 @@ protected TieredCategory(String name, Member owner) { this.owner = owner; } + public void create( + @Nullable T parent, + @Nullable T prevSibling, + @Nullable T nextSibling, + TieredCategoryValidator validator + ) { + if (isNulls(parent, prevSibling, nextSibling)) { + validator.validateNoCategories(owner); + return; + } + updateHierarchy(parent, prevSibling, nextSibling); + } + public abstract void validateOwner(Member member); public void updateHierarchy( @@ -107,6 +120,9 @@ private void validateParentAndChildRelation( @Nullable T nextSibling ) { if (isNulls(prevSibling, nextSibling)) { + if (parent == null) { + throw new CategoryHierarchyViolationException("카테고리 계층구조 변경 시 부모나 형제들 중 최소 하나와의 관계가 주어져야 합니다."); + } validateNoChildrenInParent(parent); } validateWhenNonNullWithFailCond( @@ -123,25 +139,9 @@ private void validateParentAndChildRelation( private void validateNoChildrenInParent(T parent) { - if (parent == null) { - T root = getRoot(); - if (equals(root) && root.getPreviousSibling() == null && root.getNextSibling() == null) { - return; - } - throw new CategoryHierarchyViolationException("존재하는 다른 최상위 카테고리와의 관계가 명시되지 않았습니다."); - } else { - if (!parent.getChildren().isEmpty()) { - throw new CategoryHierarchyViolationException("주어진 부모의 자식 카테고리와의 관계가 명시되지 않았습니다."); - } - } - } - - private T getRoot() { - T root = self(); - while (root.getParent() != null) { - root = root.getParent(); + if (!parent.getChildren().isEmpty()) { + throw new CategoryHierarchyViolationException("주어진 부모의 자식 카테고리와의 관계가 명시되지 않았습니다."); } - return root; } private void validateDuplicatedNameWhenParticipated( diff --git a/src/main/java/com/mallang/category/TieredCategoryValidator.java b/src/main/java/com/mallang/category/TieredCategoryValidator.java new file mode 100644 index 00000000..57f2a7c5 --- /dev/null +++ b/src/main/java/com/mallang/category/TieredCategoryValidator.java @@ -0,0 +1,8 @@ +package com.mallang.category; + +import com.mallang.auth.domain.Member; + +public interface TieredCategoryValidator { + + void validateNoCategories(Member member); +} diff --git a/src/main/java/com/mallang/post/application/PostCategoryService.java b/src/main/java/com/mallang/post/application/PostCategoryService.java index 63fd0cd9..048e90bc 100644 --- a/src/main/java/com/mallang/post/application/PostCategoryService.java +++ b/src/main/java/com/mallang/post/application/PostCategoryService.java @@ -11,6 +11,7 @@ import com.mallang.post.domain.Post; import com.mallang.post.domain.PostCategory; import com.mallang.post.domain.PostCategoryRepository; +import com.mallang.post.domain.PostCategoryValidator; import com.mallang.post.domain.PostRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -25,12 +26,16 @@ public class PostCategoryService { private final PostRepository postRepository; private final MemberRepository memberRepository; private final PostCategoryRepository postCategoryRepository; + private final PostCategoryValidator postCategoryValidator; public Long create(CreatePostCategoryCommand command) { Member member = memberRepository.getById(command.memberId()); Blog blog = blogRepository.getByName(command.blogName()); PostCategory postCategory = new PostCategory(command.name(), member, blog); - updateHierarchy(postCategory, command.parentId(), command.prevId(), command.nextId()); + PostCategory parent = postCategoryRepository.getByIdIfIdNotNull(command.parentId()); + PostCategory prev = postCategoryRepository.getByIdIfIdNotNull(command.prevId()); + PostCategory next = postCategoryRepository.getByIdIfIdNotNull(command.nextId()); + postCategory.create(parent, prev, next, postCategoryValidator); return postCategoryRepository.save(postCategory).getId(); } @@ -38,13 +43,9 @@ public void updateHierarchy(UpdatePostCategoryHierarchyCommand command) { Member member = memberRepository.getById(command.memberId()); PostCategory target = postCategoryRepository.getById(command.categoryId()); target.validateOwner(member); - updateHierarchy(target, command.parentId(), command.prevId(), command.nextId()); - } - - private void updateHierarchy(PostCategory target, Long parentId, Long prevId, Long nextId) { - PostCategory parent = postCategoryRepository.getByIdIfIdNotNull(parentId); - PostCategory prev = postCategoryRepository.getByIdIfIdNotNull(prevId); - PostCategory next = postCategoryRepository.getByIdIfIdNotNull(nextId); + PostCategory parent = postCategoryRepository.getByIdIfIdNotNull(command.parentId()); + PostCategory prev = postCategoryRepository.getByIdIfIdNotNull(command.prevId()); + PostCategory next = postCategoryRepository.getByIdIfIdNotNull(command.nextId()); target.updateHierarchy(parent, prev, next); } diff --git a/src/main/java/com/mallang/post/application/StarGroupService.java b/src/main/java/com/mallang/post/application/StarGroupService.java index 4ea42880..915d69c4 100644 --- a/src/main/java/com/mallang/post/application/StarGroupService.java +++ b/src/main/java/com/mallang/post/application/StarGroupService.java @@ -9,6 +9,7 @@ import com.mallang.post.domain.star.PostStarRepository; import com.mallang.post.domain.star.StarGroup; import com.mallang.post.domain.star.StarGroupRepository; +import com.mallang.post.domain.star.StarGroupValidator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,11 +22,15 @@ public class StarGroupService { private final MemberRepository memberRepository; private final PostStarRepository postStarRepository; private final StarGroupRepository starGroupRepository; + private final StarGroupValidator starGroupValidator; public Long create(CreateStarGroupCommand command) { Member member = memberRepository.getById(command.memberId()); StarGroup group = new StarGroup(command.name(), member); - updateHierarchy(group, command.parentId(), command.prevId(), command.nextId()); + StarGroup parent = starGroupRepository.getByIdIfIdNotNull(command.parentId()); + StarGroup prev = starGroupRepository.getByIdIfIdNotNull(command.prevId()); + StarGroup next = starGroupRepository.getByIdIfIdNotNull(command.nextId()); + group.create(parent, prev, next, starGroupValidator); return starGroupRepository.save(group).getId(); } @@ -33,13 +38,9 @@ public void updateHierarchy(UpdateStarGroupHierarchyCommand command) { Member member = memberRepository.getById(command.memberId()); StarGroup target = starGroupRepository.getById(command.groupId()); target.validateOwner(member); - updateHierarchy(target, command.parentId(), command.prevId(), command.nextId()); - } - - private void updateHierarchy(StarGroup target, Long parentId, Long prevId, Long nextId) { - StarGroup parent = starGroupRepository.getByIdIfIdNotNull(parentId); - StarGroup prev = starGroupRepository.getByIdIfIdNotNull(prevId); - StarGroup next = starGroupRepository.getByIdIfIdNotNull(nextId); + StarGroup parent = starGroupRepository.getByIdIfIdNotNull(command.parentId()); + StarGroup prev = starGroupRepository.getByIdIfIdNotNull(command.prevId()); + StarGroup next = starGroupRepository.getByIdIfIdNotNull(command.nextId()); target.updateHierarchy(parent, prev, next); } diff --git a/src/main/java/com/mallang/post/domain/PostCategoryRepository.java b/src/main/java/com/mallang/post/domain/PostCategoryRepository.java index 50fe9ae9..58758c96 100644 --- a/src/main/java/com/mallang/post/domain/PostCategoryRepository.java +++ b/src/main/java/com/mallang/post/domain/PostCategoryRepository.java @@ -1,5 +1,6 @@ package com.mallang.post.domain; +import com.mallang.auth.domain.Member; import com.mallang.post.exception.NotFoundPostCategoryException; import jakarta.annotation.Nullable; import org.springframework.data.jpa.repository.JpaRepository; @@ -17,4 +18,6 @@ default PostCategory getByIdIfIdNotNull(@Nullable Long categoryId) { } return getById(categoryId); } + + boolean existsByOwner(Member member); } diff --git a/src/main/java/com/mallang/post/domain/PostCategoryValidator.java b/src/main/java/com/mallang/post/domain/PostCategoryValidator.java new file mode 100644 index 00000000..6fdf63a7 --- /dev/null +++ b/src/main/java/com/mallang/post/domain/PostCategoryValidator.java @@ -0,0 +1,21 @@ +package com.mallang.post.domain; + +import com.mallang.auth.domain.Member; +import com.mallang.category.CategoryHierarchyViolationException; +import com.mallang.category.TieredCategoryValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class PostCategoryValidator implements TieredCategoryValidator { + + private final PostCategoryRepository postCategoryRepository; + + @Override + public void validateNoCategories(Member member) { + if (postCategoryRepository.existsByOwner(member)) { + throw new CategoryHierarchyViolationException("이미 존재하는 카테고리가 있습니다."); + } + } +} diff --git a/src/main/java/com/mallang/post/domain/star/StarGroupRepository.java b/src/main/java/com/mallang/post/domain/star/StarGroupRepository.java index 4694d391..629e1724 100644 --- a/src/main/java/com/mallang/post/domain/star/StarGroupRepository.java +++ b/src/main/java/com/mallang/post/domain/star/StarGroupRepository.java @@ -1,5 +1,6 @@ package com.mallang.post.domain.star; +import com.mallang.auth.domain.Member; import com.mallang.post.exception.NotFoundStarGroupException; import jakarta.annotation.Nullable; import org.springframework.data.jpa.repository.JpaRepository; @@ -17,4 +18,6 @@ default StarGroup getByIdIfIdNotNull(@Nullable Long id) { } return getById(id); } + + boolean existsByOwner(Member member); } diff --git a/src/main/java/com/mallang/post/domain/star/StarGroupValidator.java b/src/main/java/com/mallang/post/domain/star/StarGroupValidator.java new file mode 100644 index 00000000..b14387fe --- /dev/null +++ b/src/main/java/com/mallang/post/domain/star/StarGroupValidator.java @@ -0,0 +1,21 @@ +package com.mallang.post.domain.star; + +import com.mallang.auth.domain.Member; +import com.mallang.category.CategoryHierarchyViolationException; +import com.mallang.category.TieredCategoryValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class StarGroupValidator implements TieredCategoryValidator { + + private final StarGroupRepository starGroupRepository; + + @Override + public void validateNoCategories(Member member) { + if (starGroupRepository.existsByOwner(member)) { + throw new CategoryHierarchyViolationException("이미 존재하는 즐겨찾기 그룹이 있습니다."); + } + } +} diff --git a/src/test/java/com/mallang/category/TieredCategoryTestTemplate.java b/src/test/java/com/mallang/category/TieredCategoryTestTemplate.java index cbe82098..d5a3ff1c 100644 --- a/src/test/java/com/mallang/category/TieredCategoryTestTemplate.java +++ b/src/test/java/com/mallang/category/TieredCategoryTestTemplate.java @@ -5,8 +5,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import com.mallang.auth.domain.Member; +import com.mallang.common.execption.MallangLogException; import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -15,6 +21,9 @@ public abstract class TieredCategoryTestTemplate> { protected final Member member = 깃허브_말랑(1L); protected final Member otherMember = 깃허브_동훈(2L); + protected final TieredCategoryValidator validator = mock(TieredCategoryValidator.class); + + protected abstract T spyCategory(String name, Member owner); protected abstract T createRoot(String name, Member owner); @@ -24,15 +33,54 @@ public abstract class TieredCategoryTestTemplate> { protected abstract Class 권한_없음_예외(); + protected abstract Class 회원의_카테고리_없음_검증_실패_시_발생할_예외(); + @Nested protected class 생성_시 { @Test - void 생성한다() { + void 최초의_루트_카테고리_생성() { + // given + T mock = spyCategory("root", member); + // when & then assertDoesNotThrow(() -> { - createRoot("최상위", member); + mock.create(null, null, null, validator); }); + verify(mock, times(0)) + .updateHierarchy(any(), any(), any()); + } + + @Test + void 이미_다른_카테고리가_존재하는_상황에서_부모와_형제가_모두_null_이면_예외() { + // given + T root = createRoot("root", member); + willThrow(회원의_카테고리_없음_검증_실패_시_발생할_예외()) + .given(validator) + .validateNoCategories(any()); + + // when & then + assertThatThrownBy(() -> { + root.create(null, null, null, validator); + }).isInstanceOf(회원의_카테고리_없음_검증_실패_시_발생할_예외()); + } + + @Test + void 부모와_형제가_모두_null_이_아니면_계층_업데이트_메서드를_호출하여_계층구조를_설정한다() { + // given + T child = spyCategory("child", member); + T root = createRoot("root", member); + + // when & then + child.create(root, null, null, validator); + verify(child, times(1)) + .updateHierarchy(any(), any(), any()); + child.create(null, root, null, validator); + verify(child, times(2)) + .updateHierarchy(any(), any(), any()); + child.create(null, null, root, validator); + verify(child, times(3)) + .updateHierarchy(any(), any(), any()); } } @@ -420,76 +468,15 @@ class 직전_형제와_다음_형제가_주어졌을_때 { class 형제들이_주어지지_않았을_때 { @Test - void 부모가_주어지지_않았으며_루트의_형제가_하나라도_존재한다면_예외() { + void 부모가_주어지지_않으면_예외() { // given T target = createRoot("target", member); - T prev = createRoot("prev", member); - prev.updateHierarchy(null, null, target); // when & then assertThatThrownBy(() -> { target.updateHierarchy(null, null, null); }).isInstanceOf(CategoryHierarchyViolationException.class) - .hasMessage("존재하는 다른 최상위 카테고리와의 관계가 명시되지 않았습니다."); - assertThatThrownBy(() -> { - prev.updateHierarchy(null, null, null); - }).isInstanceOf(CategoryHierarchyViolationException.class) - .hasMessage("존재하는 다른 최상위 카테고리와의 관계가 명시되지 않았습니다."); - } - - @Test - void 부모가_주어지지_않았으며_루트의_형제가_존재하지_않을_때_내가_루트라면_업데이트() { - // given - T target = createRoot("target", member); - - // when & then - assertDoesNotThrow(() -> { - target.updateHierarchy(null, null, null); - }); - } - - @Test - void 부모가_주어지지_않았으며_루트의_형제가_존재하지_않을_때_내가_루트가_아니라면_예외() { - // given - T root = createRoot("root", member); - T child = createRoot("child", member); - child.updateHierarchy(root, null, null); - - // when & then - assertThatThrownBy(() -> { - child.updateHierarchy(null, null, null); - }).isInstanceOf(CategoryHierarchyViolationException.class) - .hasMessage("존재하는 다른 최상위 카테고리와의 관계가 명시되지 않았습니다."); - } - - @Test - void 부모가_주어지지_않았으며_내가_루트가_아닌_경우_예외() { - // given - T root = createRoot("root", member); - T child = createRoot("target", member); - child.updateHierarchy(root, null, null); - T descendant = createRoot("descendant", member); - descendant.updateHierarchy(child, null, null); - - // when - assertThatThrownBy(() -> { - child.updateHierarchy(null, null, null); - }).isInstanceOf(CategoryHierarchyViolationException.class) - .hasMessage("존재하는 다른 최상위 카테고리와의 관계가 명시되지 않았습니다."); - } - - @Test - void 부모가_주어지지_않았으며_내가_루트이나_내_형제가_존재하면_예외() { - // given - T root = createRoot("root", member); - T next = createRoot("next", member); - next.updateHierarchy(null, root, null); - - // when - assertThatThrownBy(() -> { - root.updateHierarchy(null, null, null); - }).isInstanceOf(CategoryHierarchyViolationException.class) - .hasMessage("존재하는 다른 최상위 카테고리와의 관계가 명시되지 않았습니다."); + .hasMessage("카테고리 계층구조 변경 시 부모나 형제들 중 최소 하나와의 관계가 주어져야 합니다."); } @Test @@ -606,7 +593,6 @@ class 계층_참여_시_중복_이름이_존재하게_되는_경우 { // given T prev = createRoot("prev", member); T next = createRoot("next", member); - prev.updateHierarchy(null, null, null); next.updateHierarchy(null, prev, null); T target = createRoot("next", member); diff --git a/src/test/java/com/mallang/post/application/PostCategoryServiceTest.java b/src/test/java/com/mallang/post/application/PostCategoryServiceTest.java index e0342430..5bb0675b 100644 --- a/src/test/java/com/mallang/post/application/PostCategoryServiceTest.java +++ b/src/test/java/com/mallang/post/application/PostCategoryServiceTest.java @@ -61,6 +61,27 @@ class 저장_시 { assertThat(postCategory.getName()).isEqualTo("최상위 카테고리"); } + @Test + void 이미_카테고리가_존재하는데_이와의_관계를_명시하지_않으면_예외() { + // given + CreatePostCategoryCommand command = CreatePostCategoryCommand.builder() + .memberId(mallangId) + .blogName(mallangBlogName) + .name("최상위 카테고리") + .build(); + Long 최상위_카테고리 = postCategoryService.create(command); + CreatePostCategoryCommand command2 = CreatePostCategoryCommand.builder() + .memberId(mallangId) + .blogName(mallangBlogName) + .name("최상위 카테고리2") + .build(); + + // when & then + assertThatThrownBy(() -> { + postCategoryService.create(command2); + }).isInstanceOf(CategoryHierarchyViolationException.class); + } + @Test void 계층형으로_저장할_수_있다() { // given diff --git a/src/test/java/com/mallang/post/domain/PostCategoryTest.java b/src/test/java/com/mallang/post/domain/PostCategoryTest.java index 429106f3..c6fa17b5 100644 --- a/src/test/java/com/mallang/post/domain/PostCategoryTest.java +++ b/src/test/java/com/mallang/post/domain/PostCategoryTest.java @@ -3,11 +3,14 @@ import static com.mallang.post.PostCategoryFixture.루트_카테고리; import static com.mallang.post.PostCategoryFixture.하위_카테고리; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.spy; import com.mallang.auth.domain.Member; import com.mallang.blog.domain.Blog; import com.mallang.blog.exception.NoAuthorityBlogException; +import com.mallang.category.CategoryHierarchyViolationException; import com.mallang.category.TieredCategoryTestTemplate; +import com.mallang.common.execption.MallangLogException; import com.mallang.post.exception.NoAuthorityPostCategoryException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayNameGeneration; @@ -20,6 +23,12 @@ @DisplayNameGeneration(ReplaceUnderscores.class) class PostCategoryTest extends TieredCategoryTestTemplate { + @Override + protected PostCategory spyCategory(String name, Member owner) { + PostCategory category = new PostCategory(name, owner, new Blog("blog", owner)); + return spy(category); + } + @Override protected PostCategory createRoot(String name, Member owner) { return 루트_카테고리(name, owner, new Blog("blog", owner)); @@ -46,6 +55,11 @@ protected PostCategory createChild( return NoAuthorityPostCategoryException.class; } + @Override + protected Class 회원의_카테고리_없음_검증_실패_시_발생할_예외() { + return CategoryHierarchyViolationException.class; + } + @Nested class 생성_시 extends TieredCategoryTestTemplate.생성_시 { diff --git a/src/test/java/com/mallang/post/domain/star/StarGroupTest.java b/src/test/java/com/mallang/post/domain/star/StarGroupTest.java index 34d9c7e8..05d59559 100644 --- a/src/test/java/com/mallang/post/domain/star/StarGroupTest.java +++ b/src/test/java/com/mallang/post/domain/star/StarGroupTest.java @@ -1,7 +1,11 @@ package com.mallang.post.domain.star; +import static org.mockito.Mockito.spy; + import com.mallang.auth.domain.Member; +import com.mallang.category.CategoryHierarchyViolationException; import com.mallang.category.TieredCategoryTestTemplate; +import com.mallang.common.execption.MallangLogException; import com.mallang.post.exception.NoAuthorityStarGroupException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayNameGeneration; @@ -12,6 +16,12 @@ @DisplayNameGeneration(ReplaceUnderscores.class) class StarGroupTest extends TieredCategoryTestTemplate { + @Override + protected StarGroup spyCategory(String name, Member owner) { + StarGroup starGroup = new StarGroup(name, owner); + return spy(starGroup); + } + @Override protected StarGroup createRoot(String name, Member owner) { return new StarGroup(name, owner); @@ -39,4 +49,9 @@ protected StarGroup createChild( protected Class 권한_없음_예외() { return NoAuthorityStarGroupException.class; } + + @Override + protected Class 회원의_카테고리_없음_검증_실패_시_발생할_예외() { + return CategoryHierarchyViolationException.class; + } }