From e5d6d1026f124e211e8d691aa02687f21c984fbd Mon Sep 17 00:00:00 2001 From: Seunghyun Ko Date: Mon, 29 Jul 2024 14:20:15 +0900 Subject: [PATCH 01/22] feat: init last week codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 지난 주까지 진행했던 kakao-gift-order의 코드를 옮겨왔습니다. --- .gitignore | 3 + README.md | 14 +- build.gradle | 11 + src/main/java/gift/Application.java | 2 + .../gift/product/config/SecurityConfig.java | 15 ++ .../gift/product/config/SwaggerConfig.java | 35 +++ .../gift/product/config/WebMvcConfig.java | 22 ++ .../controller/AdminCategoryController.java | 79 ++++++ .../controller/AdminOptionController.java | 79 ++++++ .../controller/AdminProductController.java | 101 ++++++++ .../controller/ApiCategoryController.java | 131 ++++++++++ .../controller/ApiMemberController.java | 108 +++++++++ .../controller/ApiOptionController.java | 190 +++++++++++++++ .../controller/ApiOrderController.java | 89 +++++++ .../controller/ApiProductController.java | 227 ++++++++++++++++++ .../controller/ApiWishListController.java | 139 +++++++++++ .../product/controller/KakaoController.java | 27 +++ .../controller/KakaoShopController.java | 28 +++ .../java/gift/product/dto/CategoryDTO.java | 30 +++ src/main/java/gift/product/dto/MemberDTO.java | 47 ++++ src/main/java/gift/product/dto/OptionDTO.java | 61 +++++ src/main/java/gift/product/dto/OrderDTO.java | 42 ++++ .../gift/product/dto/OrderRequestDTO.java | 46 ++++ .../gift/product/dto/OrderResponseDTO.java | 42 ++++ .../java/gift/product/dto/ProductDTO.java | 100 ++++++++ src/main/java/gift/product/dto/TokenDTO.java | 17 ++ .../java/gift/product/dto/WishRequestDTO.java | 31 +++ .../gift/product/dto/WishResponseDTO.java | 23 ++ .../product/exception/DuplicateException.java | 7 + .../exception/GlobalExceptionHandler.java | 123 ++++++++++ .../exception/InstanceValueException.java | 7 + .../product/exception/InvalidIdException.java | 7 + .../exception/InvalidValueException.java | 7 + .../exception/LastOptionException.java | 7 + .../exception/LoginFailedException.java | 7 + .../product/exception/RequestException.java | 8 + .../product/exception/ResponseException.java | 8 + .../exception/UnauthorizedException.java | 7 + .../product/intercepter/AuthInterceptor.java | 35 +++ .../java/gift/product/model/Category.java | 35 +++ src/main/java/gift/product/model/Member.java | 50 ++++ src/main/java/gift/product/model/Option.java | 74 ++++++ src/main/java/gift/product/model/Order.java | 75 ++++++ src/main/java/gift/product/model/Product.java | 76 ++++++ .../java/gift/product/model/SnsMember.java | 50 ++++ src/main/java/gift/product/model/Wish.java | 37 +++ .../repository/CategoryRepository.java | 9 + .../product/repository/MemberRepository.java | 12 + .../product/repository/OptionRepository.java | 22 ++ .../product/repository/OrderRepository.java | 8 + .../product/repository/ProductRepository.java | 14 ++ .../repository/SnsMemberRepository.java | 10 + .../repository/WishListRepository.java | 13 + .../gift/product/service/CategoryService.java | 61 +++++ .../gift/product/service/KakaoProperties.java | 10 + .../gift/product/service/KakaoService.java | 114 +++++++++ .../gift/product/service/MemberService.java | 47 ++++ .../gift/product/service/OptionService.java | 64 +++++ .../gift/product/service/OrderService.java | 117 +++++++++ .../gift/product/service/ProductService.java | 87 +++++++ .../gift/product/service/WishListService.java | 63 +++++ src/main/java/gift/product/util/JwtUtil.java | 100 ++++++++ .../validation/CategoryValidation.java | 53 ++++ .../product/validation/MemberValidation.java | 41 ++++ .../product/validation/OptionValidation.java | 74 ++++++ .../product/validation/ProductValidation.java | 69 ++++++ src/main/resources/application.properties | 24 ++ .../resources/templates/category-form.html | 25 ++ .../templates/category-update-form.html | 26 ++ src/main/resources/templates/category.html | 38 +++ .../resources/templates/product-form.html | 57 +++++ .../resources/templates/product-list.html | 47 ++++ .../templates/product-management-list.html | 60 +++++ .../templates/product-option-form.html | 36 +++ .../templates/product-option-update-form.html | 37 +++ .../resources/templates/product-option.html | 80 ++++++ .../templates/product-search-list.html | 57 +++++ .../templates/product-update-form.html | 50 ++++ .../controller/ApiProductControllerTest.java | 55 +++++ .../gift/product/integration/KakaoTest.java | 71 ++++++ .../repository/MemberRepositoryTest.java | 24 ++ .../repository/ProductRepositoryTest.java | 91 +++++++ .../repository/WishListRepositoryTest.java | 50 ++++ .../product/service/MemberServiceTest.java | 84 +++++++ .../product/service/ProductServiceTest.java | 116 +++++++++ .../validation/MemberValidationTest.java | 109 +++++++++ .../validation/ProductValidationTest.java | 47 ++++ 87 files changed, 4530 insertions(+), 1 deletion(-) create mode 100644 src/main/java/gift/product/config/SecurityConfig.java create mode 100644 src/main/java/gift/product/config/SwaggerConfig.java create mode 100644 src/main/java/gift/product/config/WebMvcConfig.java create mode 100644 src/main/java/gift/product/controller/AdminCategoryController.java create mode 100644 src/main/java/gift/product/controller/AdminOptionController.java create mode 100644 src/main/java/gift/product/controller/AdminProductController.java create mode 100644 src/main/java/gift/product/controller/ApiCategoryController.java create mode 100644 src/main/java/gift/product/controller/ApiMemberController.java create mode 100644 src/main/java/gift/product/controller/ApiOptionController.java create mode 100644 src/main/java/gift/product/controller/ApiOrderController.java create mode 100644 src/main/java/gift/product/controller/ApiProductController.java create mode 100644 src/main/java/gift/product/controller/ApiWishListController.java create mode 100644 src/main/java/gift/product/controller/KakaoController.java create mode 100644 src/main/java/gift/product/controller/KakaoShopController.java create mode 100644 src/main/java/gift/product/dto/CategoryDTO.java create mode 100644 src/main/java/gift/product/dto/MemberDTO.java create mode 100644 src/main/java/gift/product/dto/OptionDTO.java create mode 100644 src/main/java/gift/product/dto/OrderDTO.java create mode 100644 src/main/java/gift/product/dto/OrderRequestDTO.java create mode 100644 src/main/java/gift/product/dto/OrderResponseDTO.java create mode 100644 src/main/java/gift/product/dto/ProductDTO.java create mode 100644 src/main/java/gift/product/dto/TokenDTO.java create mode 100644 src/main/java/gift/product/dto/WishRequestDTO.java create mode 100644 src/main/java/gift/product/dto/WishResponseDTO.java create mode 100644 src/main/java/gift/product/exception/DuplicateException.java create mode 100644 src/main/java/gift/product/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/gift/product/exception/InstanceValueException.java create mode 100644 src/main/java/gift/product/exception/InvalidIdException.java create mode 100644 src/main/java/gift/product/exception/InvalidValueException.java create mode 100644 src/main/java/gift/product/exception/LastOptionException.java create mode 100644 src/main/java/gift/product/exception/LoginFailedException.java create mode 100644 src/main/java/gift/product/exception/RequestException.java create mode 100644 src/main/java/gift/product/exception/ResponseException.java create mode 100644 src/main/java/gift/product/exception/UnauthorizedException.java create mode 100644 src/main/java/gift/product/intercepter/AuthInterceptor.java create mode 100644 src/main/java/gift/product/model/Category.java create mode 100644 src/main/java/gift/product/model/Member.java create mode 100644 src/main/java/gift/product/model/Option.java create mode 100644 src/main/java/gift/product/model/Order.java create mode 100644 src/main/java/gift/product/model/Product.java create mode 100644 src/main/java/gift/product/model/SnsMember.java create mode 100644 src/main/java/gift/product/model/Wish.java create mode 100644 src/main/java/gift/product/repository/CategoryRepository.java create mode 100644 src/main/java/gift/product/repository/MemberRepository.java create mode 100644 src/main/java/gift/product/repository/OptionRepository.java create mode 100644 src/main/java/gift/product/repository/OrderRepository.java create mode 100644 src/main/java/gift/product/repository/ProductRepository.java create mode 100644 src/main/java/gift/product/repository/SnsMemberRepository.java create mode 100644 src/main/java/gift/product/repository/WishListRepository.java create mode 100644 src/main/java/gift/product/service/CategoryService.java create mode 100644 src/main/java/gift/product/service/KakaoProperties.java create mode 100644 src/main/java/gift/product/service/KakaoService.java create mode 100644 src/main/java/gift/product/service/MemberService.java create mode 100644 src/main/java/gift/product/service/OptionService.java create mode 100644 src/main/java/gift/product/service/OrderService.java create mode 100644 src/main/java/gift/product/service/ProductService.java create mode 100644 src/main/java/gift/product/service/WishListService.java create mode 100644 src/main/java/gift/product/util/JwtUtil.java create mode 100644 src/main/java/gift/product/validation/CategoryValidation.java create mode 100644 src/main/java/gift/product/validation/MemberValidation.java create mode 100644 src/main/java/gift/product/validation/OptionValidation.java create mode 100644 src/main/java/gift/product/validation/ProductValidation.java create mode 100644 src/main/resources/templates/category-form.html create mode 100644 src/main/resources/templates/category-update-form.html create mode 100644 src/main/resources/templates/category.html create mode 100644 src/main/resources/templates/product-form.html create mode 100644 src/main/resources/templates/product-list.html create mode 100644 src/main/resources/templates/product-management-list.html create mode 100644 src/main/resources/templates/product-option-form.html create mode 100644 src/main/resources/templates/product-option-update-form.html create mode 100644 src/main/resources/templates/product-option.html create mode 100644 src/main/resources/templates/product-search-list.html create mode 100644 src/main/resources/templates/product-update-form.html create mode 100644 src/test/java/gift/product/controller/ApiProductControllerTest.java create mode 100644 src/test/java/gift/product/integration/KakaoTest.java create mode 100644 src/test/java/gift/product/repository/MemberRepositoryTest.java create mode 100644 src/test/java/gift/product/repository/ProductRepositoryTest.java create mode 100644 src/test/java/gift/product/repository/WishListRepositoryTest.java create mode 100644 src/test/java/gift/product/service/MemberServiceTest.java create mode 100644 src/test/java/gift/product/service/ProductServiceTest.java create mode 100644 src/test/java/gift/product/validation/MemberValidationTest.java create mode 100644 src/test/java/gift/product/validation/ProductValidationTest.java diff --git a/.gitignore b/.gitignore index 0caf866b0..aa9f3d312 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ out/ ### Mac OS ### .DS_Store + +### properties ### +/src/main/resources/application.properties \ No newline at end of file diff --git a/README.md b/README.md index b2202f113..f5dad4019 100644 --- a/README.md +++ b/README.md @@ -1 +1,13 @@ -# spring-gift-point \ No newline at end of file +# spring-gift-point +> 카카오테크 캠퍼스 STEP2 - 6주차 클론 코딩 + +## 목차 +[* 코드 소개](#코드-소개)
+[* 0 단계 - 기본 코드 준비](#-0-단계----기본-코드-준비)
+ +## 코드 소개 +카카오 선물하기의 상품을 관리하는 서비스를 구현해본다 + +## < 0 단계 > - 기본 코드 준비 +### [ 기능 요구 사항 ] +- [spring-gift-order](https://github.com/chris0825/spring-gift-order.git)의 코드를 옮겨온다. \ No newline at end of file diff --git a/build.gradle b/build.gradle index df7db9334..d6051bb56 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,17 @@ dependencies { 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' + implementation 'org.springframework.boot:spring-boot-starter-logging' + implementation 'org.springframework.boot:spring-boot-starter-log4j2' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + compileOnly 'javax.servlet:javax.servlet-api:4.0.1' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' 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/Application.java b/src/main/java/gift/Application.java index 61603cca0..48078bd8e 100644 --- a/src/main/java/gift/Application.java +++ b/src/main/java/gift/Application.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +@ConfigurationPropertiesScan @SpringBootApplication public class Application { public static void main(String[] args) { diff --git a/src/main/java/gift/product/config/SecurityConfig.java b/src/main/java/gift/product/config/SecurityConfig.java new file mode 100644 index 000000000..9e9fc85b3 --- /dev/null +++ b/src/main/java/gift/product/config/SecurityConfig.java @@ -0,0 +1,15 @@ +package gift.product.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +@Configuration +public class SecurityConfig { + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + System.out.println("[SecurityConfig] bCryptPasswordEncoder()"); + return new BCryptPasswordEncoder(); + } + +} diff --git a/src/main/java/gift/product/config/SwaggerConfig.java b/src/main/java/gift/product/config/SwaggerConfig.java new file mode 100644 index 000000000..7a696dc94 --- /dev/null +++ b/src/main/java/gift/product/config/SwaggerConfig.java @@ -0,0 +1,35 @@ +package gift.product.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI().addSecurityItem(new SecurityRequirement(). + addList("Bearer Authentication")) + .components(new Components().addSecuritySchemes + ("Bearer Authentication", createAPIKeyScheme())) + .info(new Info().title("카카오 선물하기 API 문서") + .description("카카오 테크 캠퍼스 2기 강원대 BE_고승현") + .version("1.0").contact(new Contact().name("Seunghyun KO") + .email( "chris00825@naver.com").url("https://github.com/chris0825")) + .license(new License().name("MIT License") + .url("https://opensource.org/license/mit"))); + } + + private SecurityScheme createAPIKeyScheme() { + return new SecurityScheme().type(SecurityScheme.Type.HTTP) + .bearerFormat("JWT") + .scheme("bearer"); + } +} diff --git a/src/main/java/gift/product/config/WebMvcConfig.java b/src/main/java/gift/product/config/WebMvcConfig.java new file mode 100644 index 000000000..480b1591f --- /dev/null +++ b/src/main/java/gift/product/config/WebMvcConfig.java @@ -0,0 +1,22 @@ +package gift.product.config; + +import gift.product.intercepter.AuthInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Autowired + private AuthInterceptor authInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authInterceptor) + .addPathPatterns("/api/**") + .excludePathPatterns("/api/kakao/**", "/api/member/**"); + } + +} diff --git a/src/main/java/gift/product/controller/AdminCategoryController.java b/src/main/java/gift/product/controller/AdminCategoryController.java new file mode 100644 index 000000000..93a37d5e0 --- /dev/null +++ b/src/main/java/gift/product/controller/AdminCategoryController.java @@ -0,0 +1,79 @@ +package gift.product.controller; + +import gift.product.dto.CategoryDTO; +import gift.product.model.Category; +import gift.product.service.CategoryService; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +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.RequestMapping; + +@Controller +@RequestMapping("/admin/category") +public class AdminCategoryController { + + private final CategoryService categoryService; + + @Autowired + public AdminCategoryController(CategoryService categoryService) { + this.categoryService = categoryService; + } + + @GetMapping + public String category(Model model, Pageable pageable) { + model.addAttribute("categoryList", categoryService.findAllCategory(pageable)); + return "category"; + } + + @GetMapping("/register") + public String registerCategoryForm(Model model) { + System.out.println("[AdminCategoryController] registerCategoryForm()"); + model.addAttribute("categoryDTO", new CategoryDTO()); + return "category-form"; + } + + @PostMapping + public String registerCategory(@Valid @ModelAttribute CategoryDTO categoryDTO, BindingResult bindingResult, Model model) { + System.out.println("[AdminCategoryController] registerCategory()"); + if(bindingResult.hasErrors()) { + System.out.println("[AdminCategoryController] registerCategory(): validation error: " + bindingResult.getAllErrors() + ", categoryDTO: " + categoryDTO.getName()); + model.addAttribute("categoryDTO", categoryDTO); + return "category-form"; + } + categoryService.registerCategory(categoryDTO.convertToDomain()); + return "redirect:/admin/category"; + } + + @GetMapping("/{id}") + public String updateCategoryForm(@PathVariable Long id, Model model) { + System.out.println("[AdminCategoryController] updateCategoryForm()"); + model.addAttribute("categoryDTO", categoryService.findCategoryById(id)); + return "category-update-form"; + } + + @PutMapping("/{id}") + public String updateCategory(@PathVariable Long id, @Valid @ModelAttribute CategoryDTO categoryDTO, BindingResult bindingResult, Model model) { + System.out.println("[AdminCategoryController] editCategoryForm()"); + if(bindingResult.hasErrors()) { + model.addAttribute("categoryDTO", categoryDTO); + } + categoryService.updateCategory(new Category(id, categoryDTO.getName())); + return "redirect:/admin/category"; + } + + @DeleteMapping("/{id}") + public String deleteCategory(@PathVariable Long id) { + System.out.println("[AdminCategoryController] deleteCategory()"); + categoryService.deleteCategory(id); + return "redirect:/admin/category"; + } +} diff --git a/src/main/java/gift/product/controller/AdminOptionController.java b/src/main/java/gift/product/controller/AdminOptionController.java new file mode 100644 index 000000000..bda43f049 --- /dev/null +++ b/src/main/java/gift/product/controller/AdminOptionController.java @@ -0,0 +1,79 @@ +package gift.product.controller; + +import gift.product.dto.OptionDTO; +import gift.product.service.OptionService; +import gift.product.service.ProductService; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +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.RequestMapping; + +@Controller +@RequestMapping("/admin/product/{productId}/option") +public class AdminOptionController { + + private final OptionService optionService; + private final ProductService productService; + + @Autowired + public AdminOptionController( + OptionService optionService, + ProductService productService + ) { + this.optionService = optionService; + this.productService = productService; + } + + @GetMapping + public String getAllOptions(@PathVariable Long productId, Model model, Pageable pageable) { + System.out.println("[AdminOptionController] getAllOptions()"); + model.addAttribute("optionList", optionService.getAllOptions(productId, pageable)); + model.addAttribute("product", productService.findById(productId)); + return "product-option"; + } + + @GetMapping("/register") + public String showOptionRegisterForm(@PathVariable Long productId, Model model) { + System.out.println("[AdminOptionController] showOptionRegisterForm()"); + model.addAttribute("option", new OptionDTO()); + return "product-option-form"; + } + + @PostMapping + public String registerOption(@PathVariable Long productId, @Valid @ModelAttribute OptionDTO optionDTO, BindingResult bindingResult, Model model) { + System.out.println("[AdminOptionController] registerOption()"); + if (bindingResult.hasErrors()) { + model.addAttribute("option", optionDTO); + return "product-option-form"; + } + optionService.registerOption(productId, optionDTO); + return "redirect:/admin/product/" + productId + "/option"; + } + + @GetMapping("/{optionId}") + public String showOptionUpdateForm(@PathVariable Long productId, @PathVariable Long optionId, Model model) { + System.out.println("[AdminOptionController] showOptionUpdateForm()"); + model.addAttribute("option", optionService.findById(optionId)); + return "product-option-update-form"; + } + + @PutMapping("/{optionId}") + public String updateOption(@PathVariable Long optionId, @PathVariable Long productId, @Valid @ModelAttribute OptionDTO optionDTO, BindingResult bindingResult, Model model) { + System.out.println("[AdminOptionController] updateOption()"); + if(bindingResult.hasErrors()) { + model.addAttribute("option", optionDTO); + return "product-option-update-form"; + } + optionService.updateOption(optionId, productId, optionDTO); + return "redirect:/admin/product/" + productId + "/option"; + } + +} diff --git a/src/main/java/gift/product/controller/AdminProductController.java b/src/main/java/gift/product/controller/AdminProductController.java new file mode 100644 index 000000000..0c9423d8d --- /dev/null +++ b/src/main/java/gift/product/controller/AdminProductController.java @@ -0,0 +1,101 @@ +package gift.product.controller; + +import gift.product.dto.ProductDTO; +import gift.product.model.Category; +import gift.product.service.CategoryService; +import gift.product.service.ProductService; +import jakarta.validation.Valid; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +@RequestMapping("/admin/products") +public class AdminProductController { + + private final ProductService productService; + private final CategoryService categoryService; + + @Autowired + public AdminProductController( + ProductService productService, + CategoryService categoryService + ) { + this.productService = productService; + this.categoryService = categoryService; + } + + @GetMapping("/list") + public String showProductList(Model model, Pageable pageable) { + System.out.println("[ProductController] showProductList()"); + model.addAttribute("productList", productService.getAllProducts(pageable)); + return "product-management-list"; + } + + @GetMapping("/register") + public String showProductForm(Model model) { + System.out.println("[ProductController] showProductForm()"); + List categories = categoryService.getAllCategories(); + model.addAttribute("categories", categories); + model.addAttribute("product", new ProductDTO()); + return "product-form"; + } + + @PostMapping + public String registerProduct(@Valid @ModelAttribute ProductDTO productDTO, BindingResult bindingResult, Model model) { + System.out.println("[ProductController] registerProduct()"); + if (bindingResult.hasErrors()) { + model.addAttribute("product", productDTO); + return "product-form"; + } + productService.registerProduct(productDTO); + return "redirect:/admin/product/list"; + } + + @GetMapping("/{id}") + public String updateProductForm(@PathVariable Long id, Model model) { + System.out.println("[ProductController] updateProductForm()"); + List categories = categoryService.getAllCategories(); + model.addAttribute("categories", categories); + ProductDTO productDTO = productService.getDTOById(id); + model.addAttribute("product", productDTO); + return "product-update-form"; + } + + @PutMapping("/{id}") + public String updateProduct(@PathVariable Long id, @ModelAttribute ProductDTO productDTO, BindingResult bindingResult, Model model) { + System.out.println("[ProductController] updateProduct()"); + if (bindingResult.hasErrors()) { + model.addAttribute("product", productDTO); + return "product-form"; + } + productService.updateProduct(id, productDTO); + return "redirect:/admin/product/list"; + } + + @DeleteMapping("/{id}") + public String deleteProduct(@PathVariable Long id, Model model) { + System.out.println("[ProductController] deleteProduct()"); + productService.deleteProduct(id); + return "redirect:/admin/product/list"; + } + + @GetMapping("/search") + public String searchProduct(@RequestParam("keyword") String keyword, Model model, Pageable pageable) { + System.out.println("[ProductController] searchProduct()"); + model.addAttribute("searchResults", productService.searchProducts(keyword, pageable)); + model.addAttribute("keyword", keyword); + return "product-search-list"; + } +} diff --git a/src/main/java/gift/product/controller/ApiCategoryController.java b/src/main/java/gift/product/controller/ApiCategoryController.java new file mode 100644 index 000000000..bf07ee8a4 --- /dev/null +++ b/src/main/java/gift/product/controller/ApiCategoryController.java @@ -0,0 +1,131 @@ +package gift.product.controller; + +import gift.product.dto.CategoryDTO; +import gift.product.model.Category; +import gift.product.service.CategoryService; +import io.swagger.v3.oas.annotations.Operation; +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.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +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.RestController; + +@Tag(name = "Category", description = "API 컨트롤러") +@RestController +@RequestMapping("/api/category") +public class ApiCategoryController { + + private final CategoryService categoryService; + + @Autowired + public ApiCategoryController(CategoryService categoryService) { + this.categoryService = categoryService; + } + + @Operation( + summary = "카테고리 등록", + description = "상품의 분류를 정하는 카테고리를 등록합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "카테고리 등록 성공", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = Category.class) + ) + ), + @ApiResponse( + responseCode = "400", + description = "등록을 시도한 카테고리의 이름 누락" + ), + @ApiResponse( + responseCode = "401", + description = "인증과 관련된 문제 발생(인증 헤더 누락 또는 토큰 인증 실패)" + ), + @ApiResponse( + responseCode = "409", + description = "등록을 시도한 카테고리 이름이 기등록된 카테고리의 이름과 중복" + ) + }) + @PostMapping + public ResponseEntity registerCategory(@Valid @RequestBody CategoryDTO categoryDTO) { + System.out.println("[CategoryController] registerCategory()"); + Category category = categoryService.registerCategory(categoryDTO.convertToDomain()); + return ResponseEntity.status(HttpStatus.CREATED).body(category); + } + + @Operation( + summary = "카테고리 이름 수정", + description = "카테고리의 이름을 수정합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "카테고리 수정 성공", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = Category.class) + ) + ), + @ApiResponse( + responseCode = "400", + description = "수정을 시도한 카테고리의 이름 누락" + ), + @ApiResponse( + responseCode = "401", + description = "인증과 관련된 문제 발생(인증 헤더 누락 또는 토큰 인증 실패)" + ), + @ApiResponse( + responseCode = "404", + description = "등록되지 않은 카테고리에 대한 수정 시도" + ), + @ApiResponse( + responseCode = "409", + description = "수정을 시도한 카테고리 이름이 기등록된 카테고리의 이름과 중복(또는 기존과 동일한 이름으로 수정 시도)" + ) + }) + @PutMapping("/{id}") + public ResponseEntity updateCategory(@PathVariable Long id, @Valid @RequestBody CategoryDTO categoryDTO) { + System.out.println("[CategoryController] updateCategory()"); + Category category = categoryService.updateCategory(categoryDTO.convertToDomain(id)); + return ResponseEntity.status(HttpStatus.CREATED).body(category); + } + + @Operation( + summary = "카테고리 삭제", + description = "상품의 분류를 정하는 카테고리를 삭제합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "204", + description = "카테고리 삭제 성공" + ), + @ApiResponse( + responseCode = "401", + description = "인증과 관련된 문제 발생(인증 헤더 누락 또는 토큰 인증 실패)" + ), + @ApiResponse( + responseCode = "404", + description = "등록되지 않은 카테고리에 대한 삭제 시도" + ) + }) + @DeleteMapping("/{id}") + public ResponseEntity deleteCategory(@PathVariable Long id) { + System.out.println("[CategoryController] deleteCategory()"); + categoryService.deleteCategory(id); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } +} diff --git a/src/main/java/gift/product/controller/ApiMemberController.java b/src/main/java/gift/product/controller/ApiMemberController.java new file mode 100644 index 000000000..e8ab84fbb --- /dev/null +++ b/src/main/java/gift/product/controller/ApiMemberController.java @@ -0,0 +1,108 @@ +package gift.product.controller; + +import static gift.product.exception.GlobalExceptionHandler.UNKNOWN_VALIDATION_ERROR; + +import gift.product.dto.MemberDTO; +import gift.product.dto.TokenDTO; +import gift.product.service.MemberService; +import io.swagger.v3.oas.annotations.Operation; +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.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.ValidationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Member", description = "API 컨트롤러") +@RestController +@RequestMapping("/api/member") +public class ApiMemberController { + + private final MemberService memberService; + + @Autowired + public ApiMemberController(MemberService memberService) { + this.memberService = memberService; + } + + @Operation( + summary = "회원 가입", + description = "회원 정보를 애플리케이션에 등록합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "회원 가입 성공", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = TokenDTO.class) + ) + ), + @ApiResponse( + responseCode = "400", + description = "이메일 형식이 올바르지 않거나 비밀번호가 4자 이상 입력되지 않음" + ), + @ApiResponse( + responseCode = "409", + description = "중복된 이메일에 대한 회원 가입 시도" + ) + }) + @PostMapping + public ResponseEntity signUp( + @Valid @RequestBody MemberDTO memberDTO, + BindingResult bindingResult) { + System.out.println("[ApiMemberController] signUp()"); + if (bindingResult.hasErrors()) { + FieldError fieldError = bindingResult.getFieldError(); + if (fieldError != null) + throw new ValidationException(fieldError.getDefaultMessage()); + throw new ValidationException(UNKNOWN_VALIDATION_ERROR); + } + return ResponseEntity.status(HttpStatus.OK) + .body(memberService.signUp(memberDTO)); + } + + @Operation( + summary = "로그인", + description = "애플리케이션의 기능을 이용하기 위한 권한을 취득합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "로그인 성공", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = TokenDTO.class) + ) + ), + @ApiResponse( + responseCode = "400", + description = "등록된 회원 정보가 없음" + ) + }) + @PostMapping("/login") + public ResponseEntity login( + @Valid @RequestBody MemberDTO memberDTO, + BindingResult bindingResult) { + System.out.println("[ApiMemberController] login()"); + if (bindingResult.hasErrors()) { + FieldError fieldError = bindingResult.getFieldError(); + if (fieldError != null) + throw new ValidationException(fieldError.getDefaultMessage()); + throw new ValidationException(UNKNOWN_VALIDATION_ERROR); + } + return ResponseEntity.status(HttpStatus.OK) + .body(memberService.login(memberDTO)); + } +} diff --git a/src/main/java/gift/product/controller/ApiOptionController.java b/src/main/java/gift/product/controller/ApiOptionController.java new file mode 100644 index 000000000..000223c07 --- /dev/null +++ b/src/main/java/gift/product/controller/ApiOptionController.java @@ -0,0 +1,190 @@ +package gift.product.controller; + +import static gift.product.exception.GlobalExceptionHandler.UNKNOWN_VALIDATION_ERROR; + +import gift.product.dto.OptionDTO; +import gift.product.model.Option; +import gift.product.service.OptionService; +import io.swagger.v3.oas.annotations.Operation; +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.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.ValidationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +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; + +@Tag(name = "Option", description = "API 컨트롤러") +@RestController +@RequestMapping("/api/products/{productId}/options") +public class ApiOptionController { + + private final OptionService optionService; + + @Autowired + public ApiOptionController(OptionService optionService) { + this.optionService = optionService; + } + + @Operation( + summary = "옵션 목록", + description = "특정 상품에 등록된 옵션 목록을 조회합니다." + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "옵션 목록 정상 출력", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = Page.class) + ) + ), + @ApiResponse( + responseCode = "401", + description = "인증과 관련된 문제 발생(인증 헤더 누락 또는 토큰 인증 실패)" + ) + }) + @GetMapping + public Page