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

전남대 BE_지연우 4주차 과제(step1,2,3) #180

Open
wants to merge 23 commits into
base: speciling
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
44ca35a
docs: README.md 수정
speciling Jul 17, 2024
edeafe1
feat: 카테고리 기능 추가
speciling Jul 17, 2024
62cd08a
feat: 카테고리 추가 api 구현
speciling Jul 17, 2024
9fc04ac
feat: 카테고리 조회 api 구현
speciling Jul 17, 2024
1adf88e
feat: 카테고리 수정 api 구현
speciling Jul 17, 2024
b846a05
feat: 카테고리 삭제 api 구현
speciling Jul 17, 2024
1fa4004
test: 코드 변경에 따라 기존 테스트 코드 수정
speciling Jul 17, 2024
630a4fb
test: CategoryServiceTest 작성
speciling Jul 17, 2024
8cead08
refactor: 위시리스트 n+1문제 해결
speciling Jul 19, 2024
11e9122
refactor: method security 적용
speciling Jul 19, 2024
918567c
docs: README.md 수정
speciling Jul 19, 2024
02c9456
feat: 옵션 등록 기능 구현
speciling Jul 20, 2024
bed2c00
test: 옵션 등록 기능 테스트 작성
speciling Jul 20, 2024
ca72a61
fix: 상품 저장 기능 수정
speciling Jul 20, 2024
15a677e
fix: 상품 조회 기능 수정
speciling Jul 20, 2024
8ce269d
feat: 옵션 수정 기능 구현
speciling Jul 20, 2024
a60d11c
test: 옵션 수정 기능 테스트 작성
speciling Jul 20, 2024
77d782d
feat: 옵션 삭제 기능 구현
speciling Jul 20, 2024
3674dbb
test: 옵션 삭제 기능 테스트 작성
speciling Jul 20, 2024
22f50ea
docs: README.md 수정
speciling Jul 21, 2024
b53ef61
feat: 옵션 수량 차감 기능 구현
speciling Jul 21, 2024
d650025
test: 옵션 수량 차감 테스트 작성
speciling Jul 21, 2024
f9d133d
fix: 상품 조회 기능 수정
speciling Jul 21, 2024
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
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,27 @@
# spring-gift-enhancement
# spring-gift-enhancement
## 기능 요구사항
### STEP1
- 상품에는 항상 하나의 카테고리가 있어야 한다.
- 이미 등록된 카테고리만 사용 가능하다.
- 상품 카테고리는 수정할 수 있다.
- 관리자 화면에서 상품을 추가할 때 카테고리를 지정할 수 있다.
- Admin 권한의 사용자는 새로운 카테고리를 등록할 수 있다.
- Admin 권한의 사용자는 기존 카테고리의 이름을 변경할 수 있다.
- Admin 권한의 사용자는 기존 카테고리를 삭제할 수 있다.
- 등록된 모든 카테고리의 정보를 조회할 수 있다.

### STEP2
- 상품에는 항상 하나 이상의 옵션이 있어야 한다.
- 옵션 이름은 공백을 포함하여 최대 50자까지 입력할 수 있다.
- 특수 문자
- 가능: ( ), [ ], +, -, &, /, _
- 그 외 특수 문자 사용 불가
- 옵션 수량은 최소 1개 이상 1억 개 미만이다.
- 동일한 상품 내의 옵션 이름은 중복될 수 없다.
- SELLER는 자신이 등록한 상품의 옵션을 등록, 수정, 삭제 할 수 있다.
- ADMIN은 모든 상품의 옵션을 등록, 수정, 삭제 할 수 있다.
- 모든 사용자는 각 상품의 옵션을 조회할 수 있다.

### STEP3
- 상품 옵션의 수량을 지정된 숫자만큼 차감할 수 있다.
- 위 기능의 테스트 코드를 작성한다
Copy link

Choose a reason for hiding this comment

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

엔드포인트마다 인증, 인가 여부를 다르게 한 부분 좋았습니다.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package gift.doamin.category.controller;

