diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d409c6 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +1. AOP 사용 + + >BingResult를 사용하여 Valid 처리가 필요한 객체들에 대한 에러처리를 AOP 로 잡아서 만약 BindingResult에 hasErrors가 true 일 경우 응답을 AOP에서 대신한다. +이 기능은 애플리케이션이 실행하는동안 런타임에 동적으로 프록시 객체가 생성되며 해당 프록시 객체가 PointCut으로 지정한 @BindingCheck 애노테이션이 붙은 메소드를 호출할 경우 해당 호출을 가로채어 어드바이스를 실행한다. +런타임 시점에 애노테이션이 붙은 클래스의 프록시객체를 만들어 해당 프록시 객체를 통해 부가로직과 핵심로직이 함께 실행되는 것이다. + > +> +> 이런 방식을 권한체크에서도 사용했는데 권한체크에서는 request + + + +2. 내장객체 +> Spring에서는 pageContext,request,session,application과 같은 JSP 내장 객체를 직접적으로 사용하는 것이 아닌, 서블릿 기반의 웹 애플리케이션에서는 HttpServletRequest, HttpSessopm, ServletContext와 같은 웹 관련 객체를 추상화하여 컨테이너에서 관리되는 빈을 사용한다. +> 따라서 Spring에서는 HttpServletRequest, HttpSession, ServletContest와 같은 웹 관련 객체를 사용하여 웹 애플리케이션의 상태를 관리하고 데이터를 공유할 수 있다. +> 일단 ServletContext는 모든 애플리케이션 내의 모든 서블릿들이 동일한 ServletContxt객체를 공유하기 때문에 접근 권한을 체크할 때 좋은 선택은 아니기에 두가지 선택권을 두고 판단했다. 바로 HttpServletRequest,, HttpServletResponse이다. + + +>- HttpServletRequest - HTTP 요청 단위의 데이터를 저장하는 객체이다. 요청이 들어올 때 생성되고, 응답이 완료되면 소멸된다. 따라서 HttpServletRequest를 통해 저장된 데이터는 해당 요청의 처리가 완료되면 사라진다. HttpServletRequest는 주로 사용자의 요청에 대한 처리를 위해 사용되며 요청 단위의 데이터를 임시적으로 저장하고 싶을 때 유용하다. +>- HttpSession - HttpSession은 웹 애플리케이션 단위의 데이터를 저장하는 객체이다. HttpSession은 웹 브라우저와 웹 서버 간의 연결을 유지하며, 서버 측에서 웹 브라우저의 상태를 유지할 수 있도록 해준다. HttpSession은 웹 브라우저를 종료하거나 세션의 유효시간이 만료될 때까지 데이터를 보존하며 다양한 클라이언트 요청 간에 공유할 수 있다. + +3. 내 생각 +> 해당 위에 설명대로 권한을 만약 HttpSession에 저장하게 된다면 사용자의 정보를 저장하게 된다. 그런데 JWT토큰을 사용하는데 굳이 사용자의 정보를 서버에 저장을 해야하나 싶은 의문이 들어서 나는 해당 애플리케이션에서는 Request를 사용했다. +> "세션을 사용하는게 더 좋을 수 있지 않을까?" 생각을 잠시했지만 요청이 들어오면 해당 세션에 데이터가 있는지 조회하고 없으면 저장하고 이러한 로직 자체가 request를 사용하여 바로 해당 요청이 들어오면저장하고 요청이 끝나면 삭제되는 방식이 더 효율적이고 JWT 토큰을 사용하는 방식에 맞는 방식이라는 생각이 들었다. +> +> 기존 Filter를 삭제하고 OncePerRequestFilter 를 상속받는 filter를 구현하는 방식으로 변경했다. 나중에 시큐리티를 사용한다면 필터를 시큐리티 설정 클래스에 등록하여 사용해야 하는데 등록하려면 설정클래스에 등록한 방식과 겹치면서 빈으로 두번 등록해버리는 사고가 있을 수도 있다는 생각이 든다. 그래서 기존에 사용하던 방식말고 해당 Filter를 상속하는 클래스를 빈으로 등록하는 방식으로 변경했다. +> 근데 이런 사고가 없을 수 있다고 생각하지만 구글에 쳐봐도 많기에 Filter는 사용할 경우 로그를 사용해서 두번 실행되지는 않는지 체크가 꼭 필요하다. +> OncePerRequestFilter를 상속하는 클래스를 빈으로 두 번 등록해도 한 번 실행하는 걸 보장한다. +> +``` +Spring guarantees that the OncePerRequestFilter is executed only once for a given request. + +- https://www.baeldung.com/spring-onceperrequestfilter + +``` + + + + +/order/save POST 권한 (인증필요, CUSTOMER 권한 필요) + +```json +[ + { + "productId":50, + "count": 4, + "orderPrice": 4000 + } +] + +``` + +/order/save RESPONSE + +```json +{ + "status": 200, + "msg": "success", + "data": { + "id": 2, + "user": null, + "orderProductList": [ + { + "id": 2, + "product": { + "id": 50, + "name": "TEst50", + "price": 350, + "qty": 50, + "createdAt": "2023-04-07T22:40:53.544879", + "updatedAt": null + }, + "count": 4, + "orderPrice": 4000, + "createdAt": "2023-04-07T22:47:27.6473413", + "updatedAt": null, + "productName": null, + "orderSheet": null + } + ], + "totalPrice": 4000, + "createdAt": "2023-04-07T22:47:27.6448262", + "updatedAt": null, + "username": "customer" + } +} + +``` \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4943187..ed47b58 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,8 @@ repositories { } dependencies { + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation' + implementation 'com.google.code.gson:gson:2.8.8' implementation group: 'com.auth0', name: 'java-jwt', version: '4.3.0' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' diff --git a/src/main/java/shop/mtcoding/metamall/MetamallApplication.java b/src/main/java/shop/mtcoding/metamall/MetamallApplication.java index 487bb62..6a66bc3 100644 --- a/src/main/java/shop/mtcoding/metamall/MetamallApplication.java +++ b/src/main/java/shop/mtcoding/metamall/MetamallApplication.java @@ -8,21 +8,34 @@ import shop.mtcoding.metamall.model.orderproduct.OrderProductRepository; import shop.mtcoding.metamall.model.ordersheet.OrderSheet; import shop.mtcoding.metamall.model.ordersheet.OrderSheetRepository; +import shop.mtcoding.metamall.model.product.Product; import shop.mtcoding.metamall.model.product.ProductRepository; import shop.mtcoding.metamall.model.user.User; import shop.mtcoding.metamall.model.user.UserRepository; +import java.util.stream.IntStream; + @SpringBootApplication public class MetamallApplication { @Bean CommandLineRunner initData(UserRepository userRepository, ProductRepository productRepository, OrderProductRepository orderProductRepository, OrderSheetRepository orderSheetRepository){ return (args)->{ - // 여기에서 save 하면 됨. - // bulk Collector는 saveAll 하면 됨. + IntStream.rangeClosed(1,50).forEach(value -> { + productRepository.save(Product.builder() + .price(300+value) + .name("TEst"+value) + .qty(value) + .build()); + }); + User ssar = User.builder().username("ssar").password("1234").email("ssar@nate.com").role("USER").build(); userRepository.save(ssar); + userRepository.save(User.builder().username("admin").password("1234").email("admin@nate.com").role("SELLER").build()); + userRepository.save(User.builder().username("customer").password("1234").email("admin@nate.com").role("CUSTOMER").build()); }; + + } public static void main(String[] args) { diff --git a/src/main/java/shop/mtcoding/metamall/annotation/BindingCheck.java b/src/main/java/shop/mtcoding/metamall/annotation/BindingCheck.java new file mode 100644 index 0000000..41d5272 --- /dev/null +++ b/src/main/java/shop/mtcoding/metamall/annotation/BindingCheck.java @@ -0,0 +1,13 @@ +package shop.mtcoding.metamall.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface BindingCheck { + +} diff --git a/src/main/java/shop/mtcoding/metamall/annotation/Customer.java b/src/main/java/shop/mtcoding/metamall/annotation/Customer.java new file mode 100644 index 0000000..52681e0 --- /dev/null +++ b/src/main/java/shop/mtcoding/metamall/annotation/Customer.java @@ -0,0 +1,4 @@ +package shop.mtcoding.metamall.annotation; + +public @interface Customer { +} diff --git a/src/main/java/shop/mtcoding/metamall/annotation/Permission.java b/src/main/java/shop/mtcoding/metamall/annotation/Permission.java new file mode 100644 index 0000000..fd03229 --- /dev/null +++ b/src/main/java/shop/mtcoding/metamall/annotation/Permission.java @@ -0,0 +1,12 @@ +package shop.mtcoding.metamall.annotation; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Permission { +} diff --git a/src/main/java/shop/mtcoding/metamall/config/FilterRegisterConfig.java b/src/main/java/shop/mtcoding/metamall/config/FilterRegisterConfig.java index f5ea4db..b461ceb 100644 --- a/src/main/java/shop/mtcoding/metamall/config/FilterRegisterConfig.java +++ b/src/main/java/shop/mtcoding/metamall/config/FilterRegisterConfig.java @@ -3,17 +3,16 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import shop.mtcoding.metamall.core.filter.JwtVerifyFilter; @Configuration public class FilterRegisterConfig { - @Bean - public FilterRegistrationBean jwtVerifyFilterAdd() { - FilterRegistrationBean registration = new FilterRegistrationBean<>(); - registration.setFilter(new JwtVerifyFilter()); - registration.addUrlPatterns("/user/*"); - registration.setOrder(1); - return registration; - } + // @Bean +// public FilterRegistrationBean jwtVerifyFilterAdd() { +// FilterRegistrationBean registration = new FilterRegistrationBean<>(); +// registration.setFilter(new JwtVerifyFilter()); +// registration.addUrlPatterns("/user/*"); +// registration.setOrder(1); +// return registration; +// } } diff --git a/src/main/java/shop/mtcoding/metamall/controller/OrderController.java b/src/main/java/shop/mtcoding/metamall/controller/OrderController.java new file mode 100644 index 0000000..2244b6c --- /dev/null +++ b/src/main/java/shop/mtcoding/metamall/controller/OrderController.java @@ -0,0 +1,240 @@ +package shop.mtcoding.metamall.controller; + + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.coyote.Response; +import org.aspectj.weaver.ast.Or; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.interceptor.TransactionAspectSupport; +import org.springframework.web.bind.annotation.*; +import shop.mtcoding.metamall.annotation.Customer; +import shop.mtcoding.metamall.annotation.Permission; +import shop.mtcoding.metamall.core.CodeEnum; +import shop.mtcoding.metamall.core.exception.StockExhaustionException; +import shop.mtcoding.metamall.dto.ResponseDto; +import shop.mtcoding.metamall.model.orderproduct.OrderProduct; +import shop.mtcoding.metamall.model.orderproduct.OrderProductRepository; +import shop.mtcoding.metamall.model.ordersheet.OrderSheet; +import shop.mtcoding.metamall.model.ordersheet.OrderSheetRepository; +import shop.mtcoding.metamall.model.product.Product; +import shop.mtcoding.metamall.model.product.ProductRepository; +import shop.mtcoding.metamall.model.user.User; +import shop.mtcoding.metamall.model.user.UserRepository; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/order") +public class OrderController { + /** + * 일단 OrderSheet(1) - OrderProduct(N) 인데 해당 구조는 문제가 많아보임 -> 여기서는 문제 없지만 User끼면 문제생김 + * User(1) - OrderSheet(N) 이 구조가 문제인듯 함 해당 구조를 1-1 로 바꾸는게 나중에 마이페이지를 설계할 떄 좋다 + * OneToMany를 두번 join fetch는 데이터를 튀기고 튀긴 데이터를 가져오기 때문에 기본적으로 여러번 쿼리가 일어나는 마이페이지에서는 + * 이런 구조가 성능에 발목을 잡을듯 하다. + * */ + + private final OrderSheetRepository orderSheetRepository; + + private final ProductRepository productRepository; + + + + + + /** + * @apiNote 소비자 권한으로만 들어올 수 있음 아닐 경우 403 + * 상품을 번호로 받는다 -> 더티체킹으로 해야하는 부분인데 select를 통해 영속상태를 만들어야하기 때문에 Product 객체를 받든 PK를 받든 크게 다르지 않다. + */ + + @Customer + @Transactional + @PostMapping("/save") + public ResponseEntity save(@RequestBody List orderProductList, HttpServletRequest request){ + + + String username = request.getAttribute("username").toString(); + + for (OrderProduct orderProduct : orderProductList) { + if(!inventoryProcessing(orderProduct.getProductId())){ + throw new StockExhaustionException(); + } + } + + OrderSheet data = OrderSheet.builder() + .userId(username) + .totalPrice(orderProductList.stream().mapToInt(OrderProduct::getOrderPrice).sum()) + .build(); + + data.addOrderProductList(orderProductList); + + OrderSheet orderSheet = orderSheetRepository.save(data); + + if(orderSheet == null) log.error("ORDERSHEET E+MPTY"); + return ResponseEntity.status(CodeEnum.SUCCESS.getCode()) + .body(new ResponseDto() + .data(orderSheet).code(CodeEnum.SUCCESS) + .msg(CodeEnum.SUCCESS.getMessage())); + + } + + @Customer + @GetMapping("/my") + public ResponseEntity myPage(HttpServletRequest request){ + + String username = request.getAttribute("username").toString(); + log.info("MY PAGE {}",username); + Optional> orderSheet =orderSheetRepository.findOrdData(username); + List list= orderSheet.get(); + + if(orderSheet.isPresent()){ + return ResponseEntity.status(CodeEnum.SUCCESS.getCode()) + .body(new ResponseDto().data(list) + .code(CodeEnum.SUCCESS) + .msg(CodeEnum.SUCCESS.getMessage())); + }else{ + return ResponseEntity.status(CodeEnum.SUCCESS.getCode()) + .body(new ResponseDto().data(new ArrayList<>()) + .code(CodeEnum.SUCCESS) + .msg(CodeEnum.SUCCESS.getMessage())); + } + } + + @Permission + @GetMapping("/all/list") + public ResponseEntity getList(){ + Optional> orderSheet= orderSheetRepository.findAllData(); + ResponseDto responseDto; + + + if(orderSheet.isPresent()){ + List list = orderSheet.get(); + + responseDto = new ResponseDto<>() + .data(list) + .code(CodeEnum.SUCCESS) + .msg(CodeEnum.SUCCESS.getMessage()); + }else{ + responseDto = new ResponseDto<>() + .data(new ArrayList<>()) + .code(CodeEnum.SUCCESS) + .msg(CodeEnum.SUCCESS.getMessage()); + } + + return ResponseEntity.status(CodeEnum.SUCCESS.getCode()).body(responseDto); + } + + + /** + * @apiNote 현재 로그인한 사용자에 정보를 받아와서 조회한 다음 삭제하고자하는 orderSheet PK를 찾아서 해당 그걸 삭제 + * @param 삭제하고자 하는 주문에 대한 PK 를 받는다. + * 해당 PK로 데이터를 조회하고 해당 PK 데이터와 로그인한 사용자에 정보가 일치한다면 삭제하고 아니면 403을 리턴할 계획임 + * + * */ + @Customer + @Transactional + @DeleteMapping("/delete") + public ResponseEntity delete(Long id,HttpServletRequest request){ + + String username = request.getAttribute("username").toString(); + String role = request.getAttribute("role").toString(); + + Optional orderSheet = orderSheetRepository.findByIdWithUser(id); + + if(orderSheet.isPresent()){ + OrderSheet orderSheet1 = orderSheet.get(); + if(orderSheet1.getUser().getUsername().equals(username) || role.equals("SELLER")){ + + for (OrderProduct orderProduct : orderSheet1.getOrderProductList()) { + Product product = orderProduct.getProduct(); + + product.plusQty(); + } + + orderSheetRepository.delete(orderSheet1); + return ResponseEntity + .status(CodeEnum.SUCCESS.getCode()) + .body(new ResponseDto() + .code(CodeEnum.SUCCESS) + .data(orderSheet) + .msg(CodeEnum.SUCCESS.getMessage())); + }else{ + return ResponseEntity + .status(CodeEnum.FORBIDDEN.getCode()) + .body(new ResponseDto().data(orderSheet) + .code(CodeEnum.FORBIDDEN) + .msg(CodeEnum.FORBIDDEN.getMessage())); + } + }else{ + return ResponseEntity + .status(CodeEnum.NOT_FOUND.getCode()) + .body(new ResponseDto() + .code(CodeEnum.NOT_FOUND) + .msg(CodeEnum.NOT_FOUND.getMessage())); + } + } + + /** + * @apiNote admin 관리자가 해당 리소스에만 접근가능하고 id값만 있다면 어떤 주문이든 다 삭제 가능 + * + * */ + @Permission + @DeleteMapping("/admin/delete") + public ResponseEntity deleteAmin(Long id,HttpServletRequest request){ + Optional orderSheetOptional = orderSheetRepository.findById(id); + + if(orderSheetOptional.isPresent()){ + OrderSheet data = orderSheetOptional.get(); + orderSheetRepository.delete(data); + + return ResponseEntity + .status(CodeEnum.SUCCESS.getCode()) + .body(new ResponseDto().data(data) + .msg(CodeEnum.SUCCESS.getMessage())); + }else{ + return ResponseEntity + .status(CodeEnum.NOT_FOUND.getCode()) + .body(new ResponseDto() + .code(CodeEnum.NOT_FOUND) + .msg(CodeEnum.NOT_FOUND.getMessage())); + } + } + + + @ExceptionHandler(StockExhaustionException.class) + public ResponseEntity stockException(StockExhaustionException e){ + + return ResponseEntity + .status(CodeEnum.NOT_FOUND.getCode()) + .body(new ResponseDto() + .code(CodeEnum.INVALID_ARGUMENT) + .msg("재고가 없습니다. 재 주문을 넣어주세요")); + } + + + protected boolean inventoryProcessing(Long productId){ + var product = productRepository.findById(productId); + + if(product.isPresent()){ + Product productEntity = product.get(); + + if(productEntity.minusQty() > 0){ + return true; + }else{ + return false; + } + } + + return false; + } +} + diff --git a/src/main/java/shop/mtcoding/metamall/controller/ProductController.java b/src/main/java/shop/mtcoding/metamall/controller/ProductController.java new file mode 100644 index 0000000..8cbe605 --- /dev/null +++ b/src/main/java/shop/mtcoding/metamall/controller/ProductController.java @@ -0,0 +1,179 @@ +package shop.mtcoding.metamall.controller; + + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; +import shop.mtcoding.metamall.annotation.BindingCheck; +import shop.mtcoding.metamall.annotation.Permission; +import shop.mtcoding.metamall.core.CodeEnum; +import shop.mtcoding.metamall.dto.ResponseDto; +import shop.mtcoding.metamall.model.product.Product; +import shop.mtcoding.metamall.model.product.ProductRepository; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Positive; +import java.util.List; +import java.util.Optional; + + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/product") +public class ProductController { + + private final ProductRepository productRepository; + + + + @GetMapping("/list") + public ResponseEntity list(@PageableDefault(sort = "id", + direction = Sort.Direction.DESC) Pageable pageable, HttpServletRequest request){ + Page pageList =productRepository.findAll(pageable); + PageRequest pageRequest = new PageRequest(pageList.getTotalPages(), + pageList.getNumber(), + pageList.getContent()); + ResponseDto responseDto = new ResponseDto<>() + .data(pageRequest) + .code(CodeEnum.SUCCESS) + .msg("SUCCESS"); + + return ResponseEntity.ok(responseDto); + } + + @GetMapping("/{id}") + public ResponseEntity detail(@PathVariable Long id){ + Optional product = productRepository.findById(id); + + if(product.isPresent()){ + Product entity =product.get(); + ResponseDto dto =new ResponseDto<>() + .data(entity) + .code(CodeEnum.SUCCESS) + .msg(CodeEnum.SUCCESS.getMessage()); + + return ResponseEntity.ok(dto); + }else{ + ResponseDto dto =new ResponseDto<>() + .code(CodeEnum.NOT_FOUND) + .msg(CodeEnum.NOT_FOUND.getMessage()); + + return ResponseEntity.status(CodeEnum.NOT_FOUND.getCode()).body(dto); + } + } + + @Permission + @BindingCheck + @PostMapping("/save") + public ResponseEntity save(@RequestBody @Valid RequestProductDTO productDTO, + BindingResult bindingResult){ + + productRepository.save(toEntity(productDTO)); + System.out.println(productRepository); + return ResponseEntity.ok(new ResponseDto() + .code(CodeEnum.SUCCESS) + .msg(CodeEnum.SUCCESS.getMessage()) + .data(true)); + } + + @Permission + @BindingCheck + @Transactional + @PutMapping("/modify") + public ResponseEntity modify(@RequestBody @Valid RequestProductDTO productDTO){ + Optional product = productRepository.findById(productDTO.getId()); + + if(product.isPresent()){ + Product entity = product.get(); + entity.changeProduct(productDTO.getName(), productDTO.getPrice(), productDTO.getQty()); + + ResponseDto dto =new ResponseDto<>() + .data(entity) + .code(CodeEnum.SUCCESS) + .msg(CodeEnum.SUCCESS.getMessage()); + + return ResponseEntity.ok(dto); + + }else{ + + ResponseDto dto = new ResponseDto<>() + .code(CodeEnum.INVALID_ARGUMENT) + .msg(CodeEnum.INVALID_ARGUMENT.getMessage()); + + return ResponseEntity.status(CodeEnum.INVALID_ARGUMENT.getCode()).body(dto); + } + } + + @Permission + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id){ + Optional product = productRepository.findById(id); + + if(product.isPresent()){ + Product entity =product.get(); + productRepository.delete(entity); + ResponseDto dto =new ResponseDto<>() + .data(entity) + .code(CodeEnum.SUCCESS) + .msg(CodeEnum.SUCCESS.getMessage()); + + return ResponseEntity.ok(dto); + }else{ + ResponseDto dto =new ResponseDto<>() + .code(CodeEnum.NOT_FOUND) + .msg(CodeEnum.NOT_FOUND.getMessage()); + + return ResponseEntity.status(CodeEnum.NOT_FOUND.getCode()).body(dto); + } + + } + + protected Product toEntity(RequestProductDTO dto){ + return Product.builder() + .name(dto.getName()) + .price(dto.getPrice()) + .qty(dto.getQty()) + .build(); + } + + @Data + @NoArgsConstructor + @JsonInclude(JsonInclude.Include.NON_NULL) + static class RequestProductDTO{ + + private Long id; + @NotEmpty + private String name; // 상품 이름 + private Integer price; // 상품 가격 + + @Positive //0보다커야함 + private Integer qty; // 상품 재고 + + } + + @Data + @Builder + static class PageRequest{ + int total; + int nowPage; + + List data; + + + + } +} diff --git a/src/main/java/shop/mtcoding/metamall/controller/UserController.java b/src/main/java/shop/mtcoding/metamall/controller/UserController.java index ddfee94..6651811 100644 --- a/src/main/java/shop/mtcoding/metamall/controller/UserController.java +++ b/src/main/java/shop/mtcoding/metamall/controller/UserController.java @@ -1,8 +1,11 @@ package shop.mtcoding.metamall.controller; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; +import shop.mtcoding.metamall.annotation.BindingCheck; import shop.mtcoding.metamall.core.exception.Exception400; import shop.mtcoding.metamall.core.exception.Exception401; import shop.mtcoding.metamall.core.jwt.JwtProvider; @@ -16,9 +19,12 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; +import javax.validation.Valid; import java.time.LocalDateTime; import java.util.Optional; + +@Slf4j @RequiredArgsConstructor @RestController public class UserController { @@ -47,7 +53,6 @@ public ResponseEntity login(@RequestBody UserRequest.LoginDto loginDto, HttpS // 5. 로그 테이블 기록 LoginLog loginLog = LoginLog.builder() - .userId(loginUser.getId()) .userAgent(request.getHeader("User-Agent")) .clientIP(request.getRemoteAddr()) .build(); @@ -60,4 +65,24 @@ public ResponseEntity login(@RequestBody UserRequest.LoginDto loginDto, HttpS throw new Exception400("유저네임 혹은 아이디가 잘못되었습니다"); } } + + @BindingCheck + @PostMapping("/join") + public ResponseEntity join(@RequestBody @Valid UserRequest.JoinDto joinDto, BindingResult bindingResult){ + userRepository.save(toEntity(joinDto)); + + return ResponseEntity.ok(true); + } + + + + protected User toEntity(UserRequest.JoinDto joinDto){ + return User.builder() + .username(joinDto.getUsername()) + .password(joinDto.getPassword()) + .role("ROLE_USER") + .email(joinDto.getEmail()) + .build(); + + } } diff --git a/src/main/java/shop/mtcoding/metamall/core/CodeEnum.java b/src/main/java/shop/mtcoding/metamall/core/CodeEnum.java new file mode 100644 index 0000000..4402ec9 --- /dev/null +++ b/src/main/java/shop/mtcoding/metamall/core/CodeEnum.java @@ -0,0 +1,28 @@ +package shop.mtcoding.metamall.core; + +public enum CodeEnum { + + SUCCESS(200,"success"), + UNKNOWN_ERROR(500, "An unknown error has occurred."), + INVALID_ARGUMENT(400, "One or more arguments are invalid."), + NOT_FOUND(404, "The requested resource was not found."), + UNAUTHORIZED(401, "The operation requires authentication."), + FORBIDDEN(403, "The operation is forbidden."), + INTERNAL_SERVER_ERROR(500, "An internal server error has occurred."); + + private final int code; + private final String message; + + private CodeEnum(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/shop/mtcoding/metamall/core/exception/CustomException.java b/src/main/java/shop/mtcoding/metamall/core/exception/CustomException.java new file mode 100644 index 0000000..fcb4e51 --- /dev/null +++ b/src/main/java/shop/mtcoding/metamall/core/exception/CustomException.java @@ -0,0 +1,4 @@ +package shop.mtcoding.metamall.core.exception; + +public interface CustomException { +} diff --git a/src/main/java/shop/mtcoding/metamall/core/exception/Exception400.java b/src/main/java/shop/mtcoding/metamall/core/exception/Exception400.java index d1b5fec..cfb8455 100644 --- a/src/main/java/shop/mtcoding/metamall/core/exception/Exception400.java +++ b/src/main/java/shop/mtcoding/metamall/core/exception/Exception400.java @@ -7,7 +7,7 @@ // 유효성 실패 @Getter -public class Exception400 extends RuntimeException { +public class Exception400 extends RuntimeException implements CustomException { public Exception400(String message) { super(message); } diff --git a/src/main/java/shop/mtcoding/metamall/core/exception/Exception401.java b/src/main/java/shop/mtcoding/metamall/core/exception/Exception401.java index 5d2f310..9f1187e 100644 --- a/src/main/java/shop/mtcoding/metamall/core/exception/Exception401.java +++ b/src/main/java/shop/mtcoding/metamall/core/exception/Exception401.java @@ -8,7 +8,7 @@ // 인증 안됨 @Getter -public class Exception401 extends RuntimeException { +public class Exception401 extends RuntimeException implements CustomException{ public Exception401(String message) { super(message); } diff --git a/src/main/java/shop/mtcoding/metamall/core/exception/Exception403.java b/src/main/java/shop/mtcoding/metamall/core/exception/Exception403.java index c8dc137..356e8df 100644 --- a/src/main/java/shop/mtcoding/metamall/core/exception/Exception403.java +++ b/src/main/java/shop/mtcoding/metamall/core/exception/Exception403.java @@ -7,7 +7,7 @@ // 권한 없음 @Getter -public class Exception403 extends RuntimeException { +public class Exception403 extends RuntimeException implements CustomException{ public Exception403(String message) { super(message); } diff --git a/src/main/java/shop/mtcoding/metamall/core/exception/Exception404.java b/src/main/java/shop/mtcoding/metamall/core/exception/Exception404.java index c20b64f..f8830ba 100644 --- a/src/main/java/shop/mtcoding/metamall/core/exception/Exception404.java +++ b/src/main/java/shop/mtcoding/metamall/core/exception/Exception404.java @@ -7,7 +7,7 @@ // 리소스 없음 @Getter -public class Exception404 extends RuntimeException { +public class Exception404 extends RuntimeException implements CustomException{ public Exception404(String message) { super(message); } diff --git a/src/main/java/shop/mtcoding/metamall/core/exception/Exception500.java b/src/main/java/shop/mtcoding/metamall/core/exception/Exception500.java index d3d4468..bb53128 100644 --- a/src/main/java/shop/mtcoding/metamall/core/exception/Exception500.java +++ b/src/main/java/shop/mtcoding/metamall/core/exception/Exception500.java @@ -7,7 +7,7 @@ // 서버 에러 @Getter -public class Exception500 extends RuntimeException { +public class Exception500 extends RuntimeException implements CustomException{ public Exception500(String message) { super(message); } diff --git a/src/main/java/shop/mtcoding/metamall/core/exception/StockExhaustionException.java b/src/main/java/shop/mtcoding/metamall/core/exception/StockExhaustionException.java new file mode 100644 index 0000000..bc7f17e --- /dev/null +++ b/src/main/java/shop/mtcoding/metamall/core/exception/StockExhaustionException.java @@ -0,0 +1,4 @@ +package shop.mtcoding.metamall.core.exception; + +public class StockExhaustionException extends RuntimeException{ +} diff --git a/src/main/java/shop/mtcoding/metamall/core/exception/TokenException.java b/src/main/java/shop/mtcoding/metamall/core/exception/TokenException.java new file mode 100644 index 0000000..ea59e6a --- /dev/null +++ b/src/main/java/shop/mtcoding/metamall/core/exception/TokenException.java @@ -0,0 +1,54 @@ +package shop.mtcoding.metamall.core.exception; + +import com.google.gson.Gson; +import lombok.Getter; +import org.springframework.http.MediaType; +import shop.mtcoding.metamall.dto.ResponseDto; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class TokenException extends RuntimeException{ + + TOKEN_ERROR tokenError; + + + @Getter + public enum TOKEN_ERROR{ + UNACCEPT(401,"Token is null or too short"), + BADTYPE(401,"Token type Bearer"), + MALFORM(403,"Malformed Token"), + BADSIGN(403,"BadSignatured Token"), + EXPIRED(403,"Expired Token"); + + private int status; + private String msg; + + TOKEN_ERROR(int status,String msg){ + this.status = status; + this.msg = msg; + } + } + public TokenException(TOKEN_ERROR tokenError){ + super(tokenError.msg); + this.tokenError = tokenError; + } + public void sendResponseError(HttpServletResponse response){ + response.setStatus(tokenError.getStatus()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + Gson gson = new Gson(); + + ResponseDto responseDto = new ResponseDto<>() + .msg(tokenError.getMsg()) + .code(tokenError.getStatus()); + + String result = gson.toJson(responseDto); + + try{ + response.getWriter().println(result); + }catch (IOException e){ + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/shop/mtcoding/metamall/core/filter/AccessFilter.java b/src/main/java/shop/mtcoding/metamall/core/filter/AccessFilter.java new file mode 100644 index 0000000..467991b --- /dev/null +++ b/src/main/java/shop/mtcoding/metamall/core/filter/AccessFilter.java @@ -0,0 +1,98 @@ +package shop.mtcoding.metamall.core.filter; + +import com.auth0.jwt.exceptions.InvalidClaimException; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.exceptions.TokenExpiredException; +import com.auth0.jwt.interfaces.Claim; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.gson.Gson; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import shop.mtcoding.metamall.core.exception.TokenException; +import shop.mtcoding.metamall.core.jwt.JwtProvider; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + + +@Slf4j +@RequiredArgsConstructor +@Order(1) +@Component +public class AccessFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + String path = request.getRequestURI(); + + if(path.startsWith("/user") || path.startsWith("/login")){ //user ㄱ관련된 돗은 통과 + filterChain.doFilter(request,response); + return; + } + + try{ + Map token = validateAccessToken(request); + + System.out.println(token); + String role = token.get("role").asString(); + String username = token.get("id").asString(); + + log.info("username ===> {}",username); + log.info("role ===> {}",role); + + request.setAttribute("role",role); + request.setAttribute("username",username); + filterChain.doFilter(request,response); + + }catch (TokenException tokenExpiredException){ + tokenExpiredException.sendResponseError(response); + } + + + } + + public Map validateAccessToken(HttpServletRequest request) throws TokenException{ + String headerStr = request.getHeader("Authorization"); + + if(headerStr == null || headerStr.length()<8){ + throw new TokenException(TokenException.TOKEN_ERROR.UNACCEPT); + } + + String tokenType = headerStr.substring(0,6); + String tokenStr = headerStr.substring(7); + + try{ + DecodedJWT decodedJWT = JwtProvider.verify(tokenStr); + Map claimMap = decodedJWT.getClaims(); + return claimMap; + }catch (TokenExpiredException tokenExpiredException){//시간만료 + log.warn("TOKEN EXPIRED - Refresh Token request Is required 403 "); + throw new TokenException(TokenException.TOKEN_ERROR.EXPIRED); + }catch (SignatureVerificationException signatureVerificationException){ //토큰의 서명 검증이 실패한 경우 + log.warn("TOKEN BADSIGN - Token has changed its signature changed "); + throw new TokenException(TokenException.TOKEN_ERROR.BADSIGN); + }catch (InvalidClaimException invalidClaimException){ //토큰의 클레임 검증 실패 클레임이 없거나 예상값과 다를 때 + log.warn("TOKEN INVALID - Invalid token."); + throw new TokenException(TokenException.TOKEN_ERROR.MALFORM); + }catch (JWTDecodeException jwtDecodeException){ //토큰손상되면 또는 올바른 형식이 아니.면 + log.warn("TOKEN MALFORM - Invalid token "); + throw new TokenException(TokenException.TOKEN_ERROR.MALFORM); + } + } + + + +} diff --git a/src/main/java/shop/mtcoding/metamall/core/filter/JwtVerifyFilter.java b/src/main/java/shop/mtcoding/metamall/core/filter/JwtVerifyFilter.java deleted file mode 100644 index 870bf93..0000000 --- a/src/main/java/shop/mtcoding/metamall/core/filter/JwtVerifyFilter.java +++ /dev/null @@ -1,58 +0,0 @@ -package shop.mtcoding.metamall.core.filter; - - -import com.auth0.jwt.exceptions.SignatureVerificationException; -import com.auth0.jwt.exceptions.TokenExpiredException; -import com.auth0.jwt.interfaces.DecodedJWT; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.http.HttpStatus; -import shop.mtcoding.metamall.core.exception.Exception400; -import shop.mtcoding.metamall.core.jwt.JwtProvider; -import shop.mtcoding.metamall.core.session.LoginUser; -import shop.mtcoding.metamall.dto.ResponseDto; - -import javax.servlet.*; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import java.io.IOException; - -public class JwtVerifyFilter implements Filter { - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - System.out.println("디버그 : JwtVerifyFilter 동작함"); - HttpServletRequest req = (HttpServletRequest) request; - HttpServletResponse resp = (HttpServletResponse) response; - String prefixJwt = req.getHeader(JwtProvider.HEADER); - if(prefixJwt == null){ - error(resp, new Exception400("토큰이 전달되지 않았습니다")); - return; - } - String jwt = prefixJwt.replace(JwtProvider.TOKEN_PREFIX, ""); - try { - DecodedJWT decodedJWT = JwtProvider.verify(jwt); - int id = decodedJWT.getClaim("id").asInt(); - String role = decodedJWT.getClaim("role").asString(); - - // 세션을 사용하는 이유는 권한처리를 하기 위해서이다. - HttpSession session = req.getSession(); - LoginUser loginUser = LoginUser.builder().id(id).role(role).build(); - session.setAttribute("loginUser", loginUser); - chain.doFilter(req, resp); - }catch (SignatureVerificationException sve){ - error(resp, sve); - }catch (TokenExpiredException tee){ - error(resp, tee); - } - } - - private void error(HttpServletResponse resp, Exception e) throws IOException { - resp.setStatus(401); - resp.setContentType("application/json; charset=utf-8"); - ResponseDto responseDto = new ResponseDto<>().fail(HttpStatus.UNAUTHORIZED, "인증 안됨", e.getMessage()); - ObjectMapper om = new ObjectMapper(); - String responseBody = om.writeValueAsString(responseDto); - resp.getWriter().println(responseBody); - } - -} diff --git a/src/main/java/shop/mtcoding/metamall/core/jwt/JwtProvider.java b/src/main/java/shop/mtcoding/metamall/core/jwt/JwtProvider.java index 93a4bae..cf18340 100644 --- a/src/main/java/shop/mtcoding/metamall/core/jwt/JwtProvider.java +++ b/src/main/java/shop/mtcoding/metamall/core/jwt/JwtProvider.java @@ -23,7 +23,7 @@ public static String create(User user) { String jwt = JWT.create() .withSubject(SUBJECT) .withExpiresAt(new Date(System.currentTimeMillis() + EXP)) - .withClaim("id", user.getId()) + .withClaim("id", user.getUsername()) .withClaim("role", user.getRole()) .sign(Algorithm.HMAC512(SECRET)); System.out.println("디버그 : 토큰 생성됨"); diff --git a/src/main/java/shop/mtcoding/metamall/dto/ResponseDto.java b/src/main/java/shop/mtcoding/metamall/dto/ResponseDto.java index 7f190c6..9a773b6 100644 --- a/src/main/java/shop/mtcoding/metamall/dto/ResponseDto.java +++ b/src/main/java/shop/mtcoding/metamall/dto/ResponseDto.java @@ -1,9 +1,12 @@ package shop.mtcoding.metamall.dto; +import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Getter; import org.springframework.http.HttpStatus; +import shop.mtcoding.metamall.core.CodeEnum; @Getter +@JsonInclude(JsonInclude.Include.NON_NULL) public class ResponseDto { private Integer status; // 에러시에 의미 있음. private String msg; // 에러시에 의미 있음. ex) badRequest @@ -15,10 +18,22 @@ public ResponseDto(){ this.data = null; } + public ResponseDto code(CodeEnum errorCode){ + this.status = errorCode.getCode(); + return this; + } + public ResponseDto code(int errorCode){ + this.status = errorCode; + return this; + } public ResponseDto data(T data){ this.data = data; // 응답할 데이터 바디 return this; } + public ResponseDto msg(String msg){ + this.msg = msg; + return this; + } public ResponseDto fail(HttpStatus httpStatus, String msg, T data){ this.status = httpStatus.value(); @@ -26,4 +41,6 @@ public ResponseDto fail(HttpStatus httpStatus, String msg, T data){ this.data = data; // 에러 내용 return this; } + + } diff --git a/src/main/java/shop/mtcoding/metamall/dto/user/UserRequest.java b/src/main/java/shop/mtcoding/metamall/dto/user/UserRequest.java index 80947db..b5a0f3f 100644 --- a/src/main/java/shop/mtcoding/metamall/dto/user/UserRequest.java +++ b/src/main/java/shop/mtcoding/metamall/dto/user/UserRequest.java @@ -3,10 +3,26 @@ import lombok.Getter; import lombok.Setter; +import javax.validation.constraints.NotEmpty; + public class UserRequest { @Getter @Setter public static class LoginDto { private String username; private String password; } + + @Getter @Setter + public static class JoinDto { + + @NotEmpty + + private String username; + @NotEmpty + private String password; + + @NotEmpty + private String email; + + } } diff --git a/src/main/java/shop/mtcoding/metamall/model/orderproduct/OrderProduct.java b/src/main/java/shop/mtcoding/metamall/model/orderproduct/OrderProduct.java index 165905e..5df2775 100644 --- a/src/main/java/shop/mtcoding/metamall/model/orderproduct/OrderProduct.java +++ b/src/main/java/shop/mtcoding/metamall/model/orderproduct/OrderProduct.java @@ -1,5 +1,6 @@ package shop.mtcoding.metamall.model.orderproduct; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -19,13 +20,19 @@ public class OrderProduct { // 주문 상품 @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_name",insertable = false,updatable = false) private Product product; private Integer count; // 상품 주문 개수 private Integer orderPrice; // 상품 주문 금액 private LocalDateTime createdAt; private LocalDateTime updatedAt; + @Column(name = "product_name") + private Long productId; + + @JsonIgnore @ManyToOne private OrderSheet orderSheet; @@ -40,7 +47,7 @@ protected void onUpdate() { } @Builder - public OrderProduct(Long id, Product product, Integer count, Integer orderPrice, LocalDateTime createdAt, LocalDateTime updatedAt, OrderSheet orderSheet) { + public OrderProduct(Long id, Product product, Long productId,Integer count, Integer orderPrice, LocalDateTime createdAt, LocalDateTime updatedAt, OrderSheet orderSheet) { this.id = id; this.product = product; this.count = count; @@ -48,5 +55,8 @@ public OrderProduct(Long id, Product product, Integer count, Integer orderPrice, this.createdAt = createdAt; this.updatedAt = updatedAt; this.orderSheet = orderSheet; + this.productId = productId; } + + } diff --git a/src/main/java/shop/mtcoding/metamall/model/ordersheet/OrderSheet.java b/src/main/java/shop/mtcoding/metamall/model/ordersheet/OrderSheet.java index 7638710..d90a80d 100644 --- a/src/main/java/shop/mtcoding/metamall/model/ordersheet/OrderSheet.java +++ b/src/main/java/shop/mtcoding/metamall/model/ordersheet/OrderSheet.java @@ -1,9 +1,7 @@ package shop.mtcoding.metamall.model.ordersheet; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.*; import shop.mtcoding.metamall.model.orderproduct.OrderProduct; import shop.mtcoding.metamall.model.product.Product; import shop.mtcoding.metamall.model.user.User; @@ -16,20 +14,31 @@ @NoArgsConstructor @Setter // DTO 만들면 삭제해야됨 @Getter +@Builder +@AllArgsConstructor @Table(name = "order_sheet_tb") @Entity public class OrderSheet { // 주문서 @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @ManyToOne + @JoinColumn(name = "username",insertable = false,updatable = false) private User user; // 주문자 - @OneToMany(mappedBy = "orderSheet") + + + @Builder.Default + @OneToMany(mappedBy = "orderSheet",cascade = CascadeType.ALL,orphanRemoval = true) private List orderProductList = new ArrayList<>(); // 총 주문 상품 리스트 private Integer totalPrice; // 총 주문 금액 (총 주문 상품 리스트의 orderPrice 합) private LocalDateTime createdAt; private LocalDateTime updatedAt; + + @Column(name = "username") + private String userId; + @PrePersist protected void onCreate() { this.createdAt = LocalDateTime.now(); @@ -39,15 +48,14 @@ protected void onCreate() { protected void onUpdate() { this.updatedAt = LocalDateTime.now(); } + + public void addOrderProductList(List orderProductList){ + orderProductList.forEach(orderProduct -> orderProduct.setOrderSheet(this)); + + this.orderProductList = orderProductList; + } // 연관관계 메서드 구현 필요 - @Builder - public OrderSheet(Long id, User user, Integer totalPrice, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.id = id; - this.user = user; - this.totalPrice = totalPrice; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } + } diff --git a/src/main/java/shop/mtcoding/metamall/model/ordersheet/OrderSheetRepository.java b/src/main/java/shop/mtcoding/metamall/model/ordersheet/OrderSheetRepository.java index 5d59249..9be537d 100644 --- a/src/main/java/shop/mtcoding/metamall/model/ordersheet/OrderSheetRepository.java +++ b/src/main/java/shop/mtcoding/metamall/model/ordersheet/OrderSheetRepository.java @@ -1,6 +1,26 @@ package shop.mtcoding.metamall.model.ordersheet; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import javax.swing.text.html.Option; +import java.util.List; +import java.util.Optional; public interface OrderSheetRepository extends JpaRepository { + + @Query("SELECT os FROM OrderSheet os " + + "JOIN FETCH os.orderProductList join fetch os.user " + + "WHERE os.user.username = :username") + public Optional> findOrdData(@Param("username") String username); + + + @Query("SELECT os FROM OrderSheet os " + + "JOIN FETCH os.orderProductList JOIN FETCH os.user") + public Optional> findAllData(); + + + @Query("SELECT os FROM OrderSheet os JOIN FETCH os.orderProductList JOIN FETCH os.user WHERE os.id = :id ") + public Optional findByIdWithUser(@Param("id") Long id); } diff --git a/src/main/java/shop/mtcoding/metamall/model/product/Product.java b/src/main/java/shop/mtcoding/metamall/model/product/Product.java index bc8c618..ff553ce 100644 --- a/src/main/java/shop/mtcoding/metamall/model/product/Product.java +++ b/src/main/java/shop/mtcoding/metamall/model/product/Product.java @@ -33,6 +33,7 @@ protected void onUpdate() { this.updatedAt = LocalDateTime.now(); } + @Builder public Product(Long id, String name, Integer price, Integer qty, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; @@ -42,4 +43,19 @@ public Product(Long id, String name, Integer price, Integer qty, LocalDateTime c this.createdAt = createdAt; this.updatedAt = updatedAt; } -} + + public void changeProduct(String name, Integer price, Integer qty) { + this.name = name; + this.price = price; + this.qty = qty; + } + + public Integer minusQty() { + this.qty = this.qty - 1; + return qty; + } + + public void plusQty() { + this.qty = this.qty + 1; + } +} \ No newline at end of file diff --git a/src/main/java/shop/mtcoding/metamall/model/user/User.java b/src/main/java/shop/mtcoding/metamall/model/user/User.java index c929ce5..88456e6 100644 --- a/src/main/java/shop/mtcoding/metamall/model/user/User.java +++ b/src/main/java/shop/mtcoding/metamall/model/user/User.java @@ -1,22 +1,25 @@ package shop.mtcoding.metamall.model.user; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import shop.mtcoding.metamall.model.ordersheet.OrderSheet; import javax.persistence.*; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @NoArgsConstructor + @Setter // DTO 만들면 삭제해야됨 @Getter @Table(name = "user_tb") @Entity public class User { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; private String username; private String password; private String email; @@ -24,6 +27,11 @@ public class User { private LocalDateTime createdAt; private LocalDateTime updatedAt; + @JsonIgnore + @OneToMany(mappedBy = "user") + private List orderSheetList = new ArrayList<>(); + + @PrePersist protected void onCreate() { this.createdAt = LocalDateTime.now(); @@ -35,8 +43,7 @@ protected void onUpdate() { } @Builder - public User(Long id, String username, String password, String email, String role, LocalDateTime createdAt) { - this.id = id; + public User(String username, String password, String email, String role, LocalDateTime createdAt) { this.username = username; this.password = password; this.email = email; diff --git a/src/main/java/shop/mtcoding/metamall/model/user/UserRepository.java b/src/main/java/shop/mtcoding/metamall/model/user/UserRepository.java index 293a101..4b3824b 100644 --- a/src/main/java/shop/mtcoding/metamall/model/user/UserRepository.java +++ b/src/main/java/shop/mtcoding/metamall/model/user/UserRepository.java @@ -10,4 +10,5 @@ public interface UserRepository extends JpaRepository { @Query("select u from User u where u.username = :username") Optional findByUsername(@Param("username") String username); + } diff --git a/src/main/java/shop/mtcoding/metamall/module/AspectConfig.java b/src/main/java/shop/mtcoding/metamall/module/AspectConfig.java new file mode 100644 index 0000000..30e57a6 --- /dev/null +++ b/src/main/java/shop/mtcoding/metamall/module/AspectConfig.java @@ -0,0 +1,67 @@ +package shop.mtcoding.metamall.module; + + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import shop.mtcoding.metamall.core.CodeEnum; +import shop.mtcoding.metamall.dto.ResponseDto; + +import java.util.HashMap; +import java.util.Map; + + +/** + * + * BindngResult 체크 + */ +@Aspect +@Slf4j +@Component +public class AspectConfig { + + @Pointcut("@annotation(shop.mtcoding.metamall.annotation.BindingCheck)") + public void pointCut(){} + + + @Around("pointCut()") + public Object module(ProceedingJoinPoint joinPoint) throws Throwable { + String type = joinPoint.getSignature().getDeclaringTypeName(); + String method = joinPoint.getSignature().getName(); + + log.info("VALIDATION TYPE : {}",type); + log.info("VALIDATION METHOD : {}",method); + + Object[] args = joinPoint.getArgs(); + + for(Object arg : args){ + if(arg instanceof BindingResult){ + BindingResult bindingResult = (BindingResult) arg; + + if(bindingResult.hasErrors()){ + Map errorMap = new HashMap<>(); + + bindingResult.getFieldErrors().forEach(fieldError -> { + errorMap.put(fieldError.getField(), fieldError.getDefaultMessage()); + }); + + ResponseDto responseDto = new ResponseDto() + .code(CodeEnum.INVALID_ARGUMENT) + .data(errorMap) + .msg(CodeEnum.INVALID_ARGUMENT.getMessage()); + + return ResponseEntity.status(CodeEnum.INVALID_ARGUMENT.getCode()).body(responseDto); + } + } + } + return joinPoint.proceed(); + + } + + +} diff --git a/src/main/java/shop/mtcoding/metamall/module/PermissionAspect.java b/src/main/java/shop/mtcoding/metamall/module/PermissionAspect.java new file mode 100644 index 0000000..1a26ca8 --- /dev/null +++ b/src/main/java/shop/mtcoding/metamall/module/PermissionAspect.java @@ -0,0 +1,93 @@ +package shop.mtcoding.metamall.module; + + +import com.google.gson.Gson; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.hibernate.mapping.Join; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import shop.mtcoding.metamall.core.CodeEnum; +import shop.mtcoding.metamall.dto.ResponseDto; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Aspect +@Slf4j +@Component +public class PermissionAspect { + + + @Pointcut("@annotation(shop.mtcoding.metamall.annotation.Permission)") + public void cut(){} + @Pointcut("@annotation(shop.mtcoding.metamall.annotation.Customer)") + public void customer(){} + + @Around("cut()") + public Object before(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { + HttpServletRequest request = + ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + + String role = request.getAttribute("role").toString(); + + log.info("ASPECT ROLE CHEKCING ++> {}",role); + if(role != null && role.equalsIgnoreCase("SELLER")){ + return proceedingJoinPoint.proceed(); + }else{ + HttpServletResponse response = + ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getResponse(); + //RequestContextHolder를 사용하면 현재 쓰레드에서 처리되고 있는 요청에 관한 HttpServletRequest와 HttpServletResponse를 가져올 수 있음 + Gson gson = new Gson(); + + ResponseDto responseDto = + new ResponseDto<>().code(CodeEnum.FORBIDDEN) + .msg(CodeEnum.FORBIDDEN.getMessage()); + + String temp = gson.toJson(responseDto); + + log.warn("Attempt to access resources without permission {}",role); + + return ResponseEntity.status(CodeEnum.FORBIDDEN.getCode()).body(responseDto); + } + } + + @Around("customer()") + public Object beforeCustomer(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ + HttpServletRequest request = + ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + + String role = request.getAttribute("role").toString(); + + if(role != null && (role.equalsIgnoreCase("CUSTOMER") || role.equalsIgnoreCase("SELLER"))){ + return proceedingJoinPoint.proceed(); + }else{ + HttpServletResponse response + = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getResponse(); + + Gson gson = new Gson(); + + ResponseDto responseDto = + new ResponseDto<>() + .code(CodeEnum.FORBIDDEN) + .msg(CodeEnum.FORBIDDEN.getMessage()); + + String temp = gson.toJson(responseDto); + + log.warn("Attempt to access resources without permission {}",role); + + return ResponseEntity.status(CodeEnum.FORBIDDEN.getCode()).body(responseDto); + + } + } + + + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1d9bd50..6999dd4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -25,4 +25,4 @@ spring: logging: level: '[shop.mtcoding.metamall]': DEBUG # DEBUG 레벨부터 에러 확인할 수 있게 설정하기 - '[org.hibernate.type]': TRACE # 콘솔 쿼리에 ? 에 주입된 값 보기 \ No newline at end of file +# '[org.hibernate.type]': TRACE # 콘솔 쿼리에 ? 에 주입된 값 보기 \ No newline at end of file diff --git a/src/test/java/shop/mtcoding/metamall/controller/OrderControllerTest.java b/src/test/java/shop/mtcoding/metamall/controller/OrderControllerTest.java new file mode 100644 index 0000000..c6919ef --- /dev/null +++ b/src/test/java/shop/mtcoding/metamall/controller/OrderControllerTest.java @@ -0,0 +1,199 @@ +package shop.mtcoding.metamall.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import shop.mtcoding.metamall.core.filter.AccessFilter; +import shop.mtcoding.metamall.model.log.error.ErrorLogRepository; +import shop.mtcoding.metamall.model.orderproduct.OrderProduct; +import shop.mtcoding.metamall.model.orderproduct.OrderProductRepository; +import shop.mtcoding.metamall.model.ordersheet.OrderSheet; +import shop.mtcoding.metamall.model.ordersheet.OrderSheetRepository; +import shop.mtcoding.metamall.model.product.Product; +import shop.mtcoding.metamall.model.product.ProductRepository; +import shop.mtcoding.metamall.model.user.User; +import shop.mtcoding.metamall.model.user.UserRepository; + +import javax.servlet.http.HttpServletRequest; +import javax.swing.text.html.Option; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@WebMvcTest(value = OrderController.class,excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = AccessFilter.class)) +class OrderControllerTest { + + + @MockBean + private OrderSheetRepository orderSheetRepository; + + @MockBean + private ProductRepository productRepository; + @MockBean + private OrderProductRepository orderProductRepository; + @MockBean + private UserRepository userRepository; + + + @MockBean + private ErrorLogRepository errorLogRepository; + + @Autowired + private MockMvc mockMvc; + + + @Test + @DisplayName("OrderController LIST GET TEST") + void OrderControllerTest() throws Exception { + //given + HttpServletRequest request = mock(HttpServletRequest.class); + OrderSheet o1 = OrderSheet.builder().totalPrice(300).build(); + OrderSheet o2 = OrderSheet.builder().totalPrice(400).build(); + + // when + given(orderSheetRepository.findAllData()).willReturn(Optional.of(List.of(o1,o2))); + + mockMvc.perform(MockMvcRequestBuilders.get("/order/all/list") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(jsonPath("$.data[0].totalPrice").value(300)) + .andExpect(jsonPath("$.data[1].totalPrice").value(400)); + } + + @Test + @DisplayName("OrderController MYPAGE GET TEST") + void test() throws Exception { + // given + OrderSheet o1 = OrderSheet.builder().totalPrice(300).build(); + OrderSheet o2 = OrderSheet.builder().totalPrice(400).build(); + // when + given(orderSheetRepository.findOrdData(anyString())).willReturn(Optional.of(List.of(o1,o2))); + // then + mockMvc.perform(MockMvcRequestBuilders.get("/order/my") + .requestAttr("username","tester")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(jsonPath("$.data[0].totalPrice").value(300)) + .andExpect(jsonPath("$.data[1].totalPrice").value(400)); + } + + + /** + * 목 객체의 메소드 호출에 대한 예상을 설정할 때는 any()와 같은 Matcher(any())를 사용하여 원하는 형태의 인자를 전달해야한다. + * 직접 객체를 넣을 경우 제대로 실행이 불가할 수 있다. Matcgher를 인자로 넣어야한다. + * json 포맷을 모를 떄는 andExpect().andReturn().getResponse().getContentAsString()으로 뽑아보자 + * */ + @Test + @DisplayName("OrderController POST TEST") + void test3() throws Exception { + // given + List list = List.of(OrderProduct.builder().orderPrice(404) + .product(Product.builder().qty(10).build()) + .productId(1L) + .build(), + OrderProduct.builder() + .product(Product.builder().qty(10).build()) + .productId(2L) + .orderPrice(500).build()); + + + OrderSheet orderSheet = OrderSheet.builder() + .orderProductList(list) + .totalPrice(400) + .userId("tester") + .build(); + + OrderSheet orderSheet1 = OrderSheet.builder() + .orderProductList(list) + .id(1L) + .totalPrice(400) + .userId("tester") + .build(); + + Optional product = Optional.of(Product.builder().id(1L).qty(10).build()); + + + // when + given(orderSheetRepository.save(any(OrderSheet.class))).willReturn(orderSheet1); + given(productRepository.findById(anyLong())).willReturn(product); + // then + + mockMvc.perform(MockMvcRequestBuilders.post("/order/save") + .requestAttr("username","tester") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(list)) + ).andExpect(status().isOk()) + .andExpect(jsonPath("$.data.orderProductList[0].product.qty").value(10)) + .andExpect(jsonPath("$.msg").value("success")) + .andExpect(jsonPath("$.status").value(200)); + + + } + + @Test + @DisplayName("DELETE REQUEST TEST") + void test4() throws Exception { + // given + List list = List.of(OrderProduct.builder().orderPrice(404) + .product(Product.builder().qty(10).build()) + .productId(1L) + .build(), + OrderProduct.builder() + .product(Product.builder().qty(10).build()) + .productId(2L) + .orderPrice(500).build()); + + + Optional orderSheet = Optional.of( + OrderSheet.builder() + .id(1L) + .totalPrice(400) + .user(User.builder().username("test").build()) + .orderProductList(list) + .build()); + + // when + given(orderSheetRepository.findByIdWithUser(any())).willReturn((Optional.of( + OrderSheet.builder() + .id(1L) + .totalPrice(400) + .user(User.builder().username("test").build()) + .orderProductList(list) + .build()))); + + // then + mockMvc.perform( + MockMvcRequestBuilders.delete("/order/delete") + .requestAttr("username","ADMIN") + .requestAttr("role","SELLER") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.data.id").value(1)) + .andExpect(jsonPath("$.data.user.username").value("test")) + .andExpect(jsonPath("$.data.orderProductList.length()").value(2)) + ; + } + +} \ No newline at end of file diff --git a/src/test/java/shop/mtcoding/metamall/model/ordersheet/OrderSheetRepositoryTest.java b/src/test/java/shop/mtcoding/metamall/model/ordersheet/OrderSheetRepositoryTest.java new file mode 100644 index 0000000..e107931 --- /dev/null +++ b/src/test/java/shop/mtcoding/metamall/model/ordersheet/OrderSheetRepositoryTest.java @@ -0,0 +1,51 @@ +package shop.mtcoding.metamall.model.ordersheet; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.transaction.annotation.Transactional; +import shop.mtcoding.metamall.model.orderproduct.OrderProduct; +import shop.mtcoding.metamall.model.orderproduct.OrderProductRepository; +import shop.mtcoding.metamall.model.product.Product; +import shop.mtcoding.metamall.model.product.ProductRepository; +import shop.mtcoding.metamall.model.user.User; +import shop.mtcoding.metamall.model.user.UserRepository; + +import static org.junit.jupiter.api.Assertions.*; + + + +@DataJpaTest +class OrderSheetRepositoryTest { + + @Autowired + public OrderSheetRepository orderSheetRepository; + + @Autowired + public UserRepository userRepository; + + @Autowired + public ProductRepository productRepository; + + @Autowired + public OrderProductRepository orderProductRepository; + + + + @BeforeEach + void setUp(){ + + User user = userRepository.save(User.builder().username("customer").password("1234").email("admin@nate.com").role("CUSTOMER").build()); + + Product product = productRepository.findById(1L).get(); + OrderSheet orderSheet = orderSheetRepository.save(OrderSheet.builder().totalPrice(40).userId(user.getUsername()).build()); + orderProductRepository.save(OrderProduct.builder().product(product).count(30).orderSheet(orderSheet).build()); + } + + + @Test + void test(){ + // orderSheetRepository.findOrderSheetByUsernameOptional("customer").get().forEach(System.out::println); + } +} \ No newline at end of file