diff --git "a/MVC\352\265\254\354\241\260\353\217\204.png" "b/MVC\352\265\254\354\241\260\353\217\204.png" new file mode 100644 index 0000000..30960c0 Binary files /dev/null and "b/MVC\352\265\254\354\241\260\353\217\204.png" differ diff --git a/OrderController_save.sdt b/OrderController_save.sdt new file mode 100644 index 0000000..d7b3708 --- /dev/null +++ b/OrderController_save.sdt @@ -0,0 +1,63 @@ +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.controller.OrderController","_attributes":["public"],"supers":[]},"_methodName":"save","_attributes":["public"],"_argNames":["saveReqDTO","errors","loginUser"],"_argTypes":["shop.minostreet.shoppingmall.dto.orderproduct.OrderReqDto.SaveReqDTO","org.springframework.validation.Errors","shop.minostreet.shoppingmall.config.auth.LoginUser"],"_returnType":"org.springframework.http.ResponseEntity\u003c?\u003e","offset":2313} +( +{"_enclosedMethodName":"save","_enclosedMethodArgTypes":["shop.minostreet.shoppingmall.dto.orderproduct.OrderReqDto.SaveReqDTO","org.springframework.validation.Errors","shop.minostreet.shoppingmall.config.auth.LoginUser"],"_classDescription":{"_className":"shop.minostreet.shoppingmall.controller.OrderController","_attributes":["public"],"supers":[]},"_methodName":"() -\u003e","_attributes":[],"_argNames":[],"_argTypes":[],"_returnType":"java.util.function.Supplier\u003cshop.minostreet.shoppingmall.handler.exception.MyApiException\u003e","offset":2597} +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.handler.exception.MyApiException","_attributes":["public"],"supers":["java.lang.RuntimeException","java.lang.Exception","java.lang.Throwable","java.io.Serializable"]},"_methodName":"new","_attributes":["public"],"_argNames":["message"],"_argTypes":["java.lang.String"],"_returnType":"shop.minostreet.shoppingmall.handler.exception.MyApiException","offset":2603} +) +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.handler.exception.MyApiException","_attributes":["public"],"supers":["java.lang.RuntimeException","java.lang.Exception","java.lang.Throwable","java.io.Serializable"]},"_methodName":"new","_attributes":["public"],"_argNames":["message"],"_argTypes":["java.lang.String"],"_returnType":"shop.minostreet.shoppingmall.handler.exception.MyApiException","offset":2716} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.service.OrderService","_attributes":["public"],"supers":[]},"_methodName":"주문등록","_attributes":["public"],"_argNames":["saveReqDTO","userPS"],"_argTypes":["shop.minostreet.shoppingmall.dto.orderproduct.OrderReqDto.SaveReqDTO","shop.minostreet.shoppingmall.domain.User"],"_returnType":"shop.minostreet.shoppingmall.dto.orderproduct.OrderRespDto.SaveRespDTO","offset":2827} +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.dto.orderproduct.OrderReqDto.SaveReqDTO","_attributes":["public","static"],"supers":[]},"_methodName":"toEntity","_attributes":["public"],"_argNames":["products"],"_argTypes":["java.util.List\u003cshop.minostreet.shoppingmall.domain.Product\u003e"],"_returnType":"java.util.List\u003cshop.minostreet.shoppingmall.domain.OrderProduct\u003e","offset":1749} +( +{"_enclosedMethodName":"toEntity","_enclosedMethodArgTypes":["java.util.List\u003cshop.minostreet.shoppingmall.domain.Product\u003e"],"_classDescription":{"_className":"shop.minostreet.shoppingmall.dto.orderproduct.OrderReqDto.SaveReqDTO","_attributes":["public","static"],"supers":[]},"_methodName":"orderProduct -\u003e","_attributes":[],"_argNames":["orderProduct"],"_argTypes":["shop.minostreet.shoppingmall.dto.orderproduct.OrderReqDto.SaveReqDTO.OrderProductDTO"],"_returnType":"java.util.function.Function\u003cshop.minostreet.shoppingmall.dto.orderproduct.OrderReqDto.SaveReqDTO.OrderProductDTO,java.util.stream.Stream\u003c? extends shop.minostreet.shoppingmall.domain.OrderProduct\u003e\u003e","offset":1357} +( +{"_enclosedMethodName":"toEntity","_enclosedMethodArgTypes":["java.util.List\u003cshop.minostreet.shoppingmall.domain.Product\u003e"],"_classDescription":{"_className":"shop.minostreet.shoppingmall.dto.orderproduct.OrderReqDto.SaveReqDTO","_attributes":["public","static"],"supers":[]},"_methodName":"product -\u003e","_attributes":[],"_argNames":["product"],"_argTypes":["shop.minostreet.shoppingmall.domain.Product"],"_returnType":"java.util.function.Predicate\u003cshop.minostreet.shoppingmall.domain.Product\u003e","offset":1699} +) +( +{"_enclosedMethodName":"toEntity","_enclosedMethodArgTypes":["java.util.List\u003cshop.minostreet.shoppingmall.domain.Product\u003e"],"_classDescription":{"_className":"shop.minostreet.shoppingmall.dto.orderproduct.OrderReqDto.SaveReqDTO","_attributes":["public","static"],"supers":[]},"_methodName":"product -\u003e","_attributes":[],"_argNames":["product"],"_argTypes":["shop.minostreet.shoppingmall.domain.Product"],"_returnType":"java.util.function.Function\u003cshop.minostreet.shoppingmall.domain.Product,shop.minostreet.shoppingmall.domain.OrderProduct\u003e","offset":1792} +) +) +) +( +{"_enclosedMethodName":"주문등록","_enclosedMethodArgTypes":["shop.minostreet.shoppingmall.dto.orderproduct.OrderReqDto.SaveReqDTO","shop.minostreet.shoppingmall.domain.User"],"_classDescription":{"_className":"shop.minostreet.shoppingmall.service.OrderService","_attributes":["public"],"supers":[]},"_methodName":"orderProduct -\u003e","_attributes":[],"_argNames":["orderProduct"],"_argTypes":["shop.minostreet.shoppingmall.domain.OrderProduct"],"_returnType":"java.util.function.ToIntFunction\u003cshop.minostreet.shoppingmall.domain.OrderProduct\u003e","offset":1863} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.OrderSheet","_attributes":["public"],"supers":[]},"_methodName":"builder","_attributes":["public","static"],"_argNames":[],"_argTypes":[],"_returnType":"shop.minostreet.shoppingmall.domain.OrderSheet.OrderSheetBuilder","offset":1961} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.OrderSheet.OrderSheetBuilder","_attributes":["public","static"],"supers":[]},"_methodName":"user","_attributes":["public"],"_argNames":["user"],"_argTypes":["shop.minostreet.shoppingmall.domain.User"],"_returnType":"shop.minostreet.shoppingmall.domain.OrderSheet.OrderSheetBuilder","offset":1971} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.OrderSheet.OrderSheetBuilder","_attributes":["public","static"],"supers":[]},"_methodName":"totalPrice","_attributes":["public"],"_argNames":["totalPrice"],"_argTypes":["java.lang.Integer"],"_returnType":"shop.minostreet.shoppingmall.domain.OrderSheet.OrderSheetBuilder","offset":1984} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.OrderSheet.OrderSheetBuilder","_attributes":["public","static"],"supers":[]},"_methodName":"build","_attributes":["public"],"_argNames":[],"_argTypes":[],"_returnType":"shop.minostreet.shoppingmall.domain.OrderSheet","offset":2007} +) +( +{"_enclosedMethodName":"주문등록","_enclosedMethodArgTypes":["shop.minostreet.shoppingmall.dto.orderproduct.OrderReqDto.SaveReqDTO","shop.minostreet.shoppingmall.domain.User"],"_classDescription":{"_className":"shop.minostreet.shoppingmall.service.OrderService","_attributes":["public"],"supers":[]},"_methodName":"orderProductPS -\u003e","_attributes":[],"_argNames":["orderProductPS"],"_argTypes":["shop.minostreet.shoppingmall.domain.OrderProduct"],"_returnType":"java.util.function.Consumer\u003cshop.minostreet.shoppingmall.domain.OrderProduct\u003e","offset":2186} +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.Product","_attributes":["public"],"supers":[]},"_methodName":"updateQty","_attributes":["public"],"_argNames":["orderCount"],"_argTypes":["java.lang.Integer"],"_returnType":"void","offset":2246} +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.handler.exception.MyApiException","_attributes":["public"],"supers":["java.lang.RuntimeException","java.lang.Exception","java.lang.Throwable","java.io.Serializable"]},"_methodName":"new","_attributes":["public"],"_argNames":["message"],"_argTypes":["java.lang.String"],"_returnType":"shop.minostreet.shoppingmall.handler.exception.MyApiException","offset":1649} +) +) +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.dto.orderproduct.OrderRespDto.SaveRespDTO","_attributes":["public","static"],"supers":[]},"_methodName":"new","_attributes":["public"],"_argNames":["orderProductListPS","orderSheetPS"],"_argTypes":["java.util.List\u003cshop.minostreet.shoppingmall.domain.OrderProduct\u003e","shop.minostreet.shoppingmall.domain.OrderSheet"],"_returnType":"shop.minostreet.shoppingmall.dto.orderproduct.OrderRespDto.SaveRespDTO","offset":2425} +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.dto.orderproduct.OrderRespDto.SaveRespDTO.OrderDto","_attributes":["public","static"],"supers":[]},"_methodName":"new","_attributes":["public"],"_argNames":["orderProduct"],"_argTypes":["shop.minostreet.shoppingmall.domain.OrderProduct"],"_returnType":"shop.minostreet.shoppingmall.dto.orderproduct.OrderRespDto.SaveRespDTO.OrderDto","offset":1837} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.util.MyDateUtil","_attributes":["public"],"supers":[]},"_methodName":"toStringFormat","_attributes":["public","static"],"_argNames":["localDateTime"],"_argTypes":["java.time.LocalDateTime"],"_returnType":"java.lang.String","offset":2036} +) +) +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.dto.ResponseDto","_attributes":["public"],"supers":[]},"_methodName":"new","_attributes":["public"],"_argNames":["code","msg","data"],"_argTypes":["java.lang.Integer","java.lang.String","T"],"_returnType":"shop.minostreet.shoppingmall.dto.ResponseDto","offset":2909} +) +) diff --git a/ProductController_registerProduct.sdt b/ProductController_registerProduct.sdt new file mode 100644 index 0000000..3b1a14a --- /dev/null +++ b/ProductController_registerProduct.sdt @@ -0,0 +1,45 @@ +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.controller.ProductController","_attributes":["public"],"supers":[]},"_methodName":"registerProduct","_attributes":["public"],"_argNames":["productRegisterReqDto","errors","loginUser"],"_argTypes":["shop.minostreet.shoppingmall.dto.product.ProductReqDto.ProductRegisterReqDto","org.springframework.validation.Errors","shop.minostreet.shoppingmall.config.auth.LoginUser"],"_returnType":"org.springframework.http.ResponseEntity\u003c?\u003e","offset":2636} +( +{"_enclosedMethodName":"registerProduct","_enclosedMethodArgTypes":["shop.minostreet.shoppingmall.dto.product.ProductReqDto.ProductRegisterReqDto","org.springframework.validation.Errors","shop.minostreet.shoppingmall.config.auth.LoginUser"],"_classDescription":{"_className":"shop.minostreet.shoppingmall.controller.ProductController","_attributes":["public"],"supers":[]},"_methodName":"() -\u003e","_attributes":[],"_argNames":[],"_argTypes":[],"_returnType":"java.util.function.Supplier\u003cshop.minostreet.shoppingmall.handler.exception.MyApiException\u003e","offset":2945} +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.handler.exception.MyApiException","_attributes":["public"],"supers":["java.lang.RuntimeException","java.lang.Exception","java.lang.Throwable","java.io.Serializable"]},"_methodName":"new","_attributes":["public"],"_argNames":["message"],"_argTypes":["java.lang.String"],"_returnType":"shop.minostreet.shoppingmall.handler.exception.MyApiException","offset":2951} +) +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.service.ProductService","_attributes":["public"],"supers":[]},"_methodName":"상품등록","_attributes":["public"],"_argNames":["productRegisterReqDto","sellerPS"],"_argTypes":["shop.minostreet.shoppingmall.dto.product.ProductReqDto.ProductRegisterReqDto","shop.minostreet.shoppingmall.domain.User"],"_returnType":"shop.minostreet.shoppingmall.dto.product.ProductRespDto.ProductRegisterRespDto","offset":3078} +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.repository.ProductRepository","_attributes":["public","abstract","interface"],"supers":["org.springframework.data.jpa.repository.JpaRepository","org.springframework.data.repository.PagingAndSortingRepository","org.springframework.data.repository.CrudRepository","org.springframework.data.repository.Repository","org.springframework.data.repository.query.QueryByExampleExecutor"]},"_methodName":"findByName","_attributes":["public","abstract","interface"],"_argNames":["name"],"_argTypes":["java.lang.String"],"_returnType":"java.util.Optional\u003cshop.minostreet.shoppingmall.domain.Product\u003e","offset":1949} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.handler.exception.MyApiException","_attributes":["public"],"supers":["java.lang.RuntimeException","java.lang.Exception","java.lang.Throwable","java.io.Serializable"]},"_methodName":"new","_attributes":["public"],"_argNames":["message"],"_argTypes":["java.lang.String"],"_returnType":"shop.minostreet.shoppingmall.handler.exception.MyApiException","offset":2082} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.dto.product.ProductReqDto.ProductRegisterReqDto","_attributes":["public","static"],"supers":[]},"_methodName":"toEntity","_attributes":["public"],"_argNames":["user"],"_argTypes":["shop.minostreet.shoppingmall.domain.User"],"_returnType":"shop.minostreet.shoppingmall.domain.Product","offset":2226} +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.Product","_attributes":["public"],"supers":[]},"_methodName":"builder","_attributes":["public","static"],"_argNames":[],"_argTypes":[],"_returnType":"shop.minostreet.shoppingmall.domain.Product.ProductBuilder","offset":1163} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.Product.ProductBuilder","_attributes":["public","static"],"supers":[]},"_methodName":"seller","_attributes":["public"],"_argNames":["seller"],"_argTypes":["shop.minostreet.shoppingmall.domain.User"],"_returnType":"shop.minostreet.shoppingmall.domain.Product.ProductBuilder","offset":1194} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.Product.ProductBuilder","_attributes":["public","static"],"supers":[]},"_methodName":"name","_attributes":["public"],"_argNames":["name"],"_argTypes":["java.lang.String"],"_returnType":"shop.minostreet.shoppingmall.domain.Product.ProductBuilder","offset":1228} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.Product.ProductBuilder","_attributes":["public","static"],"supers":[]},"_methodName":"price","_attributes":["public"],"_argNames":["price"],"_argTypes":["java.lang.Integer"],"_returnType":"shop.minostreet.shoppingmall.domain.Product.ProductBuilder","offset":1260} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.Product.ProductBuilder","_attributes":["public","static"],"supers":[]},"_methodName":"qty","_attributes":["public"],"_argNames":["qty"],"_argTypes":["java.lang.Integer"],"_returnType":"shop.minostreet.shoppingmall.domain.Product.ProductBuilder","offset":1294} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.Product.ProductBuilder","_attributes":["public","static"],"supers":[]},"_methodName":"build","_attributes":["public"],"_argNames":[],"_argTypes":[],"_returnType":"shop.minostreet.shoppingmall.domain.Product","offset":1324} +) +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.dto.product.ProductRespDto.ProductRegisterRespDto","_attributes":["public","static"],"supers":[]},"_methodName":"new","_attributes":["public"],"_argNames":["product"],"_argTypes":["shop.minostreet.shoppingmall.domain.Product"],"_returnType":"shop.minostreet.shoppingmall.dto.product.ProductRespDto.ProductRegisterRespDto","offset":2279} +) +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.dto.ResponseDto","_attributes":["public"],"supers":[]},"_methodName":"new","_attributes":["public"],"_argNames":["code","msg","data"],"_argTypes":["java.lang.Integer","java.lang.String","T"],"_returnType":"shop.minostreet.shoppingmall.dto.ResponseDto","offset":3207} +) +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..1379ca6 --- /dev/null +++ b/README.md @@ -0,0 +1,183 @@ +## [상품 주문 서비스 프로젝트](https://github.com/ji-hoooon/Springboot-MetaMall-Project) + + +## 목차 +* [프로젝트 간단 요약](#프로젝트-간단-요약)
+* [사용한 기술 스택](#사용한-기술-스택)
+* [프로젝트 정보](#프로젝트-정보)
+* [프로젝트 구조](#프로젝트-설명)
+* [프로젝트 설명](#프로젝트-설명)
+ + +## 프로젝트 간단 요약 +상품 주문 서비스 API를 만들기 위한 프로젝트 +* REST API로 요청과 응답을 JSON으로 처리 +* 스프링부트로 MVC 처리 +* 스프링 시큐리티를 이용해 권한과 인가 처리 +* JWT을 이용한 토큰 기반 인증 처리 +* Spring Data JPA로 영속 계층 처리 + +## 사용한 기술 스택 +Spring Boot +Spring Security +Spring Data JPA +MySQL +JWT +JUnit + +## 프로젝트 정보 +|진행기간|목표|팀원| +|------|---|---| +|2023-04-09~2023-04-10 | 프로젝트 설계 및 기본 구현 |이지훈| +|2023-04-10~2023-04-18 | 유저 기능, AOP 설계 및 구현 |이지훈| +|2023-04-18~2023-04-21 | 상품, 주문 기능 설계 및 구현 |이지훈| + +## 프로젝트 구조 + + + + +* ERD + +![ERD](https://user-images.githubusercontent.com/37648641/233539052-1d19db0c-6608-4578-8259-adcf72253a39.png) + +* 클래스 다이어그램 + +![mvc구조도.png](MVC구조도.png) + +* 시퀀스 다이어그램 + + 1. 사용자 회원가입 + ![UserController_join](https://user-images.githubusercontent.com/37648641/233537923-6c608c22-d783-46d0-a58b-65aed6bd967c.png) + + + 2. 상품 등록 + ![ProductController_registerProduct](https://user-images.githubusercontent.com/37648641/233537971-ca5b23df-d7e5-4fc4-a273-12c035d9711f.png) + + + 3. 상품 주문 + ![OrderController_save](https://user-images.githubusercontent.com/37648641/233537898-9faf0c0e-aa55-4763-b820-1af23245e234.png) + + +## 프로젝트 설명 + +### 개요 +1. 공통 모듈 / 공통 유틸리티 + - 사용자 권한 열거형 이용 + - Auditing 기능을 사용한 자동 타임스탬프 생성 + - 자주 사용하는 유틸을 모아둔 유틸 클래스 + - MyDateUtil + - LocalDateTime타입을 DTO 반환을 위해 String으로 변환해주는 클래스 + - MyResponseUtil + - 응답 DTO 클래스로 응답 DTO 작성시 사용하는 클래스로 성공을 응답하거나 실패를 응답한다. + - AOP + - MyErrorLogAdvice + - @MyErrorLogRecord 커스텀 어노테이션 작성해, 로그인한 유저의 예외 발생시 자동으로 로깅처리 + - MyValidationAdvice + - PostMapping과 PutMapping에 대해서 @Valid가 붙은 경우 유효성 검사 수행하고, Errors 객체에 결과를 담는다. + - MyExceptionHandler의 예외처리 메서드에 모두 @MyErrorLogRecord를 붙여서 예외발생시 기록하도록 한다. +2. 토큰을 이용한 인증 + - 헤더에 JWT 토큰 노출 + ```java + //(1) JS코드로 토큰에 접근해서 클라이언트의 로컬영역에 저장할 수 있도록 설정해야한다. (기본값이 disable) + //(2) 실제 서버에서는 JWT 탈취 위험성 때문에 보안조치가 필요하다. + //(3) 기본값은 cors-safelisted reponse header만 노출 + configuration.addExposedHeader("Authorization"); + ``` +2. ApplicationListener로 로그인 로깅 구현 + ```java + @Bean + public ApplicationListener authenticationSuccessListener() { + return (ApplicationListener) event -> { + Authentication authentication = event.getAuthentication(); + log.debug("디버그 : onAuthenticationSuccess 호출됨"); + // 1. 로그인 유저 정보 가져오기 + LoginUser userDetails = (LoginUser) authentication.getPrincipal(); + User loginUser = userDetails.getUser(); + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + + // 2. 최종 로그인 날짜 기록 (더티체킹 - update 쿼리 발생) + // loginUser.(LocalDateTime.now()); + // 3. 로그 테이블 기록 + LoginLog loginLog = LoginLog.builder() + .userId(loginUser.getId()) + .userAgent(request.getHeader("User-Agent")) + .clientIP(request.getRemoteAddr()) + .build(); + loginLogRepository.save(loginLog); + }; + } + ``` +3. LAZY 로딩과 단방향 매핑을 이용해 쿼리를 수행하도록 한다. + - OrderSheet와 OrderProduct는 일대다 관계로, 단방향 매핑으로 연결 + - 연관관계에 있는 두 엔티티는 비즈니스 로직에 따라 유기적으로 동작하도록 작성 + - @EntityGraph로 LAZY LOADING 발동 하도록 해서 N+1문제 해결 + +4. 요청과 응답은 반드시 독립적인 DTO로 처리 + - 엔티티를 노출시키면 DB 공격에 취약할 수 있으므로 DTO를 이용해 필요한 데이터만 전달 + - ResponseDto + - UserReqDto, UserRespDto + - OrderReqDto, OderRespDto + - ProductReqDto, ProductRespDto + +### 테스트 +1. 테스트시 인증처리 용이하도록 설정 + ```java + //(1) 테스트를 위한 코드 실행전에 로그인 필요 + //(2) 실제 로그인 로직 : jwt -> 인증 필터 -> 시큐리티 세션 생성 + //(3) JWT를 이용한 토큰 방식의 로그인보다는, 세션에 직접 LoginUser를 주입하는 방식으로 강제 로그인 진행 + // @WithUserDetails(value = "ssar") //DB에서 해당 유저를 조회해서 세션에 담아주는 어노테이션 + // setUp()으로 추가 했음에도 setupBefore=TEST_METHOD 에러 발생 + //(4) TEST_METHOD 설정시 @WithUserDetails가 setUp() 메서드 수행 전에 실행시간이 같다. + //(5) TEST_EXECUTION 설정으로 @WithUserDetails가 setUp() 메서드 수행 후에 실행하도록 한다. + @WithUserDetails(value = "ssar", setupBefore = TestExecutionEvent.TEST_EXECUTION) //DB에서 해당 유저를 조회해서 세션에 담아주는 어노테이션 + ``` +2. 테스트를 위한 더미 오브젝트 작성 -더미 오브젝트는 단위테스트를 위해 id를 명시적으로 지정한다. + ```java + public class DummyObject { + //모두 스태틱 메서드 + protected User newUser(String username){ + BCryptPasswordEncoder passwordEncoder= new BCryptPasswordEncoder(); + String encPassword = passwordEncoder.encode("1234"); + return User.builder() + .username(username) + // .password("1234") + .password(encPassword) + .email(username+"@nate.com") + .role(UserEnum.CUSTOMER) + .build(); + } + protected static User newMockUser(Long id,String username){ + BCryptPasswordEncoder passwordEncoder= new BCryptPasswordEncoder(); + String encPassword = passwordEncoder.encode("1234"); + return User.builder() + .id(id) + .username(username) + // .password("1234") + .password(encPassword) + .email(username+"@nate.com") + .role(UserEnum.CUSTOMER) + .build(); + } + ``` + +3. 모키토 통합 테스트시 사용하는 어노테이션 + ```java + @ActiveProfiles("test") + //(1) dev 모드에서 발동하는 DummyInit의 유저가 삽입되므로, 테스트에서 test 프로퍼티파일 사용하도록 하는 설정 + //@Transactional + @Sql("classpath:db/teardown.sql") + //(2) 테스트코드에서는 @Transactional 어노테이션 사용하는 대신, + // 외래키 제약조건 해제 후, 테이블 truncate 수행한다음 다시 외래키 제약조건 설정 + @AutoConfigureMockMvc + //(3) 모키토 환경에서 MockMvc 객체를 사용하기 위한 어노테이션 + @SpringBootTest(webEnvironment = WebEnvironment.MOCK) + //(4) 웹 애플리케이션을 위한 Mock 객체를 빈으로 주입해주는 어노테이션 + ``` + diff --git a/UserController_join.sdt b/UserController_join.sdt new file mode 100644 index 0000000..dffa146 --- /dev/null +++ b/UserController_join.sdt @@ -0,0 +1,39 @@ +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.controller.UserController","_attributes":["public"],"supers":[]},"_methodName":"join","_attributes":["public"],"_argNames":["joinReqDto","bindingResult"],"_argTypes":["shop.minostreet.shoppingmall.dto.user.UserReqDto.JoinReqDto","org.springframework.validation.BindingResult"],"_returnType":"org.springframework.http.ResponseEntity\u003c?\u003e","offset":2271} +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.service.UserService","_attributes":["public"],"supers":[]},"_methodName":"회원가입","_attributes":["public"],"_argNames":["joinReqDto"],"_argTypes":["shop.minostreet.shoppingmall.dto.user.UserReqDto.JoinReqDto"],"_returnType":"shop.minostreet.shoppingmall.dto.user.UserRespDto.JoinRespDto","offset":3047} +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.repository.UserRepository","_attributes":["public","abstract","interface"],"supers":["org.springframework.data.jpa.repository.JpaRepository","org.springframework.data.repository.PagingAndSortingRepository","org.springframework.data.repository.CrudRepository","org.springframework.data.repository.Repository","org.springframework.data.repository.query.QueryByExampleExecutor"]},"_methodName":"findByUsername","_attributes":["public","abstract","interface"],"_argNames":["username"],"_argTypes":["java.lang.String"],"_returnType":"java.util.Optional\u003cshop.minostreet.shoppingmall.domain.User\u003e","offset":1297} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.handler.exception.MyApiException","_attributes":["public"],"supers":["java.lang.RuntimeException","java.lang.Exception","java.lang.Throwable","java.io.Serializable"]},"_methodName":"new","_attributes":["public"],"_argNames":["message"],"_argTypes":["java.lang.String"],"_returnType":"shop.minostreet.shoppingmall.handler.exception.MyApiException","offset":1425} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.dto.user.UserReqDto.JoinReqDto","_attributes":["public","static"],"supers":[]},"_methodName":"toEntity","_attributes":["public"],"_argNames":["bCryptPasswordEncoder"],"_argTypes":["org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"],"_returnType":"shop.minostreet.shoppingmall.domain.User","offset":1558} +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.User","_attributes":["public"],"supers":[]},"_methodName":"builder","_attributes":["public","static"],"_argNames":[],"_argTypes":[],"_returnType":"shop.minostreet.shoppingmall.domain.User.UserBuilder","offset":1159} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.User.UserBuilder","_attributes":["public","static"],"supers":[]},"_methodName":"username","_attributes":["public"],"_argNames":["username"],"_argTypes":["java.lang.String"],"_returnType":"shop.minostreet.shoppingmall.domain.User.UserBuilder","offset":1190} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.User.UserBuilder","_attributes":["public","static"],"supers":[]},"_methodName":"password","_attributes":["public"],"_argNames":["password"],"_argTypes":["java.lang.String"],"_returnType":"shop.minostreet.shoppingmall.domain.User.UserBuilder","offset":1307} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.User.UserBuilder","_attributes":["public","static"],"supers":[]},"_methodName":"role","_attributes":["public"],"_argNames":["role"],"_argTypes":["shop.minostreet.shoppingmall.domain.UserEnum"],"_returnType":"shop.minostreet.shoppingmall.domain.User.UserBuilder","offset":1377} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.User.UserBuilder","_attributes":["public","static"],"supers":[]},"_methodName":"status","_attributes":["public"],"_argNames":["status"],"_argTypes":["java.lang.Boolean"],"_returnType":"shop.minostreet.shoppingmall.domain.User.UserBuilder","offset":1422} +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.domain.User.UserBuilder","_attributes":["public","static"],"supers":[]},"_methodName":"build","_attributes":["public"],"_argNames":[],"_argTypes":[],"_returnType":"shop.minostreet.shoppingmall.domain.User","offset":1456} +) +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.dto.user.UserRespDto.JoinRespDto","_attributes":["public","static"],"supers":[]},"_methodName":"new","_attributes":["public"],"_argNames":["user"],"_argTypes":["shop.minostreet.shoppingmall.domain.User"],"_returnType":"shop.minostreet.shoppingmall.dto.user.UserRespDto.JoinRespDto","offset":1624} +) +) +( +{"_classDescription":{"_className":"shop.minostreet.shoppingmall.dto.ResponseDto","_attributes":["public"],"supers":[]},"_methodName":"new","_attributes":["public"],"_argNames":["code","msg","data"],"_argTypes":["java.lang.Integer","java.lang.String","T"],"_returnType":"shop.minostreet.shoppingmall.dto.ResponseDto","offset":3101} +) +) diff --git a/build.gradle b/build.gradle index 4943187..d2913a7 100644 --- a/build.gradle +++ b/build.gradle @@ -2,9 +2,11 @@ plugins { id 'java' id 'org.springframework.boot' version '2.7.10' id 'io.spring.dependency-management' version '1.0.15.RELEASE' + id 'org.asciidoctor.jvm.convert' version '3.3.2' + } -group = 'shop.mtcoding' +group = 'shop.minostreet' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' @@ -14,21 +16,59 @@ configurations { } } +def snippetsDir = file('build/generated-snippets') + +asciidoctorj { + configurations.create('asciidoctorExt') +} + +tasks.bootJar { + dependsOn tasks.asciidoctor + from(tasks.asciidoctor.outputDir) { + into 'static/docs' + } +} + +tasks.test { + outputs.dir snippetsDir +} + +tasks.asciidoctor { + dependsOn tasks.test + def snippets = file('build/generated-snippets') + configurations 'asciidoctorExt' + attributes 'snippets': snippets + inputs.dir snippets + sources { + include '**/index.adoc' + } + baseDirFollowsSourceFile() +} + repositories { mavenCentral() } dependencies { - 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-security' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + implementation 'com.auth0:java-jwt:4.3.0' +// third party library compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' - runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' + implementation 'commons-io:commons-io:2.11.0' } tasks.named('test') { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 774fae8..fae0804 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/img.png b/img.png new file mode 100644 index 0000000..6cc5230 Binary files /dev/null and b/img.png differ diff --git a/img_1.png b/img_1.png new file mode 100644 index 0000000..b31381b Binary files /dev/null and b/img_1.png differ diff --git a/img_2.png b/img_2.png new file mode 100644 index 0000000..6cc5230 Binary files /dev/null and b/img_2.png differ diff --git a/settings.gradle b/settings.gradle index 69fd95c..6fcdee2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'metamall' +rootProject.name = 'shoppingmall' diff --git a/src/docs/asciidoc/Member-API.adoc b/src/docs/asciidoc/Member-API.adoc new file mode 100644 index 0000000..2bc8070 --- /dev/null +++ b/src/docs/asciidoc/Member-API.adoc @@ -0,0 +1,19 @@ +[[User-API]] +== User API + +// [[Member-단일-조회]] +// === Member 단일 조회 +띄어쓰기 하면 안됨 +// operation::member-api-test/member_get_test[snippets='http-request,path-parameters,response-fields'] +// +// [[Member-페이징-조회]] +// === Member 페이징 조회 +// operation::member-api-test/member_page_test[snippets='http-request,request-parameters,http-response'] + +[[User-회원가입]] +=== User 생성 +operation::user-controller-test/join_success_test[snippets='http-request,request-fields,http-response,response-fields'] + +// [[Member-수정]] +// === Member 수정 +// operation::member-api-test/member_modify_test[snippets='path-parameters,http-request,request-fields,http-response'] \ No newline at end of file diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc new file mode 100644 index 0000000..6bac79f --- /dev/null +++ b/src/docs/asciidoc/index.adoc @@ -0,0 +1,15 @@ +// 문서에 대한 템플릿 추가 필요 += REST DOCS 제목 +Andy Wilkinson; +:doctype: book +:icons: font +:source-highlighter: highlights +:toc: left +:toclevels: 2 +:sectlinks: + +//오버뷰 추가 +include::overview.adoc[] + +// snippet 파일을 연동 +include::Member-API.adoc[] diff --git a/src/docs/asciidoc/overview.adoc b/src/docs/asciidoc/overview.adoc new file mode 100644 index 0000000..c42c10d --- /dev/null +++ b/src/docs/asciidoc/overview.adoc @@ -0,0 +1,39 @@ +[[overview]] +== Overview + +[[overview-host]] +=== Host + +|=== +| 환경 | Host + +| Sandbox +| `https://sandbox-xxx-service.com` + +| Beta +| `https://beta-xxx-service.com` + +| Production +| `https://xxx-service.com` +|=== + +[[overview-http-status-codes]] +=== HTTP status codes + +|=== +| Status code | Usage + +| `200 OK` +| successfully + +| `400 Bad Request` +| Bad Request + +| `500 Internal Server Error` +| Server Error +|=== + +[[overview-error-response]] +=== HTTP Error Response + +operation::user-controller-test/join_fail_test[snippets='http-response,response-fields'] \ No newline at end of file diff --git a/src/main/java/shop/minostreet/shoppingmall/ShoppingmallApplication.java b/src/main/java/shop/minostreet/shoppingmall/ShoppingmallApplication.java new file mode 100644 index 0000000..0ee7547 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/ShoppingmallApplication.java @@ -0,0 +1,27 @@ +package shop.minostreet.shoppingmall; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +//Audit 기능 사용하기 위한 어노테이션 3 +@EnableJpaAuditing +@SpringBootApplication +public class ShoppingmallApplication { + +// @Bean +// CommandLineRunner initData(UserRepository userRepository, ProductRepository productRepository, OrderProductRepository orderProductRepository, OrderSheetRepository orderSheetRepository){ +// return (args)->{ +// // 여기에서 save 하면 됨. +// // bulk Collector는 saveAll 하면 됨. +// User ssar = User.builder().username("ssar").password("1234").email("ssar@nate.com").role(UserEnum.CUSTOMER).build(); +// userRepository.save(ssar); +// }; +// } + + public static void main(String[] args) { + SpringApplication.run(ShoppingmallApplication.class, args); + } + +} diff --git a/src/main/java/shop/minostreet/shoppingmall/config/SecurityConfig.java b/src/main/java/shop/minostreet/shoppingmall/config/SecurityConfig.java new file mode 100644 index 0000000..2e449cc --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/config/SecurityConfig.java @@ -0,0 +1,184 @@ +package shop.minostreet.shoppingmall.config; + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import shop.minostreet.shoppingmall.config.auth.LoginUser; +import shop.minostreet.shoppingmall.config.jwt.JwtAuthenticationFilter; +import shop.minostreet.shoppingmall.config.jwt.JwtAuthorizationFilter; +import shop.minostreet.shoppingmall.domain.LoginLog; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.domain.UserEnum; +import shop.minostreet.shoppingmall.repository.LoginLogRepository; +import shop.minostreet.shoppingmall.util.MyResponseUtil; + +import javax.servlet.http.HttpServletRequest; + +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + // @Autowired +// LoginSuccessHandler loginSuccessHandler; + private final Logger log = LoggerFactory.getLogger(getClass()); + private final LoginLogRepository loginLogRepository; + + @Bean //IoC 컨테이너에 BCryptPasswordEncoder 객체 등록 + public BCryptPasswordEncoder passwordEncoder() { + log.debug("디버그 : BCryptPasswordEncoder 빈 등록됨"); + return new BCryptPasswordEncoder(); + } + + @Bean + public ApplicationListener authenticationSuccessListener() { + return (ApplicationListener) event -> { + Authentication authentication = event.getAuthentication(); + log.debug("디버그 : onAuthenticationSuccess 호출됨"); + // 1. 로그인 유저 정보 가져오기 + LoginUser userDetails = (LoginUser) authentication.getPrincipal(); + User loginUser = userDetails.getUser(); + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + + // 2. 최종 로그인 날짜 기록 (더티체킹 - update 쿼리 발생) +// loginUser.(LocalDateTime.now()); + + // 3. 로그 테이블 기록 + LoginLog loginLog = LoginLog.builder() + .userId(loginUser.getId()) + .userAgent(request.getHeader("User-Agent")) + .clientIP(request.getRemoteAddr()) + .build(); + loginLogRepository.save(loginLog); + }; + } + + + @Bean + //1. JWT 서버를 만들기 위한 설정 + //2. JWT 필터 등록 + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + log.debug("디버그 : filterChain 빈 등록됨"); + //iframe 사용안함 (인라인 프레임을 생성하는 태그로 타 사이트의 컨텐츠 혹은 타 페이지를 삽입) + http.headers().frameOptions().disable(); + //csrf 사용안함 + //:Cross-Site Request Forgery + // 타 사이트에서 인증된 사용자의 권한을 이용해서 공격하는 방식으로, + // 페이지 로드시 CSRF 토큰을 생성해 해당 토큰을 가진 요청만 처리함으로써 방지한다. + http.csrf().disable(); + //다른 서버의 자바스크립트 요청 거부 허용으로 (거부할 사항을 Null) + //: Cross Origin Resource Sharing + // 프론트엔드와 백엔드의 도메인을 다르게 설정해서 백엔드에서 프론트엔드의 요청만 응답하도록 설정한다. + http.cors().configurationSource(configurationSource()); + + //stateless 전략으로 사용하기 위해 jSessionId를 서버쪽에서 관리안하도록 설정 + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + //react 같은 프론트엔드 프레임워크 이용하므로 제공되는 폼 사용안함 + http.formLogin().disable(); + + //httpbasic 방식 사용안함 (팝업창 이용해서 사용자 인증하는 방식) + http.httpBasic().disable(); + + //jwt 필터 등록 +// http.apply(new CustomSecurityFilterManager(loginSuccessHandler)); + http.apply(new CustomSecurityFilterManager()); + + + //응답의 일관성을 만들기 위해 인증 실패 Exception 가로채기 + http.exceptionHandling().authenticationEntryPoint( + + (request, response, authenticationException) -> { + String uri = request.getRequestURI(); + log.debug("디버그 : " + uri); + +// MyResponseUtil.unAuthentication(response, "로그인이 필요합니다."); + MyResponseUtil.fail(response, "로그인이 필요합니다.", HttpStatus.UNAUTHORIZED); + + + } + ); + //인증이 되지 않은 사용자에 대한 예외처리하는 메서드로 파라미터는 + // ExceptionTranslationFilter로 필터링 되는 AuthenticationEntryPoint 객체 + //: AuthenticationEntryPoint의 commence 메서드는 파라미터로 request, response, AuthenticationException + + //응답의 일관성을 만들기 위해 권한 실패 Exception 가로채기 + http.exceptionHandling().accessDeniedHandler( + + // void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) + // 구현 필요 + (request, response, e) -> { + String uri = request.getRequestURI(); + log.debug("디버그 : " + uri); + MyResponseUtil.fail(response, "권한이 없습니다", HttpStatus.FORBIDDEN); + } + ); + + //접근 권한 설정 + http.authorizeRequests() + .antMatchers("/api/user/**").authenticated() + .antMatchers("/api/seller/**").access("hasRole('ADMIN') or hasRole('SELLER')") + .antMatchers("/api/admin/**").hasRole("ADMIN") //default prefix가 'ROLE_' + .anyRequest().permitAll(); + + return http.build(); + } + + public CorsConfigurationSource configurationSource() { + log.debug("디버그 : CorsConfigurationSource cors 설정돼 SecurityFilterChain에 등록됨"); + CorsConfiguration configuration = new CorsConfiguration(); + configuration.addAllowedHeader("*"); + configuration.addAllowedMethod("*"); //HTTP 메서드와 자바스크립트 요청 허용 + configuration.addAllowedOriginPattern("*"); //모든 IP 주소 허용(추후 프론트 엔드 쪽 IP 허용하도록 변경) + configuration.setAllowCredentials(true); //클라이언트에서 쿠키 요청 허용 + configuration.addExposedHeader("Authorization"); + //: 실제 서버에서는 JWT 탈취 위험성 때문에 보안조치가 필요하다. + // : cors-safelisted reponse header만 노출 + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); //작성한 설정을 모든 주소에 적용 + + return source; + } + + //JWT 필터 등록 + //(1) HttpSecurity가 없기 때문에 상속해서 캐스팅 + //: extends AbstractHttpConfigurer + //(2) AuthenticationManager가 없기 때문에 생성 + //: AuthenticationManager authenticationManager=builder.getSharedObject(AuthenticationManager.class); + public class CustomSecurityFilterManager extends AbstractHttpConfigurer { + // private final LoginSuccessHandler loginSuccessHandler; +// public CustomSecurityFilterManager(LoginSuccessHandler loginSuccessHandler) { +// this.loginSuccessHandler = loginSuccessHandler; +// } + public CustomSecurityFilterManager() {} + + @Override + public void configure(HttpSecurity builder) throws Exception { + AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); + builder.addFilter(new JwtAuthenticationFilter(authenticationManager)); +// builder.addFilter(new JwtAuthenticationFilter(authenticationManager, loginSuccessHandler)); + builder.addFilter(new JwtAuthorizationFilter(authenticationManager)); + +// super.configure(builder); + } + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/config/auth/LoginService.java b/src/main/java/shop/minostreet/shoppingmall/config/auth/LoginService.java new file mode 100644 index 0000000..5b3f312 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/config/auth/LoginService.java @@ -0,0 +1,35 @@ +package shop.minostreet.shoppingmall.config.auth; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.repository.UserRepository; + +@Service +@RequiredArgsConstructor +public class LoginService implements UserDetailsService { + +// @Autowired +// private UserRepository userRepository; + + private final UserRepository userRepository; + + @Override + //로그인할 때 세션 생성 메서드 + //: 시큐리티로 로그인 시, 시큐리티가 loadUserByUsername() 실행해 username 체크 + // 없으면 예외 발생 + // 있으면 정상적으로 시큐리티 컨텍스트 내부 세션에 로그인된 세션이 만들어진다. + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User userPS=userRepository.findByUsername(username).orElseThrow( + //인증 과정에 오류가 발생하면, 시큐리티가 제어권을 가지고 있기 때문에 Exception 발동 + ()->new InternalAuthenticationServiceException("인증 실패") + //테스트할 때 확인 필요 + ); + //있으면 정상적으로 시큐리티 컨텍스트 내부 세션에 로그인된 세션 추가 + return new LoginUser(userPS); + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/config/auth/LoginUser.java b/src/main/java/shop/minostreet/shoppingmall/config/auth/LoginUser.java new file mode 100644 index 0000000..e50fdd1 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/config/auth/LoginUser.java @@ -0,0 +1,58 @@ +package shop.minostreet.shoppingmall.config.auth; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import shop.minostreet.shoppingmall.domain.User; + +import javax.persistence.EntityListeners; +import java.util.ArrayList; +import java.util.Collection; + +//Audit 기능 사용하기 위한 어노테이션 1 +@EntityListeners(AuditingEntityListener.class) +@Getter +@RequiredArgsConstructor +public class LoginUser implements UserDetails { + + private final User user; + + @Override + public Collection getAuthorities() { + Collection authorities = new ArrayList<>(); + authorities.add(()-> "ROLE_"+user.getRole()); + return authorities; + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getUsername(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return user.getStatus(); + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/config/dummy/DummyInit.java b/src/main/java/shop/minostreet/shoppingmall/config/dummy/DummyInit.java new file mode 100644 index 0000000..c7aebca --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/config/dummy/DummyInit.java @@ -0,0 +1,30 @@ +package shop.minostreet.shoppingmall.config.dummy; + + +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import shop.minostreet.shoppingmall.domain.Product; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.repository.ProductRepository; +import shop.minostreet.shoppingmall.repository.UserRepository; + + +@Configuration +public class DummyInit extends DummyObject{ + + //빈 메서드에서 프로퍼티 설정 + @Profile("dev") + @Bean + //설정 클래스에 있는 빈메서드는 서버 실행시 무조건 실행 + //: DI 두 번째 방법 메서드 주입 + CommandLineRunner init(UserRepository userRepository, ProductRepository productRepository){ + return (args) -> { + User ssar = userRepository.save(newUserAdmin("ssar")); + User cos = userRepository.save(newUser("cos")); + Product product1=productRepository.save(newProduct("시계", 2, 5000)); + Product product2=productRepository.save(newProduct("자물쇠", 5, 3000)); + }; + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/config/dummy/DummyObject.java b/src/main/java/shop/minostreet/shoppingmall/config/dummy/DummyObject.java new file mode 100644 index 0000000..a7b1e68 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/config/dummy/DummyObject.java @@ -0,0 +1,68 @@ +package shop.minostreet.shoppingmall.config.dummy; + + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import shop.minostreet.shoppingmall.domain.Product; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.domain.UserEnum; + +public class DummyObject { + //모두 스태틱 메서드 + protected User newUserAdmin(String username){ + BCryptPasswordEncoder passwordEncoder= new BCryptPasswordEncoder(); + String encPassword = passwordEncoder.encode("1234"); + return User.builder() + .username(username) +// .password("1234") + .password(encPassword) + .email(username+"@nate.com") + .role(UserEnum.ADMIN) + .status(true) + .build(); + } + protected User newUser(String username){ + BCryptPasswordEncoder passwordEncoder= new BCryptPasswordEncoder(); + String encPassword = passwordEncoder.encode("1234"); + return User.builder() + .username(username) +// .password("1234") + .password(encPassword) + .email(username+"@nate.com") + .role(UserEnum.CUSTOMER) + .status(true) + .build(); + } + protected static User newMockUser(Long id,String username){ + BCryptPasswordEncoder passwordEncoder= new BCryptPasswordEncoder(); + String encPassword = passwordEncoder.encode("1234"); + return User.builder() + .id(id) + .username(username) +// .password("1234") + .password(encPassword) + .email(username+"@nate.com") + .role(UserEnum.CUSTOMER) + .status(true) + .build(); + } + + protected static Product newProduct(String name, Integer qty, Integer price){ + return Product.builder() + .name(name) + .qty(qty) + .price(1000) + .build(); + } + + protected static Product newMockProduct(Long id,String name, Integer qty, Integer price) { + return Product.builder() + .id(id) + .name(name) + .qty(qty) + .price(price) + .build(); + } + + + +} diff --git a/src/main/java/shop/minostreet/shoppingmall/config/jwt/JwtAuthenticationFilter.java b/src/main/java/shop/minostreet/shoppingmall/config/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..0e4c68e --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/config/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,119 @@ +package shop.minostreet.shoppingmall.config.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import shop.minostreet.shoppingmall.config.auth.LoginUser; +import shop.minostreet.shoppingmall.dto.user.UserReqDto.LoginReqDto; +import shop.minostreet.shoppingmall.dto.user.UserRespDto.LoginRespDto; +import shop.minostreet.shoppingmall.util.MyResponseUtil; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + private final Logger log = LoggerFactory.getLogger(getClass()); + private AuthenticationManager authenticationManager; +// @Autowired +// private LoginSuccessHandler loginSuccessHandler; + + +// public JwtAuthenticationFilter(AuthenticationManager authenticationManager, LoginSuccessHandler loginSuccessHandler) { +// public JwtAuthenticationFilter(AuthenticationManager authenticationManager, LoginSuccessHandler loginSuccessHandler) { +// super(authenticationManager); +// this.authenticationManager=authenticationManager; +// this.setAuthenticationSuccessHandler(loginSuccessHandler); +// setFilterProcessesUrl("/api/login"); +// } + public JwtAuthenticationFilter(AuthenticationManager authenticationManager) { + super(authenticationManager); + this.authenticationManager=authenticationManager; + setFilterProcessesUrl("/api/login"); + } + + + + //Post :/login시 동작하는 메서드 + //-> Post :/api/login시 동작하는 메서드 + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + log.debug("디버그 : attemptAuthentication 호출됨"); + //cmd+option+T + try { + //(1) request 객체의 json 데이터 꺼내기 + ObjectMapper om =new ObjectMapper(); + //(2) 로그인을 위한 DTO 작성 + //: UserReqDto의 내부클래스로 LoginReqDto (필터에서는 컨트롤러 전이므로 유효성 검사 불가능) + LoginReqDto loginReqDto=om.readValue(request.getInputStream(), LoginReqDto.class); + + //(3) 강제 로그인 + //: 토큰 방식의 인증을 사용하더라도 시큐리티의 권한체크, 인증체크 기능을 사용하기 위해 세션 생성 + //-> 임시 세션으로, request와 response 완료시 끝 + UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(loginReqDto.getUsername(), loginReqDto.getPassword()); + + Authentication authentication = authenticationManager.authenticate(authenticationToken); //UserDetailsService의 LoadUserByUsername 호출 + return authentication; + + } catch (Exception e) { + e.printStackTrace(); + //필터를 모두 통과한 후에 컨트롤러 단으로 들어가고, 그때 CustomExceptionHandler로 처리 가능하므로 + //시큐리티가 가지고 있는 제어권을 가져오기 위해 예외를 던지고 예외처리 과정에서 unsuccessfulAuthentication 호출 + throw new InternalAuthenticationServiceException(e.getMessage()); + + //직접 unsuccessfulAuthentication이 실행되기가 어렵기 때문에 InternalAuthenticationServiceException 예외를 던진다. +// try { +// unsuccessfulAuthentication(request, response, new InternalAuthenticationServiceException(e.getMessage())); +// } catch (IOException ex) { +// throw new RuntimeException(ex); +// } catch (ServletException ex) { +// throw new RuntimeException(ex); +// } + } + } + + + //return authentication 잘 작동하면 successfulAuthentication 메서드 호출 + //AOP로 로깅 하기 + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { + + log.debug("디버그 : successfulAuthentication 호출됨"); + + //(1) 파라미터의 authResult에서 로그인 유저 객체 얻기 + LoginUser loginUser = (LoginUser) authResult.getPrincipal(); + //(2) 얻은 로그인 유저로 토큰 생성 + String jwtToken = JwtProcess.create(loginUser); + + //(3) 생성한 토큰을 헤더에 추가 + response.addHeader(JwtVO.HEADER, jwtToken); + //로그인을 위한 응답 DTO 작성 + + //(4) loginUser를 이용해 loginRespDto 변환 + LoginRespDto loginRespDto = new LoginRespDto(loginUser.getUser()); + + //MyResponseUtil에 JSON 응답 DTO 생성하는 메서드 작성 + //(5) JSON 응답 DTO 반환 + MyResponseUtil.success(response, loginRespDto); + } + + //로그인 실패 로직 + //시큐리티 과정 중 예외이므로, authenticationEntryPoint에 걸린다. + // : Spring Security에서 인증에 실패한 경우 처리를 담당하는 인터페이스 + //시큐리티가 가지고 있는 제어권을 가져오기 위해 예외를 던짐 + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { + MyResponseUtil.fail(response, "로그인 실패", HttpStatus.UNAUTHORIZED); + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/config/jwt/JwtAuthorizationFilter.java b/src/main/java/shop/minostreet/shoppingmall/config/jwt/JwtAuthorizationFilter.java new file mode 100644 index 0000000..410ed86 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/config/jwt/JwtAuthorizationFilter.java @@ -0,0 +1,55 @@ +package shop.minostreet.shoppingmall.config.jwt; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import shop.minostreet.shoppingmall.config.auth.LoginUser; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 모든 주소에서 동작하는 토큰 검증 필터 + */ +public class JwtAuthorizationFilter extends BasicAuthenticationFilter { + public JwtAuthorizationFilter(AuthenticationManager authenticationManager) { + super(authenticationManager); + } + + //JWT 헤더를 추가하지 않아도 필터 통과 가능 + //: 하지만 시큐리티 단에서 세션 값 검증에 실패한다. + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + if(isHeaderVerify(request, response)){ + //JWT 존재할 때 + //(1) 프로토콜로 인해 필요했던 Bearer 접두사 제거 + String token = request.getHeader(JwtVO.HEADER).replace(JwtVO.TOKEN_PREFIX, ""); + //(2) 토큰을 이용해 토큰 검증 수행해 로그인 유저 정보 얻는다. + LoginUser loginUser = JwtProcess.verify(token); + + //(3) 강제로 임시 세션에 로그인한 유저로 유저의 토큰을 생성해 넣는다. (여기서 확인할 정보는 해당 유저의 권한정보) + //UsernamePasswordAuthenticationToken의 파라미터는 로그인유저 객체 or username (null), 비밀번호 (null), 로그인한 유저의 권한 + //UsernamePasswordAuthenticationToken의 부모는 AbstractAuthenticationToken의 부모는 Authentication + Authentication authentication = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); + //(4) 강제 로그인 수행 + SecurityContextHolder.getContext().setAuthentication(authentication); + } + // 다음 필터 수행 + // : 즉, 토큰이 없어도 다음 필터를 수행한다. + chain.doFilter(request,response); + } + + //헤더에 JWT 있는지 체크하는 검증 메서드 + private boolean isHeaderVerify(HttpServletRequest request, HttpServletResponse response){ + String header = request.getHeader(JwtVO.HEADER); + if(header==null || !header.startsWith(JwtVO.TOKEN_PREFIX)){ + return false; + } + return true; + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/config/jwt/JwtProcess.java b/src/main/java/shop/minostreet/shoppingmall/config/jwt/JwtProcess.java new file mode 100644 index 0000000..c91a0b9 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/config/jwt/JwtProcess.java @@ -0,0 +1,48 @@ +package shop.minostreet.shoppingmall.config.jwt; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import shop.minostreet.shoppingmall.config.auth.LoginUser; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.domain.UserEnum; +import shop.minostreet.shoppingmall.handler.exception.MyForbiddenException; + +import java.util.Date; + +public class JwtProcess { + private final Logger log = LoggerFactory.getLogger(getClass()); + + //토큰 생성 메서드 + public static String create(LoginUser loginUser){ + String jwtToken = JWT.create() + .withSubject("bank") + .withExpiresAt(new Date(System.currentTimeMillis()+JwtVO.EXPIRATION_TIME)) + .withClaim("id", loginUser.getUser().getId()) + .withClaim("role", loginUser.getUser().getRole().name()) //getRole() -> UserEnum 타입이므로 +// .withClaim("id", loginUser.getUser().getRole()+"") //getRole() -> UserEnum 타입이므로 + .sign(Algorithm.HMAC512(JwtVO.SECRET)); + return JwtVO.TOKEN_PREFIX+jwtToken; + } + //토큰 검증 메서드 + //(1) 토큰을 받아서 클레임으로 유저 객체 생성 + //(2) User 객체를 이용해서 생성한 LoginUser 객체 생성 + //(3) 생성한 LoginUser 객체를 강제로 시큐리티 세션에 주입 (강제 로그인) + public static LoginUser verify(String token){ + //(1) 토큰을 받아서 클레임으로 유저 객체 생성 + DecodedJWT decodedJWT=JWT.require(Algorithm.HMAC512(JwtVO.SECRET)).build().verify(token); + Long id = decodedJWT.getClaim("id").asLong(); + String role = decodedJWT.getClaim("role").asString(); + User user = User.builder().id(id).role(UserEnum.valueOf(role)).build(); + + //(2) User 객체를 이용해서 생성한 LoginUser 객체 생성 + LoginUser loginUser = new LoginUser(user); + + //(3) 생성한 LoginUser 객체를 강제로 시큐리티 세션에 주입 (강제 로그인) + return loginUser; + } + +} diff --git a/src/main/java/shop/minostreet/shoppingmall/config/jwt/JwtVO.java b/src/main/java/shop/minostreet/shoppingmall/config/jwt/JwtVO.java new file mode 100644 index 0000000..50a093b --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/config/jwt/JwtVO.java @@ -0,0 +1,16 @@ +package shop.minostreet.shoppingmall.config.jwt; + +/** + * SECRET은 노출안되도록 클라우드 AWS, 환경변수, 파일에 저장 + * 액세스 토큰 만료시 리플래시 토큰 생성해 UX 향상 + */ +public class JwtVO { +// public static final String SECRET=System.getenv("HS512_SECRET"); //HS256 - 대칭키 + public static final String SECRET="HS512_SECRET"; //HS256 - 대칭키 + public static final int EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 7; //만료시간을 일주일로 설정 + public static final String TOKEN_PREFIX= "Bearer "; //프로토콜 강제사항 + public static final String HEADER="Authorization"; + + + +} diff --git a/src/main/java/shop/minostreet/shoppingmall/controller/AdminController.java b/src/main/java/shop/minostreet/shoppingmall/controller/AdminController.java new file mode 100644 index 0000000..8f19a86 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/controller/AdminController.java @@ -0,0 +1,58 @@ +package shop.minostreet.shoppingmall.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.domain.UserEnum; +import shop.minostreet.shoppingmall.dto.ResponseDto; +import shop.minostreet.shoppingmall.dto.user.UserReqDto; +import shop.minostreet.shoppingmall.handler.exception.MyApiException; +import shop.minostreet.shoppingmall.repository.UserRepository; + +import javax.validation.Valid; + +/** + * 권한 변경 + */ +@RequiredArgsConstructor +@RequestMapping("/api") +@RestController +public class AdminController { + private final UserRepository userRepository; + + @Transactional // 트랜잭션이 시작되지 않으면 강제로 em.flush() 를 할 수 없고, 더티체킹도 할 수 없다. (원래는 서비스에서) + @PutMapping("/admin/{id}/role") + public ResponseEntity updateRole(@PathVariable Long id, @RequestBody @Valid UserReqDto.RoleUpdateReqDto roleUpdateReqDto, Errors errors) { + + User userPS = userRepository.findById(id) + .orElseThrow(() -> new MyApiException("해당 유저를 찾을 수 없습니다")); + userPS.updateRole(roleUpdateReqDto.toEntity().getRole()); + +// ResponseDTO responseDto = new ResponseDTO<>(); +// return ResponseEntity.ok().body(responseDto); + + return new ResponseEntity<>(new ResponseDto<>(1, "회원 권한 변경 완료", null), HttpStatus.OK); + } + + @Transactional // 트랜잭션이 시작되지 않으면 강제로 em.flush() 를 할 수 없고, 더티체킹도 할 수 없다. (원래는 서비스에서) + @PutMapping("/admin/{id}/status") + public ResponseEntity updateStatus(@PathVariable Long id, @RequestBody @Valid UserReqDto.StatusUpdateReqDto statusUpdateReqDto, Errors errors) { + + User userPS = userRepository.findById(id) + .orElseThrow(() -> new MyApiException("해당 유저를 찾을 수 없습니다")); + userPS.updateStatus(statusUpdateReqDto.toEntity().getStatus()); + +// ResponseDTO responseDto = new ResponseDTO<>(); +// return ResponseEntity.ok().body(responseDto); + + return new ResponseEntity<>(new ResponseDto<>(1, "회원 활성상태 변경 완료", null), HttpStatus.OK); + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/controller/OrderController.java b/src/main/java/shop/minostreet/shoppingmall/controller/OrderController.java new file mode 100644 index 0000000..a269d95 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/controller/OrderController.java @@ -0,0 +1,111 @@ +package shop.minostreet.shoppingmall.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.Errors; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import shop.minostreet.shoppingmall.config.auth.LoginUser; +import shop.minostreet.shoppingmall.domain.OrderProduct; +import shop.minostreet.shoppingmall.domain.OrderSheet; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.dto.ResponseDto; +import shop.minostreet.shoppingmall.dto.orderproduct.OrderReqDto; +import shop.minostreet.shoppingmall.dto.orderproduct.OrderRespDto.OrderListBySellerRespDto; +import shop.minostreet.shoppingmall.dto.orderproduct.OrderRespDto.OrderListRespDto; +import shop.minostreet.shoppingmall.dto.orderproduct.OrderRespDto.SaveRespDTO; +import shop.minostreet.shoppingmall.handler.exception.MyApiException; +import shop.minostreet.shoppingmall.handler.exception.MyForbiddenException; +import shop.minostreet.shoppingmall.repository.OrderProductRepository; +import shop.minostreet.shoppingmall.repository.OrderSheetRepository; +import shop.minostreet.shoppingmall.repository.ProductRepository; +import shop.minostreet.shoppingmall.repository.UserRepository; +import shop.minostreet.shoppingmall.service.OrderService; + +import javax.validation.Valid; +import java.util.List; + + +/** + * 주문하기(고객), 주문목록보기(고객), 주문목록보기(판매자), 주문취소하기(고객), 주문취소하기(판매자) + */ +@RequiredArgsConstructor +@RequestMapping(value = "/api") +@RestController +public class OrderController { + private final OrderSheetRepository orderSheetRepository; + private final UserRepository userRepository; + private final OrderService orderService; + + @PostMapping("/user/orders") + public ResponseEntity save(@RequestBody @Valid OrderReqDto.SaveReqDTO saveReqDTO, Errors errors, @AuthenticationPrincipal LoginUser loginUser) { + //checkpoint : 해당 상품번호가 없어도 성공하는 에러 체크 + + // 1. 세션값으로 유저 찾기 + User userPS = userRepository.findById(loginUser.getUser().getId()).orElseThrow(() -> new MyApiException("해당 유저를 찾을 수 없습니다")); + + // 2. 상품 찾기 + if (saveReqDTO.getIds().size() == 0) throw new MyApiException("주문할 수량이 0개일 수 없습니다."); + + //3. 서비스 호출 + SaveRespDTO saveRespDTO = orderService.주문등록(saveReqDTO, userPS); + + // 4. 응답하기 + return new ResponseEntity<>(new ResponseDto<>(1, "주문등록 완료", saveRespDTO), HttpStatus.OK); + } + + // 유저 주문서 조회 + @GetMapping("/user/orders") + public ResponseEntity findByUserId(@AuthenticationPrincipal LoginUser loginUser) { + OrderListRespDto orderListRespDto = orderService.주문목록조회(loginUser); + return new ResponseEntity<>(new ResponseDto<>(1, "유저 주문목록조회 완료", orderListRespDto), HttpStatus.OK); + } + + // 그림 설명 필요!! + // 배달의 민족은 하나의 판매자에게서만 주문을 할 수 있다. (다른 판매자의 상품이 담기면, 하나만 담을 수 있게 로직이 변한다) + // 쇼핑몰은 여러 판매자에게서 주문을 할 수 있다. + + // 판매자 주문서 조회 + @GetMapping("/seller/orders") + public ResponseEntity findBySellerId() { + //여기서는 판매자 한명이므로 유저 정보 전달 안함 -> 권한 체크만 하면 됨. + OrderListBySellerRespDto orderListBySellerRespDto = orderService.판매자주문목록조회(); + + return new ResponseEntity<>(new ResponseDto<>(1, "판매자 주문목록조회 완료", orderListBySellerRespDto), HttpStatus.OK); + } + + // 유저 주문 취소 + @DeleteMapping("/user/orders/{id}") + public ResponseEntity delete(@PathVariable Long id, @AuthenticationPrincipal LoginUser loginUser) { + // 1. 주문서 찾기 + OrderSheet orderSheetPS = orderSheetRepository.findById(id).orElseThrow(() -> new MyApiException("해당 주문을 찾을 수 없습니다")); + + // 2. 해당 주문서의 주인 여부 확인 + if (!orderSheetPS.getUser().getId().equals(loginUser.getUser().getId())) { + throw new MyForbiddenException("권한이 없습니다"); + } + + orderService.유저주문취소(orderSheetPS); + + //3. 응답하기 + return new ResponseEntity<>(new ResponseDto<>(1, "유저 주문취소 완료", null), HttpStatus.OK); + } + + // 판매자 주문 취소 + @DeleteMapping("/seller/orders/{id}") + public ResponseEntity deleteSeller(@PathVariable Long id) { + // 1. 주문서 찾기 + OrderSheet orderSheetPS = orderSheetRepository.findById(id).orElseThrow(() -> new MyApiException("해당 주문을 찾을 수 없습니다")); + + orderService.판매자주문취소(orderSheetPS); + return new ResponseEntity<>(new ResponseDto<>(1, "판매자 주문취소 완료", null), HttpStatus.OK); + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/controller/ProductController.java b/src/main/java/shop/minostreet/shoppingmall/controller/ProductController.java new file mode 100644 index 0000000..6caf65a --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/controller/ProductController.java @@ -0,0 +1,98 @@ +package shop.minostreet.shoppingmall.controller; + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.security.core.annotation.AuthenticationPrincipal; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import shop.minostreet.shoppingmall.config.auth.LoginUser; +import shop.minostreet.shoppingmall.domain.Product; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.dto.ResponseDto; +import shop.minostreet.shoppingmall.dto.product.ProductReqDto; +import shop.minostreet.shoppingmall.dto.product.ProductReqDto.ProductUpdateReqDto; +import shop.minostreet.shoppingmall.dto.product.ProductRespDto.ProductDto; +import shop.minostreet.shoppingmall.dto.product.ProductRespDto.ProductListRespDto; +import shop.minostreet.shoppingmall.dto.product.ProductRespDto.ProductRegisterRespDto; +import shop.minostreet.shoppingmall.dto.product.ProductRespDto.ProductUpdateRespDto; +import shop.minostreet.shoppingmall.handler.exception.MyApiException; +import shop.minostreet.shoppingmall.handler.exception.MyValidationException; +import shop.minostreet.shoppingmall.repository.ProductRepository; +import shop.minostreet.shoppingmall.repository.UserRepository; +import shop.minostreet.shoppingmall.service.ProductService; + +import javax.validation.Valid; + +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.OK; + +@RequiredArgsConstructor +@RequestMapping("/api") +@RestController +public class ProductController { + private final Logger log = LoggerFactory.getLogger(getClass()); + private final ProductService productService; + + private final ProductRepository productRepository; + private final UserRepository userRepository; + + @PostMapping("/seller/product/register") + public ResponseEntity registerProduct(@RequestBody @Valid ProductReqDto.ProductRegisterReqDto productRegisterReqDto, Errors errors, @AuthenticationPrincipal LoginUser loginUser){ + // 1. 판매자 찾기 + User sellerPS = userRepository.findById(loginUser.getUser().getId()) + .orElseThrow( + () -> new MyApiException("판매자를 찾을 수 없습니다") + ); + ProductRegisterRespDto productRegisterRespDto = productService.상품등록(productRegisterReqDto, sellerPS); + //checkPoint: 누가 상품등록했는지 추후 추가하거나 로그 남기는 것 필요 + return new ResponseEntity<>(new ResponseDto<>(1,"상품 등록 완료", productRegisterRespDto), OK); + } + + @GetMapping("/user/product") + public ResponseEntity listProduct(@PageableDefault(size = 10, page = 0, direction = Sort.Direction.DESC) Pageable pageable) { + ProductListRespDto productListRespDto=productService.상품목록(pageable); + return new ResponseEntity<>(new ResponseDto<>(1,"상품목록 보기 완료", productListRespDto), OK); + } + @GetMapping("/user/product/{id}") + public ResponseEntity getProduct(@PathVariable Long id){ + ProductDto productdto = productService.상품상세(id); + return new ResponseEntity<>(new ResponseDto<>(1,"상품상세 보기 완료", productdto), OK); + } + + @Transactional + @PutMapping("/seller/product/{id}") + public ResponseEntity updateProduct(@PathVariable Long id, @RequestBody @Valid ProductUpdateReqDto productUpdateReqDto, Errors errors){ + Product productPS = productRepository.findById(id).orElseThrow(()-> new MyApiException("해당 상품을 찾을 수 없습니다")); + + if(productRepository.findByName(productUpdateReqDto.getName()).isPresent()){ + throw new MyApiException("존재하는 상품명입니다."); + } + + productPS.update(productUpdateReqDto); + ProductUpdateRespDto productUpdateRespDto=new ProductUpdateRespDto(productPS); + + return new ResponseEntity<>(new ResponseDto<>(1,"상품수정 완료", productUpdateRespDto), OK); + } + + @DeleteMapping("/seller/product/{id}") + public ResponseEntity removeProduct(@PathVariable Long id){ + //(1) 전달 받은 값을 확인하는 절차 + Product productPS = productRepository.findById(id).orElseThrow(() -> new MyApiException("해당 상품을 찾을 수 없습니다")); + productService.상품삭제(productPS); + return new ResponseEntity<>(new ResponseDto<>(1,"상품삭제 완료", null), OK); + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/controller/UserController.java b/src/main/java/shop/minostreet/shoppingmall/controller/UserController.java new file mode 100644 index 0000000..6d688eb --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/controller/UserController.java @@ -0,0 +1,78 @@ +package shop.minostreet.shoppingmall.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; +import shop.minostreet.shoppingmall.dto.ResponseDto; +import shop.minostreet.shoppingmall.dto.user.UserReqDto; +import shop.minostreet.shoppingmall.dto.user.UserRespDto; +import shop.minostreet.shoppingmall.repository.LoginLogRepository; +import shop.minostreet.shoppingmall.repository.UserRepository; +import shop.minostreet.shoppingmall.service.UserService; + +import javax.validation.Valid; + +@RequiredArgsConstructor +@RequestMapping("/api") +@RestController +public class UserController { + private final UserService userService; +// private final HttpSession session; + +// @PostMapping("/login") +// public ResponseEntity login(@RequestBody JoinDto loginDto, HttpServletRequest request) { +// Optional userOP = userRepository.findByUsername(loginDto.getUsername()); +// if (userOP.isPresent()) { +// // 1. 유저 정보 꺼내기 +// User loginUser = userOP.get(); +// +// // 2. 패스워드 검증하기 +// if(!loginUser.getPassword().equals(loginDto.getPassword())){ +// throw new Exception401("인증되지 않았습니다"); +// } +// +// // 3. JWT 생성하기 +// String jwt = JwtProvider.create(userOP.get()); +// +// // 4. 최종 로그인 날짜 기록 (더티체킹 - update 쿼리 발생) +// loginUser.setUpdatedAt(LocalDateTime.now()); +// +// // 5. 로그 테이블 기록 +// LoginLog loginLog = LoginLog.builder() +// .userId(loginUser.getId()) +// .userAgent(request.getHeader("User-Agent")) +// .clientIP(request.getRemoteAddr()) +// .build(); +// loginLogRepository.save(loginLog); +// +// // 6. 응답 DTO 생성 +// ResponseDto responseDto = new ResponseDto<>().data(loginUser); +// return ResponseEntity.ok().header(JwtProvider.HEADER, jwt).body(responseDto); +// } else { +// throw new Exception400("유저네임 혹은 아이디가 잘못되었습니다"); +// } +// } + + + @PostMapping("/join") + public ResponseEntity join(@RequestBody @Valid UserReqDto.JoinReqDto joinReqDto, BindingResult bindingResult){ //유효성 검사 +// public void join(UserReqDto.JoinReqDto){ //기본전략이 x-www-urlencoded +// public ResponseEntity join(@RequestBody JoinReqDto joinReqDto){ //JSON + //담긴 에러를 처리 + //: AOP로 대체 +// if(bindingResult.hasErrors()){ +// //Map으로 담는다 +// Map errorMap = new HashMap<>(); +// for (FieldError error:bindingResult.getFieldErrors()) { +// errorMap.put(error.getField(), error.getDefaultMessage()); +// } +// return new ResponseEntity<>(new ResponseDto<>(-1, "유효성 검사 실패", errorMap), HttpStatus.BAD_REQUEST); +// } + + UserRespDto.JoinRespDto joinRespDto = userService.회원가입(joinReqDto); + return new ResponseEntity<>(new ResponseDto<>(1, "회원가입 완료", joinRespDto), HttpStatus.CREATED); + } + +} diff --git a/src/main/java/shop/mtcoding/metamall/model/log/error/ErrorLog.java b/src/main/java/shop/minostreet/shoppingmall/domain/ErrorLog.java similarity index 62% rename from src/main/java/shop/mtcoding/metamall/model/log/error/ErrorLog.java rename to src/main/java/shop/minostreet/shoppingmall/domain/ErrorLog.java index fbfe7e5..30e92e7 100644 --- a/src/main/java/shop/mtcoding/metamall/model/log/error/ErrorLog.java +++ b/src/main/java/shop/minostreet/shoppingmall/domain/ErrorLog.java @@ -1,15 +1,17 @@ -package shop.mtcoding.metamall.model.log.error; +package shop.minostreet.shoppingmall.domain; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; import java.time.LocalDateTime; - +//Audit 기능 사용하기 위한 어노테이션 1 +@EntityListeners(AuditingEntityListener.class) @NoArgsConstructor -@Setter // DTO 만들면 삭제해야됨 @Getter @Table(name = "error_log_tb") @Entity @@ -20,18 +22,13 @@ public class ErrorLog { private String msg; private Long userId; + @CreatedDate + @Column(nullable = false) private LocalDateTime createdAt; - private LocalDateTime updatedAt; - - @PrePersist - protected void onCreate() { - this.createdAt = LocalDateTime.now(); - } - @PreUpdate - protected void onUpdate() { - this.updatedAt = LocalDateTime.now(); - } + @LastModifiedDate + @Column(nullable = false) + private LocalDateTime updatedAt; @Builder public ErrorLog(Long id, String msg, Long userId, LocalDateTime createdAt, LocalDateTime updatedAt) { diff --git a/src/main/java/shop/mtcoding/metamall/model/log/login/LoginLog.java b/src/main/java/shop/minostreet/shoppingmall/domain/LoginLog.java similarity index 62% rename from src/main/java/shop/mtcoding/metamall/model/log/login/LoginLog.java rename to src/main/java/shop/minostreet/shoppingmall/domain/LoginLog.java index 40b2964..2154cf9 100644 --- a/src/main/java/shop/mtcoding/metamall/model/log/login/LoginLog.java +++ b/src/main/java/shop/minostreet/shoppingmall/domain/LoginLog.java @@ -1,15 +1,19 @@ -package shop.mtcoding.metamall.model.log.login; +package shop.minostreet.shoppingmall.domain; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; import java.time.LocalDateTime; +//Audit 기능 사용하기 위한 어노테이션 1 +@EntityListeners(AuditingEntityListener.class) +//Spring이 User 객체 생성시 빈생성자로 생성하기 때문에 @NoArgsConstructor -@Setter // DTO 만들면 삭제해야됨 +//@Setter // DTO 만들면 삭제해야됨 @Getter @Table(name = "login_log_tb") @Entity @@ -20,18 +24,10 @@ public class LoginLog { private Long userId; private String userAgent; private String clientIP; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - - @PrePersist - protected void onCreate() { - this.createdAt = LocalDateTime.now(); - } - @PreUpdate - protected void onUpdate() { - this.updatedAt = LocalDateTime.now(); - } + @CreatedDate + @Column(nullable = false) + private LocalDateTime createdAt; @Builder diff --git a/src/main/java/shop/mtcoding/metamall/model/orderproduct/OrderProduct.java b/src/main/java/shop/minostreet/shoppingmall/domain/OrderProduct.java similarity index 62% rename from src/main/java/shop/mtcoding/metamall/model/orderproduct/OrderProduct.java rename to src/main/java/shop/minostreet/shoppingmall/domain/OrderProduct.java index 165905e..1e01ec6 100644 --- a/src/main/java/shop/mtcoding/metamall/model/orderproduct/OrderProduct.java +++ b/src/main/java/shop/minostreet/shoppingmall/domain/OrderProduct.java @@ -1,17 +1,18 @@ -package shop.mtcoding.metamall.model.orderproduct; +package shop.minostreet.shoppingmall.domain; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; -import shop.mtcoding.metamall.model.ordersheet.OrderSheet; -import shop.mtcoding.metamall.model.product.Product; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; import java.time.LocalDateTime; +//Audit 기능 사용하기 위한 어노테이션 1 +@EntityListeners(AuditingEntityListener.class) @NoArgsConstructor -@Setter // DTO 만들면 삭제해야됨 @Getter @Table(name = "order_product_tb") @Entity @@ -23,20 +24,21 @@ public class OrderProduct { // 주문 상품 private Product product; private Integer count; // 상품 주문 개수 private Integer orderPrice; // 상품 주문 금액 - private LocalDateTime createdAt; - private LocalDateTime updatedAt; +// @ManyToOne(cascade = CascadeType.ALL) //모두 삭제되도록하면 수량체크 안됨 @ManyToOne private OrderSheet orderSheet; - @PrePersist - protected void onCreate() { - this.createdAt = LocalDateTime.now(); - } + @CreatedDate + @Column(nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(nullable = false) + private LocalDateTime updatedAt; - @PreUpdate - protected void onUpdate() { - this.updatedAt = LocalDateTime.now(); + public void setOrderSheet(OrderSheet orderSheet) { + this.orderSheet=orderSheet; } @Builder diff --git a/src/main/java/shop/mtcoding/metamall/model/ordersheet/OrderSheet.java b/src/main/java/shop/minostreet/shoppingmall/domain/OrderSheet.java similarity index 56% rename from src/main/java/shop/mtcoding/metamall/model/ordersheet/OrderSheet.java rename to src/main/java/shop/minostreet/shoppingmall/domain/OrderSheet.java index 7638710..6cac60d 100644 --- a/src/main/java/shop/mtcoding/metamall/model/ordersheet/OrderSheet.java +++ b/src/main/java/shop/minostreet/shoppingmall/domain/OrderSheet.java @@ -1,20 +1,19 @@ -package shop.mtcoding.metamall.model.ordersheet; +package shop.minostreet.shoppingmall.domain; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; -import shop.mtcoding.metamall.model.orderproduct.OrderProduct; -import shop.mtcoding.metamall.model.product.Product; -import shop.mtcoding.metamall.model.user.User; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; - +//Audit 기능 사용하기 위한 어노테이션 1 +@EntityListeners(AuditingEntityListener.class) @NoArgsConstructor -@Setter // DTO 만들면 삭제해야됨 @Getter @Table(name = "order_sheet_tb") @Entity @@ -24,23 +23,21 @@ public class OrderSheet { // 주문서 private Long id; @ManyToOne private User user; // 주문자 - @OneToMany(mappedBy = "orderSheet") - private List orderProductList = new ArrayList<>(); // 총 주문 상품 리스트 + +// @OneToMany(mappedBy = "orderSheet") +//: 양방향 관계를 단뱡항 관계로 변경 +// private List orderProductList = new ArrayList<>(); // 총 주문 상품 리스트 private Integer totalPrice; // 총 주문 금액 (총 주문 상품 리스트의 orderPrice 합) + + @CreatedDate + @Column(nullable = false) private LocalDateTime createdAt; - private LocalDateTime updatedAt; - @PrePersist - protected void onCreate() { - this.createdAt = LocalDateTime.now(); - } + @LastModifiedDate + @Column(nullable = false) + private LocalDateTime updatedAt; - @PreUpdate - protected void onUpdate() { - this.updatedAt = LocalDateTime.now(); - } - - // 연관관계 메서드 구현 필요 + // 연관관계 메서드 구현 필요 -> 양방향 매핑 사용시에 @Builder public OrderSheet(Long id, User user, Integer totalPrice, LocalDateTime createdAt, LocalDateTime updatedAt) { diff --git a/src/main/java/shop/minostreet/shoppingmall/domain/Product.java b/src/main/java/shop/minostreet/shoppingmall/domain/Product.java new file mode 100644 index 0000000..b442796 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/domain/Product.java @@ -0,0 +1,77 @@ +package shop.minostreet.shoppingmall.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import shop.minostreet.shoppingmall.dto.product.ProductReqDto.ProductUpdateReqDto; +import shop.minostreet.shoppingmall.handler.exception.MyApiException; + +import javax.persistence.*; +import java.time.LocalDateTime; + +@EntityListeners(AuditingEntityListener.class) + +@NoArgsConstructor +@Getter +@Table(name = "product_tb") +@Entity +public class Product { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne (fetch = FetchType.LAZY) + private User seller; + @Column(nullable = false, length =20) + private String name; // 상품 이름 + @Column(nullable = false) + private Integer price; // 상품 가격 + + @Column(nullable = false, length =20) + private Integer qty; // 상품 재고 + @CreatedDate + @Column(nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(nullable = false) + private LocalDateTime updatedAt; + + + //Product와 관련된 메서드 작성 - 객체 상태 변경 + + //상품 변경 메서드 (판매자만 가능) + public void update(ProductUpdateReqDto productUpdateReqDto){ + this.name=productUpdateReqDto.getName(); + this.price=productUpdateReqDto.getPrice(); + this.qty=productUpdateReqDto.getQty(); + } + + //상품 주문시 재고 변경하는 메서드 (구매자가 호출) + public void updateQty(Integer orderCount){ + if(this.qty { + private final Integer code; // 에러시에 의미 있음. + private final String msg; // 에러시에 의미 있음. ex) badRequest + private final T data; // 에러시에는 구체적인 에러 내용 ex) username이 입력되지 않았습니다 + +// public ResponseDto(){ +// this.code = HttpStatus.OK.value(); +// this.msg = "성공"; +// this.data = null; +// } +// +// public ResponseDto data(T data){ +// this.data = data; // 응답할 데이터 바디 +// return this; +// } +// +// public ResponseDto fail(HttpStatus httpStatus, String msg, T data){ +// this.code = httpStatus.value(); +// this.msg = msg; // 에러 제목 +// this.data = data; // 에러 내용 +// return this; +// } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/dto/orderproduct/OrderReqDto.java b/src/main/java/shop/minostreet/shoppingmall/dto/orderproduct/OrderReqDto.java new file mode 100644 index 0000000..75ef4e5 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/dto/orderproduct/OrderReqDto.java @@ -0,0 +1,54 @@ +package shop.minostreet.shoppingmall.dto.orderproduct; + +import lombok.Getter; +import lombok.Setter; +import shop.minostreet.shoppingmall.domain.OrderProduct; +import shop.minostreet.shoppingmall.domain.Product; + +import java.util.List; +import java.util.stream.Collectors; + +public class OrderReqDto { + @Getter + @Setter + public static class SaveReqDTO { + + //SaveReqDTO가 의존하는 객체 + private List orderProducts; + @Getter + @Setter + public static class OrderProductDTO { + //주문 상품을 요청을 위한 DTO + //: 상품 번호와 개수 + private Long productId; + private Integer count; + } + + // 1. request 요청으로 들어온 product id만 리스트로 뽑아내기 + public List getIds() { + return orderProducts.stream().map((orderProduct) -> orderProduct.getProductId()).collect(Collectors.toList()); + } + + // 3. 주문 상품 리스트 만들어 내기 + public List toEntity(List products) { // 2. getIds() 로 뽑아낸 번호로 상품 리스트 찾아내서 주입하기 + + // 4. request 요청으로 들어온 DTO에 count 값이 필요해서 stream() 두번 돌렸음. 주문 상품 금액을 만들어 내야 해서!! + return orderProducts.stream() + // 5. map은 다시 collect로 수집해야 하기 때문에, flatMap으로 평탄화 작업함. + //: for 문돌리면서 정규 연산자를 이용해 자신의 타입을 버리고, Product를 OrderProduct 타입으로 바꾼다. + .flatMap((orderProduct) -> { + Long productId = orderProduct.productId; + Integer count = orderProduct.getCount(); + // 6. OrderProduct 객체 만들어내기 (주문한 상품만큼) + return products.stream() + .filter((product) -> product.getId().equals(productId)) + .map((product) -> OrderProduct.builder() + .product(product) + .count(count) + .orderPrice(product.getPrice() * count) + .build()); + } + ).collect(Collectors.toList()); // 7. 최종 수집 + } + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/dto/orderproduct/OrderRespDto.java b/src/main/java/shop/minostreet/shoppingmall/dto/orderproduct/OrderRespDto.java new file mode 100644 index 0000000..0578c8a --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/dto/orderproduct/OrderRespDto.java @@ -0,0 +1,121 @@ +package shop.minostreet.shoppingmall.dto.orderproduct; + +import lombok.Getter; +import lombok.Setter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.jaxb.SpringDataJaxb.OrderDto; +import shop.minostreet.shoppingmall.domain.OrderProduct; +import shop.minostreet.shoppingmall.domain.OrderSheet; +import shop.minostreet.shoppingmall.domain.Product; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.dto.orderproduct.OrderReqDto.SaveReqDTO; +import shop.minostreet.shoppingmall.dto.orderproduct.OrderReqDto.SaveReqDTO.OrderProductDTO; +import shop.minostreet.shoppingmall.dto.orderproduct.OrderRespDto.OrderListRespDto.OrderSheetDto; +import shop.minostreet.shoppingmall.dto.product.ProductRespDto.ProductDto; +import shop.minostreet.shoppingmall.util.MyDateUtil; + +import javax.persistence.Column; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +public class OrderRespDto { + private final Logger log = LoggerFactory.getLogger(getClass()); + @Getter + @Setter + public static class SaveRespDTO { + private Long id; + private String username; // 주문자 + private Integer totalPrice; // 총 주문 금액 (총 주문 상품 리스트의 orderPrice 합) + private String createdAt; + private List orderProductListPS; + + + public SaveRespDTO(List orderProductListPS, OrderSheet orderSheetPS) { + this.orderProductListPS=orderProductListPS.stream().map( + OrderDto::new + ).collect(Collectors.toList()); + this.totalPrice = orderSheetPS.getTotalPrice(); + this.id=orderSheetPS.getId(); + this.createdAt= MyDateUtil.toStringFormat(orderSheetPS.getCreatedAt()); + this.username=orderSheetPS.getUser().getUsername(); + } + @Getter + @Setter + public static class OrderDto{ + private Long id; +// private Product product; + private String productName; + + private Integer count; // 상품 주문 개수 + private Integer orderPrice; // 상품 주문 금액 + + public OrderDto(OrderProduct orderProduct) { + this.id = orderProduct.getProduct().getId(); + this.productName = orderProduct.getProduct().getName(); + this.count = orderProduct.getCount(); + this.orderPrice = orderProduct.getOrderPrice(); + } + } + } + + @Getter + @Setter + public static class OrderListRespDto{ + private List orderList; + public OrderListRespDto(List orders) { + this.orderList = orders.stream().map( + OrderSheetDto::new + ).collect(Collectors.toList()); + } + + @Getter + @Setter + public static class OrderSheetDto{ + private Long id; + private String username; // 주문자 + private Integer totalPrice; // 총 주문 금액 (총 주문 상품 리스트의 orderPrice 합) + private String createdAt; + + public OrderSheetDto(OrderSheet orderSheet) { + this.id = orderSheet.getId(); + this.username = orderSheet.getUser().getUsername(); + this.totalPrice = orderSheet.getTotalPrice(); + this.createdAt = MyDateUtil.toStringFormat(orderSheet.getCreatedAt()); + } + } + } + @Getter + @Setter + public static class OrderListBySellerRespDto{ + + private List orderList; + public OrderListBySellerRespDto(List orders) { + this.orderList = orders.stream().map( + OrderListRespDto.OrderSheetDto::new + ).collect(Collectors.toList()); + } + + @Getter + @Setter + public static class OrderSheetDto{ + private Long id; + private Integer totalPrice; // 총 주문 금액 (총 주문 상품 리스트의 orderPrice 합) + private String createdAt; + + public OrderSheetDto(OrderSheet orderSheet) { + this.id = orderSheet.getId(); + this.totalPrice = orderSheet.getTotalPrice(); + this.createdAt = MyDateUtil.toStringFormat(orderSheet.getCreatedAt()); + } + } + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/dto/product/ProductReqDto.java b/src/main/java/shop/minostreet/shoppingmall/dto/product/ProductReqDto.java new file mode 100644 index 0000000..d270447 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/dto/product/ProductReqDto.java @@ -0,0 +1,64 @@ +package shop.minostreet.shoppingmall.dto.product; + +import lombok.Getter; +import lombok.Setter; +import shop.minostreet.shoppingmall.domain.Product; +import shop.minostreet.shoppingmall.domain.User; + +import javax.validation.constraints.Digits; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +public class ProductReqDto { + // @Id +// @GeneratedValue(strategy = GenerationType.IDENTITY) +// private Long id; +// private String name; // 상품 이름 +// private Integer price; // 상품 가격 +// private Integer qty; // 상품 재고 + @Getter + @Setter + public static class ProductRegisterReqDto { + //한글, 영문, 숫자만 가능하고, 길이는 2~20자만 가능하도록, 공백도 불가능 + @Pattern(regexp = "^[ㄱ-힣A-Za-z0-9\\s]{2,20}$", message = "한글/영문/숫자 2~20자 이내로 작성해 주세요.") + @NotEmpty(message = "상품명은 빈칸으로 둘 수 없습니다.") + private String name; + + @Digits(fraction = 0, integer = 9) + @NotNull(message = "상품가격이 없습니다.") + private Integer price; + + @Digits(fraction = 0, integer = 9) + @NotNull(message = "상품개수가 없습니다.") //최소 1개부터 ~ 9999개까지 + private Integer qty; + + public Product toEntity(User user) { + return Product.builder() + .seller(user) + .name(name) + .price(price) + .qty(qty) + .build(); + } + } + + @Getter + @Setter + public static class ProductUpdateReqDto { + //한글, 영문, 숫자만 가능하고, 길이는 2~20자만 가능하도록, 공백도 불가능 + @Pattern(regexp = "^[ㄱ-힣A-Za-z0-9\\s]{2,20}$", message = "한글/영문/숫자 2~20자 이내로 작성해 주세요.") + @NotEmpty(message = "상품명은 빈칸으로 둘 수 없습니다.") + private String name; + + @Digits(fraction = 0, integer = 9) + @NotNull(message = "상품가격이 없습니다.") //: 숫자의 길이 체크 최소 3자 최대 8자 + //최소 100원부터 ~ 9000만원까지 + private Integer price; + + @Digits(fraction = 0, integer = 9) + @NotNull(message = "상품개수가 없습니다.") + //최소 1개부터 ~ 9999개까지 + private Integer qty; + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/dto/product/ProductRespDto.java b/src/main/java/shop/minostreet/shoppingmall/dto/product/ProductRespDto.java new file mode 100644 index 0000000..6f5b604 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/dto/product/ProductRespDto.java @@ -0,0 +1,80 @@ +package shop.minostreet.shoppingmall.dto.product; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.domain.Page; +import shop.minostreet.shoppingmall.domain.Product; +import shop.minostreet.shoppingmall.util.MyDateUtil; + +import javax.validation.constraints.Digits; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; +import java.util.List; +import java.util.stream.Collectors; + +public class ProductRespDto { + @Getter + @Setter + public static class ProductRegisterRespDto { + private Long id; + private String name; + private Integer price; + private Integer qty; + + public ProductRegisterRespDto(Product product) { + this.id = product.getId(); + this.name = product.getName(); + this.price = product.getPrice(); + this.qty = product.getQty(); + } + } + + @Getter + @Setter + public static class ProductListRespDto { + private List productList; + + public ProductListRespDto(Page products) { + this.productList = products.stream().map( + ProductDto::new + ).collect(Collectors.toList()); + } + } + + @Getter + @Setter + public static class ProductDto { + private Long id; + private String name; + private Integer price; + private Integer qty; + private String createdAt; + + public ProductDto(Product product) { + this.id = product.getId(); + this.name = product.getName(); + this.price = product.getPrice(); + this.qty = product.getQty(); + this.createdAt= MyDateUtil.toStringFormat(product.getCreatedAt()); + } + } + + @Getter + @Setter + public static class ProductUpdateRespDto { + private String name; + private Integer price; + private Integer qty; + private String createdAt; + private String updatedAt; + + + public ProductUpdateRespDto(Product product) { + this.name = product.getName(); + this.price = product.getPrice(); + this.qty = product.getQty(); + this.createdAt=MyDateUtil.toStringFormat(product.getCreatedAt()); + this.updatedAt=MyDateUtil.toStringFormat(product.getUpdatedAt()); + } + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/dto/user/UserReqDto.java b/src/main/java/shop/minostreet/shoppingmall/dto/user/UserReqDto.java new file mode 100644 index 0000000..0be7831 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/dto/user/UserReqDto.java @@ -0,0 +1,81 @@ +package shop.minostreet.shoppingmall.dto.user; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.domain.UserEnum; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +public class UserReqDto { + @Getter @Setter + public static class JoinReqDto { + + //영문, 숫자만 가능하고, 길이는 2~20자만 가능하도록, 공백도 불가능 + @Pattern(regexp = "^[A-Za-z0-9]{2,20}$",message = "영문/숫자 2~20자 이내로 작성해 주세요.") + @NotEmpty(message = "이름은 빈칸으로 만들 수 없습니다.") + private String username; + + //길이 4~20만 가능 + @Size(min=4, max=20) //String에만 사용가능한 어노테이션 + @NotEmpty(message = "비밀번호는 빈칸으로 만들 수 없습니다.") + private String password; + //이메일 형식을 준수하도록 + @Pattern(regexp = "^[A-Za-z0-9]{2,10}@[A-Za-z0-9]{2,6}\\.[a-zA-Z]{2,3}$",message = "이메일 형식으로 작성해 주세요.") + @NotEmpty(message = "이메일은 빈칸으로 만들 수 없습니다.") + private String email; + + //DTO를 엔티티로 변환하는 메서드 작성 + //: 패스워드 인코더를 파라미터로 받아서 패스워드 인코딩 수행 + public User toEntity(BCryptPasswordEncoder bCryptPasswordEncoder){ + return User.builder() + .username(username) + //패스워드는 인코딩 필요 +// .password(password) + .password(bCryptPasswordEncoder.encode(password)) + .role(UserEnum.CUSTOMER) + .status(true) + .build(); + } + } + + @Getter + @Setter + public static class LoginReqDto{ + private String username; + private String password; + } + + @Getter + @Setter + //관리자만 상태를 변경할 수 있는 DTO + public static class RoleUpdateReqDto{ + @NotEmpty + @Pattern(regexp = "USER|SELLER|ADMIN") + private String role; + + public User toEntity(){ + return User.builder() + .role(UserEnum.valueOf(this.getRole())) + .build(); + } + } + + @Getter + @Setter + //관리자가 유저의 활성 상태를 변경할 수 있는 DTO + public static class StatusUpdateReqDto{ + @NotEmpty + @Pattern(regexp = "TRUE||FALSE") + private String status; + + public User toEntity(){ + return User.builder() + .status(Boolean.parseBoolean(status)) + .build(); + } + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/dto/user/UserRespDto.java b/src/main/java/shop/minostreet/shoppingmall/dto/user/UserRespDto.java new file mode 100644 index 0000000..b27fa9e --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/dto/user/UserRespDto.java @@ -0,0 +1,41 @@ +package shop.minostreet.shoppingmall.dto.user; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.util.MyDateUtil; + +// 응답 DTO는 서비스 배우고 나서 하기 (할 수 있으면 해보기) +public class UserRespDto { + @ToString + @Getter + @Setter + //임시로 서비스 안에 회원가입 응답을 위한 DTO 작성 + public static class JoinRespDto{ + private Long id; + private String username; + + public JoinRespDto(User user) { + this.id = user.getId(); + this.username = user.getUsername(); + } + } + + @Getter + @Setter + public static class LoginRespDto{ + private Long id; + private String username; + private String createdAt; + + public LoginRespDto(User user) { + this.id = user.getId(); + this.username = user.getUsername(); + //String으로 응답하기 위해 LocalDateTime을 변환하는 유틸클래스 작성 +// this.createdAt = user.getCreatedAt(); + this.createdAt = MyDateUtil.toStringFormat(user.getCreatedAt()); + } + } + +} diff --git a/src/main/java/shop/minostreet/shoppingmall/handler/MyExceptionHandler.java b/src/main/java/shop/minostreet/shoppingmall/handler/MyExceptionHandler.java new file mode 100644 index 0000000..f89da25 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/handler/MyExceptionHandler.java @@ -0,0 +1,59 @@ +package shop.minostreet.shoppingmall.handler; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.NoHandlerFoundException; +import shop.minostreet.shoppingmall.dto.ResponseDto; +import shop.minostreet.shoppingmall.handler.annotation.MyErrorLogRecord; +import shop.minostreet.shoppingmall.handler.exception.MyApiException; +import shop.minostreet.shoppingmall.handler.exception.MyForbiddenException; +import shop.minostreet.shoppingmall.handler.exception.MyValidationException; + +@RestControllerAdvice +public class MyExceptionHandler { + private final Logger log = LoggerFactory.getLogger(getClass()); + + @MyErrorLogRecord + @ExceptionHandler(MyApiException.class) + public ResponseEntity apiException(MyApiException e){ + + log.error(e.getMessage()); + return new ResponseEntity<>(new ResponseDto<>(-1, e.getMessage(), null), HttpStatus.BAD_REQUEST); + } + + @MyErrorLogRecord + @ExceptionHandler(MyValidationException.class) + public ResponseEntity validationApiException(MyValidationException e){ + + log.error(e.getMessage()); + return new ResponseEntity<>(new ResponseDto<>(-1, e.getMessage(), e.getErroMap()), HttpStatus.BAD_REQUEST); + } + + @MyErrorLogRecord + @ExceptionHandler(MyForbiddenException.class) + public ResponseEntity forbiddenException(MyForbiddenException e){ + + log.error(e.getMessage()); + return new ResponseEntity<>(new ResponseDto<>(-1, e.getMessage(), null), HttpStatus.FORBIDDEN); + } + + @MyErrorLogRecord + @ExceptionHandler(NoHandlerFoundException.class) + public ResponseEntity notFoundException(NoHandlerFoundException e){ + log.error(e.getMessage()); + return new ResponseEntity<>(new ResponseDto<>(-1, e.getMessage(), null), HttpStatus.NOT_FOUND); + } + + @MyErrorLogRecord + @ExceptionHandler(Exception.class) + public ResponseEntity serverErrorException(Exception e){ + + log.error(e.getMessage()); + return new ResponseEntity<>(new ResponseDto<>(-1, e.getMessage(), null), HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/handler/annotation/MyErrorLogRecord.java b/src/main/java/shop/minostreet/shoppingmall/handler/annotation/MyErrorLogRecord.java new file mode 100644 index 0000000..29cdf2f --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/handler/annotation/MyErrorLogRecord.java @@ -0,0 +1,10 @@ +package shop.minostreet.shoppingmall.handler.annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface MyErrorLogRecord { +} diff --git a/src/main/java/shop/minostreet/shoppingmall/handler/aop/MyErrorLogAdvice.java b/src/main/java/shop/minostreet/shoppingmall/handler/aop/MyErrorLogAdvice.java new file mode 100644 index 0000000..67efdec --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/handler/aop/MyErrorLogAdvice.java @@ -0,0 +1,80 @@ +package shop.minostreet.shoppingmall.handler.aop; + +import lombok.RequiredArgsConstructor; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import shop.minostreet.shoppingmall.config.auth.LoginUser; +import shop.minostreet.shoppingmall.domain.ErrorLog; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.handler.exception.MyValidationException; +import shop.minostreet.shoppingmall.repository.ErrorLogRepository; + +import javax.servlet.http.HttpSession; +import java.util.HashMap; +import java.util.Map; +@RequiredArgsConstructor +@Aspect +//Aspect = PointCut + Advice +@Component +public class MyErrorLogAdvice { + private final Logger log = LoggerFactory.getLogger(getClass()); + private final HttpSession session; + + private final ErrorLogRepository errorLogRepository; + @Pointcut("@annotation(shop.minostreet.shoppingmall.handler.annotation.MyErrorLogRecord)") + public void myErrorLog(){} + + @Before("myErrorLog()") + public void errorLogAdvice(JoinPoint jp) throws HttpMessageNotReadableException { + log.debug("디버그 : errorLogAdvice 호출됨"); + Object[] args = jp.getArgs(); + + for (Object arg : args) { + //매개변수를 돌면서 Exception이 존재하는지 체크한다. + //: Exception의 자식까지 모두 확인 + if(arg instanceof Exception){ + Exception e = (Exception) arg; + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.getPrincipal() instanceof UserDetails) { + User userDetails = (User) authentication.getPrincipal(); + LoginUser loginUser = new LoginUser(userDetails); // UserDetails 객체를 LoginUser 객체로 변환합니다. + + ErrorLog errorLog = ErrorLog.builder() + .userId(loginUser.getUser().getId()) + .msg(e.getMessage()) + .build(); + errorLogRepository.save(errorLog); + } + +// Authentication authentication=(Authentication) SecurityContextHolder.getContext().getAuthentication(); +// if(authentication != null){ +// LoginUser loginUser = (LoginUser)authentication.getPrincipal(); +// +//// LoginUser loginUser = (LoginUser) session.getAttribute("loginUser"); +// +// ErrorLog errorLog =ErrorLog.builder().userId(loginUser.getUser().getId()).msg(e.getMessage()).build(); +// //에러 로그의 아이디, 에러 로그 메시지를 전달해 객체 생성 +// errorLogRepository.save(errorLog); +// } + } + } + } +} +/** + * 유효성 검사 + * get, delete, post, put에서 body가 존재하는 post, put만 존재 + */ \ No newline at end of file diff --git a/src/main/java/shop/minostreet/shoppingmall/handler/aop/MyValidationAdvice.java b/src/main/java/shop/minostreet/shoppingmall/handler/aop/MyValidationAdvice.java new file mode 100644 index 0000000..c4413a6 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/handler/aop/MyValidationAdvice.java @@ -0,0 +1,59 @@ +package shop.minostreet.shoppingmall.handler.aop; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.stereotype.Component; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import shop.minostreet.shoppingmall.handler.exception.MyValidationException; + +import java.util.HashMap; +import java.util.Map; + +@Aspect +//Aspect = PointCut + Advice +@Component +public class MyValidationAdvice { + + @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)") + public void postMapping() { + + } + + @Pointcut("@annotation(org.springframework.web.bind.annotation.PutMapping)") + public void putMapping() { + + } + + //@Before, @After + @Before("postMapping() || putMapping()") //1. @PostMapping(), @PutMapping() 어노테이션이 붙은 모든 메서드에서 + //: joinPoint 메서드 실행 전 후 제어 가능한 어노테이션 + public void validationAdvice(JoinPoint jp) throws Throwable { + Object[] args = jp.getArgs(); //joinPoint의 매개변수 + for (Object arg : args) { + if (arg instanceof BindingResult) { + //2. 에러가 존재할 경우 -> 예외 던짐 + BindingResult bindingResult = (BindingResult) arg; + //담긴 에러를 처리 + if (bindingResult.hasErrors()) { + //Map으로 담는다 + Map errorMap = new HashMap<>(); + for (FieldError error : bindingResult.getFieldErrors()) { + //errorMap.put("ErrorField", error.getDefaultMessage()); + errorMap.put(error.getField(), error.getDefaultMessage()); + } + //return new ResponseEntity<>(new ResponseDto<>(-1, "유효성 검사 실패", errorMap), HttpStatus.BAD_REQUEST); + //유효성 검사 예외를 던진다. + throw new MyValidationException("유효성검사 실패", errorMap); + } + } + + } + } +} +/** + * 유효성 검사 + * get, delete, post, put에서 body가 존재하는 post, put만 존재 + */ \ No newline at end of file diff --git a/src/main/java/shop/minostreet/shoppingmall/handler/exception/MyApiException.java b/src/main/java/shop/minostreet/shoppingmall/handler/exception/MyApiException.java new file mode 100644 index 0000000..29d1969 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/handler/exception/MyApiException.java @@ -0,0 +1,9 @@ +package shop.minostreet.shoppingmall.handler.exception; + + +//커스텀 예외클래스 작성 +public class MyApiException extends RuntimeException{ + public MyApiException(String message) { + super(message); + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/handler/exception/MyForbiddenException.java b/src/main/java/shop/minostreet/shoppingmall/handler/exception/MyForbiddenException.java new file mode 100644 index 0000000..f609622 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/handler/exception/MyForbiddenException.java @@ -0,0 +1,9 @@ + package shop.minostreet.shoppingmall.handler.exception; + + +//커스텀 예외클래스 작성 +public class MyForbiddenException extends RuntimeException{ + public MyForbiddenException(String message) { + super(message); + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/handler/exception/MyValidationException.java b/src/main/java/shop/minostreet/shoppingmall/handler/exception/MyValidationException.java new file mode 100644 index 0000000..a472dba --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/handler/exception/MyValidationException.java @@ -0,0 +1,15 @@ +package shop.minostreet.shoppingmall.handler.exception; + +import lombok.Getter; + +import java.util.Map; + +@Getter +public class MyValidationException extends RuntimeException{ + private Map erroMap; + + public MyValidationException(String message, Map erroMap) { + super(message); + this.erroMap = erroMap; + } +} diff --git a/src/main/java/shop/mtcoding/metamall/model/log/error/ErrorLogRepository.java b/src/main/java/shop/minostreet/shoppingmall/repository/ErrorLogRepository.java similarity index 58% rename from src/main/java/shop/mtcoding/metamall/model/log/error/ErrorLogRepository.java rename to src/main/java/shop/minostreet/shoppingmall/repository/ErrorLogRepository.java index 53c8a4f..bd56f3d 100644 --- a/src/main/java/shop/mtcoding/metamall/model/log/error/ErrorLogRepository.java +++ b/src/main/java/shop/minostreet/shoppingmall/repository/ErrorLogRepository.java @@ -1,6 +1,7 @@ -package shop.mtcoding.metamall.model.log.error; +package shop.minostreet.shoppingmall.repository; import org.springframework.data.jpa.repository.JpaRepository; +import shop.minostreet.shoppingmall.domain.ErrorLog; public interface ErrorLogRepository extends JpaRepository { } diff --git a/src/main/java/shop/mtcoding/metamall/model/log/login/LoginLogRepository.java b/src/main/java/shop/minostreet/shoppingmall/repository/LoginLogRepository.java similarity index 58% rename from src/main/java/shop/mtcoding/metamall/model/log/login/LoginLogRepository.java rename to src/main/java/shop/minostreet/shoppingmall/repository/LoginLogRepository.java index c8b66c4..287e300 100644 --- a/src/main/java/shop/mtcoding/metamall/model/log/login/LoginLogRepository.java +++ b/src/main/java/shop/minostreet/shoppingmall/repository/LoginLogRepository.java @@ -1,6 +1,7 @@ -package shop.mtcoding.metamall.model.log.login; +package shop.minostreet.shoppingmall.repository; import org.springframework.data.jpa.repository.JpaRepository; +import shop.minostreet.shoppingmall.domain.LoginLog; public interface LoginLogRepository extends JpaRepository { } diff --git a/src/main/java/shop/minostreet/shoppingmall/repository/OrderProductRepository.java b/src/main/java/shop/minostreet/shoppingmall/repository/OrderProductRepository.java new file mode 100644 index 0000000..1882b09 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/repository/OrderProductRepository.java @@ -0,0 +1,12 @@ +package shop.minostreet.shoppingmall.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import shop.minostreet.shoppingmall.domain.OrderProduct; + +import java.util.List; + +public interface OrderProductRepository extends JpaRepository { + + + List findByOrderSheetId(Long id); +} diff --git a/src/main/java/shop/minostreet/shoppingmall/repository/OrderSheetRepository.java b/src/main/java/shop/minostreet/shoppingmall/repository/OrderSheetRepository.java new file mode 100644 index 0000000..158eaf6 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/repository/OrderSheetRepository.java @@ -0,0 +1,14 @@ +package shop.minostreet.shoppingmall.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import shop.minostreet.shoppingmall.domain.OrderSheet; + +import java.util.List; + +public interface OrderSheetRepository extends JpaRepository { + //주문한 목록 보기에 필요한 메서드 +// @Query("select os from OrderSheet os where os.user.id=:userId") + List findByUserId(@Param("userId") Long userId); +} diff --git a/src/main/java/shop/minostreet/shoppingmall/repository/ProductRepository.java b/src/main/java/shop/minostreet/shoppingmall/repository/ProductRepository.java new file mode 100644 index 0000000..b4123b1 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/repository/ProductRepository.java @@ -0,0 +1,18 @@ +package shop.minostreet.shoppingmall.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.data.web.PageableDefault; +import shop.minostreet.shoppingmall.domain.Product; + +import java.util.Optional; + +public interface ProductRepository extends JpaRepository { + Optional findByName(@Param("name") String name); + @EntityGraph(attributePaths = "seller") + Page findAll(Pageable pageable); +} diff --git a/src/main/java/shop/minostreet/shoppingmall/repository/UserRepository.java b/src/main/java/shop/minostreet/shoppingmall/repository/UserRepository.java new file mode 100644 index 0000000..c47d605 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/repository/UserRepository.java @@ -0,0 +1,13 @@ +package shop.minostreet.shoppingmall.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.query.Param; +import shop.minostreet.shoppingmall.domain.User; + +import java.util.Optional; + +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/minostreet/shoppingmall/service/OrderService.java b/src/main/java/shop/minostreet/shoppingmall/service/OrderService.java new file mode 100644 index 0000000..c58cc89 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/service/OrderService.java @@ -0,0 +1,92 @@ +package shop.minostreet.shoppingmall.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import shop.minostreet.shoppingmall.config.auth.LoginUser; +import shop.minostreet.shoppingmall.domain.OrderProduct; +import shop.minostreet.shoppingmall.domain.OrderSheet; +import shop.minostreet.shoppingmall.domain.Product; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.dto.ResponseDto; +import shop.minostreet.shoppingmall.dto.orderproduct.OrderReqDto.SaveReqDTO; +import shop.minostreet.shoppingmall.dto.orderproduct.OrderRespDto.OrderListBySellerRespDto; +import shop.minostreet.shoppingmall.dto.orderproduct.OrderRespDto.OrderListRespDto; +import shop.minostreet.shoppingmall.dto.orderproduct.OrderRespDto.SaveRespDTO; +import shop.minostreet.shoppingmall.repository.OrderProductRepository; +import shop.minostreet.shoppingmall.repository.OrderSheetRepository; +import shop.minostreet.shoppingmall.repository.ProductRepository; + +import javax.validation.Valid; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class OrderService { + + private final OrderProductRepository orderProductRepository; + private final ProductRepository productRepository; + private final OrderSheetRepository orderSheetRepository; + + @Transactional + public SaveRespDTO 주문등록(@Valid SaveReqDTO saveReqDTO, User userPS) { + // 1. 주문 상품 + List productListPS = productRepository.findAllById(saveReqDTO.getIds()); + + List orderProductListPS = saveReqDTO.toEntity(productListPS); + + // 2. 주문서 만들기 + Integer totalPrice = orderProductListPS.stream().mapToInt((orderProduct) -> orderProduct.getOrderPrice()).sum(); + OrderSheet orderSheet = OrderSheet.builder().user(userPS).totalPrice(totalPrice).build(); + OrderSheet orderSheetPS = orderSheetRepository.save(orderSheet); //영속화된 상태의 주문서 + + // 3. 주문서에 상품추가하고 재고감소하기 + orderProductListPS.stream().forEach((orderProductPS -> { + orderProductPS.getProduct().updateQty(orderProductPS.getCount()); + orderProductPS.setOrderSheet(orderSheetPS); + orderProductRepository.save(orderProductPS); + })); + return new SaveRespDTO(orderProductListPS, orderSheetPS); + } + + public OrderListRespDto 주문목록조회(@AuthenticationPrincipal LoginUser loginUser) { + + List orderSheetListPS = orderSheetRepository.findByUserId(loginUser.getUser().getId()); + return new OrderListRespDto(orderSheetListPS); + } + + public OrderListBySellerRespDto 판매자주문목록조회(){ + // 판매자는 한명이기 때문에 orderProductRepository.findAll() 해도 된다. + List orderSheetListPS = orderSheetRepository.findAll(); + return new OrderListBySellerRespDto(orderSheetListPS); + } + + public void 판매자주문취소(OrderSheet orderSheetPS){ + // 1. 재고 변경하기 + List orderProductList= orderProductRepository.findByOrderSheetId(orderSheetPS.getId()); + orderProductList.stream().forEach(orderProductPS -> { + orderProductPS.getProduct().rollbackQty(orderProductPS.getCount()); + orderProductPS.setOrderSheet(orderSheetPS); + orderProductRepository.delete(orderProductPS); //더티체킹하겠지?? + }); + + // 2. 주문서 삭제하기 + orderSheetRepository.delete(orderSheetPS); + } + + public void 유저주문취소(OrderSheet orderSheetPS) { + //1. 주문 상품목록 조회 + List orderProductList= orderProductRepository.findByOrderSheetId(orderSheetPS.getId()); + //2. 재고 변경하기 + orderProductList.stream().forEach(orderProductPS -> { + orderProductPS.getProduct().rollbackQty(orderProductPS.getCount()); + orderProductPS.setOrderSheet(orderSheetPS); + orderProductRepository.delete(orderProductPS); //더티체킹하겠지?? + }); + + //3. 주문서 삭제하기 + orderSheetRepository.delete(orderSheetPS); + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/service/ProductService.java b/src/main/java/shop/minostreet/shoppingmall/service/ProductService.java new file mode 100644 index 0000000..0175e5b --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/service/ProductService.java @@ -0,0 +1,79 @@ +package shop.minostreet.shoppingmall.service; + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import shop.minostreet.shoppingmall.config.auth.LoginUser; +import shop.minostreet.shoppingmall.domain.Product; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.dto.product.ProductReqDto; + +import shop.minostreet.shoppingmall.dto.product.ProductRespDto.ProductDto; +import shop.minostreet.shoppingmall.dto.product.ProductRespDto.ProductListRespDto; +import shop.minostreet.shoppingmall.dto.product.ProductRespDto.ProductRegisterRespDto; +import shop.minostreet.shoppingmall.handler.exception.MyApiException; +import shop.minostreet.shoppingmall.repository.ProductRepository; + +import javax.validation.Valid; +import java.util.List; +import java.util.Optional; +@RequiredArgsConstructor +@Service +public class ProductService { + private final ProductRepository productRepository; + private final Logger log = LoggerFactory.getLogger(getClass()); + private final BCryptPasswordEncoder bCryptPasswordEncoder; + + + //서비스는 DTO를 요청받고 DTO로 응답한다. + @Transactional + //메서드 시작할 때 트랜잭션 시작 + //메서드 종료시 트랜잭션 함께 종료 + /** + * 상품등록 로직 -상품 이름, 패스워드, 이메일, 이름 필요 + * 1. 상품 이름 중복 체크 + * 2. 패스워드 인코딩 + * 3. dto 응답 + */ + public ProductRegisterRespDto 상품등록(@Valid ProductReqDto.ProductRegisterReqDto productRegisterReqDto, User sellerPS){ +// 1. 상품 이름 중복 체크 + Optional productOP = productRepository.findByName(productRegisterReqDto.getName()); + if(productOP.isPresent()){ + //중복된 상품이 존재하는 경우 예외발생 + throw new MyApiException("동일한 이름의 상품이 존재합니다."); + } + //2. 상품 등록 + Product productPS = productRepository.save(productRegisterReqDto.toEntity(sellerPS)); +// 3. dto 응답 + return new ProductRegisterRespDto(productPS); + + } + + public ProductListRespDto 상품목록(Pageable pageable) { + Page productListPS=productRepository.findAll(pageable); + ///checkpoint: 나중에 페이징 처리 필요 + return new ProductListRespDto(productListPS); + } + + public ProductDto 상품상세(Long id) { + //해당 아이디의 상품 존재 확인 + Product productPS = productRepository.findById(id).orElseThrow( + () -> new MyApiException("해당 상품이 존재하지 않습니다.") + ); + return new ProductDto(productPS); + } + + public void 상품삭제(Product productPS){ + productRepository.delete(productPS); + + // productRepository.deleteById(id); + } +} diff --git a/src/main/java/shop/minostreet/shoppingmall/service/UserService.java b/src/main/java/shop/minostreet/shoppingmall/service/UserService.java new file mode 100644 index 0000000..bd7bcdf --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/service/UserService.java @@ -0,0 +1,50 @@ +package shop.minostreet.shoppingmall.service; + + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.dto.user.UserReqDto; +import shop.minostreet.shoppingmall.dto.user.UserRespDto.JoinRespDto; +import shop.minostreet.shoppingmall.handler.exception.MyApiException; +import shop.minostreet.shoppingmall.repository.UserRepository; + +import javax.validation.Valid; +import java.util.Optional; + +@RequiredArgsConstructor +@Service +public class UserService { + private final UserRepository userRepository; + private final Logger log = LoggerFactory.getLogger(getClass()); + private final BCryptPasswordEncoder bCryptPasswordEncoder; + + + //서비스는 DTO를 요청받고 DTO로 응답한다. + @Transactional + //메서드 시작할 때 트랜잭션 시작 + //메서드 종료시 트랜잭션 함께 종료 + /** + * 회원가입 로직 -사용자 이름, 패스워드, 이메일, 이름 필요 + * 1. 사용자 이름 중복 체크 + * 2. 패스워드 인코딩 + * 3. dto 응답 + */ + public JoinRespDto 회원가입(@Valid UserReqDto.JoinReqDto joinReqDto){ +// 1. 사용자 이름 중복 체크 + Optional userOP = userRepository.findByUsername(joinReqDto.getUsername()); + if(userOP.isPresent()){ + //중복된 아이디가 존재하는 경우 예외발생 + throw new MyApiException("동일한 username이 존재합니다."); + } +// 2. 패스워드 인코딩 + 회원가입 + User userPS = userRepository.save(joinReqDto.toEntity(bCryptPasswordEncoder)); +// 3. dto 응답 + return new JoinRespDto(userPS); + + } +} diff --git a/src/main/java/shop/mtcoding/metamall/util/MyDateUtil.java b/src/main/java/shop/minostreet/shoppingmall/util/MyDateUtil.java similarity index 86% rename from src/main/java/shop/mtcoding/metamall/util/MyDateUtil.java rename to src/main/java/shop/minostreet/shoppingmall/util/MyDateUtil.java index 42ba271..f9c64eb 100644 --- a/src/main/java/shop/mtcoding/metamall/util/MyDateUtil.java +++ b/src/main/java/shop/minostreet/shoppingmall/util/MyDateUtil.java @@ -1,4 +1,4 @@ -package shop.mtcoding.metamall.util; +package shop.minostreet.shoppingmall.util; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; diff --git a/src/main/java/shop/minostreet/shoppingmall/util/MyResponseUtil.java b/src/main/java/shop/minostreet/shoppingmall/util/MyResponseUtil.java new file mode 100644 index 0000000..cb42d86 --- /dev/null +++ b/src/main/java/shop/minostreet/shoppingmall/util/MyResponseUtil.java @@ -0,0 +1,91 @@ +package shop.minostreet.shoppingmall.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import shop.minostreet.shoppingmall.dto.ResponseDto; + +import javax.servlet.http.HttpServletResponse; + +public class MyResponseUtil { + private static final Logger log = LoggerFactory.getLogger(MyResponseUtil.class); + //fail로 리팩토링 +// public static void unAuthentication(HttpServletResponse response, String msg){ +// //파싱 오류가 날 경우 예외 처리 +// try{ +// //응답을 JSON으로 만들기 +// ObjectMapper objectMapper=new ObjectMapper(); +//// ResponseDto responseDto=new ResponseDto<>(-1, "인증되지 않은 사용자", null); +// ResponseDto responseDto=new ResponseDto<>(-1, msg, null); +// String responseBody = objectMapper.writeValueAsString(responseDto); +// +// response.setContentType("application/json; charset=utf-8"); +// response.setStatus(401); +// //response.getWriter().println("error"); +// response.getWriter().println(responseBody); +// //공통적인 응답 DTO 작성 필요 +// }catch (Exception e){ +// log.error("서버 파싱 에러"); +// +// } +// } + public static void unAuthorization(HttpServletResponse response, String msg){ + //파싱 오류가 날 경우 예외 처리 + try{ + //응답을 JSON으로 만들기 + ObjectMapper objectMapper=new ObjectMapper(); +// ResponseDto responseDto=new ResponseDto<>(-1, "권한이 없는 사용자", null); + ResponseDto responseDto=new ResponseDto<>(-1, msg, null); + String responseBody = objectMapper.writeValueAsString(responseDto); + + response.setContentType("application/json; charset=utf-8"); + response.setStatus(403); + //response.getWriter().println("error"); + response.getWriter().println(responseBody); + //공통적인 응답 DTO 작성 필요 + }catch (Exception e){ + log.error("서버 파싱 에러"); + + } + } + + public static void success(HttpServletResponse response, Object dto){ + //파싱 오류가 날 경우 예외 처리 + try{ + //응답을 JSON으로 만들기 + ObjectMapper objectMapper=new ObjectMapper(); + ResponseDto responseDto=new ResponseDto<>(1, "로그인 완료", dto); + String responseBody = objectMapper.writeValueAsString(responseDto); + + response.setContentType("application/json; charset=utf-8"); + response.setStatus(200); + //response.getWriter().println("error"); + response.getWriter().println(responseBody); + //공통적인 응답 DTO 작성 필요 + }catch (Exception e){ + log.error("서버 파싱 에러"); + + } + } + + public static void fail(HttpServletResponse response, String msg, HttpStatus httpStatus){ + //파싱 오류가 날 경우 예외 처리 + try{ + //응답을 JSON으로 만들기 + ObjectMapper objectMapper=new ObjectMapper(); +// ResponseDto responseDto=new ResponseDto<>(-1, "인증되지 않은 사용자", null); + ResponseDto responseDto=new ResponseDto<>(-1, msg, null); + String responseBody = objectMapper.writeValueAsString(responseDto); + + response.setContentType("application/json; charset=utf-8"); + response.setStatus(httpStatus.value()); //권한 없음 에러 + //response.getWriter().println("error"); + response.getWriter().println(responseBody); + //공통적인 응답 DTO 작성 필요 + }catch (Exception e){ + log.error("서버 파싱 에러"); + + } + } +} diff --git a/src/main/java/shop/mtcoding/metamall/MetamallApplication.java b/src/main/java/shop/mtcoding/metamall/MetamallApplication.java deleted file mode 100644 index 487bb62..0000000 --- a/src/main/java/shop/mtcoding/metamall/MetamallApplication.java +++ /dev/null @@ -1,32 +0,0 @@ -package shop.mtcoding.metamall; - -import org.springframework.boot.CommandLineRunner; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -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.ProductRepository; -import shop.mtcoding.metamall.model.user.User; -import shop.mtcoding.metamall.model.user.UserRepository; - -@SpringBootApplication -public class MetamallApplication { - - @Bean - CommandLineRunner initData(UserRepository userRepository, ProductRepository productRepository, OrderProductRepository orderProductRepository, OrderSheetRepository orderSheetRepository){ - return (args)->{ - // 여기에서 save 하면 됨. - // bulk Collector는 saveAll 하면 됨. - User ssar = User.builder().username("ssar").password("1234").email("ssar@nate.com").role("USER").build(); - userRepository.save(ssar); - }; - } - - public static void main(String[] args) { - SpringApplication.run(MetamallApplication.class, args); - } - -} diff --git a/src/main/java/shop/mtcoding/metamall/config/FilterRegisterConfig.java b/src/main/java/shop/mtcoding/metamall/config/FilterRegisterConfig.java deleted file mode 100644 index f5ea4db..0000000 --- a/src/main/java/shop/mtcoding/metamall/config/FilterRegisterConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package shop.mtcoding.metamall.config; - -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; - } -} diff --git a/src/main/java/shop/mtcoding/metamall/config/WebMvcConfig.java b/src/main/java/shop/mtcoding/metamall/config/WebMvcConfig.java deleted file mode 100644 index 64f5d9b..0000000 --- a/src/main/java/shop/mtcoding/metamall/config/WebMvcConfig.java +++ /dev/null @@ -1,18 +0,0 @@ -package shop.mtcoding.metamall.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class WebMvcConfig implements WebMvcConfigurer { - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedHeaders("*") - .allowedMethods("*") // GET, POST, PUT, DELETE (Javascript 요청 허용) - .allowedOriginPatterns("*") // 모든 IP 주소 허용 (프론트 앤드 IP만 허용하게 변경해야함. * 안됨) - .allowCredentials(true) - .exposedHeaders("Authorization"); // 옛날에는 디폴트로 브라우저에 노출되어 있었는데 지금은 아님 - } -} diff --git a/src/main/java/shop/mtcoding/metamall/controller/UserController.java b/src/main/java/shop/mtcoding/metamall/controller/UserController.java deleted file mode 100644 index ddfee94..0000000 --- a/src/main/java/shop/mtcoding/metamall/controller/UserController.java +++ /dev/null @@ -1,63 +0,0 @@ -package shop.mtcoding.metamall.controller; - -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import shop.mtcoding.metamall.core.exception.Exception400; -import shop.mtcoding.metamall.core.exception.Exception401; -import shop.mtcoding.metamall.core.jwt.JwtProvider; -import shop.mtcoding.metamall.dto.ResponseDto; -import shop.mtcoding.metamall.dto.user.UserRequest; -import shop.mtcoding.metamall.dto.user.UserResponse; -import shop.mtcoding.metamall.model.log.login.LoginLog; -import shop.mtcoding.metamall.model.log.login.LoginLogRepository; -import shop.mtcoding.metamall.model.user.User; -import shop.mtcoding.metamall.model.user.UserRepository; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; -import java.time.LocalDateTime; -import java.util.Optional; - -@RequiredArgsConstructor -@RestController -public class UserController { - - private final UserRepository userRepository; - private final LoginLogRepository loginLogRepository; - private final HttpSession session; - - @PostMapping("/login") - public ResponseEntity login(@RequestBody UserRequest.LoginDto loginDto, HttpServletRequest request) { - Optional userOP = userRepository.findByUsername(loginDto.getUsername()); - if (userOP.isPresent()) { - // 1. 유저 정보 꺼내기 - User loginUser = userOP.get(); - - // 2. 패스워드 검증하기 - if(!loginUser.getPassword().equals(loginDto.getPassword())){ - throw new Exception401("인증되지 않았습니다"); - } - - // 3. JWT 생성하기 - String jwt = JwtProvider.create(userOP.get()); - - // 4. 최종 로그인 날짜 기록 (더티체킹 - update 쿼리 발생) - loginUser.setUpdatedAt(LocalDateTime.now()); - - // 5. 로그 테이블 기록 - LoginLog loginLog = LoginLog.builder() - .userId(loginUser.getId()) - .userAgent(request.getHeader("User-Agent")) - .clientIP(request.getRemoteAddr()) - .build(); - loginLogRepository.save(loginLog); - - // 6. 응답 DTO 생성 - ResponseDto responseDto = new ResponseDto<>().data(loginUser); - return ResponseEntity.ok().header(JwtProvider.HEADER, jwt).body(responseDto); - } else { - throw new Exception400("유저네임 혹은 아이디가 잘못되었습니다"); - } - } -} diff --git a/src/main/java/shop/mtcoding/metamall/core/advice/MyExceptionAdvice.java b/src/main/java/shop/mtcoding/metamall/core/advice/MyExceptionAdvice.java deleted file mode 100644 index 50ebee2..0000000 --- a/src/main/java/shop/mtcoding/metamall/core/advice/MyExceptionAdvice.java +++ /dev/null @@ -1,42 +0,0 @@ -package shop.mtcoding.metamall.core.advice; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import shop.mtcoding.metamall.core.exception.*; -import shop.mtcoding.metamall.model.log.error.ErrorLogRepository; - -@Slf4j -@RequiredArgsConstructor -@RestControllerAdvice -public class MyExceptionAdvice { - - private final ErrorLogRepository errorLogRepository; - - @ExceptionHandler(Exception400.class) - public ResponseEntity badRequest(Exception400 e){ - return new ResponseEntity<>(e.body(), e.status()); - } - - @ExceptionHandler(Exception401.class) - public ResponseEntity unAuthorized(Exception401 e){ - return new ResponseEntity<>(e.body(), e.status()); - } - - @ExceptionHandler(Exception403.class) - public ResponseEntity forbidden(Exception403 e){ - return new ResponseEntity<>(e.body(), e.status()); - } - - @ExceptionHandler(Exception404.class) - public ResponseEntity notFound(Exception404 e){ - return new ResponseEntity<>(e.body(), e.status()); - } - - @ExceptionHandler(Exception500.class) - public ResponseEntity serverError(Exception500 e){ - return new ResponseEntity<>(e.body(), e.status()); - } -} diff --git a/src/main/java/shop/mtcoding/metamall/core/exception/Exception400.java b/src/main/java/shop/mtcoding/metamall/core/exception/Exception400.java deleted file mode 100644 index d1b5fec..0000000 --- a/src/main/java/shop/mtcoding/metamall/core/exception/Exception400.java +++ /dev/null @@ -1,24 +0,0 @@ -package shop.mtcoding.metamall.core.exception; - -import lombok.Getter; -import org.springframework.http.HttpStatus; -import shop.mtcoding.metamall.dto.ResponseDto; - - -// 유효성 실패 -@Getter -public class Exception400 extends RuntimeException { - public Exception400(String message) { - super(message); - } - - public ResponseDto body(){ - ResponseDto responseDto = new ResponseDto<>(); - responseDto.fail(HttpStatus.BAD_REQUEST, "badRequest", getMessage()); - return responseDto; - } - - public HttpStatus status(){ - return HttpStatus.BAD_REQUEST; - } -} \ No newline at end of file diff --git a/src/main/java/shop/mtcoding/metamall/core/exception/Exception401.java b/src/main/java/shop/mtcoding/metamall/core/exception/Exception401.java deleted file mode 100644 index 5d2f310..0000000 --- a/src/main/java/shop/mtcoding/metamall/core/exception/Exception401.java +++ /dev/null @@ -1,25 +0,0 @@ -package shop.mtcoding.metamall.core.exception; - - -import lombok.Getter; -import org.springframework.http.HttpStatus; -import shop.mtcoding.metamall.dto.ResponseDto; - - -// 인증 안됨 -@Getter -public class Exception401 extends RuntimeException { - public Exception401(String message) { - super(message); - } - - public ResponseDto body(){ - ResponseDto responseDto = new ResponseDto<>(); - responseDto.fail(HttpStatus.UNAUTHORIZED, "unAuthorized", getMessage()); - return responseDto; - } - - public HttpStatus status(){ - return HttpStatus.UNAUTHORIZED; - } -} \ No newline at end of file diff --git a/src/main/java/shop/mtcoding/metamall/core/exception/Exception403.java b/src/main/java/shop/mtcoding/metamall/core/exception/Exception403.java deleted file mode 100644 index c8dc137..0000000 --- a/src/main/java/shop/mtcoding/metamall/core/exception/Exception403.java +++ /dev/null @@ -1,24 +0,0 @@ -package shop.mtcoding.metamall.core.exception; - -import lombok.Getter; -import org.springframework.http.HttpStatus; -import shop.mtcoding.metamall.dto.ResponseDto; - - -// 권한 없음 -@Getter -public class Exception403 extends RuntimeException { - public Exception403(String message) { - super(message); - } - - public ResponseDto body(){ - ResponseDto responseDto = new ResponseDto<>(); - responseDto.fail(HttpStatus.FORBIDDEN, "forbidden", getMessage()); - return responseDto; - } - - public HttpStatus status(){ - return HttpStatus.FORBIDDEN; - } -} \ No newline at end of file diff --git a/src/main/java/shop/mtcoding/metamall/core/exception/Exception404.java b/src/main/java/shop/mtcoding/metamall/core/exception/Exception404.java deleted file mode 100644 index c20b64f..0000000 --- a/src/main/java/shop/mtcoding/metamall/core/exception/Exception404.java +++ /dev/null @@ -1,24 +0,0 @@ -package shop.mtcoding.metamall.core.exception; - -import lombok.Getter; -import org.springframework.http.HttpStatus; -import shop.mtcoding.metamall.dto.ResponseDto; - - -// 리소스 없음 -@Getter -public class Exception404 extends RuntimeException { - public Exception404(String message) { - super(message); - } - - public ResponseDto body(){ - ResponseDto responseDto = new ResponseDto<>(); - responseDto.fail(HttpStatus.NOT_FOUND, "notFound", getMessage()); - return responseDto; - } - - public HttpStatus status(){ - return HttpStatus.NOT_FOUND; - } -} \ No newline at end of file diff --git a/src/main/java/shop/mtcoding/metamall/core/exception/Exception500.java b/src/main/java/shop/mtcoding/metamall/core/exception/Exception500.java deleted file mode 100644 index d3d4468..0000000 --- a/src/main/java/shop/mtcoding/metamall/core/exception/Exception500.java +++ /dev/null @@ -1,24 +0,0 @@ -package shop.mtcoding.metamall.core.exception; - -import lombok.Getter; -import org.springframework.http.HttpStatus; -import shop.mtcoding.metamall.dto.ResponseDto; - - -// 서버 에러 -@Getter -public class Exception500 extends RuntimeException { - public Exception500(String message) { - super(message); - } - - public ResponseDto body(){ - ResponseDto responseDto = new ResponseDto<>(); - responseDto.fail(HttpStatus.INTERNAL_SERVER_ERROR, "serverError", getMessage()); - return responseDto; - } - - public HttpStatus status(){ - return HttpStatus.INTERNAL_SERVER_ERROR; - } -} \ No newline at end of file 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 deleted file mode 100644 index 93a4bae..0000000 --- a/src/main/java/shop/mtcoding/metamall/core/jwt/JwtProvider.java +++ /dev/null @@ -1,39 +0,0 @@ -package shop.mtcoding.metamall.core.jwt; - - - -import com.auth0.jwt.JWT; -import com.auth0.jwt.algorithms.Algorithm; -import com.auth0.jwt.exceptions.SignatureVerificationException; -import com.auth0.jwt.exceptions.TokenExpiredException; -import com.auth0.jwt.interfaces.DecodedJWT; -import shop.mtcoding.metamall.model.user.User; - -import java.util.Date; - -public class JwtProvider { - - private static final String SUBJECT = "jwtstudy"; - private static final int EXP = 1000 * 60 * 60; - public static final String TOKEN_PREFIX = "Bearer "; // 스페이스 필요함 - public static final String HEADER = "Authorization"; - private static final String SECRET = "메타코딩"; - - public static String create(User user) { - String jwt = JWT.create() - .withSubject(SUBJECT) - .withExpiresAt(new Date(System.currentTimeMillis() + EXP)) - .withClaim("id", user.getId()) - .withClaim("role", user.getRole()) - .sign(Algorithm.HMAC512(SECRET)); - System.out.println("디버그 : 토큰 생성됨"); - return TOKEN_PREFIX + jwt; - } - - public static DecodedJWT verify(String jwt) throws SignatureVerificationException, TokenExpiredException { - DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC512(SECRET)) - .build().verify(jwt); - System.out.println("디버그 : 토큰 검증됨"); - return decodedJWT; - } -} diff --git a/src/main/java/shop/mtcoding/metamall/core/session/LoginUser.java b/src/main/java/shop/mtcoding/metamall/core/session/LoginUser.java deleted file mode 100644 index 59f402c..0000000 --- a/src/main/java/shop/mtcoding/metamall/core/session/LoginUser.java +++ /dev/null @@ -1,16 +0,0 @@ -package shop.mtcoding.metamall.core.session; - -import lombok.Builder; -import lombok.Getter; - -@Getter -public class LoginUser { - private Integer id; - private String role; - - @Builder - public LoginUser(Integer id, String role) { - this.id = id; - this.role = role; - } -} diff --git a/src/main/java/shop/mtcoding/metamall/dto/ResponseDto.java b/src/main/java/shop/mtcoding/metamall/dto/ResponseDto.java deleted file mode 100644 index 7f190c6..0000000 --- a/src/main/java/shop/mtcoding/metamall/dto/ResponseDto.java +++ /dev/null @@ -1,29 +0,0 @@ -package shop.mtcoding.metamall.dto; - -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@Getter -public class ResponseDto { - private Integer status; // 에러시에 의미 있음. - private String msg; // 에러시에 의미 있음. ex) badRequest - private T data; // 에러시에는 구체적인 에러 내용 ex) username이 입력되지 않았습니다 - - public ResponseDto(){ - this.status = HttpStatus.OK.value(); - this.msg = "성공"; - this.data = null; - } - - public ResponseDto data(T data){ - this.data = data; // 응답할 데이터 바디 - return this; - } - - public ResponseDto fail(HttpStatus httpStatus, String msg, T data){ - this.status = httpStatus.value(); - this.msg = msg; // 에러 제목 - 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 deleted file mode 100644 index 80947db..0000000 --- a/src/main/java/shop/mtcoding/metamall/dto/user/UserRequest.java +++ /dev/null @@ -1,12 +0,0 @@ -package shop.mtcoding.metamall.dto.user; - -import lombok.Getter; -import lombok.Setter; - -public class UserRequest { - @Getter @Setter - public static class LoginDto { - private String username; - private String password; - } -} diff --git a/src/main/java/shop/mtcoding/metamall/dto/user/UserResponse.java b/src/main/java/shop/mtcoding/metamall/dto/user/UserResponse.java deleted file mode 100644 index ae218ec..0000000 --- a/src/main/java/shop/mtcoding/metamall/dto/user/UserResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package shop.mtcoding.metamall.dto.user; - -// 응답 DTO는 서비스 배우고 나서 하기 (할 수 있으면 해보기) -public class UserResponse { - -} diff --git a/src/main/java/shop/mtcoding/metamall/model/orderproduct/OrderProductRepository.java b/src/main/java/shop/mtcoding/metamall/model/orderproduct/OrderProductRepository.java deleted file mode 100644 index 6f1238c..0000000 --- a/src/main/java/shop/mtcoding/metamall/model/orderproduct/OrderProductRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package shop.mtcoding.metamall.model.orderproduct; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface OrderProductRepository extends JpaRepository { -} diff --git a/src/main/java/shop/mtcoding/metamall/model/ordersheet/OrderSheetRepository.java b/src/main/java/shop/mtcoding/metamall/model/ordersheet/OrderSheetRepository.java deleted file mode 100644 index 5d59249..0000000 --- a/src/main/java/shop/mtcoding/metamall/model/ordersheet/OrderSheetRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package shop.mtcoding.metamall.model.ordersheet; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface OrderSheetRepository extends JpaRepository { -} diff --git a/src/main/java/shop/mtcoding/metamall/model/product/Product.java b/src/main/java/shop/mtcoding/metamall/model/product/Product.java deleted file mode 100644 index bc8c618..0000000 --- a/src/main/java/shop/mtcoding/metamall/model/product/Product.java +++ /dev/null @@ -1,45 +0,0 @@ -package shop.mtcoding.metamall.model.product; - -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import javax.persistence.*; -import java.time.LocalDateTime; - -@NoArgsConstructor -@Setter // DTO 만들면 삭제해야됨 -@Getter -@Table(name = "product_tb") -@Entity -public class Product { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - private String name; // 상품 이름 - private Integer price; // 상품 가격 - private Integer qty; // 상품 재고 - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - - @PrePersist - protected void onCreate() { - this.createdAt = LocalDateTime.now(); - } - - @PreUpdate - 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; - this.name = name; - this.price = price; - this.qty = qty; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } -} diff --git a/src/main/java/shop/mtcoding/metamall/model/product/ProductRepository.java b/src/main/java/shop/mtcoding/metamall/model/product/ProductRepository.java deleted file mode 100644 index ba5def3..0000000 --- a/src/main/java/shop/mtcoding/metamall/model/product/ProductRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package shop.mtcoding.metamall.model.product; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ProductRepository extends JpaRepository { -} diff --git a/src/main/java/shop/mtcoding/metamall/model/user/User.java b/src/main/java/shop/mtcoding/metamall/model/user/User.java deleted file mode 100644 index c929ce5..0000000 --- a/src/main/java/shop/mtcoding/metamall/model/user/User.java +++ /dev/null @@ -1,46 +0,0 @@ -package shop.mtcoding.metamall.model.user; - -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import javax.persistence.*; -import java.time.LocalDateTime; - -@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; - private String role; // USER(고객), SELLER(판매자), ADMIN(관리자) - private LocalDateTime createdAt; - private LocalDateTime updatedAt; - - @PrePersist - protected void onCreate() { - this.createdAt = LocalDateTime.now(); - } - - @PreUpdate - protected void onUpdate() { - this.updatedAt = LocalDateTime.now(); - } - - @Builder - public User(Long id, String username, String password, String email, String role, LocalDateTime createdAt) { - this.id = id; - this.username = username; - this.password = password; - this.email = email; - this.role = role; - this.createdAt = createdAt; - } -} diff --git a/src/main/java/shop/mtcoding/metamall/model/user/UserRepository.java b/src/main/java/shop/mtcoding/metamall/model/user/UserRepository.java deleted file mode 100644 index 293a101..0000000 --- a/src/main/java/shop/mtcoding/metamall/model/user/UserRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package shop.mtcoding.metamall.model.user; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.Optional; - -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/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..5d3fd8e --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,25 @@ +server: + servlet: + encoding: + charset: utf-8 + force: true + +spring: + datasource: + url: jdbc:mysql://localhost:3306/toyprj?serverTimezone=Asia/Seoul + driver-class-name: com.mysql.cj.jdbc.Driver + username: mino + password: 1234 + jpa: + hibernate: + ddl-auto: create + show-sql: true + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 100 # in query 자동 작성 + +logging: + level: + '[shop.minostreet.shoppingmall]': DEBUG # DEBUG 레벨부터 에러 확인할 수 있게 설정하기 + '[org.hibernate.type]': TRACE # 콘솔 쿼리에 ? 에 주입된 값 보기 \ No newline at end of file diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 0000000..5d3fd8e --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,25 @@ +server: + servlet: + encoding: + charset: utf-8 + force: true + +spring: + datasource: + url: jdbc:mysql://localhost:3306/toyprj?serverTimezone=Asia/Seoul + driver-class-name: com.mysql.cj.jdbc.Driver + username: mino + password: 1234 + jpa: + hibernate: + ddl-auto: create + show-sql: true + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 100 # in query 자동 작성 + +logging: + level: + '[shop.minostreet.shoppingmall]': DEBUG # DEBUG 레벨부터 에러 확인할 수 있게 설정하기 + '[org.hibernate.type]': TRACE # 콘솔 쿼리에 ? 에 주입된 값 보기 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1d9bd50..5c4d45f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,28 +1,5 @@ -server: - servlet: - encoding: - charset: utf-8 - force: true spring: - datasource: - url: jdbc:h2:mem:test;MODE=MySQL - driver-class-name: org.h2.Driver - username: sa - password: - h2: - console: - enabled: true - jpa: - hibernate: - ddl-auto: create - show-sql: true - properties: - hibernate: - format_sql: true - default_batch_fetch_size: 100 # in query 자동 작성 - -logging: - level: - '[shop.mtcoding.metamall]': DEBUG # DEBUG 레벨부터 에러 확인할 수 있게 설정하기 - '[org.hibernate.type]': TRACE # 콘솔 쿼리에 ? 에 주입된 값 보기 \ No newline at end of file + profiles: + active: + dev diff --git a/src/main/resources/db/teardown.sql b/src/main/resources/db/teardown.sql new file mode 100644 index 0000000..1b584d6 --- /dev/null +++ b/src/main/resources/db/teardown.sql @@ -0,0 +1,13 @@ +SET FOREIGN_KEY_CHECKS=0; +-- SET REFERENTIAL_INTEGRITY FALSE; +-- drop table transaction_tb; +-- drop table account_tb; +-- drop table user_tb; +-- truncate table transaction_tb; +truncate table product_tb; +truncate table order_product_tb; +truncate table order_sheet_tb; +truncate table user_tb; +-- 테이블 안의 모든 내용을 지운다. +-- SET REFERENTIAL_INTEGRITY TRUE; +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/src/test/java/shop/mtcoding/metamall/MetamallApplicationTests.java b/src/test/java/shop/minostreet/shoppingmall/ShoppingmallApplicationTests.java similarity index 66% rename from src/test/java/shop/mtcoding/metamall/MetamallApplicationTests.java rename to src/test/java/shop/minostreet/shoppingmall/ShoppingmallApplicationTests.java index ccfa55d..451cc59 100644 --- a/src/test/java/shop/mtcoding/metamall/MetamallApplicationTests.java +++ b/src/test/java/shop/minostreet/shoppingmall/ShoppingmallApplicationTests.java @@ -1,10 +1,10 @@ -package shop.mtcoding.metamall; +package shop.minostreet.shoppingmall; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class MetamallApplicationTests { +class ShoppingmallApplicationTests { @Test void contextLoads() { diff --git a/src/test/java/shop/minostreet/shoppingmall/config/SecurityConfigTest.java b/src/test/java/shop/minostreet/shoppingmall/config/SecurityConfigTest.java new file mode 100644 index 0000000..41ef911 --- /dev/null +++ b/src/test/java/shop/minostreet/shoppingmall/config/SecurityConfigTest.java @@ -0,0 +1,88 @@ +package shop.minostreet.shoppingmall.config; + +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.security.test.context.support.TestExecutionEvent; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import shop.minostreet.shoppingmall.config.dummy.DummyObject; +import shop.minostreet.shoppingmall.repository.UserRepository; + +import javax.persistence.EntityManager; + +import static org.assertj.core.api.Assertions.assertThat; + +@ActiveProfiles("test") +@Sql("classpath:db/teardown.sql") + +//가짜 환경에 MockMvc가 등록됨 +@AutoConfigureMockMvc +//통합 테스트 수행 +//: 가짜 환경에서 수행하는 Mockito 테스트 +@SpringBootTest(webEnvironment = WebEnvironment.MOCK) +public class SecurityConfigTest extends DummyObject { + //가짜 환경에 등록된 MockMvc를 의존성 주입 + @Autowired + private MockMvc mvc; + + @Autowired + private UserRepository userRepository; + + @Autowired + private EntityManager em; + + + //서버는 일관성있게 에러가 리턴되어야 하므로, 프론트에 전달되기 전 모든 에러를 제어 + @Test + public void authentication_test() throws Exception{ + //given + + //when + ResultActions resultActions=mvc.perform(MockMvcRequestBuilders.get(("/api/user/hello"))); + + //웹, PostMan, 테스트에서 응답의 일관성을 유지하기 위해서 코드 변경 필요 + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + int httpStatusCode = resultActions.andReturn().getResponse().getStatus(); + System.out.println("테스트 : "+responseBody); + //:바디에 담기는 데이터가 없음 + System.out.println("테스트 : "+httpStatusCode); + //:403 출력 -> 401 출력이 필요 + + //then + + assertThat(httpStatusCode).isEqualTo(401); + } + + @BeforeEach + public void setUp(){ + userRepository.save(newUser("ssar")); + em.clear(); + } + @Test + @WithUserDetails(value = "ssar", setupBefore = TestExecutionEvent.TEST_EXECUTION) + public void authorization_test() throws Exception{ + //given + + //when + ResultActions resultActions=mvc.perform(MockMvcRequestBuilders.get(("/api/admin/hello"))); + + //웹, PostMan, 테스트에서 응답의 일관성을 유지하기 위해서 코드 변경 필요 + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + int httpStatusCode = resultActions.andReturn().getResponse().getStatus(); + System.out.println("테스트 : "+responseBody); + //:바디에 담기는 데이터가 없음 + System.out.println("테스트 : "+httpStatusCode); + //:403 출력 + //then + + assertThat(httpStatusCode).isEqualTo(403); + } +} diff --git a/src/test/java/shop/minostreet/shoppingmall/config/jwt/JwtAuthenticationFilterTest.java b/src/test/java/shop/minostreet/shoppingmall/config/jwt/JwtAuthenticationFilterTest.java new file mode 100644 index 0000000..93bd5c6 --- /dev/null +++ b/src/test/java/shop/minostreet/shoppingmall/config/jwt/JwtAuthenticationFilterTest.java @@ -0,0 +1,133 @@ +package shop.minostreet.shoppingmall.config.jwt; + + +//각각의 테스트 메서드가 실행이 끝나면 롤백이 진행된다. +//: 테스트 코드에서는 롤백이 진행, 본 코드에서는 커밋이 진행 +//@Transactional + +import com.fasterxml.jackson.databind.ObjectMapper; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import shop.minostreet.shoppingmall.config.dummy.DummyObject; +import shop.minostreet.shoppingmall.dto.user.UserReqDto.LoginReqDto; +import shop.minostreet.shoppingmall.repository.UserRepository; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +//SpringBootTest를 이용해 통합테스트 하는 부분엔 모두 테이블 truncate를 수행하자 +@Sql("classpath:db/teardown.sql") //실행시점은 BeforeEach실행 직전마다 수행한다. +//작성해둔 프로퍼티 설정을 적용하기 위한 어노테이션 +// '[org.hibernate.type]': TRACE 적용 +//: 쿼리에 들어가는 값까지 확인이 가능 +@ActiveProfiles("test") +//가짜 환경으로 스프링에 있는 컴포넌트들을 스캔해서 빈으로 등록 +@SpringBootTest(webEnvironment = WebEnvironment.MOCK) +//MockMvc를 모키토환경에서 사용하기 위한 어노테이션 +@AutoConfigureMockMvc +class JwtAuthenticationFilterTest extends DummyObject { + @Autowired + private ObjectMapper om; + @Autowired + private MockMvc mvc; + + @Autowired + private UserRepository userRepository; + + @BeforeEach + public void setUp(){ + //테스트를 위해 데이터 셋업 + //(1) UserRepository 의존성 주입 + //(2) extends DummyObject + //(3) 유저 객체를 DB에 삽입 + userRepository.save(newUser("ssar")); + } + @Test + void successfulAuthentication_test() throws Exception { + //given + //(1) request, response 데이터를 받아 getInputStream()으로 JSON 데이터 파싱 + //: 파싱 결과로 받은 LoginReqDto가 given데이터로 바디에 담겨온다. + //-> ObjectMapper를 의존성 주입 + LoginReqDto loginReqDto = new LoginReqDto(); + //실제로 사용하지 않을 생성자를 굳이 테스트를 위해서 만들어서 작성하지말것 + loginReqDto.setUsername("ssar"); + loginReqDto.setPassword("1234"); + + //(2) ObjectMapper로 LoginReqDto를 JSON으로 변환 + String requestBody = om.writeValueAsString(loginReqDto); + System.out.println("테스트 : "+requestBody); + + //when + //(3) 강제 로그인 부분은 UserDetailsService의 LoadUserByUsername 실행되는 부분이므로 Post 요청을 수행해서 테스트 + //: 가짜환경에서 요청을 수행한 결과를 ResultActions에 담는다. + //가짜환경의 요청에는 HTTP메서드, 컨텐트, 컨텐트타입을 명시해야한다. + ResultActions resultActions = mvc.perform(post("/api/login").content(requestBody).contentType(MediaType.APPLICATION_JSON)); + //(4) 요청에 대한 리턴에서 응답을 얻어서 내용을 문자열로 만들어서 responseBody에 담는다. + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : "+responseBody); + //현재 DB에 해당 유저가 존재하지 않으므로, 테스트 실패 -> @BeforeEach나, @SetUp으로 미리 만들어준다. + + //(5) CustomResponseUtil.success 호출 완료 -> 헤더 확인 필요 + String jwtToken = resultActions.andReturn().getResponse().getHeader(JwtVO.HEADER); + System.out.println("테스트 : "+jwtToken); + + //then + //(6) HttpStatus 확인해 200인지 확인 + resultActions.andExpect(status().isOk()); + + //(7) 토큰을 확인해 null이 아닌지 확인 + assertNotNull(jwtToken); + //(8) 토큰을 확인해 접두사 확인 (Bearer) + assertTrue(jwtToken.startsWith(JwtVO.TOKEN_PREFIX)); + //(9) JSON 데이터의 데이터 객체를 까봐서 username 키의 value를 확인 + resultActions.andExpect(jsonPath("$.data.username").value("ssar")); + } + + @Test + void unsuccessfulAuthentication_test() throws Exception { + //given + //(1) request, response 데이터를 받아 getInputStream()으로 JSON 데이터 파싱 + //: 파싱 결과로 받은 LoginReqDto가 given데이터로 바디에 담겨온다. + //-> ObjectMapper를 의존성 주입 + LoginReqDto loginReqDto = new LoginReqDto(); + //실제로 사용하지 않을 생성자를 굳이 테스트를 위해서 만들어서 작성하지말것 + loginReqDto.setUsername("ssar"); + loginReqDto.setPassword("12345"); //비밀번호 오류 + + //(2) ObjectMapper로 LoginReqDto를 JSON으로 변환 + String requestBody = om.writeValueAsString(loginReqDto); + System.out.println("테스트 : "+requestBody); + + //when + //(3) 강제 로그인 부분은 UserDetailsService의 LoadUserByUsername 실행되는 부분이므로 Post 요청을 수행해서 테스트 + //: 가짜환경에서 요청을 수행한 결과를 ResultActions에 담는다. + //가짜환경의 요청에는 HTTP메서드, 컨텐트, 컨텐트타입을 명시해야한다. + ResultActions resultActions = mvc.perform(post("/api/login").content(requestBody).contentType(MediaType.APPLICATION_JSON)); + //(4) 요청에 대한 리턴에서 응답을 얻어서 내용을 문자열로 만들어서 responseBody에 담는다. + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : "+responseBody); + //현재 DB에 해당 유저가 존재하지 않으므로, 테스트 실패 -> @BeforeEach나, @SetUp으로 미리 만들어준다. + + //(5) CustomResponseUtil.success 호출 완료 -> 헤더 확인 필요 + String jwtToken = resultActions.andReturn().getResponse().getHeader(JwtVO.HEADER); + System.out.println("테스트 : "+jwtToken); + + + //then + //(6) 로그인 실패 - 인증 실패, 파싱 실패 : InternalAuthenticationServiceException + //-> HttpStatus가 401인지 확인 + resultActions.andExpect(status().isUnauthorized()); + } +} \ No newline at end of file diff --git a/src/test/java/shop/minostreet/shoppingmall/config/jwt/JwtAuthorizationFilterTest.java b/src/test/java/shop/minostreet/shoppingmall/config/jwt/JwtAuthorizationFilterTest.java new file mode 100644 index 0000000..ab62493 --- /dev/null +++ b/src/test/java/shop/minostreet/shoppingmall/config/jwt/JwtAuthorizationFilterTest.java @@ -0,0 +1,99 @@ +package shop.minostreet.shoppingmall.config.jwt; + + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import shop.minostreet.shoppingmall.config.auth.LoginUser; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.domain.UserEnum; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +//작성해둔 프로퍼티 설정을 적용하기 위한 어노테이션 +// '[org.hibernate.type]': TRACE 적용 +//: 쿼리에 들어가는 값까지 확인이 가능 +@Sql("classpath:db/teardown.sql") + +@ActiveProfiles("test") +//가짜 환경으로 스프링에 있는 컴포넌트들을 스캔해서 빈으로 등록 +@SpringBootTest(webEnvironment = WebEnvironment.MOCK) +//MockMvc를 모키토환경에서 사용하기 위한 어노테이션 +@AutoConfigureMockMvc +class JwtAuthorizationFilterTest { + @Autowired + private MockMvc mvc; + @Test + void authorization_success_test() throws Exception { + //given + //(1) 권한 체크를 위한 유저 객체 생성 + User user = User.builder() + .id(1L) + .role(UserEnum.CUSTOMER) + .build(); + LoginUser loginUser = new LoginUser(user); + //(2) 로그인 유저 객체를 이용해 JWT 직접 생성 + String jwtToken = JwtProcess.create(loginUser); + System.out.println("테스트 : "+jwtToken); + + //when + //(3) 인증이 필요하지만, 없는 페이지 요청 +// ResultActions resultActions = mvc.perform(get("/api/s/hello/test")); + + //(3) 인증이 필요하지만, jwt을 담은 헤더를 담아서 없는 페이지 요청 + ResultActions resultActions = mvc.perform(get("/api/s/hello/test").header(JwtVO.HEADER, jwtToken)); + + //then + //(4) 404에러 발생 예상 + resultActions.andExpect(status().isNotFound()); + } + + @Test + void authorization_fail_test() throws Exception { + //given + + //when + //(1) 인증이 필요하지만, 토큰 없이 페이지 요청 + ResultActions resultActions = mvc.perform(get("/api/user")); + + //then + //(2) 인증이 필요한 401에러 발생 예상 + resultActions.andExpect(status().isUnauthorized()); + + } + + @Test + void authorization_admin_test() throws Exception { + //given + //(1) 권한 체크를 위한 유저 객체 생성 + User user = User.builder() + .id(1L) + .role(UserEnum.CUSTOMER) + .build(); + LoginUser loginUser = new LoginUser(user); + //(2) 로그인 유저 객체를 이용해 JWT 직접 생성 + String jwtToken = JwtProcess.create(loginUser); + System.out.println("테스트 : "+jwtToken); + + //when + //(3) 인증이 필요하지만, 없는 페이지 요청 +// ResultActions resultActions = mvc.perform(get("/api/s/hello/test")); + + //(3) 인증이 필요하지만, jwt을 담은 헤더를 담아서 admin권한이 필요한 페이지 요청 + ResultActions resultActions = mvc.perform(get("/api/admin/hello/test").header(JwtVO.HEADER, jwtToken)); + + //then + //(4) 403에러 발생 예상 - 권한이 없음 + resultActions.andExpect(status().isForbidden()); + } + + + +} \ No newline at end of file diff --git a/src/test/java/shop/minostreet/shoppingmall/config/jwt/JwtProcessTest.java b/src/test/java/shop/minostreet/shoppingmall/config/jwt/JwtProcessTest.java new file mode 100644 index 0000000..7ecff4c --- /dev/null +++ b/src/test/java/shop/minostreet/shoppingmall/config/jwt/JwtProcessTest.java @@ -0,0 +1,70 @@ +package shop.minostreet.shoppingmall.config.jwt; + + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import shop.minostreet.shoppingmall.config.auth.LoginUser; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.domain.UserEnum; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class JwtProcessTest { + + @Test + void create_test() { + //given + //(1) 테스트에 사용할 유저 객체 생성 + User user = User.builder() + .id(1L) + .role(UserEnum.CUSTOMER) + .build(); + LoginUser loginUser = new LoginUser(user); + + //when + //(2) 테스트 진행하기 위해 토큰 생성 + String jwtToken = JwtProcess.create(loginUser); + System.out.println("테스트 : "+jwtToken); + + //then + //(3) 토큰은 생성시마다 값이 바뀌기 때문에, Token에 Bearer가 붙어있는지만 체크 + assertThat(jwtToken.startsWith(JwtVO.TOKEN_PREFIX)); + } + + //테스트에서 사용하는 토큰 반환하는 메서드 + private String createToken() { + //given + //(1) 테스트에 사용할 유저 객체 생성 + User user = User.builder() + .id(1L) + .role(UserEnum.CUSTOMER) + .build(); + LoginUser loginUser = new LoginUser(user); + + //when + //(2) 테스트 진행하기 위해 토큰 반환 + return JwtProcess.create(loginUser); + } + + @Test + void verify_test() { + //given + String jwtToken = createToken(); +// jwtToken=jwtToken.substring(7); + jwtToken=jwtToken.replace(JwtVO.TOKEN_PREFIX, ""); + + //(1) 토큰 검증을 위해 Bearer를 제외한 토큰 값을 가져옴 +// String jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJiYW5rIiwicm9sZSI6IkNVU1RPTUVSIiwiaWQiOjEsImV4cCI6MTY4MTUzNzg1M30.8yhovOveayIJN-bCnFKx7ucQRbP0FVH8gLo9tD9a0HG0F2PKZu1cl6RWazwtMVi59ENh_Krve1xQGqFMBBANXA"; + //: 토큰값을 이렇게 명시적으로 준다면, 만료시간이 이후에 사용 불가능 + + //when + //(2) 토큰 검증 후, 리턴값을 LoginUser 객체로 저장 + LoginUser loginUser = JwtProcess.verify(jwtToken); + System.out.println("테스트 : "+loginUser.getUser().getId()); + System.out.println("테스트 : "+loginUser.getUser().getRole().name()); + + //then + Assertions.assertThat(loginUser.getUser().getId()).isEqualTo(1L); + Assertions.assertThat(loginUser.getUser().getRole()).isEqualTo(UserEnum.CUSTOMER); + } +} \ No newline at end of file diff --git a/src/test/java/shop/minostreet/shoppingmall/controller/UserControllerTest.java b/src/test/java/shop/minostreet/shoppingmall/controller/UserControllerTest.java new file mode 100644 index 0000000..1579901 --- /dev/null +++ b/src/test/java/shop/minostreet/shoppingmall/controller/UserControllerTest.java @@ -0,0 +1,135 @@ +package shop.minostreet.shoppingmall.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; + +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import shop.minostreet.shoppingmall.config.dummy.DummyObject; +import shop.minostreet.shoppingmall.dto.user.UserReqDto.JoinReqDto; +import shop.minostreet.shoppingmall.repository.UserRepository; +import shop.minostreet.shoppingmall.restdocs.TestSupport; + +import javax.persistence.EntityManager; + +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ActiveProfiles("test") +//: dev 모드에서 발동하는 DummyInit의 유저가 삽입되므로 +//@Transactional +@Sql("classpath:db/teardown.sql") +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = WebEnvironment.MOCK) +//통합테스트 +public class UserControllerTest extends TestSupport { + @Autowired + private ObjectMapper om; + + @Test + public void join_success_test() throws Exception { + //given + JoinReqDto joinReqDto = new JoinReqDto(); + joinReqDto.setUsername("love"); +// joinReqDto.setUsername("ssar"); //dev모드일 때 DummyInit 발동해서 오류 발생하는걸 확인하기위해 + joinReqDto.setPassword("1234"); + joinReqDto.setEmail("love@nate.com"); + + //Object -> JSON + String requestBody = om.writeValueAsString(joinReqDto); + System.out.println("테스트 : " + requestBody); + //when + ResultActions resultActions = mvc.perform(post("/api/join").content(requestBody).contentType(MediaType.APPLICATION_JSON)); + //컨텐트를 넣으면 반드시 컨텐트를 설명하는 컨텐트타입이 필요하다. + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : " + responseBody); + + //then + resultActions.andExpect(status().isCreated()) //201 + .andDo( + restDocs.document( + //static import 진행 + requestFields( + //optional이 아니라 필수 값이므로 + fieldWithPath("username").description("username"), + fieldWithPath("password").description("password"), + fieldWithPath("email").description("email") + ), + //응답 필드에 대한 문서 작성 + responseFields( + fieldWithPath("code").description("code"), + fieldWithPath("msg").description("msg"), + fieldWithPath("data").description("data object"), + fieldWithPath("data.id").description("id"), + fieldWithPath("data.username").description("username") + ) + ) + ); + } + + + @Autowired + private UserRepository userRepository; + + @Autowired + private EntityManager em; + + // @BeforeEach +// public void setUp(){ +// dataSetting(); +// } +// +// private void dataSetting() { +// //DummyObject의 newUser() +// userRepository.save(newUser("ssar", "pepe ssar")); +// } +// + @BeforeEach + public void setUp() { + userRepository.save(newUser("ssar")); + em.clear(); + } + + + @Test + //통합테스트이므로, 서비스단에서 중복체크에서 예외가 발생하는 경우 테스트 + //: @BeforeEach로 미리 해당 유저를 생성 + public void join_fail_test() throws Exception { + //given + JoinReqDto joinReqDto = new JoinReqDto(); + joinReqDto.setUsername("ssar"); + joinReqDto.setPassword("1234"); + joinReqDto.setEmail("love@nate.com"); + + //Object -> JSON + String requestBody = om.writeValueAsString(joinReqDto); + System.out.println("테스트 : " + requestBody); + //when + ResultActions resultActions = mvc.perform(post("/api/join").content(requestBody).contentType(MediaType.APPLICATION_JSON)); + //컨텐트를 넣으면 반드시 컨텐트를 설명하는 컨텐트타입이 필요하다. + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + System.out.println("테스트 : " + responseBody); + + //then + resultActions.andExpect(status().isBadRequest()) + .andDo(restDocs.document( + responseFields( + fieldWithPath("code").description("code"), + fieldWithPath("msg").description("message"), + fieldWithPath("data").description("Data Object") + ))); //400 +// resultActions.andExpect(status().isCreated()); //201 +// resultActions.andExpect(status().isOk()); //200 + } +} diff --git a/src/test/java/shop/minostreet/shoppingmall/restdocs/RestDocsConfiguration.java b/src/test/java/shop/minostreet/shoppingmall/restdocs/RestDocsConfiguration.java new file mode 100644 index 0000000..6c757e2 --- /dev/null +++ b/src/test/java/shop/minostreet/shoppingmall/restdocs/RestDocsConfiguration.java @@ -0,0 +1,22 @@ +package shop.minostreet.shoppingmall.restdocs; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.operation.preprocess.Preprocessors; + +//테스트 메서드마다 반복되는 코드의 중복을 제거하고 +//제목 규칙을 설정하고, 문서를 식별하기 좋게 생성하도록 설정한다. +@TestConfiguration +public class RestDocsConfiguration { + @Bean + public RestDocumentationResultHandler write(){ + return MockMvcRestDocumentation.document( + "{class-name}/{method-name}", + + Preprocessors.preprocessRequest(Preprocessors.prettyPrint()), + Preprocessors.preprocessResponse(Preprocessors.prettyPrint()) + ); + } +} diff --git a/src/test/java/shop/minostreet/shoppingmall/restdocs/TestSupport.java b/src/test/java/shop/minostreet/shoppingmall/restdocs/TestSupport.java new file mode 100644 index 0000000..1ae21bf --- /dev/null +++ b/src/test/java/shop/minostreet/shoppingmall/restdocs/TestSupport.java @@ -0,0 +1,120 @@ +package shop.minostreet.shoppingmall.restdocs; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.core.io.ResourceLoader; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import shop.minostreet.shoppingmall.domain.Product; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.domain.UserEnum; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +//테스트 코드를 쉽게 작성하기 위한 클래스로 관례적인 코드를 모아둔다. +@SpringBootTest +@AutoConfigureMockMvc +//웹에 대한 테스트를 모킹하기 위한 어노테이션 +@ExtendWith(RestDocumentationExtension.class) +//RestDoc 짜기 위해 필요한 클래스를 빈으로 등록하기 위한 어노테이션 +@Import(RestDocsConfiguration.class) +//Docs 설정 임포트 -> TestConfiguration 설정했고, 빈으로 등록했으므로 임포트 가능 +public class TestSupport { + @Autowired + protected MockMvc mvc; + + @Autowired + protected RestDocumentationResultHandler restDocs; + @Autowired + private ResourceLoader resourceLoader; + + @BeforeEach + void setUp( + final WebApplicationContext context, + final RestDocumentationContextProvider provider + ) { + this.mvc = MockMvcBuilders.webAppContextSetup(context) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) + //매번 반복되는 코드를 추가 + .alwaysDo(MockMvcResultHandlers.print()) + .alwaysDo(restDocs) + //JSON 포맷팅과 쉽게 작성하기 위한 클래스 + .build(); + } + //하위 클래스에서 사용할 JSON 클래스 + protected String readJson(final String path) throws IOException { + return IOUtils.toString( + resourceLoader.getResource("classpath:" + path).getInputStream(), + StandardCharsets.UTF_8 + ); + } + + protected User newUserAdmin(String username){ + BCryptPasswordEncoder passwordEncoder= new BCryptPasswordEncoder(); + String encPassword = passwordEncoder.encode("1234"); + return User.builder() + .username(username) +// .password("1234") + .password(encPassword) + .email(username+"@nate.com") + .role(UserEnum.ADMIN) + .status(true) + .build(); + } + protected User newUser(String username){ + BCryptPasswordEncoder passwordEncoder= new BCryptPasswordEncoder(); + String encPassword = passwordEncoder.encode("1234"); + return User.builder() + .username(username) +// .password("1234") + .password(encPassword) + .email(username+"@nate.com") + .role(UserEnum.CUSTOMER) + .status(true) + .build(); + } + protected static User newMockUser(Long id,String username){ + BCryptPasswordEncoder passwordEncoder= new BCryptPasswordEncoder(); + String encPassword = passwordEncoder.encode("1234"); + return User.builder() + .id(id) + .username(username) +// .password("1234") + .password(encPassword) + .email(username+"@nate.com") + .role(UserEnum.CUSTOMER) + .status(true) + .build(); + } + + protected static Product newProduct(String name, Integer qty, Integer price){ + return Product.builder() + .name(name) + .qty(qty) + .price(1000) + .build(); + } + + protected static Product newMockProduct(Long id,String name, Integer qty, Integer price) { + return Product.builder() + .id(id) + .name(name) + .qty(qty) + .price(price) + .build(); + } + +} diff --git a/src/test/java/shop/minostreet/shoppingmall/service/UserServiceTest.java b/src/test/java/shop/minostreet/shoppingmall/service/UserServiceTest.java new file mode 100644 index 0000000..c422fd8 --- /dev/null +++ b/src/test/java/shop/minostreet/shoppingmall/service/UserServiceTest.java @@ -0,0 +1,86 @@ +package shop.minostreet.shoppingmall.service; + + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import shop.minostreet.shoppingmall.config.dummy.DummyObject; +import shop.minostreet.shoppingmall.domain.User; +import shop.minostreet.shoppingmall.dto.user.UserReqDto.JoinReqDto; +import shop.minostreet.shoppingmall.dto.user.UserRespDto.JoinRespDto; +import shop.minostreet.shoppingmall.repository.UserRepository; + +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +//서비스 테스트를 위한 모키토 환경은 스프링 관련 빈들이 존재하지 않는다. +@ExtendWith(MockitoExtension.class) +public class UserServiceTest extends DummyObject { + + //가짜 환경으로 주입하기 위한 어노테이션 + //: Mock으로 만든 가짜 객체와, Spy로 가져온 진짜 객체를 Inject한다. + @InjectMocks + private UserService userService; + + //모키토 환경에서는 + //빈의 직접 등록이 필요하므로 @Mock을 이용해 가짜로 메모리에 띄운다. + @Mock + private UserRepository userRepository; + + //진짜 객체를 가짜 객체로 집어넣는 방법 + @Spy + private BCryptPasswordEncoder bCryptPasswordEncoder; + + @Test + public void 회원가입_test() throws Exception{ + //given + //요청 데이터 전달하기 위한 JoinReqDto 필요 + JoinReqDto joinReqDto = new JoinReqDto(); + joinReqDto.setUsername("ssar"); + joinReqDto.setPassword("1234"); + joinReqDto.setEmail("ssar@nate.com"); + + //stub1 + //가짜로 띄워줘도 해당 클래스의 메서드가 존재하지 않기 때문에, stub 처리 필요 + when(userRepository.findByUsername(any())).thenReturn(Optional.empty()); + //: 리포지토리의 중복 체크 메서드를 실행시, 빈 옵셔널 객체를 리턴한다. +// when(userRepository.findByUsername(any())).thenReturn(Optional.of(new User())); + //: 동일한 유저네임을 전달 리턴할 경우 + + + //stub2 + //user 객체를 리턴하는 스텁 + //: 패스워드 인코딩도 필요 +// User user= User.builder() +// .id(1L) +// .username("ssar") +// .password("1234") +// .email("ssar@nate.com") +// .role(UserEnum.CUSTOMER) +// .createdAt(LocalDateTime.now()) +// .updatedAt(LocalDateTime.now()) +// .build(); + // 더미 오브젝트로 리팩토링 + User user = newMockUser(1L, "ssar"); + + when(userRepository.save(any())).thenReturn(user); + + //when + JoinRespDto joinRespDto = userService.회원가입(joinReqDto); + System.out.println("테스트 : "+joinRespDto); + //then + + + assertThat(joinRespDto.getId()).isEqualTo(1L); + //: Long값인데 EuqalsTo로 127L이 넘아가도 가능할까? -> 가능 + + assertThat(joinRespDto.getUsername()).isEqualTo("ssar"); + } +} diff --git a/src/test/java/shop/minostreet/shoppingmall/temp/LongTest.java b/src/test/java/shop/minostreet/shoppingmall/temp/LongTest.java new file mode 100644 index 0000000..190b1e1 --- /dev/null +++ b/src/test/java/shop/minostreet/shoppingmall/temp/LongTest.java @@ -0,0 +1,48 @@ +package shop.minostreet.shoppingmall.temp; + +import org.junit.jupiter.api.Test; + +public class LongTest { + + @Test + public void long_test() throws Exception{ + //given + Long number1 = 1111L; + Long number2 = 1111L; + + + //when +// if(number1==number2){ + if(number1.longValue()==number2.longValue()){ + System.out.println("테스트 : 동일합니다."); + }else + System.out.println("테스트 : 동일하지 않습니다.."); + + Long amount1= 100L; + Long amount2= 1000L; + + if(amount1\\/?]{4,12}$", value); + //영어와 숫자,특수문자로 시작(^)하거나 영어와 숫자,특수문자로 끝나($)는 것+ 무한으로 반복 가능(*), 길이는 4,12만 가능 + + System.out.println("테스트 : " + result); + //빈 regex에 공백이면 참 + + + } + + //username, email, fullname 테스트 진행 + @Test + //영문,숫자, 2~20자이내 + public void user_username_test() throws Exception { + //given + String username = "ssar123"; + boolean result = Pattern.matches("^[A-Za-z0-9]{2,20}$", username); + + System.out.println("테스트 : " + result); + } + + @Test + //영문,숫자, 2~20자이내 + public void user_email_test() throws Exception { + //given + String email = "ssar@nate.com"; + //com만 가능한 테스트 + //메타문자의 경우 \\를 추가해 메타문자가 아닌걸 표현해야한다. + boolean result = Pattern.matches("^[A-Za-z0-9]{2,10}@[A-Za-z0-9]{2,6}\\.[a-zA-Z]{2,3}$", email); + + System.out.println("테스트 : " + result); + } + + @Test + //영문,한글, 1~20자이내 + public void user_fullname_test() throws Exception { + //given + String fullname = "ssar"; + boolean result = Pattern.matches("^[A-Za-z가-힣]{1,20}$", fullname); + + System.out.println("테스트 : " + result); + } + + + @Test + public void account_gubun_test1() throws Exception{ + //given + String gubun = "DEPOSIT"; + boolean result = Pattern.matches("^(DEPOSIT)$", gubun); + //boolean result = Pattern.matches("DEPOSIT", gubun); //하나인 경우엔 문자열자체로도 가능 +// boolean result = Pattern.matches("^(DEPOSIT|TRANSFER)$", gubun); //두 개인 경우에도 가능 (띄우면 안됨) + + //배열은 범위, 정확한 문자열은 괄호 + System.out.println("테스트 : "+result); + } + @Test + public void account_gubun_test2() throws Exception{ + //given + String gubun = "DEPOSIT"; +// boolean result = Pattern.matches("^(DEPOSIT)$", gubun); + //boolean result = Pattern.matches("DEPOSIT", gubun); //하나인 경우엔 문자열자체로도 가능 + boolean result = Pattern.matches("^(DEPOSIT|TRANSFER)$", gubun); //두 개인 경우에도 가능 (띄우면 안됨) + + //배열은 범위, 정확한 문자열은 괄호 + System.out.println("테스트 : "+result); + } + + @Test + public void account_tel_test1() throws Exception{ + //given + String tel = "010-3333-7777"; + boolean result = Pattern.matches("^[0-9]{3}-[0-9]{4}-[0-9]{4}", tel); + System.out.println("테스트 : "+result); + } + @Test + public void account_tel_test2() throws Exception{ + //given + String tel = "01033337777"; + boolean result = Pattern.matches("^[0-9]{11}", tel); + System.out.println("테스트 : "+result); + } + + @Test + public void productNameTest()throws Exception{ + String name="딸기 수정"; + boolean result =Pattern.matches("^[ㄱ-힣A-Za-z0-9\\s]{2,20}$", name); + System.out.println("테스트 : "+result); + } + +} \ No newline at end of file diff --git a/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet b/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet new file mode 100644 index 0000000..2333ca7 --- /dev/null +++ b/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet @@ -0,0 +1,10 @@ +|=== +|Field|Type|Required|Description|Length +{{#fields}} +|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} +|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{^optional}}true{{/optional}}{{#optional}}false{{/optional}}{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} +|{{#tableCellContent}}{{#length}}{{.}}{{/length}}{{/tableCellContent}} +{{/fields}} +|=== \ No newline at end of file diff --git a/src/test/resources/org/springframework/restdocs/templates/request-parameters.snippet b/src/test/resources/org/springframework/restdocs/templates/request-parameters.snippet new file mode 100644 index 0000000..bd1f913 --- /dev/null +++ b/src/test/resources/org/springframework/restdocs/templates/request-parameters.snippet @@ -0,0 +1,8 @@ +|=== +|Parameter|Required|Description +{{#parameters}} +|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{^optional}}true{{/optional}}{{#optional}}false{{/optional}}{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} +{{/parameters}} +|=== \ No newline at end of file diff --git a/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet b/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet new file mode 100644 index 0000000..bb12d83 --- /dev/null +++ b/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet @@ -0,0 +1,9 @@ +|=== +|Path|Type|Required|Description +{{#fields}} +|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} +|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{^optional}}true{{/optional}}{{#optional}}false{{/optional}}{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} +{{/fields}} +|=== \ No newline at end of file diff --git "a/\354\203\201\355\222\210\354\243\274\353\254\270\355\224\204\353\241\234\354\240\235\355\212\270ERD.png" "b/\354\203\201\355\222\210\354\243\274\353\254\270\355\224\204\353\241\234\354\240\235\355\212\270ERD.png" new file mode 100644 index 0000000..8107619 Binary files /dev/null and "b/\354\203\201\355\222\210\354\243\274\353\254\270\355\224\204\353\241\234\354\240\235\355\212\270ERD.png" differ diff --git "a/\355\214\250\355\202\244\354\247\200 \353\213\244\354\235\264\354\226\264\352\267\270\353\236\250.png" "b/\355\214\250\355\202\244\354\247\200 \353\213\244\354\235\264\354\226\264\352\267\270\353\236\250.png" new file mode 100644 index 0000000..1f4c728 Binary files /dev/null and "b/\355\214\250\355\202\244\354\247\200 \353\213\244\354\235\264\354\226\264\352\267\270\353\236\250.png" differ