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
+
+
+
+* 클래스 다이어그램
+
+
+
+* 시퀀스 다이어그램
+
+ 1. 사용자 회원가입
+ 
+
+
+ 2. 상품 등록
+ 
+
+
+ 3. 상품 주문
+ 
+
+
+## 프로젝트 설명
+
+### 개요
+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 extends GrantedAuthority> 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