-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #252 from woowacourse-teams/feat/domain_update
템플릿 CRUD에 태그 및 카테고리 추가, 카테고리 CRUD
- Loading branch information
Showing
44 changed files
with
1,404 additions
and
114 deletions.
There are no files selected for viewing
60 changes: 60 additions & 0 deletions
60
backend/src/main/java/codezap/category/controller/CategoryController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package codezap.category.controller; | ||
|
||
import java.net.URI; | ||
|
||
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; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
import codezap.category.dto.request.CreateCategoryRequest; | ||
import codezap.category.dto.request.UpdateCategoryRequest; | ||
import codezap.category.dto.response.FindAllCategoriesResponse; | ||
import codezap.category.service.CategoryService; | ||
import codezap.global.validation.ValidationSequence; | ||
|
||
@RestController | ||
@RequestMapping("/categories") | ||
public class CategoryController implements SpringDocCategoryController { | ||
|
||
private final CategoryService categoryService; | ||
|
||
public CategoryController(CategoryService categoryService) { | ||
this.categoryService = categoryService; | ||
} | ||
|
||
@PostMapping | ||
public ResponseEntity<Void> createCategory( | ||
@Validated(ValidationSequence.class) @RequestBody CreateCategoryRequest createCategoryRequest | ||
) { | ||
Long createdCategoryId = categoryService.create(createCategoryRequest); | ||
return ResponseEntity.created(URI.create("/categories/" + createdCategoryId)) | ||
.build(); | ||
} | ||
|
||
@GetMapping | ||
public ResponseEntity<FindAllCategoriesResponse> getCategories() { | ||
return ResponseEntity.ok(categoryService.findAll()); | ||
} | ||
|
||
@PutMapping("/{id}") | ||
public ResponseEntity<Void> updateCategory( | ||
@PathVariable Long id, | ||
@Validated(ValidationSequence.class) @RequestBody UpdateCategoryRequest updateCategoryRequest | ||
) { | ||
categoryService.update(id, updateCategoryRequest); | ||
return ResponseEntity.ok().build(); | ||
} | ||
|
||
@DeleteMapping("/{id}") | ||
public ResponseEntity<Void> deleteCategory(@PathVariable Long id) { | ||
categoryService.deleteById(id); | ||
return ResponseEntity.noContent().build(); | ||
} | ||
} |
56 changes: 56 additions & 0 deletions
56
backend/src/main/java/codezap/category/controller/SpringDocCategoryController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package codezap.category.controller; | ||
|
||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
|
||
import codezap.category.dto.request.CreateCategoryRequest; | ||
import codezap.category.dto.request.UpdateCategoryRequest; | ||
import codezap.category.dto.response.FindAllCategoriesResponse; | ||
import codezap.global.swagger.error.ApiErrorResponse; | ||
import codezap.global.swagger.error.ErrorCase; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.headers.Header; | ||
import io.swagger.v3.oas.annotations.media.Content; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
|
||
@Tag(name = "카테고리 CRUD API", description = "카테고리 생성, 목록 조회, 삭제, 수정 API") | ||
public interface SpringDocCategoryController { | ||
|
||
@Operation(summary = "카테고리 생성", description = """ | ||
새로운 카테고리를 생성합니다. \n | ||
새로운 카테고리의 이름이 필요합니다. \n | ||
""") | ||
@ApiResponse(responseCode = "201", description = "카테고리 생성 성공", headers = { | ||
@Header(name = "생성된 카테고리의 API 경로", example = "/categories/1")}) | ||
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/categories", errorCases = { | ||
@ErrorCase(description = "모든 필드 중 null인 값이 있는 경우", exampleMessage = "카테고리 이름이 null 입니다."), | ||
@ErrorCase(description = "카테고리 이름이 255자를 초과한 경우", exampleMessage = "카테고리 이름은 최대 255자까지 입력 가능합니다."), | ||
@ErrorCase(description = "동일한 이름의 카테고리가 존재하는 경우", exampleMessage = "이름이 Spring 인 카테고리가 이미 존재합니다.") | ||
}) | ||
ResponseEntity<Void> createCategory(CreateCategoryRequest createCategoryRequest); | ||
|
||
@Operation(summary = "카테고리 목록 조회", description = "생성된 모든 카테고리를 조회합니다.") | ||
@ApiResponse(responseCode = "200", description = "조회 성공", | ||
content = {@Content(schema = @Schema(implementation = FindAllCategoriesResponse.class))}) | ||
ResponseEntity<FindAllCategoriesResponse> getCategories(); | ||
|
||
@Operation(summary = "카테고리 수정", description = "해당하는 식별자의 카테고리를 수정합니다.") | ||
@ApiResponse(responseCode = "200", description = "카테고리 수정 성공") | ||
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/categories/1", errorCases = { | ||
@ErrorCase(description = "해당하는 id 값인 카테고리가 없는 경우", | ||
exampleMessage = "식별자 1에 해당하는 카테고리가 존재하지 않습니다."), | ||
@ErrorCase(description = "동일한 이름의 카테고리가 존재하는 경우", | ||
exampleMessage = "이름이 Spring 인 카테고리가 이미 존재합니다.") | ||
}) | ||
ResponseEntity<Void> updateCategory(Long id, UpdateCategoryRequest updateCategoryRequest); | ||
|
||
@Operation(summary = "카테고리 삭제", description = "해당하는 식별자의 카테고리를 삭제합니다.") | ||
@ApiResponse(responseCode = "204", description = "카테고리 삭제 성공") | ||
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/categories/1", errorCases = { | ||
@ErrorCase(description = "삭제하려는 카테고리에 템플릿이 존재하는 경우", | ||
exampleMessage = "템플릿이 존재하는 카테고리는 삭제할 수 없습니다."), | ||
}) | ||
ResponseEntity<Void> deleteCategory(Long id); | ||
} |
32 changes: 32 additions & 0 deletions
32
backend/src/main/java/codezap/category/domain/Category.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package codezap.category.domain; | ||
|
||
import jakarta.persistence.Column; | ||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.GeneratedValue; | ||
import jakarta.persistence.GenerationType; | ||
import jakarta.persistence.Id; | ||
|
||
import codezap.global.auditing.BaseTimeEntity; | ||
import lombok.AccessLevel; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Entity | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
@Getter | ||
public class Category extends BaseTimeEntity { | ||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
@Column(nullable = false, unique = true) | ||
private String name; | ||
|
||
public Category(String name) { | ||
this.name = name; | ||
} | ||
|
||
public void updateName(String name) { | ||
this.name = name; | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
backend/src/main/java/codezap/category/dto/request/CreateCategoryRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package codezap.category.dto.request; | ||
|
||
import jakarta.validation.constraints.NotBlank; | ||
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 CreateCategoryRequest( | ||
@Schema(description = "카테고리 이름", example = "Spring") | ||
@NotBlank(message = "카테고리 이름이 null 입니다.", groups = NotNullGroup.class) | ||
@Size(max = 255, message = "카테고리 이름은 최대 255자까지 입력 가능합니다.", groups = SizeCheckGroup.class) | ||
String name | ||
) { | ||
} |
16 changes: 16 additions & 0 deletions
16
backend/src/main/java/codezap/category/dto/request/UpdateCategoryRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package codezap.category.dto.request; | ||
|
||
import jakarta.validation.constraints.NotBlank; | ||
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 = "카테고리 이름", example = "Spring") | ||
@NotBlank(message = "카테고리 이름이 null 입니다.", groups = NotNullGroup.class) | ||
@Size(max = 255, message = "카테고리 이름은 최대 255자까지 입력 가능합니다.", groups = SizeCheckGroup.class) | ||
String name | ||
) { | ||
} |
19 changes: 19 additions & 0 deletions
19
backend/src/main/java/codezap/category/dto/response/FindAllCategoriesResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package codezap.category.dto.response; | ||
|
||
import java.util.List; | ||
|
||
import codezap.category.domain.Category; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
|
||
public record FindAllCategoriesResponse( | ||
@Schema(description = "카테고리 목록") | ||
List<FindCategoryResponse> categories | ||
) { | ||
public static FindAllCategoriesResponse from(List<Category> categories) { | ||
return new FindAllCategoriesResponse( | ||
categories.stream() | ||
.map(FindCategoryResponse::from) | ||
.toList() | ||
); | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
backend/src/main/java/codezap/category/dto/response/FindCategoryResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package codezap.category.dto.response; | ||
|
||
import codezap.category.domain.Category; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
|
||
public record FindCategoryResponse( | ||
@Schema(description = "카테고리 식별자", example = "1") | ||
Long id, | ||
@Schema(description = "카테고리 이름", example = "Spring") | ||
String name | ||
) { | ||
public static FindCategoryResponse from(Category category) { | ||
return new FindCategoryResponse(category.getId(), category.getName()); | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
backend/src/main/java/codezap/category/repository/CategoryRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package codezap.category.repository; | ||
|
||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import org.springframework.http.HttpStatus; | ||
|
||
import codezap.category.domain.Category; | ||
import codezap.global.exception.CodeZapException; | ||
|
||
public interface CategoryRepository extends JpaRepository<Category, Long> { | ||
|
||
default Category fetchById(Long id) { | ||
return findById(id).orElseThrow( | ||
() -> new CodeZapException(HttpStatus.NOT_FOUND, "식별자 " + id + "에 해당하는 카테고리가 존재하지 않습니다.")); | ||
} | ||
|
||
boolean existsByName(String name); | ||
} |
69 changes: 69 additions & 0 deletions
69
backend/src/main/java/codezap/category/service/CategoryService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package codezap.category.service; | ||
|
||
import org.springframework.http.HttpStatus; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
import codezap.category.domain.Category; | ||
import codezap.category.dto.request.CreateCategoryRequest; | ||
import codezap.category.dto.request.UpdateCategoryRequest; | ||
import codezap.category.dto.response.FindAllCategoriesResponse; | ||
import codezap.category.repository.CategoryRepository; | ||
import codezap.global.exception.CodeZapException; | ||
import codezap.template.repository.TemplateRepository; | ||
|
||
@Service | ||
public class CategoryService { | ||
|
||
private static final long DEFAULT_CATEGORY = 1L; | ||
private final CategoryRepository categoryRepository; | ||
private final TemplateRepository templateRepository; | ||
|
||
public CategoryService(CategoryRepository categoryRepository, TemplateRepository templateRepository) { | ||
this.categoryRepository = categoryRepository; | ||
this.templateRepository = templateRepository; | ||
} | ||
|
||
@Transactional | ||
public Long create(CreateCategoryRequest createCategoryRequest) { | ||
String categoryName = createCategoryRequest.name(); | ||
validateDuplicatedCategory(categoryName); | ||
Category category = new Category(categoryName); | ||
return categoryRepository.save(category).getId(); | ||
} | ||
|
||
public FindAllCategoriesResponse findAll() { | ||
return FindAllCategoriesResponse.from(categoryRepository.findAll()); | ||
} | ||
|
||
@Transactional | ||
public void update(Long id, UpdateCategoryRequest updateCategoryRequest) { | ||
validateDuplicatedCategory(updateCategoryRequest.name()); | ||
Category category = categoryRepository.fetchById(id); | ||
category.updateName(updateCategoryRequest.name()); | ||
} | ||
|
||
private void validateDuplicatedCategory(String categoryName) { | ||
if (categoryRepository.existsByName(categoryName)) { | ||
throw new CodeZapException(HttpStatus.CONFLICT, "이름이 " + categoryName + "인 카테고리가 이미 존재합니다."); | ||
} | ||
} | ||
|
||
public void deleteById(Long id) { | ||
assertNoTemplates(id); | ||
assertDefaultCategory(id); | ||
categoryRepository.deleteById(id); | ||
} | ||
|
||
private void assertNoTemplates(Long id) { | ||
if (templateRepository.existsByCategoryId(id)) { | ||
throw new CodeZapException(HttpStatus.BAD_REQUEST, "템플릿이 존재하는 카테고리는 삭제할 수 없습니다."); | ||
} | ||
} | ||
|
||
private static void assertDefaultCategory(Long id) { | ||
if (id == DEFAULT_CATEGORY) { | ||
throw new CodeZapException(HttpStatus.BAD_REQUEST, "기본 카테고리는 삭제할 수 없습니다."); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
backend/src/main/java/codezap/global/validation/ValidationGroups.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package codezap.global.validation; | ||
|
||
public class ValidationGroups { | ||
public interface NotNullGroup {} | ||
|
||
public interface SnippetOrdinalGroup {} | ||
|
||
public interface SnippetCountGroup {} | ||
|
||
public interface SizeCheckGroup {} | ||
} |
16 changes: 16 additions & 0 deletions
16
backend/src/main/java/codezap/global/validation/ValidationSequence.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package codezap.global.validation; | ||
|
||
import jakarta.validation.GroupSequence; | ||
|
||
import codezap.global.validation.ValidationGroups.NotNullGroup; | ||
import codezap.global.validation.ValidationGroups.SizeCheckGroup; | ||
import codezap.global.validation.ValidationGroups.SnippetCountGroup; | ||
import codezap.global.validation.ValidationGroups.SnippetOrdinalGroup; | ||
|
||
@GroupSequence({ | ||
NotNullGroup.class, | ||
SizeCheckGroup.class, | ||
SnippetCountGroup.class, | ||
SnippetOrdinalGroup.class}) | ||
public interface ValidationSequence { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.