-
Notifications
You must be signed in to change notification settings - Fork 8
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 #304 from woowacourse-teams/dev/be
[BE] 3차 데모데이 - 인증인가, 카테고리, 태그, 검색, 로깅
- Loading branch information
Showing
80 changed files
with
3,567 additions
and
214 deletions.
There are no files selected for viewing
65 changes: 65 additions & 0 deletions
65
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,65 @@ | ||
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; | ||
import codezap.member.configuration.BasicAuthentication; | ||
import codezap.member.dto.MemberDto; | ||
|
||
@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, | ||
@BasicAuthentication MemberDto memberDto | ||
) { | ||
Long createdCategoryId = categoryService.create(createCategoryRequest, memberDto); | ||
return ResponseEntity.created(URI.create("/categories/" + createdCategoryId)) | ||
.build(); | ||
} | ||
|
||
@GetMapping | ||
public ResponseEntity<FindAllCategoriesResponse> getCategories(@BasicAuthentication MemberDto memberDto) { | ||
return ResponseEntity.ok(categoryService.findAllByMember(memberDto)); | ||
} | ||
|
||
@PutMapping("/{id}") | ||
public ResponseEntity<Void> updateCategory( | ||
@PathVariable Long id, | ||
@Validated(ValidationSequence.class) @RequestBody UpdateCategoryRequest updateCategoryRequest, | ||
@BasicAuthentication MemberDto memberDto | ||
) { | ||
categoryService.update(id, updateCategoryRequest, memberDto); | ||
return ResponseEntity.ok().build(); | ||
} | ||
|
||
@DeleteMapping("/{id}") | ||
public ResponseEntity<Void> deleteCategory(@PathVariable Long id, @BasicAuthentication MemberDto memberDto) { | ||
categoryService.deleteById(id, memberDto); | ||
return ResponseEntity.noContent() | ||
.build(); | ||
} | ||
} |
57 changes: 57 additions & 0 deletions
57
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,57 @@ | ||
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 codezap.member.dto.MemberDto; | ||
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, MemberDto memberDto); | ||
|
||
@Operation(summary = "카테고리 목록 조회", description = "생성된 모든 카테고리를 조회합니다.") | ||
@ApiResponse(responseCode = "200", description = "조회 성공", | ||
content = {@Content(schema = @Schema(implementation = FindAllCategoriesResponse.class))}) | ||
ResponseEntity<FindAllCategoriesResponse> getCategories(MemberDto memberDto); | ||
|
||
@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, MemberDto memberDto); | ||
|
||
@Operation(summary = "카테고리 삭제", description = "해당하는 식별자의 카테고리를 삭제합니다.") | ||
@ApiResponse(responseCode = "204", description = "카테고리 삭제 성공") | ||
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/categories/1", errorCases = { | ||
@ErrorCase(description = "삭제하려는 카테고리에 템플릿이 존재하는 경우", | ||
exampleMessage = "템플릿이 존재하는 카테고리는 삭제할 수 없습니다."), | ||
}) | ||
ResponseEntity<Void> deleteCategory(Long id, MemberDto memberDto); | ||
} |
62 changes: 62 additions & 0 deletions
62
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,62 @@ | ||
package codezap.category.domain; | ||
|
||
import jakarta.persistence.Column; | ||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.FetchType; | ||
import jakarta.persistence.GeneratedValue; | ||
import jakarta.persistence.GenerationType; | ||
import jakarta.persistence.Id; | ||
import jakarta.persistence.ManyToOne; | ||
import jakarta.persistence.Table; | ||
import jakarta.persistence.UniqueConstraint; | ||
|
||
import codezap.global.auditing.BaseTimeEntity; | ||
import codezap.member.domain.Member; | ||
import lombok.AccessLevel; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Entity | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
@AllArgsConstructor | ||
@Getter | ||
@Table( | ||
uniqueConstraints={ | ||
@UniqueConstraint( | ||
name="name_with_member", | ||
columnNames={"member_id", "name"} | ||
) | ||
} | ||
) | ||
public class Category extends BaseTimeEntity { | ||
|
||
private static final String DEFAULT_CATEGORY_NAME = "카테고리 없음"; | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
@ManyToOne(fetch = FetchType.LAZY, optional = false) | ||
private Member member; | ||
|
||
@Column(nullable = false) | ||
private String name; | ||
|
||
@Column(nullable = false) | ||
private Boolean isDefault; | ||
|
||
public Category(String name, Member member) { | ||
this.name = name; | ||
this.member = member; | ||
this.isDefault = false; | ||
} | ||
|
||
public static Category createDefaultCategory(Member member) { | ||
return new Category(null, member, DEFAULT_CATEGORY_NAME, true); | ||
} | ||
|
||
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()); | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
backend/src/main/java/codezap/category/repository/CategoryJpaRepository.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,23 @@ | ||
package codezap.category.repository; | ||
|
||
import java.util.List; | ||
|
||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import org.springframework.http.HttpStatus; | ||
|
||
import codezap.category.domain.Category; | ||
import codezap.global.exception.CodeZapException; | ||
import codezap.member.domain.Member; | ||
|
||
@SuppressWarnings("unused") | ||
public interface CategoryJpaRepository extends CategoryRepository, JpaRepository<Category, Long> { | ||
|
||
default Category fetchById(Long id) { | ||
return findById(id).orElseThrow( | ||
() -> new CodeZapException(HttpStatus.NOT_FOUND, "식별자 " + id + "에 해당하는 카테고리가 존재하지 않습니다.")); | ||
} | ||
|
||
List<Category> findAllByMember(Member member); | ||
|
||
boolean existsByNameAndMember(String categoryName, Member member); | ||
} |
21 changes: 21 additions & 0 deletions
21
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,21 @@ | ||
package codezap.category.repository; | ||
|
||
import java.util.List; | ||
|
||
import codezap.category.domain.Category; | ||
import codezap.member.domain.Member; | ||
|
||
public interface CategoryRepository { | ||
|
||
Category fetchById(Long id); | ||
|
||
List<Category> findAllByMember(Member member); | ||
|
||
List<Category> findAll(); | ||
|
||
boolean existsByNameAndMember(String categoryName, Member member); | ||
|
||
Category save(Category category); | ||
|
||
void deleteById(Long id); | ||
} |
86 changes: 86 additions & 0 deletions
86
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,86 @@ | ||
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.member.domain.Member; | ||
import codezap.member.dto.MemberDto; | ||
import codezap.member.repository.MemberJpaRepository; | ||
import codezap.member.repository.MemberRepository; | ||
import codezap.template.repository.TemplateRepository; | ||
|
||
@Service | ||
public class CategoryService { | ||
|
||
private final CategoryRepository categoryRepository; | ||
private final TemplateRepository templateRepository; | ||
private final MemberRepository memberJpaRepository; | ||
|
||
public CategoryService(CategoryRepository categoryRepository, TemplateRepository templateRepository, | ||
MemberJpaRepository memberJpaRepository | ||
) { | ||
this.categoryRepository = categoryRepository; | ||
this.templateRepository = templateRepository; | ||
this.memberJpaRepository = memberJpaRepository; | ||
} | ||
|
||
@Transactional | ||
public Long create(CreateCategoryRequest createCategoryRequest, MemberDto memberDto) { | ||
String categoryName = createCategoryRequest.name(); | ||
Member member = memberJpaRepository.fetchById(memberDto.id()); | ||
validateDuplicatedCategory(categoryName, member); | ||
Category category = new Category(categoryName, member); | ||
return categoryRepository.save(category).getId(); | ||
} | ||
|
||
public FindAllCategoriesResponse findAllByMember(MemberDto memberDto) { | ||
Member member = memberJpaRepository.fetchById(memberDto.id()); | ||
return FindAllCategoriesResponse.from(categoryRepository.findAllByMember(member)); | ||
} | ||
|
||
public FindAllCategoriesResponse findAll() { | ||
return FindAllCategoriesResponse.from(categoryRepository.findAll()); | ||
} | ||
|
||
@Transactional | ||
public void update(Long id, UpdateCategoryRequest updateCategoryRequest, MemberDto memberDto) { | ||
Member member = memberJpaRepository.fetchById(memberDto.id()); | ||
validateDuplicatedCategory(updateCategoryRequest.name(), member); | ||
Category category = categoryRepository.fetchById(id); | ||
validateAuthorizeMember(category, member); | ||
category.updateName(updateCategoryRequest.name()); | ||
} | ||
|
||
private void validateDuplicatedCategory(String categoryName, Member member) { | ||
if (categoryRepository.existsByNameAndMember(categoryName, member)) { | ||
throw new CodeZapException(HttpStatus.CONFLICT, "이름이 " + categoryName + "인 카테고리가 이미 존재합니다."); | ||
} | ||
} | ||
|
||
public void deleteById(Long id, MemberDto memberDto) { | ||
Member member = memberJpaRepository.fetchById(memberDto.id()); | ||
Category category = categoryRepository.fetchById(id); | ||
validateAuthorizeMember(category, member); | ||
|
||
if (templateRepository.existsByCategoryId(id)) { | ||
throw new CodeZapException(HttpStatus.BAD_REQUEST, "템플릿이 존재하는 카테고리는 삭제할 수 없습니다."); | ||
} | ||
if (category.getIsDefault()) { | ||
throw new CodeZapException(HttpStatus.BAD_REQUEST, "기본 카테고리는 삭제할 수 없습니다."); | ||
} | ||
categoryRepository.deleteById(id); | ||
} | ||
|
||
private void validateAuthorizeMember(Category category, Member member) { | ||
if (!category.getMember().equals(member)) { | ||
throw new CodeZapException(HttpStatus.UNAUTHORIZED, "해당 카테고리를 수정 또는 삭제할 권한이 없는 유저입니다."); | ||
} | ||
} | ||
} |
Oops, something went wrong.