Skip to content

Commit

Permalink
refactor: 카테고리 검증 로직 별 책임 분리
Browse files Browse the repository at this point in the history
  • Loading branch information
HoeSeong123 committed Dec 27, 2024
1 parent 6d27648 commit 347eb85
Show file tree
Hide file tree
Showing 14 changed files with 199 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ ResponseEntity<CreateCategoryResponse> createCategory(
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/categories", errorCases = {
@ErrorCase(description = "기본 카테고리를 수정 또는 삭제한 경우", exampleMessage = "기본 카테고리는 수정 및 삭제할 수 없습니다."),
@ErrorCase(description = "카테고리 이름이 15자를 초과한 경우", exampleMessage = "카테고리 이름은 최대 15자까지 입력 가능합니다."),
@ErrorCase(description = "카테고리의 순서가 잘못된 경우", exampleMessage = "카테고리 순서가 잘못되었습니다."),
@ErrorCase(description = "카테고리의 순서가 잘못된 경우", exampleMessage = "순서가 잘못되었습니다."),
@ErrorCase(description = "모든 필드 중 null인 값이 있는 경우", exampleMessage = "카테고리 이름이 null 입니다."),
@ErrorCase(description = "삭제하려는 카테고리에 템플릿이 존재하는 경우", exampleMessage = "템플릿이 존재하는 카테고리는 삭제할 수 없습니다."),
@ErrorCase(description = "카테고리의 순서가 1보다 작은 경우", exampleMessage = "카테고리의 순서는 1 이상이어야 합니다."),
@ErrorCase(description = "중복된 카테고리 이름이 있는 경우", exampleMessage = "요청에 중복된 카테고리 이름이 존재합니다."),
@ErrorCase(description = "중복된 id가 있는 경우", exampleMessage = "요청에 중복된 id가 존재합니다."),
@ErrorCase(description = "중복된 카테고리 이름이 있는 경우", exampleMessage = "카테고리명이 중복되었습니다."),
@ErrorCase(description = "중복된 id가 있는 경우", exampleMessage = "id가 중복되었습니다."),
@ErrorCase(description = "카테고리의 개수가 일치하지 않는 경우(수정되지 않은 카테고리도 모두 보내주어야 합니다.)",
exampleMessage = "카테고리의 개수가 일치하지 않습니다."),
})
Expand Down
6 changes: 6 additions & 0 deletions backend/src/main/java/codezap/category/domain/Category.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ public void validateAuthorization(Member member) {
}
}

public void validateDefaultCategory() {
if (isDefault()) {
throw new CodeZapException(ErrorCode.DEFAULT_CATEGORY, "기본 카테고리는 수정 및 삭제할 수 없습니다.");
}
}

