Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

카테고리 순서 변경 #988

Merged
merged 61 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
2e7fd59
refactor: category에 ordinal 필드 추가
HoeSeong123 Dec 16, 2024
fc4681a
test: countByMember 테스트 코드 작성
HoeSeong123 Dec 16, 2024
2d61a2c
feat: 카테고리 조회 응답 api 변경
HoeSeong123 Dec 16, 2024
8e1037d
refactor: 중복된 클거스 MemberFixture 제
HoeSeong123 Dec 16, 2024
2b966cd
test: 카테고리 생성시 멤버별로 마지막 순서로 생성에 대한 테스트 코드 추가
HoeSeong123 Dec 16, 2024
c4f283f
feat: 카테고리 수정 요청 api 변경 및 순서 변경 로직 추가
HoeSeong123 Dec 16, 2024
d90802b
feat: 카테고리 삭제 요청 api 변경 및 순서 변경 로직 추가
HoeSeong123 Dec 17, 2024
339286a
feat: Category에 ordinal 필드 추가 sql 파일 생성
HoeSeong123 Dec 17, 2024
52810d3
fix: 카테고리를 여러개 삭제 시 순서가 제대로 정렬되지 않는 오류 수정
HoeSeong123 Dec 17, 2024
f8dfd92
test: 카테고리 삭제시 순서 정렬 테스트 코드 작성
HoeSeong123 Dec 17, 2024
ddcb834
refactor: 기본 카테고리 수정에 대한 검증 코드 추가
HoeSeong123 Dec 17, 2024
64096db
refactor: 카테고리 수정 시 순서 검증 로직 추가
HoeSeong123 Dec 17, 2024
bc199d8
refactor: 템플릿 순서 변경시 잘못된 순서로 요청할 경우 에러코드 변경
HoeSeong123 Dec 18, 2024
0102194
refactor: swagger 문서 변경사항 반영
HoeSeong123 Dec 18, 2024
fa04a1d
refactor: 매직넘버 상수화
HoeSeong123 Dec 18, 2024
d61ac47
refactor: requestDto 스키마 예시 변경
HoeSeong123 Dec 18, 2024
25e0e47
merge conflicts 해결
HoeSeong123 Dec 23, 2024
11ab595
refactor: 카테고리 편집 api 수정
HoeSeong123 Dec 23, 2024
9eea751
test: 카테고리 편집 테스트 코드 추가
HoeSeong123 Dec 23, 2024
5a2e765
refactor: 불필요한 클래스 삭제
HoeSeong123 Dec 23, 2024
a760937
docs: 스웨거 파일 수정
HoeSeong123 Dec 23, 2024
d795cbe
feat: 동일한 카테고리에 대해 수정 및 삭제 요청에 대한 예외처리
HoeSeong123 Dec 23, 2024
f94b833
feat: 카테고리의 개수가 정확하지 않은 경우에 대한 예외 처리
HoeSeong123 Dec 23, 2024
ff27d42
refactor: 개수 검증하는 시점 변경
HoeSeong123 Dec 23, 2024
ff3acb9
refactor: 불필요한 import 제거
HoeSeong123 Dec 23, 2024
92794bd
fix: 실패하는 테스트 코드 수정
HoeSeong123 Dec 23, 2024
29be344
docs: 카테고리 수정 문서 수정
HoeSeong123 Dec 27, 2024
1f93dc0
fix: 기본 카테고리 순서 0으로 고정
HoeSeong123 Dec 27, 2024
85e87cb
refactor: 생성자 체이닝
HoeSeong123 Dec 27, 2024
f96d279
feat: 카테고리 생성 dto에 최소 순서 검증 추가
HoeSeong123 Dec 27, 2024
39d0e0c
refactor: 카테고리 순서의 타입 int로 변경
HoeSeong123 Dec 27, 2024
26a0fab
feat(response): 카테고리 생성 응답 api 변경
HoeSeong123 Dec 27, 2024
c4a6ffc
refactor: 소스 코드 순서 검증 validator를 범용적으로 변경
HoeSeong123 Dec 27, 2024
ec7a539
refactor(category): OrdinalValidator를 사용하여 카테고리 순서 검증
HoeSeong123 Dec 27, 2024
74fc71c
refactor(service): for문을 forEach로 변경
HoeSeong123 Dec 27, 2024
9757b17
refactor(service): createCategories 파라미터 변경
HoeSeong123 Dec 27, 2024
771d391
docs: 카테고리 편집 요청에 대한 예외 문서 추가
HoeSeong123 Dec 27, 2024
f79c1c3
refactor: Category 테이블에 member, name 유니크 조건 삭제
HoeSeong123 Dec 27, 2024
601ff62
test: 실패하는 테스트 코드 수정
HoeSeong123 Dec 27, 2024
bb06d03
merge conflicts 해결
HoeSeong123 Dec 27, 2024
fa429ec
fix: 실패하는 테스트 코드 수정
HoeSeong123 Dec 27, 2024
6d27648
refactor(global): 순서 검증 관련 클래스 패키지 변경
HoeSeong123 Dec 27, 2024
347eb85
refactor: 카테고리 검증 로직 별 책임 분리
HoeSeong123 Dec 27, 2024
051e800
docs: 카테고리 편집 설명 문서 수정
HoeSeong123 Dec 28, 2024
73023b3
refactor(service): 반복적으로 사용되는 메서드 분리
HoeSeong123 Dec 28, 2024
85bf564
refactor(category): 순서, 이름, id에 대한 검증 로직을 서비스 계층에도 추가
HoeSeong123 Dec 28, 2024
3753a32
refactor: 검증 로직 클래스 분리
HoeSeong123 Dec 28, 2024
dcc09b5
feat(category): member, ordinal 필드에 대해 복합 유니크 키 설정
HoeSeong123 Dec 30, 2024
d9da7e5
refactor(category): 도메인에 대한 검증 책임 분리
HoeSeong123 Dec 30, 2024
f83d47a
refactor(category): 예외 메세지 통합
HoeSeong123 Dec 30, 2024
d4d756f
merge conflicts 해결
HoeSeong123 Dec 30, 2024
bc8f98a
refactor: 카테고리 검증에 대한 책임 분리
HoeSeong123 Dec 30, 2024
cc681f9
fix(service): 실패하는 테스트 코드 수정
HoeSeong123 Dec 30, 2024
0802f93
fix(service): 실패하는 테스트 코드 수정
HoeSeong123 Dec 30, 2024
008eceb
refactor: 중복 검증에 대한 책임을 서비스로 이동
HoeSeong123 Jan 4, 2025
2aace81
refactor: 사용하지 않는 코드 제거
HoeSeong123 Jan 4, 2025
6c4431a
Merge branch 'dev/be' into feat/966-change-category-ordinal
HoeSeong123 Jan 4, 2025
9e84fe5
fix: sql문 수정
HoeSeong123 Jan 4, 2025
3e870f6
Merge branch 'dev/be' into feat/966-change-category-ordinal
HoeSeong123 Jan 6, 2025
1ca4fcf
Merge branch 'dev/be' into feat/966-change-category-ordinal
HoeSeong123 Jan 6, 2025
ae70fa9
fix: flyway 버전 수정
HoeSeong123 Jan 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -16,7 +14,7 @@

