From b50f9c2c2289bdab4d6f57ac4f9d18759da020d1 Mon Sep 17 00:00:00 2001 From: ajy4304 Date: Mon, 22 Jul 2024 10:17:57 +0900 Subject: [PATCH 01/37] =?UTF-8?q?0=EB=8B=A8=EA=B3=84=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=A4=80=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 - build.gradle | 7 +- src/main/java/gift/config/LoginMember.java | 9 ++ .../config/LoginMemberArgumentResolver.java | 49 +++++++++ src/main/java/gift/config/WebConfig.java | 22 ++++ .../gift/controller/CategoryController.java | 50 +++++++++ .../gift/controller/MemberController.java | 34 ++++++ .../gift/controller/OptionController.java | 54 ++++++++++ .../gift/controller/ProductController.java | 65 +++++++++++ .../ThymeleafProductController.java | 94 ++++++++++++++++ .../gift/controller/WishListController.java | 40 +++++++ .../java/gift/dto/CategoryRequestDTO.java | 19 ++++ .../java/gift/dto/CategoryResponseDTO.java | 25 +++++ src/main/java/gift/dto/LoginResponseDTO.java | 14 +++ src/main/java/gift/dto/MemberRequestDTO.java | 42 ++++++++ src/main/java/gift/dto/MemberResponseDTO.java | 25 +++++ src/main/java/gift/dto/OptionRequestDTO.java | 39 +++++++ src/main/java/gift/dto/OptionResponseDTO.java | 34 ++++++ src/main/java/gift/dto/ProductRequestDTO.java | 42 ++++++++ .../java/gift/dto/ProductResponseDTO.java | 34 ++++++ .../gift/exception/DuplicateException.java | 8 ++ .../exception/GlobalExceptionHandler.java | 60 +++++++++++ src/main/java/gift/model/Category.java | 48 +++++++++ src/main/java/gift/model/Member.java | 71 ++++++++++++ src/main/java/gift/model/Option.java | 82 ++++++++++++++ src/main/java/gift/model/Product.java | 102 ++++++++++++++++++ src/main/java/gift/model/WishList.java | 61 +++++++++++ .../gift/repository/CategoryRepository.java | 8 ++ .../gift/repository/MemberRepository.java | 9 ++ .../gift/repository/OptionRepository.java | 10 ++ .../gift/repository/ProductRepository.java | 8 ++ .../gift/repository/WishListRepository.java | 13 +++ .../java/gift/service/CategoryService.java | 58 ++++++++++ src/main/java/gift/service/MemberService.java | 70 ++++++++++++ src/main/java/gift/service/OptionService.java | 61 +++++++++++ .../java/gift/service/ProductService.java | 75 +++++++++++++ .../java/gift/service/WishListService.java | 45 ++++++++ src/main/java/gift/util/JwtUtil.java | 61 +++++++++++ src/main/resources/application.properties | 9 +- src/main/resources/application.yml | 21 ++++ src/main/resources/data.sql | 8 ++ src/main/resources/schema.sql | 21 ++++ .../resources/templates/create-product.html | 29 +++++ .../resources/templates/edit-product.html | 30 ++++++ .../resources/templates/product-list.html | 34 ++++++ .../gift/repository/MemberRepositoryTest.java | 39 +++++++ .../repository/ProductRepositoryTest.java | 28 +++++ .../repository/WishListRepositoryTest.java | 54 ++++++++++ 48 files changed, 1819 insertions(+), 3 deletions(-) create mode 100644 src/main/java/gift/config/LoginMember.java create mode 100644 src/main/java/gift/config/LoginMemberArgumentResolver.java create mode 100644 src/main/java/gift/config/WebConfig.java create mode 100644 src/main/java/gift/controller/CategoryController.java create mode 100644 src/main/java/gift/controller/MemberController.java create mode 100644 src/main/java/gift/controller/OptionController.java create mode 100644 src/main/java/gift/controller/ProductController.java create mode 100644 src/main/java/gift/controller/ThymeleafProductController.java create mode 100644 src/main/java/gift/controller/WishListController.java create mode 100644 src/main/java/gift/dto/CategoryRequestDTO.java create mode 100644 src/main/java/gift/dto/CategoryResponseDTO.java create mode 100644 src/main/java/gift/dto/LoginResponseDTO.java create mode 100644 src/main/java/gift/dto/MemberRequestDTO.java create mode 100644 src/main/java/gift/dto/MemberResponseDTO.java create mode 100644 src/main/java/gift/dto/OptionRequestDTO.java create mode 100644 src/main/java/gift/dto/OptionResponseDTO.java create mode 100644 src/main/java/gift/dto/ProductRequestDTO.java create mode 100644 src/main/java/gift/dto/ProductResponseDTO.java create mode 100644 src/main/java/gift/exception/DuplicateException.java create mode 100644 src/main/java/gift/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/gift/model/Category.java create mode 100644 src/main/java/gift/model/Member.java create mode 100644 src/main/java/gift/model/Option.java create mode 100644 src/main/java/gift/model/Product.java create mode 100644 src/main/java/gift/model/WishList.java create mode 100644 src/main/java/gift/repository/CategoryRepository.java create mode 100644 src/main/java/gift/repository/MemberRepository.java create mode 100644 src/main/java/gift/repository/OptionRepository.java create mode 100644 src/main/java/gift/repository/ProductRepository.java create mode 100644 src/main/java/gift/repository/WishListRepository.java create mode 100644 src/main/java/gift/service/CategoryService.java create mode 100644 src/main/java/gift/service/MemberService.java create mode 100644 src/main/java/gift/service/OptionService.java create mode 100644 src/main/java/gift/service/ProductService.java create mode 100644 src/main/java/gift/service/WishListService.java create mode 100644 src/main/java/gift/util/JwtUtil.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/data.sql create mode 100644 src/main/resources/schema.sql create mode 100644 src/main/resources/templates/create-product.html create mode 100644 src/main/resources/templates/edit-product.html create mode 100644 src/main/resources/templates/product-list.html create mode 100644 src/test/java/gift/repository/MemberRepositoryTest.java create mode 100644 src/test/java/gift/repository/ProductRepositoryTest.java create mode 100644 src/test/java/gift/repository/WishListRepositoryTest.java diff --git a/README.md b/README.md index 8bbd0354f..e69de29bb 100644 --- a/README.md +++ b/README.md @@ -1 +0,0 @@ -# spring-gift-order \ No newline at end of file diff --git a/build.gradle b/build.gradle index df7db9334..a285cc94b 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ version = '0.0.1-SNAPSHOT' java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(17) } } @@ -18,9 +18,14 @@ repositories { } dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2' runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/src/main/java/gift/config/LoginMember.java b/src/main/java/gift/config/LoginMember.java new file mode 100644 index 000000000..ebc1f76cd --- /dev/null +++ b/src/main/java/gift/config/LoginMember.java @@ -0,0 +1,9 @@ +package gift.config; + +import java.lang.annotation.*; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface LoginMember { +} diff --git a/src/main/java/gift/config/LoginMemberArgumentResolver.java b/src/main/java/gift/config/LoginMemberArgumentResolver.java new file mode 100644 index 000000000..07dd23414 --- /dev/null +++ b/src/main/java/gift/config/LoginMemberArgumentResolver.java @@ -0,0 +1,49 @@ +package gift.config; + +import gift.dto.MemberRequestDTO; +import gift.service.MemberService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.MethodParameter; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.stereotype.Component; + +@Component +public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + private static final Logger logger = LoggerFactory.getLogger(LoginMemberArgumentResolver.class); + private final MemberService memberService; + + public LoginMemberArgumentResolver(MemberService memberService) { + this.memberService = memberService; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterAnnotation(LoginMember.class) != null; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + String token = webRequest.getHeader("Authorization"); + if (token == null || !token.startsWith("Bearer ")) { + logger.error("Authorization 헤더가 존재하지 않거나 유효하지 않습니다."); + throw new IllegalArgumentException("Authorization 헤더가 존재하지 않거나 유효하지 않습니다."); + } + token = token.substring(7); + logger.debug("추출된 토큰: {}", token); + String email = memberService.extractEmailFromToken(token); // 토큰에서 이메일 추출 + + if (email == null) { + logger.error("유효하지 않은 토큰입니다."); + throw new IllegalArgumentException("유효하지 않은 토큰입니다."); + } + + MemberRequestDTO memberRequestDTO = new MemberRequestDTO(); + memberRequestDTO.setToken(token); + memberRequestDTO.setEmail(email); // 이메일 설정 + return memberRequestDTO; + } +} diff --git a/src/main/java/gift/config/WebConfig.java b/src/main/java/gift/config/WebConfig.java new file mode 100644 index 000000000..dbc587a56 --- /dev/null +++ b/src/main/java/gift/config/WebConfig.java @@ -0,0 +1,22 @@ +package gift.config; + +import gift.service.MemberService; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + private final MemberService memberService; + + public WebConfig(MemberService memberService) { + this.memberService = memberService; + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new LoginMemberArgumentResolver(memberService)); + } +} diff --git a/src/main/java/gift/controller/CategoryController.java b/src/main/java/gift/controller/CategoryController.java new file mode 100644 index 000000000..e48369654 --- /dev/null +++ b/src/main/java/gift/controller/CategoryController.java @@ -0,0 +1,50 @@ +package gift.controller; + +import gift.dto.CategoryRequestDTO; +import gift.dto.CategoryResponseDTO; +import gift.model.Category; +import gift.service.CategoryService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/categories") +public class CategoryController { + + @Autowired + private CategoryService categoryService; + + // 모든 카테고리 조회 + @GetMapping + public ResponseEntity> getAllCategories() { + return ResponseEntity.ok(categoryService.getAllCategories()); + } + + // 특정 ID의 카테고리 조회 + @GetMapping("/{id}") + public ResponseEntity getCategoryById(@PathVariable Long id) { + return ResponseEntity.ok(categoryService.getCategoryById(id)); + } + + // 새로운 카테고리 생성 + @PostMapping + public ResponseEntity createCategory(@RequestBody CategoryRequestDTO categoryRequestDTO) { + return ResponseEntity.status(201).body(categoryService.createCategory(categoryRequestDTO)); + } + + // 기존 카테고리 업데이트 + @PutMapping("/{id}") + public ResponseEntity updateCategory(@PathVariable Long id, @RequestBody CategoryRequestDTO categoryRequestDTO) { + return ResponseEntity.ok(categoryService.updateCategory(id, categoryRequestDTO)); + } + + // 특정 ID의 카테고리 삭제 + @DeleteMapping("/{id}") + public ResponseEntity deleteCategory(@PathVariable Long id) { + categoryService.deleteCategory(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/gift/controller/MemberController.java b/src/main/java/gift/controller/MemberController.java new file mode 100644 index 000000000..fba8572c5 --- /dev/null +++ b/src/main/java/gift/controller/MemberController.java @@ -0,0 +1,34 @@ +package gift.controller; + +import gift.dto.LoginResponseDTO; +import gift.dto.MemberRequestDTO; +import gift.service.MemberService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api") +public class MemberController { + + @Autowired + private MemberService memberService; + + @PostMapping("/register") + public ResponseEntity register(@RequestBody MemberRequestDTO memberRequestDTO) { + memberService.register(memberRequestDTO); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + // 사용자 로그인 + @PostMapping("/login/token") + public ResponseEntity login(@RequestBody MemberRequestDTO memberRequestDTO) { + try { + LoginResponseDTO response = memberService.authenticate(memberRequestDTO); + return ResponseEntity.ok(response); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + } +} diff --git a/src/main/java/gift/controller/OptionController.java b/src/main/java/gift/controller/OptionController.java new file mode 100644 index 000000000..0164740fe --- /dev/null +++ b/src/main/java/gift/controller/OptionController.java @@ -0,0 +1,54 @@ +package gift.controller; + +import gift.dto.OptionRequestDTO; +import gift.dto.OptionResponseDTO; +import gift.service.OptionService; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/api/products/{productId}/options") +public class OptionController { + + @Autowired + private OptionService optionService; + + // 특정 상품의 모든 옵션 조회 + @GetMapping + public ResponseEntity> getOptionsByProductId(@PathVariable Long productId) { + List options = optionService.getOptionsByProductId(productId); + return ResponseEntity.ok(options); + } + + // 새로운 옵션 생성 + @PostMapping + public ResponseEntity createOption(@PathVariable Long productId, @Valid @RequestBody OptionRequestDTO optionRequestDTO) { + optionRequestDTO.setProductId(productId); // 상품 ID 설정 + OptionResponseDTO option = optionService.createOption(optionRequestDTO); + return ResponseEntity.status(201).body(option); + } + + // 옵션 업데이트 + @PutMapping("/{id}") + public ResponseEntity updateOption(@PathVariable Long productId, @PathVariable Long id, @Valid @RequestBody OptionRequestDTO optionRequestDTO) { + Optional updatedOption = optionService.updateOption(id, optionRequestDTO); + if (updatedOption.isEmpty()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(updatedOption.get()); + } + + // 옵션 삭제 + @DeleteMapping("/{id}") + public ResponseEntity deleteOption(@PathVariable Long productId, @PathVariable Long id) { + if (!optionService.deleteOption(id)) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/gift/controller/ProductController.java b/src/main/java/gift/controller/ProductController.java new file mode 100644 index 000000000..12d54f5b6 --- /dev/null +++ b/src/main/java/gift/controller/ProductController.java @@ -0,0 +1,65 @@ +package gift.controller; + +import gift.dto.ProductRequestDTO; +import gift.dto.ProductResponseDTO; +import gift.model.Product; +import gift.service.ProductService; +import jakarta.validation.Valid; +import java.util.List; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/products") +public class ProductController { + + @Autowired + private ProductService productService; + + // 모든 상품 조회 + @GetMapping + public ResponseEntity> getAllProducts(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { + Page products = productService.getAllProducts(page, size); + return ResponseEntity.ok(products); + } + // 특정 ID의 상품 조회 + @GetMapping("/{id}") + public ResponseEntity getProductById(@PathVariable Long id) { + Optional product = productService.getProductById(id); + if (product.isEmpty()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(product.get()); + + } + + // 새로운 상품 생성 + @PostMapping + public ResponseEntity> createProduct(@Valid @RequestBody ProductRequestDTO productRequest, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { + Page products = productService.createProduct(productRequest, page, size); + return ResponseEntity.status(201).body(products); + } + + // 기존 상품 업데이트 + @PutMapping("/{id}") + public ResponseEntity updateProduct(@PathVariable Long id, @Valid @RequestBody ProductRequestDTO productRequest) { + Optional updatedProduct = productService.updateProduct(id, productRequest); + if (updatedProduct.isEmpty()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(updatedProduct.get()); + } + + // 특정 ID 상품 삭제 + @DeleteMapping("/{id}") + public ResponseEntity deleteProduct(@PathVariable Long id) { + if (!productService.deleteProduct(id)) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/gift/controller/ThymeleafProductController.java b/src/main/java/gift/controller/ThymeleafProductController.java new file mode 100644 index 000000000..6800a557d --- /dev/null +++ b/src/main/java/gift/controller/ThymeleafProductController.java @@ -0,0 +1,94 @@ +package gift.controller; + +import gift.repository.ProductRepository; +import gift.model.Product; + +import jakarta.validation.Valid; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Controller +@RequestMapping("/products") +public class ThymeleafProductController { + + @Autowired + private ProductRepository productDatabase; + + // 모든 상품을 조회하여 모델에 추가하고, product-list 뷰를 반환하는 메서드 + @GetMapping + public String getAllProducts(Model model) { + List products = productDatabase.findAll(); + model.addAttribute("products", products); + return "product-list"; + } + + + // 새로운 상품을 추가하기 위한 폼을 만들고 모델에 추가 + // 현재 기본 생성자를 protected 로 변경해둬서 어플리케이션 실행에 지장 없도록 해당 메서드 주석 처리 +// @GetMapping("/new") +// public String createProductForm(Model model) { +// Product product = new Product(); +// model.addAttribute("product", product); +// return "create-product"; +// } + + + // 유효성 검사 후 통과하면 새로운 상품 추가 + @PostMapping + public String addProduct(@Valid @ModelAttribute("product") Product product, BindingResult bindingResult, Model model) { + if (bindingResult.hasErrors()) { + // 유효성 검사에 실패한 경우, 오류 메세지와 함께 다시 폼을 보여줌 + return "create-product"; + } + // 유효성 검사 성공 시, 데이터를 저장 + productDatabase.save(product); + // 데이터 저장 후, 제품 목록 페이지로 리다이렉션 + return "redirect:/products"; + } + + // 상품 수정 폼 생성 후 기존 상품 데이터를 모델에 추가 + @GetMapping("/edit/{id}") + public String editProductForm(@PathVariable("id") Long id, Model model) { + Optional productOpt = productDatabase.findById(id); + if (productOpt.isEmpty()) { + return "redirect:/products"; + } + model.addAttribute("product", productOpt.get()); + return "edit-product"; + } + + // 유효성 검증 후 통과하면 상품 정보 업데이트 + @PutMapping("/{id}") + public String updateProduct(@PathVariable("id") Long id,@Valid @ModelAttribute Product product, BindingResult bindingResult, Model model) { + if (bindingResult.hasErrors()) { + return "edit-product"; + } + + Optional existingProductOpt = productDatabase.findById(id); + if (existingProductOpt.isPresent()) { + Product existingProduct = existingProductOpt.get(); + existingProduct.setName(product.getName()); + existingProduct.setPrice(product.getPrice()); + existingProduct.setImageUrl(product.getImageUrl()); + productDatabase.save(existingProduct); + } + return "redirect:/products"; + } + + // 상품 삭제 + @DeleteMapping("/{id}") + public String deleteProduct(@PathVariable("id") Long id) { + Optional productOpt = productDatabase.findById(id); + if (productOpt.isPresent()) { + productDatabase.deleteById(id); + } + return "redirect:/products"; + } + +} diff --git a/src/main/java/gift/controller/WishListController.java b/src/main/java/gift/controller/WishListController.java new file mode 100644 index 000000000..ddb2d1643 --- /dev/null +++ b/src/main/java/gift/controller/WishListController.java @@ -0,0 +1,40 @@ +package gift.controller; + +import gift.config.LoginMember; +import gift.dto.MemberRequestDTO; +import gift.model.WishList; +import gift.service.WishListService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/wishlist") +public class WishListController { + + @Autowired + private final WishListService wishListService; + + public WishListController(WishListService wishListService) { + this.wishListService = wishListService; + } + + // 사용자의 위시 리스트를 조회 + @GetMapping + public List getWishlist(@LoginMember MemberRequestDTO memberRequestDTO, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { + return wishListService.getWishlist(memberRequestDTO, page, size); + } + + // 위시 리스트에 상품을 추가 + @PostMapping + public void addProductToWishlist(@LoginMember MemberRequestDTO memberRequestDTO, @RequestParam("productId") Long productId) { + wishListService.addProductToWishlist(memberRequestDTO, productId); + } + + // 위시 리스트에서 상품을 삭제 + @DeleteMapping("/{id}") + public void removeProductFromWishlist(@LoginMember MemberRequestDTO memberRequestDTO, @PathVariable("id") Long id) { + wishListService.removeProductFromWishlist(memberRequestDTO, id); + } +} diff --git a/src/main/java/gift/dto/CategoryRequestDTO.java b/src/main/java/gift/dto/CategoryRequestDTO.java new file mode 100644 index 000000000..77f544dfb --- /dev/null +++ b/src/main/java/gift/dto/CategoryRequestDTO.java @@ -0,0 +1,19 @@ +package gift.dto; + +import gift.model.Category; + +public class CategoryRequestDTO { + private String name; + + public CategoryRequestDTO(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public Category toEntity() { + return new Category(this.name); + } +} diff --git a/src/main/java/gift/dto/CategoryResponseDTO.java b/src/main/java/gift/dto/CategoryResponseDTO.java new file mode 100644 index 000000000..861f7dc95 --- /dev/null +++ b/src/main/java/gift/dto/CategoryResponseDTO.java @@ -0,0 +1,25 @@ +package gift.dto; + +import gift.model.Category; + +public class CategoryResponseDTO { + private Long id; + private String name; + + public CategoryResponseDTO(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public static CategoryResponseDTO fromEntity(Category category) { + return new CategoryResponseDTO(category.getId(), category.getName()); + } +} diff --git a/src/main/java/gift/dto/LoginResponseDTO.java b/src/main/java/gift/dto/LoginResponseDTO.java new file mode 100644 index 000000000..d07f45262 --- /dev/null +++ b/src/main/java/gift/dto/LoginResponseDTO.java @@ -0,0 +1,14 @@ +package gift.dto; + +public class LoginResponseDTO { + + private String accessToken; + + public LoginResponseDTO(String accessToken) { + this.accessToken = accessToken; + } + + public String getAccessToken() { + return accessToken; + } +} diff --git a/src/main/java/gift/dto/MemberRequestDTO.java b/src/main/java/gift/dto/MemberRequestDTO.java new file mode 100644 index 000000000..5dcb16304 --- /dev/null +++ b/src/main/java/gift/dto/MemberRequestDTO.java @@ -0,0 +1,42 @@ +package gift.dto; + +import gift.model.Member; + +public class MemberRequestDTO { + private String email; + private String password; + private String token; + + public MemberRequestDTO(String email, String password, String token) { + this.email = email; + this.password = password; + this.token = token; + } + + public MemberRequestDTO() { + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public void setEmail(String email) { + this.email = email; + } + + public Member toEntity() { + return new Member(this.email, this.password); + } +} diff --git a/src/main/java/gift/dto/MemberResponseDTO.java b/src/main/java/gift/dto/MemberResponseDTO.java new file mode 100644 index 000000000..28b59002f --- /dev/null +++ b/src/main/java/gift/dto/MemberResponseDTO.java @@ -0,0 +1,25 @@ +package gift.dto; + +import gift.model.Member; + +public class MemberResponseDTO { + private Long id; + private String email; + + public MemberResponseDTO(Long id, String email) { + this.id = id; + this.email = email; + } + + public Long getId() { + return id; + } + + public String getEmail() { + return email; + } + + public static MemberResponseDTO fromEntity(Member member) { + return new MemberResponseDTO(member.getId(), member.getEmail()); + } +} \ No newline at end of file diff --git a/src/main/java/gift/dto/OptionRequestDTO.java b/src/main/java/gift/dto/OptionRequestDTO.java new file mode 100644 index 000000000..98911a0cc --- /dev/null +++ b/src/main/java/gift/dto/OptionRequestDTO.java @@ -0,0 +1,39 @@ +package gift.dto; + +import gift.model.Option; +import gift.model.Product; + +public class OptionRequestDTO { + + private String name; + private int quantity; + private Long productId; + + protected OptionRequestDTO() {} + + public OptionRequestDTO(String name, int quantity, Long productId) { + this.name = name; + this.quantity = quantity; + this.productId = productId; + } + + public String getName() { + return name; + } + + public int getQuantity() { + return quantity; + } + + public Long getProductId() { + return productId; + } + + public Option toEntity(Product product) { + return new Option(this.name, this.quantity, product); + } + + public void setProductId(Long productId) { + this.productId = productId; + } +} diff --git a/src/main/java/gift/dto/OptionResponseDTO.java b/src/main/java/gift/dto/OptionResponseDTO.java new file mode 100644 index 000000000..e07c3a834 --- /dev/null +++ b/src/main/java/gift/dto/OptionResponseDTO.java @@ -0,0 +1,34 @@ +package gift.dto; + +import gift.model.Option; + +public class OptionResponseDTO { + + private Long id; + private String name; + private int quantity; + + protected OptionResponseDTO() {} + + public OptionResponseDTO(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; + } + + public static OptionResponseDTO fromEntity(Option option) { + return new OptionResponseDTO(option.getId(), option.getName(), option.getQuantity()); + } +} diff --git a/src/main/java/gift/dto/ProductRequestDTO.java b/src/main/java/gift/dto/ProductRequestDTO.java new file mode 100644 index 000000000..47bff7e87 --- /dev/null +++ b/src/main/java/gift/dto/ProductRequestDTO.java @@ -0,0 +1,42 @@ +package gift.dto; + +import gift.model.Category; +import gift.model.Product; +import jakarta.validation.constraints.*; + +public class ProductRequestDTO { + @NotBlank(message = "이름을 입력해주세요.") + @Pattern(regexp = "^[a-zA-Z0-9ㄱ-ㅎㅏ-ㅣ가-힣 ()\\[\\]\\+\\-\\&/_]*$", message = "이름에 허용되지 않은 특수문자가 포함되어 있습니다.(가능한 특수문자: ( ), [ ], +, -, &, /, _)") + @Pattern(regexp = "^(?!.*카카오).*$", message = "'카카오'라는 문구를 사용하시려면 담당 MD에게 문의 부탁드립니다.") + private String name; + + @NotBlank(message = "Image URL을 입력해주세요.") + private String imageUrl; + + @NotNull(message = "가격을 입력해주세요.") + @Min(value = 1, message = "가격은 1 미만이 될 수 없습니다.") + private Integer price; + + @Min(value = 1, message = "카테고리 ID는 1 이상이어야 합니다.") + private Long categoryId; + + public String getName() { + return name; + } + + public String getImageUrl() { + return imageUrl; + } + + public Integer getPrice() { + return price; + } + + public Long getCategoryId() { + return categoryId; + } + + public Product toEntity(Category category) { + return new Product(this.name, this.price, this.imageUrl, category); + } +} diff --git a/src/main/java/gift/dto/ProductResponseDTO.java b/src/main/java/gift/dto/ProductResponseDTO.java new file mode 100644 index 000000000..3749ec167 --- /dev/null +++ b/src/main/java/gift/dto/ProductResponseDTO.java @@ -0,0 +1,34 @@ +package gift.dto; + +import gift.model.Product; + +public class ProductResponseDTO { + private String name; + private int price; + private String imageUrl; + private String categoryName; + + public ProductResponseDTO(Product product) { + this.name = product.getName(); + this.price = product.getPrice(); + this.imageUrl = product.getImageUrl(); + this.categoryName = product.getCategory().getName(); + } + + public String getName() { + return name; + } + + public int getPrice() { + return price; + } + + public String getImageUrl() { + return imageUrl; + } + + public static ProductResponseDTO fromEntity(Product product) { + return new ProductResponseDTO(product); + } + +} diff --git a/src/main/java/gift/exception/DuplicateException.java b/src/main/java/gift/exception/DuplicateException.java new file mode 100644 index 000000000..83db0fdb9 --- /dev/null +++ b/src/main/java/gift/exception/DuplicateException.java @@ -0,0 +1,8 @@ +package gift.exception; + +public class DuplicateException extends RuntimeException{ + public DuplicateException(String message) { + super(message); + } + +} diff --git a/src/main/java/gift/exception/GlobalExceptionHandler.java b/src/main/java/gift/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..2d1b903ad --- /dev/null +++ b/src/main/java/gift/exception/GlobalExceptionHandler.java @@ -0,0 +1,60 @@ +package gift.exception; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestControllerAdvice +public class GlobalExceptionHandler { + private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationExceptions(MethodArgumentNotValidException ex) { + List> invalidParams = ex.getBindingResult().getFieldErrors().stream().map(fieldError -> { + Map error = new HashMap<>(); + error.put("field", fieldError.getField()); + error.put("rejectedValue", String.valueOf(fieldError.getRejectedValue())); + error.put("reason", fieldError.getDefaultMessage()); + return error; + }).collect(Collectors.toList()); + + Map response = new HashMap<>(); + response.put("type", "http://localhost:8080/api/members/validation-error"); + response.put("title", "유효하지 않은 요청입니다."); + response.put("status", HttpStatus.BAD_REQUEST.value()); + response.put("invalid-params", invalidParams); + + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(DuplicateException.class) + public ResponseEntity> handleDuplicateException(DuplicateException ex) { + Map response = new HashMap<>(); + response.put("type", "http://localhost:8080/api/members/duplicate"); + response.put("title", "이미 가입된 회원입니다."); + response.put("status", String.valueOf(HttpStatus.CONFLICT.value())); + response.put("message", ex.getMessage()); + return new ResponseEntity<>(response, HttpStatus.CONFLICT); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException ex) { + logger.error("IllegalArgumentException 처리: ", ex); + Map response = new HashMap<>(); + response.put("type", "http://localhost:8080/api/members/illegal-argument"); + response.put("title", "유효하지 않은 이메일 or 비밀번호입니다."); + response.put("status", String.valueOf(HttpStatus.BAD_REQUEST.value())); + response.put("message", ex.getMessage()); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } +} + diff --git a/src/main/java/gift/model/Category.java b/src/main/java/gift/model/Category.java new file mode 100644 index 000000000..854555e99 --- /dev/null +++ b/src/main/java/gift/model/Category.java @@ -0,0 +1,48 @@ +package gift.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import java.util.HashSet; +import java.util.Set; + +@Entity +public class Category { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "카테고리 이름을 입력해주세요") + private String name; + + @OneToMany(mappedBy = "category") + private Set products = new HashSet<>(); + + protected Category() { + } + + public Category(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Set getProducts() { + return products; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/gift/model/Member.java b/src/main/java/gift/model/Member.java new file mode 100644 index 000000000..12987b806 --- /dev/null +++ b/src/main/java/gift/model/Member.java @@ -0,0 +1,71 @@ +package gift.model; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import java.util.HashSet; +import java.util.Set; + +@Entity +public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Email(message = "유효한 이메일 주소여야 합니다.") + @NotBlank(message = "이메일은 공백이 될 수 없습니다.") + private String email; + + @NotBlank(message = "비밀번호는 공백이 될 수 없습니다.") + private String password; + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) + private Set wishLists = new HashSet<>(); + + protected Member() {} + + public Member(String email, String password) { + this.email = email; + this.password = password; + } + + public Long getId() { + return id; + } + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } + + // 현재 이메일과 비밀번호가 일치하는 경우에만 이메일을 바꿀 수 있도록 함 + public void setEmail(String email, String currentPassword) { + if (this.password.equals(currentPassword)) { + this.email = email; + } else { + throw new IllegalArgumentException("비밀번호가 일치하지 않습니다."); + } + } + + + // 현재 이메일과 비밀번호가 일치하는 경우에만 비밀번호를 바꿀 수 있도록 함 + public void setPassword(String newPassword, String currentPassword) { + if (this.password.equals(currentPassword)) { + this.password = newPassword; + } else { + throw new IllegalArgumentException("비밀번호가 일치하지 않습니다."); + } + } + + public boolean checkPassword(String password) { + return this.password.equals(password); + } +} diff --git a/src/main/java/gift/model/Option.java b/src/main/java/gift/model/Option.java new file mode 100644 index 000000000..66c49af8a --- /dev/null +++ b/src/main/java/gift/model/Option.java @@ -0,0 +1,82 @@ +package gift.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +@Entity +public class Option { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "옵션 이름을 입력해주세요.") + @Size(max = 50, message = "옵션 이름은 최대 50자까지 가능합니다.") + @Pattern(regexp = "^[a-zA-Z0-9ㄱ-ㅎㅏ-ㅣ가-힣 ()\\[\\]\\+\\-\\&/_]*$", message = "옵션 이름에 허용되지 않은 특수문자가 포함되어 있습니다.") + private String name; + + @Min(value = 1, message = "수량은 최소 1개 이상이어야 합니다.") + @Max(value = 99999999, message = "수량은 1억 개 미만이어야 합니다.") + private int quantity; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id", nullable = false) + private Product product; + + protected Option() { + + } + + public Option(String name, int quantity, Product product) { + this.name = name; + this.quantity = quantity; + this.product = product; + } + + public Long getId() { + return id; + } + + public @NotBlank(message = "옵션 이름을 입력해주세요.") @Size(max = 50, message = "옵션 이름은 최대 50자까지 가능합니다.") @Pattern(regexp = "^[a-zA-Z0-9ㄱ-ㅎㅏ-ㅣ가-힣 ()\\[\\]\\+\\-\\&/_]*$", message = "옵션 이름에 허용되지 않은 특수문자가 포함되어 있습니다.") String getName() { + return name; + } + + @Min(value = 1, message = "수량은 최소 1개 이상이어야 합니다.") + @Max(value = 99999999, message = "수량은 1억 개 미만이어야 합니다.") + public int getQuantity() { + return quantity; + } + + public Product getProduct() { + return product; + } + + public void setName(String name) { + this.name = name; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } + + public void setProduct(Product product) { + this.product = product; + } + + public void subtractQuantity(int quantityToSubtract) { + if (this.quantity < quantityToSubtract) { + throw new IllegalArgumentException("수량이 부족합니다."); + } + this.quantity -= quantityToSubtract; + } +} diff --git a/src/main/java/gift/model/Product.java b/src/main/java/gift/model/Product.java new file mode 100644 index 000000000..6f521ced3 --- /dev/null +++ b/src/main/java/gift/model/Product.java @@ -0,0 +1,102 @@ +package gift.model; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import java.util.HashSet; +import java.util.Set; + +@Entity +public class Product { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "이름은 공백이 될 수 없습니다.") + @Size(max = 15, message = "이름은 15자를 넘길 수 없습니다.") + @Pattern(regexp = "^[a-zA-Z0-9ㄱ-ㅎㅏ-ㅣ가-힣 ()\\[\\]\\+\\-\\&/_]*$", message = "이름에 허용되지 않은 특수문자가 포함되어 있습니다.(가능한 특수문자: ( ), [ ], +, -, &, /, _)") + @Pattern(regexp = "^(?!.*카카오).*$", message = "'카카오'라는 문구를 사용하시려면 담당 MD에게 문의 부탁드립니다.") + private String name; + + @Min(value = 1, message = "가격은 1 미만이 될 수 없습니다.") + private int price; + + @NotBlank(message = "Image URL을 입력해주세요.") + private String imageUrl; + + @ManyToOne + @JoinColumn(name = "category_id", nullable = false) + private Category category; + + @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) + private Set wishLists = new HashSet<>(); + + protected Product(){ + + } + + public Product(Long id, String name, int price, String imageUrl, Category category) { + this.id = id; + this.name = name; + this.price = price; + this.imageUrl = imageUrl; + this.category = category; + } + + public Product(String name, int price, String imageUrl, Category category) { + this.name = name; + this.price = price; + this.imageUrl = imageUrl; + this.category = category; + } + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public int getPrice() { + return price; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setName(String name) { + this.name = name; + } + + public void setPrice(int price) { + this.price = price; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public void setId(Long id) { + this.id = id; + } + + public Category getCategory() { + return category; + } + + public void setCategory(Category category) { + this.category = category; + } +} diff --git a/src/main/java/gift/model/WishList.java b/src/main/java/gift/model/WishList.java new file mode 100644 index 000000000..9195fdbf2 --- /dev/null +++ b/src/main/java/gift/model/WishList.java @@ -0,0 +1,61 @@ +package gift.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "wish_list") +public class WishList { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + public WishList(Member member, Product product) { + this.member = member; + this.product = product; + } + + protected WishList() { + } + + public Long getId() { + return id; + } + + + public Product getProduct() { + return product; + } + + public Member getMember() { + return member; + } + + public void setId(Long id) { + this.id = id; + } + + public void setMember(Member member) { + this.member = member; + } + + public void setProduct(Product product) { + this.product = product; + } +} diff --git a/src/main/java/gift/repository/CategoryRepository.java b/src/main/java/gift/repository/CategoryRepository.java new file mode 100644 index 000000000..a1b05dbc5 --- /dev/null +++ b/src/main/java/gift/repository/CategoryRepository.java @@ -0,0 +1,8 @@ +package gift.repository; + +import gift.model.Category; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CategoryRepository extends JpaRepository { + +} diff --git a/src/main/java/gift/repository/MemberRepository.java b/src/main/java/gift/repository/MemberRepository.java new file mode 100644 index 000000000..4172b0f8a --- /dev/null +++ b/src/main/java/gift/repository/MemberRepository.java @@ -0,0 +1,9 @@ +package gift.repository; + +import gift.model.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/gift/repository/OptionRepository.java b/src/main/java/gift/repository/OptionRepository.java new file mode 100644 index 000000000..fdde2d1e1 --- /dev/null +++ b/src/main/java/gift/repository/OptionRepository.java @@ -0,0 +1,10 @@ +package gift.repository; + +import gift.model.Option; +import gift.model.Product; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface OptionRepository extends JpaRepository { + List