public boolean isDefault() {
return isDefault;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;

import codezap.category.dto.request.validation.ValidatedDuplicateNameRequest;
import codezap.category.dto.request.validation.ValidatedDuplicateIdRequest;
import codezap.global.validation.ValidationGroups.NotNullGroup;
import codezap.template.dto.request.validation.ValidatedOrdinalRequest;
import codezap.global.validation.ValidatedOrdinalRequest;
import io.swagger.v3.oas.annotations.media.Schema;

public record UpdateAllCategoriesRequest(
Expand All @@ -22,12 +24,28 @@ public record UpdateAllCategoriesRequest(
@Schema(description = "삭제할 카테고리 목록")
@NotNull(message = "삭제하는 카테고리 ID 목록이 null 입니다.", groups = NotNullGroup.class)
List<Long> deleteCategoryIds
) implements ValidatedOrdinalRequest {
) implements ValidatedOrdinalRequest, ValidatedDuplicateIdRequest, ValidatedDuplicateNameRequest {
@Override
public List<Integer> extractOrdinal() {
return Stream.concat(
createCategories.stream().map(CreateCategoryRequest::ordinal),
updateCategories.stream().map(UpdateCategoryRequest::ordinal)
).sorted().toList();
}

@Override
public List<Long> extractIds() {
return Stream.concat(
updateCategories.stream().map(UpdateCategoryRequest::id),
deleteCategoryIds.stream()
).toList();
}

@Override
public List<String> extractNames() {
return Stream.concat(
createCategories.stream().map(CreateCategoryRequest::name),
updateCategories.stream().map(UpdateCategoryRequest::name)
).toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package codezap.category.dto.request.validation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DuplicateIdValidator.class)
public @interface DuplicateId {

String message();

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package codezap.category.dto.request.validation;

import java.util.HashSet;
import java.util.List;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class DuplicateIdValidator implements ConstraintValidator<DuplicateId, ValidatedDuplicateIdRequest> {

@Override
public boolean isValid(ValidatedDuplicateIdRequest request,
ConstraintValidatorContext constraintValidatorContext
) {
List<Long> ids = request.extractIds();
return ids.size() == new HashSet<>(ids).size();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package codezap.category.dto.request.validation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DuplicateNameValidator.class)
public @interface DuplicateName {

String message();

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package codezap.category.dto.request.validation;

import java.util.HashSet;
import java.util.List;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class DuplicateNameValidator implements ConstraintValidator<DuplicateName, ValidatedDuplicateNameRequest> {
@Override
public boolean isValid(ValidatedDuplicateNameRequest request,
ConstraintValidatorContext constraintValidatorContext
) {
List<String> names = request.extractNames();
return names.size() == new HashSet<>(names).size();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package codezap.category.dto.request.validation;

import java.util.List;

import codezap.global.validation.ValidationGroups.DuplicateIdGroup;

@DuplicateId(message = "id가 중복되었습니다.", groups = DuplicateIdGroup.class)
public interface ValidatedDuplicateIdRequest {

List<Long> extractIds();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package codezap.category.dto.request.validation;

import java.util.List;

import codezap.global.validation.ValidationGroups.DuplicateNameGroup;

@DuplicateName(message = "카테고리명이 중복되었습니다.", groups = DuplicateNameGroup.class)
public interface ValidatedDuplicateNameRequest {

List<String> extractNames();
}
77 changes: 23 additions & 54 deletions backend/src/main/java/codezap/category/service/CategoryService.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package codezap.category.service;

import java.util.HashSet;
import java.util.List;
import java.util.stream.Stream;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -32,6 +30,7 @@ public class CategoryService {
public CreateCategoryResponse create(Member member, CreateCategoryRequest request) {
validateDuplicatedCategory(request.name(), member);
validateOrdinal(member, request);

Category category = categoryRepository.save(createCategory(member, request));
return CreateCategoryResponse.from(category);
}
Expand All @@ -50,38 +49,36 @@ public Category fetchById(Long id) {

@Transactional
public void updateCategories(Member member, UpdateAllCategoriesRequest request) {
validateDuplicateNameRequest(request);
validateIds(request);

createCategories(member, request.createCategories());
request.updateCategories().forEach(category -> update(member, category));
request.deleteCategoryIds().forEach(id -> delete(member, id));
createCategories(member, request);
updateCategories(request.updateCategories(), member);
deleteCategories(request.deleteCategoryIds(), member);

validateCategoriesCount(member, request);
}

private void createCategories(Member member, List<CreateCategoryRequest> requests) {
categoryRepository.saveAll(
requests.stream()
.map(createRequest -> createCategory(member, createRequest))
.toList()
);
private void createCategories(Member member, UpdateAllCategoriesRequest request) {
categoryRepository.saveAll(request.createCategories().stream()
.map(createRequest -> new Category(createRequest.name(), member, createRequest.ordinal()))
.toList());
}

private void update(Member member, UpdateCategoryRequest request) {
Category category = categoryRepository.fetchById(request.id());
category.validateAuthorization(member);
validateDefaultCategory(category);
category.update(request.name(), request.ordinal());
private void updateCategories(List<UpdateCategoryRequest> updates, Member member) {
updates.forEach(update -> {
Category category = categoryRepository.fetchById(update.id());
category.validateAuthorization(member);
category.validateDefaultCategory();
category.update(update.name(), update.ordinal());
});
}

private void delete(Member member, Long categoryId) {
Category category = categoryRepository.fetchById(categoryId);
category.validateAuthorization(member);

validateDefaultCategory(category);
validateHasTemplate(categoryId);
categoryRepository.deleteById(categoryId);
private void deleteCategories(List<Long> ids, Member member) {
ids.forEach(id -> {
Category category = categoryRepository.fetchById(id);
category.validateAuthorization(member);
category.validateDefaultCategory();
validateHasTemplate(id);
categoryRepository.deleteById(id);
});
}

private void validateDuplicatedCategory(String categoryName, Member member) {
Expand All @@ -90,29 +87,12 @@ private void validateDuplicatedCategory(String categoryName, Member member) {
}
}

private void validateDefaultCategory(Category category) {
if (category.isDefault()) {
throw new CodeZapException(ErrorCode.DEFAULT_CATEGORY, "기본 카테고리는 수정 및 삭제할 수 없습니다.");
}
}

private void validateHasTemplate(Long id) {
if (templateRepository.existsByCategoryId(id)) {
throw new CodeZapException(ErrorCode.CATEGORY_HAS_TEMPLATES, "템플릿이 존재하는 카테고리는 삭제할 수 없습니다.");
}
}

private void validateDuplicateNameRequest(UpdateAllCategoriesRequest request) {
List<String> allNames = Stream.concat(
request.createCategories().stream().map(CreateCategoryRequest::name),
request.updateCategories().stream().map(UpdateCategoryRequest::name)
).toList();

if (allNames.size() != new HashSet<>(allNames).size()) {
throw new CodeZapException(ErrorCode.INVALID_REQUEST, "요청에 중복된 카테고리 이름이 존재합니다.");
}
}

private void validateOrdinal(Member member, CreateCategoryRequest request) {
long ordinal = request.ordinal();
long count = categoryRepository.countByMember(member);
Expand All @@ -121,17 +101,6 @@ private void validateOrdinal(Member member, CreateCategoryRequest request) {
}
}

private void validateIds(UpdateAllCategoriesRequest request) {
List<Long> allIds = Stream.concat(
request.updateCategories().stream().map(UpdateCategoryRequest::id),
request.deleteCategoryIds().stream()
).toList();

if (allIds.size() != new HashSet<>(allIds).size()) {
throw new CodeZapException(ErrorCode.INVALID_REQUEST, "요청에 중복된 id가 존재합니다.");
}
}

private void validateCategoriesCount(Member member, UpdateAllCategoriesRequest request) {
if (request.updateCategories().size() + request.createCategories().size()
!= categoryRepository.countByMember(member) - 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ public interface OrdinalGroup {}
public interface SourceCodeCountGroup {}

public interface SizeCheckGroup {}

public interface DuplicateIdGroup{}

public interface DuplicateNameGroup {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@

import jakarta.validation.GroupSequence;

import codezap.global.validation.ValidationGroups.DuplicateIdGroup;
import codezap.global.validation.ValidationGroups.DuplicateNameGroup;
import codezap.global.validation.ValidationGroups.NotNullGroup;
import codezap.global.validation.ValidationGroups.OrdinalGroup;
import codezap.global.validation.ValidationGroups.SizeCheckGroup;
import codezap.global.validation.ValidationGroups.SourceCodeCountGroup;
import codezap.global.validation.ValidationGroups.OrdinalGroup;

@GroupSequence({
NotNullGroup.class,
SizeCheckGroup.class,
SourceCodeCountGroup.class,
OrdinalGroup.class})
OrdinalGroup.class,
DuplicateIdGroup.class,
DuplicateNameGroup.class})
public interface ValidationSequence {
}
Original file line number Diff line number Diff line change
Expand Up @@ -217,5 +217,43 @@ void nonSequentialCategoryOrdinal() throws Exception {
.andExpect(jsonPath("$.detail").value("순서가 잘못되었습니다."))
.andExpect(jsonPath("$.errorCode").value(1101));
}

@Test
@DisplayName("카테고리 편집 실패: 중복된 카테고리 이름")
void duplicatedCategoryName() throws Exception {
String duplicatedName = "duplicatedName";
CreateCategoryRequest createRequest = new CreateCategoryRequest(duplicatedName, 2);
UpdateCategoryRequest updateRequest = new UpdateCategoryRequest(1L, duplicatedName, 1);

var request = new UpdateAllCategoriesRequest(
List.of(createRequest),
List.of(updateRequest),
List.of());

mvc.perform(put("/categories")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.detail").value("카테고리명이 중복되었습니다."))
.andExpect(jsonPath("$.errorCode").value(1101));
}

@Test
@DisplayName("카테고리 편집 실패: 중복된 id 수정 및 삭제")
void deleteByIdFailDuplicatedId() throws Exception {
UpdateCategoryRequest updateRequest = new UpdateCategoryRequest(1L, "category1", 1);

var request = new UpdateAllCategoriesRequest(
List.of(),
List.of(updateRequest),
List.of(1L));

mvc.perform(put("/categories")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.detail").value("id가 중복되었습니다."))
.andExpect(jsonPath("$.errorCode").value(1101));
}
}
}
Loading

0 comments on commit 347eb85

Please sign in to comment.