import gift.doamin.category.dto.CategoryForm;
import gift.doamin.category.dto.CategoryParam;
import gift.doamin.category.service.CategoryService;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
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.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("api/categories")
public class CategoryController {

private final CategoryService categoryService;

public CategoryController(CategoryService categoryService) {
this.categoryService = categoryService;
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@PreAuthorize("hasRole('ROLE_ADMIN')")
public void createNewCategory(@RequestBody CategoryForm categoryForm) {
categoryService.createCategory(categoryForm);
}

@GetMapping
public List<CategoryParam> getAllCategories() {
return categoryService.getAllCategories();
}

@GetMapping("/{id}")
public CategoryParam getOneCategory(@PathVariable Long id) {
return categoryService.getCategory(id);
}

@PutMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasRole('ROLE_ADMIN')")
public void changeCategoryName(@RequestBody CategoryForm categoryForm, @PathVariable Long id) {
categoryForm.setId(id);
categoryService.updateCategory(categoryForm);
}
Comment on lines +49 to +52
Copy link

Choose a reason for hiding this comment

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

Suggested change
public void changeCategoryName(@RequestBody CategoryForm categoryForm, @PathVariable Long id) {
categoryForm.setId(id);
categoryService.updateCategory(categoryForm);
}
public void changeCategoryName(@RequestBody CategoryForm categoryForm, @PathVariable Long id) {
categoryService.updateCategory(id, categoryForm);
}

이런 방식은 어떨까요?


@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasRole('ROLE_ADMIN')")
public void deleteCategory(@PathVariable Long id) {
categoryService.deleteCategory(id);
}
}
29 changes: 29 additions & 0 deletions src/main/java/gift/doamin/category/dto/CategoryForm.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package gift.doamin.category.dto;

import com.fasterxml.jackson.annotation.JsonCreator;
import jakarta.validation.constraints.NotBlank;

public class CategoryForm {

private Long id;

@NotBlank
private String name;

@JsonCreator
Copy link

Choose a reason for hiding this comment

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

JsonCreator는 필요하지 않습니다~

public CategoryForm(String name) {
this.name = name;
}

public Long getId() {
return id;
}

public String getName() {
return name;
}

public void setId(Long id) {
this.id = id;
}
}
23 changes: 23 additions & 0 deletions src/main/java/gift/doamin/category/dto/CategoryParam.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package gift.doamin.category.dto;

import gift.doamin.category.entity.Category;

public class CategoryParam {

private Long id;

private String name;

public CategoryParam(Category category) {
this.id = category.getId();
this.name = category.getName();
}
Comment on lines +11 to +14
Copy link

Choose a reason for hiding this comment

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

정적 팩토리 메서드 사용을 추천드립니다.


public Long getId() {
return id;
}

public String getName() {
return name;
}
}
38 changes: 38 additions & 0 deletions src/main/java/gift/doamin/category/entity/Category.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package gift.doamin.category.entity;

import gift.doamin.category.dto.CategoryForm;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Category {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String name;

protected Category() {
}

public Category(String name) {
this.name = name;
}

public Long getId() {
return id;
}

public String getName() {
return name;
}

public void update(CategoryForm categoryForm) {
this.name = categoryForm.getName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package gift.doamin.category.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;

public class CategoryNotFoundException extends ResponseStatusException {

public CategoryNotFoundException() {
super(HttpStatus.BAD_REQUEST, "해당 카테고리가 존재하지 않습니다");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package gift.doamin.category.repository;

import gift.doamin.category.entity.Category;
import org.springframework.data.jpa.repository.JpaRepository;

public interface JpaCategoryRepository extends JpaRepository<Category, Long> {

}
46 changes: 46 additions & 0 deletions src/main/java/gift/doamin/category/service/CategoryService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package gift.doamin.category.service;

import gift.doamin.category.dto.CategoryForm;
import gift.doamin.category.dto.CategoryParam;
import gift.doamin.category.entity.Category;
import gift.doamin.category.exception.CategoryNotFoundException;
import gift.doamin.category.repository.JpaCategoryRepository;
import java.util.List;
import org.springframework.stereotype.Service;

@Service
public class CategoryService {

private final JpaCategoryRepository categoryRepository;
Copy link

Choose a reason for hiding this comment

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

Suggested change
private final JpaCategoryRepository categoryRepository;
private final CategoryRepository categoryRepository;

이렇게 작성해도 잘 작동되게 해주세요~
스프링의 핵신 기능 DI/IoC를 올바르게 사용하지 않고 계세요.


public CategoryService(JpaCategoryRepository categoryRepository) {
this.categoryRepository = categoryRepository;
}

public void createCategory(CategoryForm categoryForm) {
categoryRepository.save(new Category(categoryForm.getName()));
}

public List<CategoryParam> getAllCategories() {
return categoryRepository.findAll().stream().map(CategoryParam::new).toList();
}

public CategoryParam getCategory(Long id) {
return categoryRepository.findById(id).map(CategoryParam::new)
.orElseThrow(CategoryNotFoundException::new);
}

public void updateCategory(CategoryForm categoryForm) {
Copy link

Choose a reason for hiding this comment

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

@Transactional을 사용하는게 좋겠습니다.


Category category = categoryRepository.findById(categoryForm.getId())
.orElseThrow(CategoryNotFoundException::new);

category.update(categoryForm);

categoryRepository.save(category);
}

public void deleteCategory(Long id) {
categoryRepository.deleteById(id);
}
}
Copy link

Choose a reason for hiding this comment

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

옵션만을 조회하는 경우는 없을까요?
개발자 도구를 사용해 실제 카카오 선물하기에서 API를 어떻게 구성하고 있는지 살펴보시는 것도 좋겠습니다.

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package gift.doamin.product.controller;

import gift.doamin.product.dto.OptionForm;
import gift.doamin.product.service.OptionService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
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.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/products/{productId}/options")
public class OptionsController {

private final OptionService optionService;

public OptionsController(OptionService optionService) {
this.optionService = optionService;
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void addOption(@PathVariable Long productId, @Valid @RequestBody OptionForm optionForm) {
optionService.create(productId, optionForm);
}

@PutMapping("/{optionId}")
public void updateOption(@PathVariable Long productId, @PathVariable Long optionId,
@Valid @RequestBody OptionForm optionForm) {
optionService.update(productId, optionId, optionForm);
}

@DeleteMapping("/{optionId}")
public void deleteOption(@PathVariable Long productId, @PathVariable Long optionId) {
optionService.delete(productId, optionId);
}
Comment on lines +26 to +41
Copy link

Choose a reason for hiding this comment

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

해당 부분도 admin 성격이라서 인증, 인가가 필요합니다.

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@
import gift.doamin.product.dto.ProductForm;
import gift.doamin.product.dto.ProductParam;
import gift.doamin.product.service.ProductService;
import gift.doamin.user.entity.UserRole;
import jakarta.validation.Valid;
import java.security.Principal;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand Down Expand Up @@ -52,23 +49,16 @@ public ProductParam getOneProduct(@PathVariable Long id) {
}

@PutMapping("/{id}")
public ProductParam updateProduct(@PathVariable Long id, @RequestBody ProductForm productForm,
Authentication authentication) {
Long userId = Long.valueOf(authentication.getName());
boolean isSeller = authentication.getAuthorities()
.contains(new SimpleGrantedAuthority(UserRole.SELLER.getValue()));
productForm.setUserId(userId);
public ProductParam updateProduct(@PathVariable Long id,
@Valid @RequestBody ProductForm productForm) {

return productService.update(id, productForm, isSeller);
return productService.update(id, productForm);
}

@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteProduct(@PathVariable Long id, Authentication authentication) {
Long userId = Long.valueOf(authentication.getName());
boolean isSeller = authentication.getAuthorities()
.contains(new SimpleGrantedAuthority(UserRole.SELLER.getValue()));
public void deleteProduct(@PathVariable Long id) {

productService.delete(userId, id, isSeller);
productService.delete(id);
}
}
33 changes: 33 additions & 0 deletions src/main/java/gift/doamin/product/dto/OptionForm.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package gift.doamin.product.dto;

import gift.doamin.product.entity.Option;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;

public class OptionForm {

@Pattern(regexp = "^[ㄱ-ㅎㅏ-ㅣ가-힣\\w ()\\[\\]+\\-&/_]{1,50}$", message = "이름 형식이 잘못되었습니다.")
private String name;

@Positive
@Max(99_999_999)
private Integer quantity;

public OptionForm(String name, Integer quantity) {
this.name = name;
this.quantity = quantity;
}

public String getName() {
return name;
}

public Integer getQuantity() {
return quantity;
}

public Option toEntity() {
return new Option(name, quantity);
}
}
26 changes: 26 additions & 0 deletions src/main/java/gift/doamin/product/dto/OptionParam.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package gift.doamin.product.dto;

public class OptionParam {

private Long id;
private String name;
private int quantity;

public OptionParam(Long id, String name, int quantity) {
this.id = id;
this.name = name;
this.quantity = quantity;
}

public Long getId() {
return id;
}

public String getName() {
return name;
}

public int getQuantity() {
return quantity;
}
}
Loading