import codezap.auth.configuration.AuthenticationPrinciple;
import codezap.category.dto.request.CreateCategoryRequest;
import codezap.category.dto.request.UpdateCategoryRequest;
import codezap.category.dto.request.UpdateAllCategoriesRequest;
import codezap.category.dto.response.CreateCategoryResponse;
import codezap.category.dto.response.FindAllCategoriesResponse;
import codezap.category.service.CategoryService;
Expand Down Expand Up @@ -45,19 +43,12 @@ public ResponseEntity<FindAllCategoriesResponse> getCategories(@RequestParam Lon
return ResponseEntity.ok(categoryService.findAllByMemberId(memberId));
}

@PutMapping("/{id}")
@PutMapping
public ResponseEntity<Void> updateCategory(
@AuthenticationPrinciple Member member,
@PathVariable Long id,
@Validated(ValidationSequence.class) @RequestBody UpdateCategoryRequest request
@Validated(ValidationSequence.class) @RequestBody UpdateAllCategoriesRequest request
) {
categoryService.update(member, id, request);
categoryService.updateCategories(member, request);
return ResponseEntity.ok().build();
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteCategory(@AuthenticationPrinciple Member member, @PathVariable Long id) {
categoryService.deleteById(member, id);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import org.springframework.http.ResponseEntity;

import codezap.category.dto.request.CreateCategoryRequest;
import codezap.category.dto.request.UpdateCategoryRequest;
import codezap.category.dto.request.UpdateAllCategoriesRequest;
import codezap.category.dto.response.CreateCategoryResponse;
import codezap.category.dto.response.FindAllCategoriesResponse;
import codezap.global.swagger.error.ApiErrorResponse;
Expand All @@ -31,9 +31,10 @@ public interface SpringDocCategoryController {
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/categories", errorCases = {
@ErrorCase(description = "모든 필드 중 null인 값이 있는 경우", exampleMessage = "카테고리 이름이 null 입니다."),
@ErrorCase(description = "카테고리 이름이 15자를 초과한 경우", exampleMessage = "카테고리 이름은 최대 15자까지 입력 가능합니다."),
@ErrorCase(description = "카테고리의 순서가 1보다 작은 경우", exampleMessage = "카테고리의 순서는 1 이상이어야 합니다."),
})
@ApiErrorResponse(status = HttpStatus.CONFLICT, instance = "/categories", errorCases = {
@ErrorCase(description = "동일한 이름의 카테고리가 존재하는 경우", exampleMessage = "이름이 Spring 인 카테고리가 이미 존재합니다."),
@ErrorCase(description = "동일한 이름의 카테고리가 존재하는 경우", exampleMessage = "카테고리명이 중복되었습니다."),
})
ResponseEntity<CreateCategoryResponse> createCategory(
Member member,
Expand All @@ -47,36 +48,27 @@ ResponseEntity<CreateCategoryResponse> createCategory(
ResponseEntity<FindAllCategoriesResponse> getCategories(Long memberId);

@SecurityRequirement(name = "쿠키 인증 토큰")
@Operation(summary = "카테고리 수정", description = "해당하는 식별자의 카테고리를 수정합니다.")
@Operation(summary = "카테고리 생성, 수정, 삭제", description = "카테고리를 생성, 수정, 삭제할 수 있습니다.")
@ApiResponse(responseCode = "200", description = "카테고리 수정 성공")
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/categories/1", errorCases = {
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/categories", errorCases = {
@ErrorCase(description = "기본 카테고리를 수정 또는 삭제한 경우", exampleMessage = "기본 카테고리는 수정 및 삭제할 수 없습니다."),
@ErrorCase(description = "카테고리 이름이 15자를 초과한 경우", exampleMessage = "카테고리 이름은 최대 15자까지 입력 가능합니다."),
@ErrorCase(description = "카테고리의 순서가 잘못된 경우", exampleMessage = "순서가 잘못되었습니다."),
@ErrorCase(description = "모든 필드 중 null인 값이 있는 경우", exampleMessage = "카테고리 이름이 null 입니다."),
@ErrorCase(description = "삭제하려는 카테고리에 템플릿이 존재하는 경우", exampleMessage = "템플릿이 존재하는 카테고리는 삭제할 수 없습니다."),
@ErrorCase(description = "카테고리의 순서가 1보다 작은 경우", exampleMessage = "카테고리의 순서는 1 이상이어야 합니다."),
@ErrorCase(description = "카테고리의 개수가 일치하지 않는 경우(수정되지 않은 카테고리도 모두 보내주어야 합니다.)",
exampleMessage = "카테고리의 개수가 일치하지 않습니다."),
})
@ApiErrorResponse(status = HttpStatus.NOT_FOUND, instance = "/categories/1", errorCases = {
@ApiErrorResponse(status = HttpStatus.NOT_FOUND, instance = "/categories", errorCases = {
@ErrorCase(description = "해당하는 id 값인 카테고리가 없는 경우", exampleMessage = "식별자 1에 해당하는 카테고리가 존재하지 않습니다."),
})
@ApiErrorResponse(status = HttpStatus.CONFLICT, instance = "/categories", errorCases = {
@ErrorCase(description = "동일한 이름의 카테고리가 존재하는 경우", exampleMessage = "이름이 Spring 인 카테고리가 이미 존재합니다."),
})
@ApiErrorResponse(status = HttpStatus.FORBIDDEN, instance = "/categories/1", errorCases = {
@ErrorCase(description = "카테고리를 수정할 권한이 없는 경우", exampleMessage = "해당 카테고리를 수정 또는 삭제할 권한이 없는 유저입니다.")
})
ResponseEntity<Void> updateCategory(Member member, Long id, UpdateCategoryRequest updateCategoryRequest);

@SecurityRequirement(name = "쿠키 인증 토큰")
@Operation(summary = "카테고리 삭제", description = "해당하는 식별자의 카테고리를 삭제합니다.")
@ApiResponse(responseCode = "204", description = "카테고리 삭제 성공")
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/categories/1", errorCases = {
@ErrorCase(description = "삭제하려는 카테고리에 템플릿이 존재하는 경우",
exampleMessage = "템플릿이 존재하는 카테고리는 삭제할 수 없습니다."),
})
@ApiErrorResponse(status = HttpStatus.NOT_FOUND, instance = "/categories/1", errorCases = {
@ErrorCase(description = "존재하지 않는 카테고리인 경우",
exampleMessage = "식별자 1에 해당하는 카테고리가 존재하지 않습니다."),
@ErrorCase(description = "동일한 이름의 카테고리가 존재하는 경우", exampleMessage = "카테고리명이 중복되었습니다."),
@ErrorCase(description = "중복된 id가 있는 경우", exampleMessage = "id가 중복되었습니다."),
})
@ApiErrorResponse(status = HttpStatus.FORBIDDEN, instance = "/categories/1", errorCases = {
@ErrorCase(description = "카테고리를 수정할 권한이 없는 경우",
exampleMessage = "해당 카테고리를 수정 또는 삭제할 권한이 없는 유저입니다.")
@ApiErrorResponse(status = HttpStatus.FORBIDDEN, instance = "/categories", errorCases = {
@ErrorCase(description = "카테고리를 수정 또는 삭제할 권한이 없는 경우", exampleMessage = "해당 카테고리를 수정 또는 삭제할 권한이 없는 유저입니다."),
})
ResponseEntity<Void> deleteCategory(Member member, Long id);
ResponseEntity<Void> updateCategory(Member member, UpdateAllCategoriesRequest request);
}
37 changes: 21 additions & 16 deletions backend/src/main/java/codezap/category/domain/Category.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
import jakarta.persistence.UniqueConstraint;

import codezap.global.auditing.BaseTimeEntity;
import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;
import codezap.member.domain.Member;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
Expand All @@ -25,17 +23,24 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Table(
uniqueConstraints = @UniqueConstraint(
name = "name_with_member",
columnNames = {"member_id", "name"}
),
uniqueConstraints = {
@UniqueConstraint(
name = "name_with_member",
columnNames = {"member_id", "name"}
),
@UniqueConstraint(
name = "ordinal_with_member",
columnNames = {"member_id", "ordinal"}
)
},
indexes = @Index(name = "idx_member_id", columnList = "member_id")
)
@Getter
@EqualsAndHashCode(of = "id", callSuper = false)
public class Category extends BaseTimeEntity {

private static final String DEFAULT_CATEGORY_NAME = "카테고리 없음";
private static final int DEFAULT_CATEGORY_ORDINAL = 0;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand All @@ -50,24 +55,24 @@ public class Category extends BaseTimeEntity {
@Column(nullable = false)
private Boolean isDefault;

public Category(String name, Member member) {
this.name = name;
this.member = member;
this.isDefault = false;
@Column(nullable = false)
private int ordinal;

public Category(String name, Member member, int ordinal) {
this(null, member, name, false, ordinal);
}

public static Category createDefaultCategory(Member member) {
return new Category(null, member, DEFAULT_CATEGORY_NAME, true);
return new Category(null, member, DEFAULT_CATEGORY_NAME, true, DEFAULT_CATEGORY_ORDINAL);
}

public void updateName(String name) {
public void update(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}

public void validateAuthorization(Member member) {
if (!getMember().equals(member)) {
throw new CodeZapException(ErrorCode.FORBIDDEN_ACCESS, "해당 카테고리를 수정 또는 삭제할 권한이 없는 유저입니다.");
}
public boolean hasAuthorization(Member member) {
return this.member.equals(member);
}

public boolean isDefault() {
Expand Down
23 changes: 23 additions & 0 deletions backend/src/main/java/codezap/category/domain/Ids.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package codezap.category.domain;

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

public class Ids {

private List<Long> ids;

public Ids(List<Long> ids) {
this.ids = ids;
}

public void validateIds() {
if (hasDuplicates()) {
throw new IllegalArgumentException("id가 중복되었습니다.");
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생성 시 즉시 검증되지 않고 public 메서드로 외부에서 호출하게 한 이유가 궁금해요!

생성자에서 처리하지 않는다면 외부에서 따로 검증 메서드를 호출해야 하는데, 휴먼 에러로 이 검증을 호출하지 않아버릴 수 있을 것 같아요 🤔 또 도메인 규칙을 지키지 않는 도메인 객체가 생성되어 남게 될 수도 있겠네요!

이것에 대해 어떻게 생각하는지 궁금해요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public void validateOrdinals(UpdateAllCategoriesRequest request) {
    try {
        Ordinals ordinals = new Ordinals(request.extractOrdinal());
    } catch (IllegalArgumentException e) {
        throw new CodeZapException(ErrorCode.INVALID_REQUEST, e.getMessage());
    }
}


public void validateIds(UpdateAllCategoriesRequest request) {
    try {
        Ids ids = new Ids(request.extractIds());
    } catch (IllegalArgumentException e) {
        throw new CodeZapException(ErrorCode.DUPLICATE_ID, e.getMessage());
    }
}

public void validateNames(UpdateAllCategoriesRequest request) {
    try {
        Names names = new Names(request.extractNames());
    } catch (IllegalArgumentException e) {
        throw new CodeZapException(ErrorCode.DUPLICATE_CATEGORY, e.getMessage());
    }
}

코드가 이런 느낌이 돼서 뭔가뭔가 어색하더라고요..ㅋㅋ
근데 몰리 말대로 도메인 규칙을 지키지 않는 객체가 생성될 수 있는 문제가 생기긴 하겠네요.
다른 분들 의견만 더 들어보고 수정해보겠습니다!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 이해한 몰리의 "생성 시 즉시 검증"은 다음과 같이 코드를 작성하는 것을 의미하는 것 같아요.

public class Ids {

    private final List<Long> ids;

    public Ids(List<Long> ids) {
        this.ids = validateDuplicates(ids);
    }

    private List<Long> validateDuplicates(List<Long> ids) {
        if (ids.size() != new HashSet<>(ids).size()) {
            throw new IllegalArgumentException("id가 중복되었습니다.");
        }
        return ids;
    }
}

저도 이런 경우의 도메인 검증 로직은 외부에서 호출되는 게 아니라 생성자에서 바로 이루어져야 한다고 생각해요.

Copy link
Contributor

@zeus6768 zeus6768 Dec 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그런데... 코드 리뷰를 하며 곰곰이 생각해보니 저는 ids라는 클래스를 도메인으로 바라보는 게 맞지 않는 것 같아요.

우리가 검증을 하는 이유는 DB에 저장될 데이터의 생성, 수정, 삭제라는 서비스 로직이 실행되기 위한 입력이 정상적인지 확인하기 위해서에요.
우리가 "요청을 받는 방식"을 정책적으로 결정함에 따라 생겨난 요구사항이에요.
그 과정에서 사용되는 ID라는 속성은 RDBMS와 JPA에 의존하기 때문에 생겨났을 뿐이고, 카테고리라는 도메인과 무관해요.

본 클래스는 컬렉션에서의 중복 검증 로직일 뿐이에요.
로직이 도메인의 특성에서 비롯되지 않았어요.
카테고리가 아닌 다른 도메인에서도 컬렉션의 중복 검증 로직은 얼마든지 이루어질 수 있어요.
실제로 SourceCodeService에서도 동일한 검증 로직을 수행하고 있죠.
그런 의미에서 본 ID 중복 검증 클래스가 하는 일은 JPA Entity의 ID의 컬렉션을 이용한 서비스 로직으로 간주해야 한다고 생각해요.
SourceCodeService에서 같은 로직을 사용하고 있으니, global 패키지의 util 로 분리해도 되겠군요.

Ordinals, Names 클래스도 같은 의견이에요.
두 클래스는 서비스 레이어에서 DTO를 사용하는 방식 때문에 만들어졌을 뿐이고, 다른 도메인 객체와 상호작용하지 않아요.
Ordinals, Names가 도메인 영역에서 의미를 갖고 다른 도메인 객체와 상호작용이 있다면 도메인으로 볼 수 있겠지만, 그렇지 않으므로 categoryValidationService로 옮겨져야 한다고 생각해요.

저는 몰리가 일급컬렉션을 이야기했을때, Categories를 만드는 것을 생각했어요.
그렇게되면 Categories 객체가 생성될 때 각 원소의 ordinal과 name 필드의 중복을 검사할 수 있죠.
다만 중복 검증 로직은 여전히 util로 분리할 여지가 있다고 생각하고, Categories 클래스를 만든다면 생성자에서 util 클래스의 정적 메서드를 호출해서 검증하는 게 좋을 것 같아요.

핵심은 검증이라고 해서 다 같은 검증이 아니라는 거에요. 도메인의 검증과, 서비스의 검증을 구분하자는 이야기를 하고 싶었어요. 예를 들어 Default 카테고리가 있다는 정책은 도메인 영역으로 봐도 좋다고 생각해요. 카테고리를 수정할 수 있다는 것도 도메인 영역이에요. 그래서 카테고리를 수정할 때, Default 카테고리를 수정하려 할 경우 도메인 객체에서 예외를 발생시켜도 된다고 생각해요. 그래서 위 Category 클래스에 같은 의도로 의견을 남겼답니다.

너무 길게 써서 죄송합니다... 😭
그러나 제 생각을 가능한 그대로 전달하는 게 서로에게 더 도움이 된다고 생각했어요.
혹시 제가 모르는 게 있거나, 다른 의견이 있다면 얼마든지 부탁해요!!

private boolean hasDuplicates() {
return ids.size() != new HashSet<>(ids).size();
}
}
23 changes: 23 additions & 0 deletions backend/src/main/java/codezap/category/domain/Names.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package codezap.category.domain;

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

public class Names {

private List<String> names;

public Names(List<String> names) {
this.names = names;
}

public void validateNames() {
if (hasDuplicates()) {
throw new IllegalArgumentException("카테고리명이 중복되었습니다.");
}
}

private boolean hasDuplicates() {
return names.size() != new HashSet<>(names).size();
}
}
24 changes: 24 additions & 0 deletions backend/src/main/java/codezap/category/domain/Ordinals.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package codezap.category.domain;

import java.util.List;
import java.util.stream.IntStream;

public class Ordinals {

private List<Integer> ordinals;

public Ordinals(List<Integer> ordinals) {
this.ordinals = ordinals;
}

public void validateOrdinals() {
if (!isSequential()) {
throw new IllegalArgumentException("순서가 잘못되었습니다.");
}
}

private boolean isSequential() {
return IntStream.range(0, ordinals.size())
.allMatch(index -> ordinals.get(index) == index + 1);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package codezap.category.dto.request;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import codezap.global.validation.ValidationGroups.NotNullGroup;
Expand All @@ -11,6 +13,11 @@ public record CreateCategoryRequest(
@Schema(description = "카테고리 이름", example = "Spring")
@NotBlank(message = "카테고리 이름이 null 입니다.", groups = NotNullGroup.class)
@Size(max = 15, message = "카테고리 이름은 최대 15자까지 입력 가능합니다.", groups = SizeCheckGroup.class)
String name
String name,

@Schema(description = "카테고리 순서", example = "1")
@NotNull(message = "카테고리 순서가 null 입니다.", groups = NotNullGroup.class)
@Min(value = 1, message = "카테고리의 순서는 1 이상이어야 합니다.")
Integer ordinal
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package codezap.category.dto.request;

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

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.global.validation.ValidatedOrdinalRequest;
import io.swagger.v3.oas.annotations.media.Schema;

public record UpdateAllCategoriesRequest(
@Schema(description = "생성할 카테고리 목록")
@Valid
List<CreateCategoryRequest> createCategories,

@Schema(description = "수정할 카테고리 목록")
@Valid
List<UpdateCategoryRequest> updateCategories,

@Schema(description = "삭제할 카테고리 목록")
@NotNull(message = "삭제하는 카테고리 ID 목록이 null 입니다.", groups = NotNullGroup.class)
List<Long> deleteCategoryIds
) 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
@@ -1,16 +1,27 @@
package codezap.category.dto.request;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import codezap.global.validation.ValidationGroups.NotNullGroup;
import codezap.global.validation.ValidationGroups.SizeCheckGroup;
import io.swagger.v3.oas.annotations.media.Schema;

public record UpdateCategoryRequest(
@Schema(description = "카테고리 ID", example = "1")
@NotNull(message = "카테고리 ID가 null 입니다.", groups = NotNullGroup.class)
Long id,

@Schema(description = "카테고리 이름", example = "Spring")
@NotBlank(message = "카테고리 이름이 null 입니다.", groups = NotNullGroup.class)
@Size(max = 15, message = "카테고리 이름은 최대 15자까지 입력 가능합니다.", groups = SizeCheckGroup.class)
String name
String name,

@Schema(description = "카테고리 순서", example = "1")
@NotNull(message = "카테고리 순서가 null 입니다.", groups = NotNullGroup.class)
@Min(value = 1, message = "카테고리의 순서는 1 이상이어야 합니다.")
Integer ordinal
) {
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package codezap.template.dto.request.validation;
package codezap.category.dto.request.validation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
Expand All @@ -10,8 +10,8 @@

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

String message();

Expand Down
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();
}
}
Loading
Loading