diff --git a/README.md b/README.md index ae3bc98..cb1695f 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,111 @@ # ClothStar 프로젝트 ## 프로젝트 소개 -- 개요 : 의류 온라인 쇼핑몰에서의 까다로운 도메인 규칙을 설계하고 구현하여 의류 쇼핑몰에 대한 도메인에 대한 이해를 높임과 동시에 개발과 협업의 역량을 향상 시키기 위해 시작하였습니다. + +- 개요 : 의류 온라인 쇼핑몰에서의 까다로운 도메인 규칙을 설계하고 구현하여 의류 쇼핑몰에 대한 도메인에 대한 이해를 높임과 동시에 개발과 협업의 역량을 향상 시키기 위해 시작하였습니다. - Scrum 방법론 사용 : 1주, 2주 단위로 Sprint를 나누고, 주간 미팅과 데일리 스탠딩 미팅을 통해 각 팀원의 진행사항 및 계획에 대해 공유하며 진행하였습니다. - 컨벤션 사용과 깃 브랜치 전략 사용 : 커밋 커벤션과, 네이버 코드 컨벤션을 따랐습니다. - - [커밋 컨벤션 규칙 참고 URL](https://velog.io/@shin6403/Git-git-%EC%BB%A4%EB%B0%8B-%EC%BB%A8%EB%B2%A4%EC%85%98-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0) - - [네이버 Java 코드 컨벤션 참고 URL](https://bestinu.tistory.com/64) - - [깃 브랜치 전략 참고 URL](https://hudi.blog/git-branch-strategy/) + - [커밋 컨벤션 규칙 참고 URL](https://velog.io/@shin6403/Git-git-%EC%BB%A4%EB%B0%8B-%EC%BB%A8%EB%B2%A4%EC%85%98-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0) + - [네이버 Java 코드 컨벤션 참고 URL](https://bestinu.tistory.com/64) + - [깃 브랜치 전략 참고 URL](https://hudi.blog/git-branch-strategy/) ## 팀 멤버 + - 유수빈 - 김민아 - 강현수 ## 프로젝트 스프린트 계획 + 이 프로젝트는 스크럼 방법론을 따라 진행됩니다. 1주~2주일 단위로 Sprint를 나누고, 주간 미팅을 통해 각 팀원의 진행사항 및 계획에 대해 공유하였습니다. 아래는 프로젝트의 스프린트 계획입니다. ### 스프린트 1 + - **멤버**: 강현수 -- **목표**: 회원가입, 로그인 API 구현 (스프링 시큐리티 적용) +- **목표**: 회원가입, 로그인, 판매자, 배송지 기본 API 구현 - **기간**: 2024년 3월 13일부터 2024년 3월 31일까지 - **주요 작업**: - - 스프링 시큐리티 적용하여 회원가입, 로그인 API 구현 + - 회원가입, 회원조회, 로그인 API + - 배송지 입력, 조회 API 구현 + - 판매자 신청, 조회 API 구현 - 테스트 작성 및 실행 - **멤버**: 유수빈 - **목표**: 주문 API 구현(반품, 교환 제외) - **기간**: 2024년 3월 14일부터 2024년 3월 31일까지 - **주요 작업**: - - (사용자)주문 생성 구현 - - (사용자)주문 내역 구현 - - (사용자)주문 취소 구현 - - (판매자)주문 관리 구현 - - 테스트 작성 및 실행 - - 사용자 피드백 수집 + - (사용자)주문 생성 구현 + - (사용자)주문 내역 구현 + - (사용자)주문 취소 구현 + - (판매자)주문 관리 구현 + - 테스트 작성 및 실행 + - 사용자 피드백 수집 - **멤버**: 김민아 - **목표**: [스프린트 목표 및 완료 기준] - **기간**: 2024년 3월 14일부터 2024년 3월 31일까지 - **주요 작업**: - - 기능 1 구현 - - 기능 2 구현 - - 테스트 작성 및 실행 - - 사용자 피드백 수집 + - 주문 상품 반품 구현 + - 주문 상품 교환 + - (판매자)상품 관리 구현 + - (사용자)상품 조회 및 검색 구현 + - 테스트 작성 및 실행 + - 사용자 피드백 수집 ### 스프린트 2 + - **멤버**: 강현수 -- **목표**: JWT 토큰 검증 사용한 비밀번호 수정, 회원정보 수정 +- **목표**: 스프링 시큐리티 적용과, Jwt 적용후 인증및 권한 구현 - **기간**: 2024년 4월 01일부터 2024년 4월 10일까지 - **주요 작업**: - - JWT 토큰으로 이메일로 전송한 비밀번호 수정 링크 타고 왔는지 확인 - - 회원 정보 수정 - - 테스트 작성 및 실행 + - 스프링 시큐리티 적용 + - 스프링 시큐리티 Jwt 이용한 인증 및 권한구현 + - 회원정보 수정, 회원 도메인 validation 체크및 수정 + - 테스트 작성 및 실행 - **멤버**: 유수빈 - **목표**: 주문 상태 알림 API 구현 - **기간**: 2024년 4월 01일부터 2024년 4월 10일까지 - **주요 작업**: - - 주문 상태 알림 구현 - - 테스트 작성 및 실행 - - 사용자 피드백 수집 + - 주문 상태 알림 구현 + - 입점 신청/관리 구현 + - 테스트 작성 및 실행 + - 사용자 피드백 수집 - **멤버**: 김민아 - **목표**: 재입고 알림 API 구현 - **기간**: 2024년 4월 01일부터 2024년 4월 10일까지 - **주요 작업**: - - 재입고 알림 구현 - - 테스트 작성 및 실행 - - 사용자 피드백 수집 + - 재입고 알림 구현 + - 상품 리뷰 구현 + - 장바구니, 위시리스트 구현 + - 테스트 작성 및 실행 + - 사용자 피드백 수집 ### 스프린트 3 + - **멤버**: 강현수 -- **목표**: 배송지 입력 API 개발 +- **목표**: refresh 토큰 구현및 코드 리팩토링 - **기간**: 2024년 4월 11일부터 2024년 4월 21일까지 - **주요 작업**: - - 배송지 입력, 추가, 삭제 API 개발 - - 테스트 작성 및 실행 + - access 토큰으로 refresh 토큰 재발급 + - 테스트 작성 및 실행 - **멤버**: 유수빈 - **목표**: 최종 사용자 피드백 수집 및 수정 - **기간**: 2024년 4월 11일부터 2024년 4월 21일까지 - **주요 작업**: - - 최종 사용자 피드백 수집 및 수정 + - 최종 사용자 피드백 수집 및 수정 - **멤버**: 김민아 - **목표**: [스프린트 목표 및 완료 기준] - **기간**: 2024년 4월 11일부터 2024년 4월 21일까지 - **주요 작업**: - - 기능 5 구현 - - 기능 6 구현 - - 테스트 작성 및 실행 - - 최종 사용자 피드백 수집 및 수정 + - 검색 엔진 구현 + - 기능 6 구현 + - 테스트 작성 및 실행 + - 최종 사용자 피드백 수집 및 수정 ## 프로젝트 기간 -이 프로젝트의 예상 기간은 2024년 3월 13일부터 2024년 4월 21일까지입니다. \ No newline at end of file + +이 프로젝트의 예상 기간은 2024년 3월 13일부터 2024년 4월 21일까지입니다. diff --git a/build.gradle b/build.gradle index a90326c..d9ec8d9 100644 --- a/build.gradle +++ b/build.gradle @@ -20,10 +20,27 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springdoc:springdoc-openapi-ui:1.8.0' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation "javax.validation:validation-api:2.0.1.Final" runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.mysql:mysql-connector-j' + testImplementation "org.hamcrest:hamcrest:2.2" + testImplementation "org.junit.jupiter:junit-jupiter:5.10.2" + testImplementation "org.mockito:mockito-junit-jupiter:4.11.0" + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' + + compileOnly 'org.projectlombok:lombok' + + annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' } tasks.named('test') { diff --git a/src/main/java/org/store/clothstar/Application.java b/src/main/java/org/store/clothstar/Application.java index 95af3cf..9df6463 100644 --- a/src/main/java/org/store/clothstar/Application.java +++ b/src/main/java/org/store/clothstar/Application.java @@ -9,5 +9,4 @@ public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } - } diff --git a/src/main/java/org/store/clothstar/category/controller/CategoryController.java b/src/main/java/org/store/clothstar/category/controller/CategoryController.java new file mode 100644 index 0000000..759959f --- /dev/null +++ b/src/main/java/org/store/clothstar/category/controller/CategoryController.java @@ -0,0 +1,61 @@ +package org.store.clothstar.category.controller; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.store.clothstar.category.dto.request.CreateCategoryRequest; +import org.store.clothstar.category.dto.request.UpdateCategoryRequest; +import org.store.clothstar.category.dto.response.CategoryDetailResponse; +import org.store.clothstar.category.dto.response.CategoryResponse; +import org.store.clothstar.category.service.CategoryService; +import org.store.clothstar.common.dto.MessageDTO; +import org.store.clothstar.common.util.URIBuilder; + +import java.net.URI; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/categories") +public class CategoryController { + + private final CategoryService categoryService; + + @Operation(summary = "전체 카테고리 조회", description = "모든 카테고리를 조회한다.") + @GetMapping + public ResponseEntity> getAllCategories() { + List categoryResponses = categoryService.getAllCategories(); + return ResponseEntity.ok().body(categoryResponses); + } + + @Operation(summary = "카테고리 상세 조회", description = "id로 카테고리 한개를 상세 조회한다.") + @GetMapping("/{categoryId}") + public ResponseEntity getCategory(@PathVariable Long categoryId) { + CategoryDetailResponse categoryDetailResponse = categoryService.getCategory(categoryId); + return ResponseEntity.ok().body(categoryDetailResponse); + } + + @Operation(summary = "카테고리 등록", description = "카테고리 타입(이름)을 입력하여 신규 카테고리를 등록한다.") + @PostMapping + public ResponseEntity createCategory(@Validated @RequestBody CreateCategoryRequest createCategoryRequest) { + Long categoryId = categoryService.createCategory(createCategoryRequest); + + URI location = URIBuilder.buildURI(categoryId); + + return ResponseEntity.created(location).build(); + } + + @Operation(summary = "카테고리 수정", description = "카테고리 이름을 수정한다.") + @PutMapping("/{categoryId}") + public ResponseEntity updateCategories( + @PathVariable Long categoryId, + @Validated @RequestBody UpdateCategoryRequest updateCategoryRequest) { + + categoryService.updateCategory(categoryId, updateCategoryRequest); + + return ResponseEntity.ok().body(new MessageDTO(HttpStatus.OK.value(), "Category updated successfully", null)); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/category/domain/Category.java b/src/main/java/org/store/clothstar/category/domain/Category.java new file mode 100644 index 0000000..c3ecd36 --- /dev/null +++ b/src/main/java/org/store/clothstar/category/domain/Category.java @@ -0,0 +1,16 @@ +package org.store.clothstar.category.domain; + +import lombok.Builder; +import lombok.Getter; +import org.store.clothstar.category.dto.request.UpdateCategoryRequest; + +@Getter +@Builder +public class Category { + private Long categoryId; + private String categoryType; + + public void updateCategory(UpdateCategoryRequest updateCategoryRequest) { + this.categoryType = updateCategoryRequest.getCategoryType(); + } +} diff --git a/src/main/java/org/store/clothstar/category/dto/request/CreateCategoryRequest.java b/src/main/java/org/store/clothstar/category/dto/request/CreateCategoryRequest.java new file mode 100644 index 0000000..5fc901f --- /dev/null +++ b/src/main/java/org/store/clothstar/category/dto/request/CreateCategoryRequest.java @@ -0,0 +1,27 @@ +package org.store.clothstar.category.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.store.clothstar.category.domain.Category; + +import javax.validation.constraints.NotBlank; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CreateCategoryRequest { + + @Schema(description = "카테고리 타입(이름)", nullable = false) + @NotBlank(message = "카테고리 타입을 입력해주세요.") + private String categoryType; + + public Category toCategory() { + return Category.builder() + .categoryType(categoryType) + .build(); + } +} diff --git a/src/main/java/org/store/clothstar/category/dto/request/UpdateCategoryRequest.java b/src/main/java/org/store/clothstar/category/dto/request/UpdateCategoryRequest.java new file mode 100644 index 0000000..4fb2926 --- /dev/null +++ b/src/main/java/org/store/clothstar/category/dto/request/UpdateCategoryRequest.java @@ -0,0 +1,20 @@ +package org.store.clothstar.category.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class UpdateCategoryRequest { + + @Schema(description = "카테고리 타입(이름)", nullable = false) + @NotBlank(message = "카테고리 타입을 입력해주세요.") + private String categoryType; +} diff --git a/src/main/java/org/store/clothstar/category/dto/response/CategoryDetailResponse.java b/src/main/java/org/store/clothstar/category/dto/response/CategoryDetailResponse.java new file mode 100644 index 0000000..30e3f13 --- /dev/null +++ b/src/main/java/org/store/clothstar/category/dto/response/CategoryDetailResponse.java @@ -0,0 +1,20 @@ +package org.store.clothstar.category.dto.response; + +import lombok.Builder; +import lombok.Getter; +import org.store.clothstar.category.domain.Category; + +@Getter +@Builder +public class CategoryDetailResponse { + private Long categoryId; + private String categoryType; + + public static CategoryDetailResponse from(Category category) { + return CategoryDetailResponse.builder() + .categoryId(category.getCategoryId()) + .categoryType(category.getCategoryType()) + .build(); + } + +} diff --git a/src/main/java/org/store/clothstar/category/dto/response/CategoryResponse.java b/src/main/java/org/store/clothstar/category/dto/response/CategoryResponse.java new file mode 100644 index 0000000..1374d14 --- /dev/null +++ b/src/main/java/org/store/clothstar/category/dto/response/CategoryResponse.java @@ -0,0 +1,19 @@ +package org.store.clothstar.category.dto.response; + +import lombok.Builder; +import lombok.Getter; +import org.store.clothstar.category.domain.Category; + +@Getter +@Builder +public class CategoryResponse { + private Long categoryId; + private String categoryType; + + public static CategoryResponse from(Category category) { + return CategoryResponse.builder() + .categoryId(category.getCategoryId()) + .categoryType(category.getCategoryType()) + .build(); + } +} diff --git a/src/main/java/org/store/clothstar/category/repository/CategoryRepository.java b/src/main/java/org/store/clothstar/category/repository/CategoryRepository.java new file mode 100644 index 0000000..1a1857c --- /dev/null +++ b/src/main/java/org/store/clothstar/category/repository/CategoryRepository.java @@ -0,0 +1,17 @@ +package org.store.clothstar.category.repository; + +import org.apache.ibatis.annotations.Mapper; +import org.store.clothstar.category.domain.Category; + +import java.util.List; + +@Mapper +public interface CategoryRepository { + List selectAllCategory(); + + Category selectCategoryById(Long categoryId); + + int save(Category category); + + int updateCategory(Category category); +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/category/service/CategoryService.java b/src/main/java/org/store/clothstar/category/service/CategoryService.java new file mode 100644 index 0000000..f0119b0 --- /dev/null +++ b/src/main/java/org/store/clothstar/category/service/CategoryService.java @@ -0,0 +1,49 @@ +package org.store.clothstar.category.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.store.clothstar.category.domain.Category; +import org.store.clothstar.category.dto.request.CreateCategoryRequest; +import org.store.clothstar.category.dto.request.UpdateCategoryRequest; +import org.store.clothstar.category.dto.response.CategoryDetailResponse; +import org.store.clothstar.category.dto.response.CategoryResponse; +import org.store.clothstar.category.repository.CategoryRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class CategoryService { + + private final CategoryRepository categoryRepository; + + @Transactional(readOnly = true) + public List getAllCategories() { + return categoryRepository.selectAllCategory().stream() + .map(CategoryResponse::from) + .collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public CategoryDetailResponse getCategory(Long categoryId) { + Category category = categoryRepository.selectCategoryById(categoryId); + return CategoryDetailResponse.from(category); + } + + @Transactional + public Long createCategory(CreateCategoryRequest createCategoryRequest) { + Category category = createCategoryRequest.toCategory(); + categoryRepository.save(category); + return category.getCategoryId(); + } + + @Transactional + public void updateCategory(Long categoryId, UpdateCategoryRequest updateProductRequest) { + Category category = categoryRepository.selectCategoryById(categoryId); + category.updateCategory(updateProductRequest); + + categoryRepository.updateCategory(category); + } +} diff --git a/src/main/java/org/store/clothstar/common/config/SecurityConfiguration.java b/src/main/java/org/store/clothstar/common/config/SecurityConfiguration.java new file mode 100644 index 0000000..056df97 --- /dev/null +++ b/src/main/java/org/store/clothstar/common/config/SecurityConfiguration.java @@ -0,0 +1,70 @@ +package org.store.clothstar.common.config; + +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.security.servlet.PathRequest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.store.clothstar.common.config.jwt.JwtAuthenticationFilter; +import org.store.clothstar.common.config.jwt.JwtUtil; +import org.store.clothstar.common.config.jwt.LoginFilter; + +@Configuration +@RequiredArgsConstructor +public class SecurityConfiguration { + private static final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class); + private final AuthenticationConfiguration authenticationConfiguration; + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final JwtUtil jwtUtil; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager() throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + public WebSecurityCustomizer configure() { + return (web -> web.ignoring() + .requestMatchers(PathRequest.toStaticResources().atCommonLocations())); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.cors().disable() + .csrf().disable() + .httpBasic().disable() + .formLogin().disable(); + + http.authorizeRequests() + .antMatchers("/", "/login", "/v1/login", "/signup").permitAll() + .antMatchers("/user**").authenticated() + .antMatchers("/admin**").hasRole("ADMIN") + .antMatchers("/seller**").hasRole("SELLER") + .anyRequest().permitAll(); + + // JWT 토큰 인증 방식 사용하기에 session 유지 비활성화 + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + //UsernamePasswordAuthenticationFilter 대신에 LoginFilter가 실행된다. + //LoginFilter 이전에 jwtAhthenticationFilter가 실행된다. + http.addFilterBefore(jwtAuthenticationFilter, LoginFilter.class); + http.addFilterAt(new LoginFilter(authenticationManager(), jwtUtil), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/common/config/SwaggerConfig.java b/src/main/java/org/store/clothstar/common/config/SwaggerConfig.java new file mode 100644 index 0000000..43fd860 --- /dev/null +++ b/src/main/java/org/store/clothstar/common/config/SwaggerConfig.java @@ -0,0 +1,24 @@ +package org.store.clothstar.common.config; + +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; + +@Configuration +public class SwaggerConfig { + @Bean + public GroupedOpenApi publicApi() { + return GroupedOpenApi.builder() + .group("v1-clothstar") + .pathsToMatch("/**") + .build(); + } + + @Bean + public OpenAPI springShopOpenAPI() { + return new OpenAPI().info(new Info().title("API").description(" API 명세").version("v0.1")); + } +} diff --git a/src/main/java/org/store/clothstar/common/config/jwt/JwtAuthenticationFilter.java b/src/main/java/org/store/clothstar/common/config/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..0b8f9cf --- /dev/null +++ b/src/main/java/org/store/clothstar/common/config/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,61 @@ +package org.store.clothstar.common.config.jwt; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.store.clothstar.member.domain.CustomUserDetails; +import org.store.clothstar.member.domain.Member; +import org.store.clothstar.member.repository.MemberRepository; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +@RequiredArgsConstructor +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtUtil jwtUtil; + private final MemberRepository memberRepository; + + /** + * 요청이 왔을때 token이 있는지 확인하고 token에 대한 유효성 검사를 진행한다. + */ + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String token = jwtUtil.resolveToken((HttpServletRequest) request); + log.info("doFilterInternal() 실행, token={}", token); + + if (token == null) { + log.info("JWT 토큰정보가 없습니다."); + } else if (!jwtUtil.validateToken(token)) { + log.info("JWT 토큰이 만료되거나 잘못되었습니다."); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } else { + authenticateUserWithToken(token); + } + + filterChain.doFilter(request, response); + } + + private void authenticateUserWithToken(String token) { + Long memberId = jwtUtil.getMemberId(token); + log.info("refresh 토큰 memberId: {}", memberId); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("not found by memberId: " + memberId)); + + CustomUserDetails customUserDetails = new CustomUserDetails(member); + + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + customUserDetails, null, customUserDetails.getAuthorities()); + + SecurityContextHolder.getContext().setAuthentication(authToken); + } +} diff --git a/src/main/java/org/store/clothstar/common/config/jwt/JwtController.java b/src/main/java/org/store/clothstar/common/config/jwt/JwtController.java new file mode 100644 index 0000000..cbaeae4 --- /dev/null +++ b/src/main/java/org/store/clothstar/common/config/jwt/JwtController.java @@ -0,0 +1,59 @@ +package org.store.clothstar.common.config.jwt; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import org.store.clothstar.common.dto.AccessTokenResponse; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Tag(name = "Jwt", description = "Jwt와 관련된 API 입니다.") +@RestController +@RequiredArgsConstructor +@Slf4j +public class JwtController { + private final JwtUtil jwtUtil; + private final JwtService jwtService; + + @Operation(summary = "access 토큰 재발급", description = "refresh 토큰으로 access 토큰을 재발급 한다.") + @PostMapping("/v1/access") + public ResponseEntity reissue(HttpServletRequest request, HttpServletResponse response) { + log.info("access 토큰 refresh 요청"); + String refreshToken = jwtService.getRefreshToken(request); + + if (refreshToken == null) { + log.info("refresh 토큰이 없습니다."); + return new ResponseEntity<>(getAccessTokenResponse(null, "refresh 토큰이 없습니다.", false), + HttpStatus.BAD_REQUEST); + } + + if (!jwtUtil.validateToken(refreshToken)) { + log.info("refresh 토큰이 만료되었거나 유효하지 않습니다."); + return new ResponseEntity<>( + getAccessTokenResponse(null, "refresh 토큰이 만료되었거나 유효하지 않습니다.", false), + HttpStatus.BAD_REQUEST); + } + + String accessToken = jwtService.getAccessTokenByRefreshToken(refreshToken); + response.addHeader("Authorization", "Bearer " + accessToken); + log.info("access 토큰이 갱신 되었습니다."); + + return ResponseEntity.ok(getAccessTokenResponse(accessToken, "access 토큰이 생성 되었습니다.", true)); + } + + private static AccessTokenResponse getAccessTokenResponse(String accessToken, String message, boolean success) { + AccessTokenResponse accessTokenResponse = null; + + return accessTokenResponse.builder() + .accessToken(accessToken) + .message(message) + .success(success) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/common/config/jwt/JwtProperties.java b/src/main/java/org/store/clothstar/common/config/jwt/JwtProperties.java new file mode 100644 index 0000000..f4301c8 --- /dev/null +++ b/src/main/java/org/store/clothstar/common/config/jwt/JwtProperties.java @@ -0,0 +1,17 @@ +package org.store.clothstar.common.config.jwt; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Component +@ConfigurationProperties("jwt") +public class JwtProperties { + private String secretKey; + private Long accessTokenValidTimeMillis; + private Long refreshTokenValidTimeMillis; +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/common/config/jwt/JwtService.java b/src/main/java/org/store/clothstar/common/config/jwt/JwtService.java new file mode 100644 index 0000000..b3a5414 --- /dev/null +++ b/src/main/java/org/store/clothstar/common/config/jwt/JwtService.java @@ -0,0 +1,41 @@ +package org.store.clothstar.common.config.jwt; + +import java.util.Arrays; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.stereotype.Service; +import org.store.clothstar.member.domain.Member; +import org.store.clothstar.member.repository.MemberRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +@RequiredArgsConstructor +public class JwtService { + private final JwtUtil jwtUtil; + private final MemberRepository memberRepository; + + public String getRefreshToken(HttpServletRequest request) { + if (request.getCookies() == null) { + return null; + } + + return Arrays.stream(request.getCookies()) + .filter(cookie -> cookie.getName().equals("refreshToken")) + .findFirst() + .map(Cookie::getValue) + .orElse(null); + } + + public String getAccessTokenByRefreshToken(String refreshToken) { + Long memberId = jwtUtil.getMemberId(refreshToken); + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new IllegalArgumentException("not found by memberId: " + memberId)); + + return jwtUtil.createAccessToken(member); + } +} diff --git a/src/main/java/org/store/clothstar/common/config/jwt/JwtUtil.java b/src/main/java/org/store/clothstar/common/config/jwt/JwtUtil.java new file mode 100644 index 0000000..073c14f --- /dev/null +++ b/src/main/java/org/store/clothstar/common/config/jwt/JwtUtil.java @@ -0,0 +1,103 @@ +package org.store.clothstar.common.config.jwt; + +import io.jsonwebtoken.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.store.clothstar.member.domain.Member; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.servlet.http.HttpServletRequest; +import java.util.Date; + +@Slf4j +@Component +public class JwtUtil { + private final String AUTHORIZATION_HEADER = "Authorization"; + private final String TOKEN_PREFIX = "Bearer "; + private final String ACCESS_TOKEN = "ACCESS_TOKEN"; + private final String REFRESH_TOKEN = "REFRESH_TOKEN"; + private final JwtProperties jwtProperties; + private final SecretKey secretKey; + + public JwtUtil(JwtProperties jwtProperties) { + this.jwtProperties = jwtProperties; + secretKey = new SecretKeySpec(jwtProperties.getSecretKey().getBytes(), + Jwts.SIG.HS256.key().build().getAlgorithm()); + } + + //http 헤더 Authorization의 값이 jwt token인지 확인하고 token값을 넘기는 메서드 + public String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(AUTHORIZATION_HEADER); + + if (bearerToken != null && bearerToken.startsWith(TOKEN_PREFIX)) { + return bearerToken.substring(TOKEN_PREFIX.length()); + } + + return null; + } + + public String createAccessToken(Member member) { + return createToken(member, jwtProperties.getAccessTokenValidTimeMillis(), ACCESS_TOKEN); + } + + public String createRefreshToken(Member member) { + return createToken(member, jwtProperties.getRefreshTokenValidTimeMillis(), REFRESH_TOKEN); + } + + private String createToken(Member member, Long tokenValidTimeMillis, String tokenType) { + Long memberId = member.getMemberId(); + String memberEmail = member.getEmail(); + Date currentDate = new Date(); + Date expireDate = new Date(currentDate.getTime() + tokenValidTimeMillis); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setIssuedAt(currentDate) + .setExpiration(expireDate) + .claim("tokenType", tokenType) + .claim("email", memberEmail) + .claim("id", memberId) + .claim("role", member.getRole()) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + } + + public Claims getClaims(String token) { + return Jwts + .parser() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public Long getMemberId(String token) { + Claims claims = getClaims(token); + return claims.get("id", Long.class); + } + + public String getTokenType(String token) { + Claims claims = getClaims(token); + return claims.get("tokenType", String.class); + } + + public boolean validateToken(String token) { + try { + Jwts.parser() + .verifyWith(secretKey) + .build() + .parseClaimsJws(token); + return true; + } catch (MalformedJwtException ex) { + log.error("Invalid JWT token"); + } catch (ExpiredJwtException ex) { + log.error("Expired JWT token"); + } catch (UnsupportedJwtException ex) { + log.error("Unsupported JWT token"); + } catch (IllegalArgumentException ex) { + log.error("JWT claims string is empty."); + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/common/config/jwt/LoginFilter.java b/src/main/java/org/store/clothstar/common/config/jwt/LoginFilter.java new file mode 100644 index 0000000..a6ade03 --- /dev/null +++ b/src/main/java/org/store/clothstar/common/config/jwt/LoginFilter.java @@ -0,0 +1,100 @@ +package org.store.clothstar.common.config.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; +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 org.store.clothstar.member.domain.CustomUserDetails; +import org.store.clothstar.member.domain.Member; +import org.store.clothstar.member.dto.request.MemberLoginRequest; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Slf4j +public class LoginFilter extends UsernamePasswordAuthenticationFilter { + private final AuthenticationManager authenticationManager; + private final JwtUtil jwtUtil; + + public LoginFilter(AuthenticationManager authenticationManager, JwtUtil jwtUtil) { + this.authenticationManager = authenticationManager; + this.jwtUtil = jwtUtil; + setFilterProcessesUrl("/v1/login"); + } + + /** + * 로그인 창에서 입력한 id, password를 받아서 + * Authentication Manager에 던져 줘야 하는데 그 DTO역할을 하는 객체가 UsernamePasswordAuthenticationToken이다. + * Authentication Manager에 전달하면 최종적으로 Authentication에 전달 된다. + * return 하면 Authentication Manager에 던져진다. + *

+ * AuthenticationManager에 던지기 위해서 주입을 받아야 한다. + */ + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException { + + log.info("attemptAuthentication() 실행"); + ObjectMapper om = new ObjectMapper(); + MemberLoginRequest memberLoginRequest; + String email; + String password; + + try { + memberLoginRequest = om.readValue(request.getInputStream(), MemberLoginRequest.class); + log.info("login parameter memberLoginRequest: {}", memberLoginRequest.toString()); + + email = memberLoginRequest.getEmail(); + password = memberLoginRequest.getPassword(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + UsernamePasswordAuthenticationToken authTokenDTO + = new UsernamePasswordAuthenticationToken(email, password, null); + + return authenticationManager.authenticate(authTokenDTO); + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, + Authentication authentication) throws IOException, ServletException { + log.info("로그인 성공"); + CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal(); + Member member = customUserDetails.getMember(); + log.info("member: {}", member.toString()); + + String accessToken = jwtUtil.createAccessToken(member); + log.info("생성 accessToken: Bearer {}", accessToken); + String refreshToken = jwtUtil.createRefreshToken(member); + log.info("생성 refreshToken: Bearer {}", refreshToken); + + response.addHeader("Authorization", "Bearer " + accessToken); + response.addCookie(createCookie("refreshToken", refreshToken)); + response.setStatus(HttpStatus.OK.value()); + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, + AuthenticationException failed) throws IOException, ServletException { + + log.info("로그인 실패"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + public Cookie createCookie(String key, String value) { + Cookie cookie = new Cookie(key, value); + cookie.setMaxAge(60 * 30); //refresh token하고 같은 생명주기 30분으로 세팅 + cookie.setHttpOnly(true); //자바스크립트로 쿠키 접근 못하게 막음 + + return cookie; + } +} diff --git a/src/main/java/org/store/clothstar/common/dto/AccessTokenResponse.java b/src/main/java/org/store/clothstar/common/dto/AccessTokenResponse.java new file mode 100644 index 0000000..41dca79 --- /dev/null +++ b/src/main/java/org/store/clothstar/common/dto/AccessTokenResponse.java @@ -0,0 +1,12 @@ +package org.store.clothstar.common.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class AccessTokenResponse { + String accessToken; + String message; + Boolean success; +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/common/dto/MessageDTO.java b/src/main/java/org/store/clothstar/common/dto/MessageDTO.java new file mode 100644 index 0000000..327eb98 --- /dev/null +++ b/src/main/java/org/store/clothstar/common/dto/MessageDTO.java @@ -0,0 +1,24 @@ +package org.store.clothstar.common.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@RequiredArgsConstructor +public class MessageDTO { + private Long id; + private int status; + private String message; + private String redirectURI; + private boolean success = true; + + public MessageDTO(int status, String message, String redirectURI) { + this.status = status; + this.message = message; + this.redirectURI = redirectURI; + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/common/exception/ErrorResponse.java b/src/main/java/org/store/clothstar/common/exception/ErrorResponse.java new file mode 100644 index 0000000..3f7cb77 --- /dev/null +++ b/src/main/java/org/store/clothstar/common/exception/ErrorResponse.java @@ -0,0 +1,14 @@ +package org.store.clothstar.common.exception; + +import lombok.Getter; + +@Getter +public class ErrorResponse { + private boolean success; + private String message; + + public ErrorResponse(String message) { + this.success = false; + this.message = message; + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/common/exception/GlobalExceptionHandler.java b/src/main/java/org/store/clothstar/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..0ff321d --- /dev/null +++ b/src/main/java/org/store/clothstar/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,29 @@ +package org.store.clothstar.common.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler + private ErrorResponse illegalArgumentHandler(IllegalArgumentException e) { + log.error("[IllegalArgument Handler] {}", e.getMessage()); + ErrorResponse errorResponse = new ErrorResponse(e.getMessage()); + return errorResponse; + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(Exception.class) + private ErrorResponse exHandler(Exception e) { + log.error("[Exception Handler] {}", e.getMessage()); + ErrorResponse errorResponse = new ErrorResponse(e.getMessage()); + return errorResponse; + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/common/util/MessageDTOBuilder.java b/src/main/java/org/store/clothstar/common/util/MessageDTOBuilder.java new file mode 100644 index 0000000..4620af9 --- /dev/null +++ b/src/main/java/org/store/clothstar/common/util/MessageDTOBuilder.java @@ -0,0 +1,29 @@ +package org.store.clothstar.common.util; + +import org.store.clothstar.common.dto.MessageDTO; + +public class MessageDTOBuilder { + + public static MessageDTO buildMessage(Long id, int status, String message) { + return MessageDTO.builder() + .id(id) + .status(status) + .message(message) + .build(); + } + + public static MessageDTO buildMessage(int status, String message) { + return MessageDTO.builder() + .status(status) + .message(message) + .build(); + } + + public static MessageDTO buildMessage(int status, String message, boolean success) { + return MessageDTO.builder() + .status(status) + .message(message) + .success(success) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/common/util/URIBuilder.java b/src/main/java/org/store/clothstar/common/util/URIBuilder.java new file mode 100644 index 0000000..581cc93 --- /dev/null +++ b/src/main/java/org/store/clothstar/common/util/URIBuilder.java @@ -0,0 +1,16 @@ +package org.store.clothstar.common.util; + +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; + +public class URIBuilder { + + public static URI buildURI(Long id) { + return ServletUriComponentsBuilder + .fromCurrentRequest() // 현재 요청의 URI를 사용 + .path("/{id}") // 경로 변수 추가 + .buildAndExpand(id) // {/id} 자리에 실제 id 값을 삽입 + .toUri(); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/member/controller/AddressController.java b/src/main/java/org/store/clothstar/member/controller/AddressController.java new file mode 100644 index 0000000..afa0cc9 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/controller/AddressController.java @@ -0,0 +1,39 @@ +package org.store.clothstar.member.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.store.clothstar.common.dto.MessageDTO; +import org.store.clothstar.member.dto.request.CreateAddressRequest; +import org.store.clothstar.member.dto.response.AddressResponse; +import org.store.clothstar.member.service.AddressService; + +import java.util.List; + +@Tag(name = "Address", description = "회원 배송지 주소 관련 API 입니다.") +@RestController +@RequiredArgsConstructor +@Slf4j +public class AddressController { + private final AddressService addressService; + + @Operation(summary = "상품 옵션 상세 조회", description = "회원 한 명에 대한 배송지를 전부 가져온다.") + @GetMapping("/v1/members/{id}/address") + public ResponseEntity> getMemberAllAddress(@PathVariable("id") Long memberId) { + List memberList = addressService.getMemberAllAddress(memberId); + return ResponseEntity.ok(memberList); + } + + @Operation(summary = "회원 배송지 저장", description = "회원 한 명에 대한 배송지를 저장한다.") + @PostMapping("/v1/members/{id}/address") + public ResponseEntity addrSave(@Validated @RequestBody CreateAddressRequest createAddressRequest, + @PathVariable("id") Long memberId) { + log.info("회원 배송지 저장 요청 데이터 : {}", createAddressRequest.toString()); + return new ResponseEntity<>(addressService.addrSave(memberId, createAddressRequest), HttpStatus.CREATED); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/member/controller/MemberController.java b/src/main/java/org/store/clothstar/member/controller/MemberController.java new file mode 100644 index 0000000..40a3e30 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/controller/MemberController.java @@ -0,0 +1,60 @@ +package org.store.clothstar.member.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.store.clothstar.common.dto.MessageDTO; +import org.store.clothstar.member.dto.request.CreateMemberRequest; +import org.store.clothstar.member.dto.request.ModifyMemberRequest; +import org.store.clothstar.member.dto.response.MemberResponse; +import org.store.clothstar.member.service.MemberService; + +import java.util.List; + +@Tag(name = "Member", description = "회원 정보 관리에 대한 API 입니다.") +@RestController +@RequiredArgsConstructor +@Slf4j +public class MemberController { + private final MemberService memberService; + + @Operation(summary = "전체 회원 조회", description = "전체 회원 리스트를 가져온다.") + @GetMapping("/v1/members") + public ResponseEntity> getAllMember() { + List memberList = memberService.getAllMember(); + return ResponseEntity.ok(memberList); + } + + @Operation(summary = "회원 상세정보 조회", description = "회원 한 명에 대한 상세 정보를 가져온다.") + @GetMapping("/v1/members/{id}") + public ResponseEntity getMember(@PathVariable("id") Long memberId) { + MemberResponse member = memberService.getMemberById(memberId); + return ResponseEntity.ok(member); + } + + @Operation(summary = "이메일 중복 체크", description = "이메일 중복체크를 한다.") + @GetMapping("/v1/members/email/{email}") + public ResponseEntity emailCheck(@PathVariable String email) { + return ResponseEntity.ok(memberService.emailCheck(email)); + } + + @Operation(summary = "회원 상세정보 수정", description = "회원 정보를 수정한다.") + @PutMapping("/v1/members/{id}") + public ResponseEntity putModifyMember(@PathVariable("id") Long memberId, + @RequestBody ModifyMemberRequest modifyMemberRequest) { + log.info("회원수정 요청 데이터 : memberId={}, {}", memberId, modifyMemberRequest.toString()); + return ResponseEntity.ok(memberService.modifyMember(memberId, modifyMemberRequest)); + } + + @Operation(summary = "회원가입", description = "회원가입시 회원 정보를 저장한다.") + @PostMapping("/v1/members") + public ResponseEntity signup(@Validated @RequestBody CreateMemberRequest createMemberDTO) { + log.info("회원가입 요청 데이터 : {}", createMemberDTO.toString()); + return new ResponseEntity<>(memberService.signup(createMemberDTO), HttpStatus.CREATED); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/member/controller/MemberViewController.java b/src/main/java/org/store/clothstar/member/controller/MemberViewController.java new file mode 100644 index 0000000..0dd6a38 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/controller/MemberViewController.java @@ -0,0 +1,55 @@ +package org.store.clothstar.member.controller; + +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.store.clothstar.member.domain.Member; + +@Tag(name = "index", description = "회원가입, 로그인, 로그아웃 기능과 user, seller, admin 페이지로 이동하기 위한 API 입니다.") +@Controller +public class MemberViewController { + @GetMapping("/signup") + public String signup() { + return "signup"; + } + + @GetMapping("/login") + public String login() { + return "login"; + } + + @GetMapping("/user") + @ResponseBody + public Member userPage() { + return (Member) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + } + + @GetMapping("/userPage") + public String viewUserPage() { + return "/userPage"; + } + + @GetMapping("/seller") + @ResponseBody + public Member sellerPage() { + return (Member) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + } + + @GetMapping("/sellerPage") + public String viewSellerPage() { + return "/sellerPage"; + } + + @GetMapping("/admin") + @ResponseBody + public Member adminPage() { + return (Member) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + } + + @GetMapping("/adminPage") + public String viewAdminPage() { + return "/adminPage"; + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/member/controller/SellerController.java b/src/main/java/org/store/clothstar/member/controller/SellerController.java new file mode 100644 index 0000000..7c28905 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/controller/SellerController.java @@ -0,0 +1,37 @@ +package org.store.clothstar.member.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.store.clothstar.common.dto.MessageDTO; +import org.store.clothstar.member.dto.request.CreateSellerRequest; +import org.store.clothstar.member.dto.response.SellerResponse; +import org.store.clothstar.member.service.SellerService; + +@Tag(name = "Seller", description = "판매자 정보 관리에 대한 API 입니다.") +@RestController +@RequiredArgsConstructor +@Slf4j +public class SellerController { + private final SellerService sellerService; + + @Operation(summary = "판매자 상세정보 조회", description = "판매자 한 명에 대한 상세정보를 가져온다.") + @GetMapping("/v1/sellers/{id}") + public ResponseEntity getSeller(@PathVariable("id") Long memberId) { + SellerResponse seller = sellerService.getSellerById(memberId); + return ResponseEntity.ok(seller); + } + + @Operation(summary = "판매자 가입", description = "판매자 정보를 저장된다.") + @PostMapping("/v1/sellers/{id}") + public ResponseEntity saveSeller(@Validated @RequestBody CreateSellerRequest createSellerRequest, + @PathVariable("id") Long memberId) { + log.info("판매자 가입 요청 데이터 : {}", createSellerRequest.toString()); + return new ResponseEntity<>(sellerService.sellerSave(memberId, createSellerRequest), HttpStatus.CREATED); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/member/domain/Address.java b/src/main/java/org/store/clothstar/member/domain/Address.java new file mode 100644 index 0000000..7e8c8ef --- /dev/null +++ b/src/main/java/org/store/clothstar/member/domain/Address.java @@ -0,0 +1,18 @@ +package org.store.clothstar.member.domain; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class Address { + private Long addressId; + private Long memberId; + private String receiverName; + private String zipNo; + private String addressBasic; + private String addressDetail; + private String telNo; + private String deliveryRequest; + private boolean defaultAddress; +} diff --git a/src/main/java/org/store/clothstar/member/domain/CustomUserDetails.java b/src/main/java/org/store/clothstar/member/domain/CustomUserDetails.java new file mode 100644 index 0000000..677e03f --- /dev/null +++ b/src/main/java/org/store/clothstar/member/domain/CustomUserDetails.java @@ -0,0 +1,57 @@ +package org.store.clothstar.member.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Getter +@ToString +@RequiredArgsConstructor +public class CustomUserDetails implements UserDetails { + private final Member member; + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority("ROLE_" + String.valueOf(member.getRole()))); + } + + @Override + public String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + //true -> 계정 만료되지 않았음 + return true; + } + + @Override + public boolean isAccountNonLocked() { + //true -> 계정 잠금되지 않음 + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + //true -> 패스워드 만료 되지 않음 + return true; + } + + @Override + public boolean isEnabled() { + //ture -> 계정 사용 가능 + return true; + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/member/domain/Member.java b/src/main/java/org/store/clothstar/member/domain/Member.java new file mode 100644 index 0000000..fd40725 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/domain/Member.java @@ -0,0 +1,25 @@ +package org.store.clothstar.member.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Getter +@Builder +@ToString +public class Member { + private Long memberId; + private String email; + private String password; + private String name; + private String telNo; + private int totalPaymentPrice; + private int point; + private MemberRole role; + private MemberGrade grade; + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; + private LocalDateTime deletedAt; +} diff --git a/src/main/java/org/store/clothstar/member/domain/MemberGrade.java b/src/main/java/org/store/clothstar/member/domain/MemberGrade.java new file mode 100644 index 0000000..1ea85c8 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/domain/MemberGrade.java @@ -0,0 +1,5 @@ +package org.store.clothstar.member.domain; + +public enum MemberGrade { + BRONZE, SILVER, GOLD, PLATINUM, DIAMOND +} diff --git a/src/main/java/org/store/clothstar/member/domain/MemberRole.java b/src/main/java/org/store/clothstar/member/domain/MemberRole.java new file mode 100644 index 0000000..57858d7 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/domain/MemberRole.java @@ -0,0 +1,5 @@ +package org.store.clothstar.member.domain; + +public enum MemberRole { + ADMIN, SELLER, USER +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/member/domain/Seller.java b/src/main/java/org/store/clothstar/member/domain/Seller.java new file mode 100644 index 0000000..9b45a0e --- /dev/null +++ b/src/main/java/org/store/clothstar/member/domain/Seller.java @@ -0,0 +1,16 @@ +package org.store.clothstar.member.domain; + +import java.time.LocalDateTime; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class Seller { + private Long memberId; + private String brandName; + private String bizNo; + private int totalSellPrice; + private LocalDateTime createdAt; +} diff --git a/src/main/java/org/store/clothstar/member/dto/request/CreateAddressRequest.java b/src/main/java/org/store/clothstar/member/dto/request/CreateAddressRequest.java new file mode 100644 index 0000000..0825e46 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/dto/request/CreateAddressRequest.java @@ -0,0 +1,43 @@ +package org.store.clothstar.member.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.store.clothstar.member.domain.Address; + +import javax.validation.constraints.NotBlank; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class CreateAddressRequest { + @NotBlank(message = "받는 사람 이름은 비어 있을 수 없습니다.") + private String receiverName; + + @NotBlank(message = "우편번호는 비어 있을 수 없습니다.") + private String zipNo; + + @NotBlank(message = "기본 주소는 비어 있을 수 없습니다.") + private String addressBasic; + private String addressDetail; + + @NotBlank(message = "전화번호는 비어 있울 수 없습니다.") + private String telNo; + private String deliveryRequest; + private boolean defaultAddress; + + public Address toAddress(Long memberId) { + return Address.builder() + .memberId(memberId) + .receiverName(receiverName) + .zipNo(zipNo) + .addressBasic(addressBasic) + .addressDetail(addressDetail) + .telNo(telNo) + .deliveryRequest(deliveryRequest) + .defaultAddress(defaultAddress) + .build(); + } +} diff --git a/src/main/java/org/store/clothstar/member/dto/request/CreateMemberRequest.java b/src/main/java/org/store/clothstar/member/dto/request/CreateMemberRequest.java new file mode 100644 index 0000000..87665fd --- /dev/null +++ b/src/main/java/org/store/clothstar/member/dto/request/CreateMemberRequest.java @@ -0,0 +1,47 @@ +package org.store.clothstar.member.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.store.clothstar.member.domain.Member; +import org.store.clothstar.member.domain.MemberGrade; +import org.store.clothstar.member.domain.MemberRole; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class CreateMemberRequest { + @Email(message = "유효하지 않은 이메일 형식입니다.") + private String email; + + @Size(min = 8, message = "비밀번호는 최소 8자 이상이어야 합니다.") + private String password; + + @NotBlank(message = "이름은 비어 있을 수 없습니다.") + private String name; + + @Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "유효하지 않은 전화번호 형식입니다.") + private String telNo; + + public Member toMember(String encryptedPassword) { + return Member.builder() + .email(email) + .password(encryptedPassword) + .name(name) + .telNo(telNo) + .totalPaymentPrice(0) + .point(0) + .role(MemberRole.USER) + .grade(MemberGrade.BRONZE) + .createdAt(LocalDateTime.now()) + .build(); + } +} diff --git a/src/main/java/org/store/clothstar/member/dto/request/CreateSellerRequest.java b/src/main/java/org/store/clothstar/member/dto/request/CreateSellerRequest.java new file mode 100644 index 0000000..3b75abd --- /dev/null +++ b/src/main/java/org/store/clothstar/member/dto/request/CreateSellerRequest.java @@ -0,0 +1,33 @@ +package org.store.clothstar.member.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.store.clothstar.member.domain.Seller; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class CreateSellerRequest { + @NotBlank(message = "브랜드 이름은 필수 입력값 입니다.") + private String brandName; + + @Pattern(regexp = "([0-9]{3})-?([0-9]{2})-?([0-9]{5})", message = "유효하지 않은 사업자 번호 형식입니다.") + private String bizNo; + + public Seller toSeller(Long memberId) { + return Seller.builder() + .memberId(memberId) + .brandName(brandName) + .bizNo(bizNo) + .totalSellPrice(0) + .createdAt(LocalDateTime.now()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/member/dto/request/MemberLoginRequest.java b/src/main/java/org/store/clothstar/member/dto/request/MemberLoginRequest.java new file mode 100644 index 0000000..d1369c1 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/dto/request/MemberLoginRequest.java @@ -0,0 +1,20 @@ +package org.store.clothstar.member.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import javax.validation.constraints.NotBlank; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class MemberLoginRequest { + @NotBlank(message = "이메일은 필수 입력값 입니다.") + String email; + + @NotBlank(message = "비밀번호는 필수 입력값 입니다.") + String password; +} diff --git a/src/main/java/org/store/clothstar/member/dto/request/ModifyMemberRequest.java b/src/main/java/org/store/clothstar/member/dto/request/ModifyMemberRequest.java new file mode 100644 index 0000000..dd58698 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/dto/request/ModifyMemberRequest.java @@ -0,0 +1,26 @@ +package org.store.clothstar.member.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.store.clothstar.member.domain.Member; +import org.store.clothstar.member.domain.MemberRole; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class ModifyMemberRequest { + private MemberRole role; + + public Member toMember(Long memberId) { + return Member.builder() + .memberId(memberId) + .role(getRole()) + .modifiedAt(LocalDateTime.now()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/member/dto/response/AddressResponse.java b/src/main/java/org/store/clothstar/member/dto/response/AddressResponse.java new file mode 100644 index 0000000..aa1e21c --- /dev/null +++ b/src/main/java/org/store/clothstar/member/dto/response/AddressResponse.java @@ -0,0 +1,28 @@ +package org.store.clothstar.member.dto.response; + +import org.store.clothstar.member.domain.Address; + +import lombok.Getter; + +@Getter +public class AddressResponse { + private Long memberId; + private String receiverName; + private String zipNo; + private String addressBasic; + private String addressDetail; + private String telNo; + private String deliveryRequest; + private boolean defaultAddress; + + public AddressResponse(Address address) { + this.memberId = address.getMemberId(); + this.receiverName = address.getReceiverName(); + this.zipNo = address.getZipNo(); + this.addressBasic = address.getAddressBasic(); + this.addressDetail = address.getAddressDetail(); + this.telNo = address.getTelNo(); + this.deliveryRequest = address.getDeliveryRequest(); + this.defaultAddress = address.isDefaultAddress(); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/member/dto/response/MemberResponse.java b/src/main/java/org/store/clothstar/member/dto/response/MemberResponse.java new file mode 100644 index 0000000..dcf9715 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/dto/response/MemberResponse.java @@ -0,0 +1,25 @@ +package org.store.clothstar.member.dto.response; + +import org.store.clothstar.member.domain.Member; +import org.store.clothstar.member.domain.MemberGrade; + +import lombok.Getter; + +@Getter +public class MemberResponse { + private Long memberId; + private String email; + private String name; + private String telNo; + private int totalPaymentPrice; + private MemberGrade grade; + + public MemberResponse(Member member) { + this.memberId = member.getMemberId(); + this.email = member.getEmail(); + this.name = member.getName(); + this.telNo = member.getTelNo(); + this.totalPaymentPrice = member.getTotalPaymentPrice(); + this.grade = member.getGrade(); + } +} diff --git a/src/main/java/org/store/clothstar/member/dto/response/SellerResponse.java b/src/main/java/org/store/clothstar/member/dto/response/SellerResponse.java new file mode 100644 index 0000000..82eb53e --- /dev/null +++ b/src/main/java/org/store/clothstar/member/dto/response/SellerResponse.java @@ -0,0 +1,24 @@ +package org.store.clothstar.member.dto.response; + +import java.time.LocalDateTime; + +import org.store.clothstar.member.domain.Seller; + +import lombok.Getter; + +@Getter +public class SellerResponse { + private Long memberId; + private String brandName; + private String bizNo; + private int totalPaymentPrice; + private LocalDateTime createdAt; + + public SellerResponse(Seller seller) { + this.memberId = seller.getMemberId(); + this.brandName = seller.getBrandName(); + this.bizNo = seller.getBizNo(); + this.totalPaymentPrice = seller.getTotalSellPrice(); + this.createdAt = seller.getCreatedAt(); + } +} diff --git a/src/main/java/org/store/clothstar/member/memberREADME.md b/src/main/java/org/store/clothstar/member/memberREADME.md new file mode 100644 index 0000000..77f9247 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/memberREADME.md @@ -0,0 +1,182 @@ +# member 패키지 README + +## 패키지 개요 + +이 패키지는 회원에 대한 기능을 구현하기 위한 패키지입니다. 회원 도메인에 대한 시큐리티 적용을 달성하기 위한 규칙과 기획을 포함합니다. + +### 회원가입 설계안 + +1. 이메일, 비밀번호, 이름, 나이, 전화번호를 입력 + 1-1. 이메일 중복체크 + 1-2. 비밀번호 정규식으로 규칙 확인(규칙 : 영문자(대,소문자), 숫자, 특수문자를 포함하여 최소 8자 이상 20자 이하) +2. 유저 이메일로 링크 전송 + 2-1.전송된 이메일의 링크안에 JWT 임시 아이디가 전송됨 +3. 링크를 눌렀을때 기입한 이메일 인증 완료 + - JWT의 임시 아이디와 동일한지 확인 +4. 회원가입 완료 +5. 가입 후 회원등급, 포인트 부여 + - 신규회원의 등급은 [ 브론즈 ]로 자동 할당됨 + - 회원등급은 구매금액에 따라 조정될 수 있음 +6. DB에 저장되는 비밀번호는 암호화 + - 암호화는 현재 표준에 부합해야함(bcrypt, Argon2) +7. 생성일자는 현재일자 시간으로 자동 DB에 저장 + +### 로그인 설계안 + +1. 아이디, 비밀번호 체크후 로그인 +2. JWT 토큰의 사용자 아이디, 비밀번호 인증절차를 진행 후 로그인 + - 아이디가 존재하지 않을 경우, ‘존재하지 않는 아이디입니다’ 알림 + - 비밀번호가 맞지 않을 경우, ‘잘못된 비밀번호입니다’ 알림 + - access 2분, refresh 20분 발급 + - 일반 회원의 경우 관리자 페이지, 판매자 페이지 못들어가도록 access 관리 + - 판매자 페이지는 관리자 페이지 못들어가도록 access 관리 + - refresh 토큰 발급 필요시 다시 로그인 필요 + +### 비밀번호 찾기 설계안 + +1. 가입한 이메일과 + 1-1. 이메일이 가입한 유저가 맞는지 유효성 체크 +2. 이메일로 비밀번호 변경 링크 전송 + 2-1. JWT 토큰으로 사용자 이메일로 링크를 타고 왔는지 검증 +3. 비밀번호 변경 URL로 이동 +4. 비밀번호 변경 URL에서 새로운 비밀번호, 새 비밀번호 확인 입력 + - 입력한 2개 비밀번호 동일한지 확인 +5. 비밀번호 변경 완료 + +### 회원정보 수정페이지 이동 설계 + +1. 비밀번호 입력 (회원 정보 수정 시 비밀번호를 재확인해야 한다.) + - 비밀번호 유효성 체크 +2. 회원정보 수정 페이지 이동 + +### 비밀번호 수정 설계안 + +1. [ 현재 비밀번호, 새 비밀번호, 새 비밀번호 확인 ] 입력 + 1-1. 현재 비밀번호 맞는지 확인 + 1-3. [ 새 비밀번호 / 새 비밀번호 확인 ]이 동일한지 확인 + 1-4. 새 비밀번호가 기존 비밀번호가 다른지 확인 + 1.5. 비밀번호 정규식으로 규칙 확인(규칙 : 영문자(대,소문자), 숫자, 특수문자를 포함하여 최소 8자 이상 20자 이하) + 1-6. 만약 기존의 비밀번호와 같다면 ‘기존의 비밀번호와 동일합니다’ 알림 + +### 이메일 수정 설계안 + +1. 수정할 이메일 입력 + 1-1 입력한 이메일이 기존과 다른 이메일인지 확인 +2. 인증메일 전송 버튼 누르면 수정한 이메일로 인증메일 전송, 인증메일 재전송 버튼과 이메일 변경 버튼 생성 +3. 인증메일로 전송된 링크 누르면 이메일 인증 완료 +4. 이메일 변경 버튼 클릭시 이메일 수정 완료 + +### 회원 배송지 설계안 + +1. 받는사람, 우편번호, 연락처 입력 + 1-1. 우편번호 API 호출 + 1-2. 받는사람, 배송지, 연락처 필수 항목 체크 + 1-3. 기본 배송지는 한 회원마다 한개만 가능 +2. 배송 요청사항 입력 + [목록] + - 문 앞 + - 직접 받고 부재 시 문 앞 + - 경비실 + - 택배함 + - 기타사항 +3. 공동현관 출입번호 입력 + - 비밀번호 없이 출입 가능 선택하면 입력 안해도 됨 + +### 판매자 설계안 + +1. 브랜드 대표 카테고리, 브랜드명, 공식 홈페이지, 회사명, 사업자 번호, 사업장 주소, 담당자명, 휴대전화번호, 이메일을 입력 + 1-1. 사업자 번호는 중복 돼서는 안된다. +2. 입점 관리자가 입점을 승인하면 로그인이 가능하다. + 2-1. 사업자로 로그인 후 판매/상품/배송 관리를 할 수 있다. + +### API 디자인 + +- 회원 가입 : POST /v1/members + - 프로세스 + 1. 이메일, 비밀번호, 이름, 생년월일, 전화번호 데이터 받음 + 2. 이에밀 중복체크, 비밀번호 정규식 규칙 확인을 진행 + - 비밀번호 정규식 규칙 : 영문자(대,소문자), 숫자, 특수문자를 포함하여 최소 8자 이상 20자 이하 + 3. 기입한 이메일로 인증용 링크 전송 + 4. 임시 아이디를 주고 전송한 JWT 토큰의 아이디 값이랑 값이 맞으면 회원 가입 완료 + +- 회원 이메일 중복 체크 : GET /v1/members/email/{email} + - 설명 : 회원 가입전 입력한 이메일이 중복 됐는지 체크 하는 api + - 프로세스 + 1. 성공하면 success : true + 2. 중복 됐으면 success : false, message : 중복된 이메일 입니다. + +- 회원 가입 이메일 링크 확인 : POST /v1/members/signup/{id}/email + - 설명 : 이메일로 전송된 링크를 눌렀는지 확인 + - 프로세스 + 1. 링크에 있는 JWT 토큰의 임시 아이디로 검증 + 2. 회원등급 브론즈 등급 부여 + 3. 포인트 0 초기화 + 4. 비밀번호 암호화 해서 DB 인입 + 5. 생성시간 현재일 시간으로 자동 DB 인입 + +- 회원 전체 조회 : GET /v1/members + - 설명 : 회원 전체 리스트를 조회 한다. + +- 회원 조회 : GET /v1/members/{id} + - 설명 : 멤버 아이디, 이메일, 이름, 전화번호, 총구매금액, 등급을 조회 한다. + +- 회원 로그인 : POST /v1/members/login + - 프로세스 + 1. 입력한 아이디와 비밀번호 검증 + 2. JWT 토큰의 사용자 아이디, 비밀번호 인증절차를 진행 후 로그인 + - 아이디가 유효하지 않을땐 ‘존재하지 않는 아이디입니다’ error 메시지 응답 + - 비밀번호가 맞지 않을 경우, ‘잘못된 비밀번호입니다’ error 메시지 응답 + - access 2분, refresh 20분 발급 + - 일반 회원의 경우 관리자 페이지, 판매자 페이지 못들어가도록 access 관리 + - 판매자 페이지는 관리자 페이지 못들어가도록 access 관리 + - refresh 토큰 발급 필요시 다시 로그인 필요 + +- 회원 비밀번호 찾기 : POST /v1/members/{id}/email + - 프로세스 + 1. 입력한 이메일, 아이디 검증 + 2. 이메일로 비밀번호 변경 URL 전송 + +- 회원 유효성 확인 : POST /v1/members/{id}/{key} + - 설명 : 회원의 비밀번호등 여러 필드들에 대한 유효성 검증을 위한 API + - 비밀번호 유효성 프로세스 + 1. 입력한 비밀번호가 맞는지 확인 + - 비밀번호가 맞지 않을 경우, ‘잘못된 비밀번호입니다’ error 메시지 응답 + +- 회원 정보 수정 : PATCH /v1/members/{id}/{key} + - 설명 : Path 파라미터, {key}를 통해서 어떤 필드를 수정할 것인지 구별이 가능하다. + - 이메일 수정 프로세스 + 1. key값이 email 인지 확인 + 2. 인증메일 API로 전송된 링크 클릭 했는지 확인 + 2. 링크 클릭했으면 이메일 수정 + - 비밀번호 변경 프로세스 + 1. key값이 password 인지 확인 + 2. 입력한 비밀번호 정규식 규칙 확인 + 2. 정규식 규칙에 맞으면 비밀번호 수정 + - 회원 권한 수정 + 1. key값이 role 인지 확인 + +- 회원 배송지 입력 : POST /v1/members/{id}/address + - 설명 : Path 파라미터, 멤버 아이디 파라미터를 받아서 회원에 대한 배송지를 저장 + - 프로세스 + 1. 받는사람, 우편번호, 기본주소, 상세주소, 전화번호, 배송요청, 기본 배송지 유무 데이터를 + 2. 기본 배송지는 한 회원마다 한개만 가능 + 3. DB에 회원에 대한 주소를 저장 + +- 회원 배송지 조회 : GET /v1/members/{id}/address + - 설명 : 회원 한명에 대한 전체 주소를 가져온다. + - 프로세스 + 1. 멤버 아이디, 받는자 이름, 우편번호, 기본주소, 상세주소, 전화번호, 배송요청, 기본 배송지 유무 조회 + +- 회원 기본 배송지 조회 : GET /v1/members/{id}/address/default + - 설명 : 회원 한명에 대한 기본 배송지를 가져온다. + +- 판매자 정보 입력 : POST /v1/sellers/{id} + - 설명 : 멤버 아이디에 대한 판매자 정보를 기입 한다. + - 프로세스 + 1. 브랜드명, 사업자명 받는다. + 2. 전체 판매금액 0원 초기화 한다. + 3. 생성시간 현재일 시간으로 자동 DB 인입 + +- 판매자 정보 조회 : GET /v1/sellers/{id} + - 설명 : 멤버아이디로 판매자 정보 조회(멤버 아이디, 브랜드명, 사업자명, 총 판매금액, 생성시간) + diff --git a/src/main/java/org/store/clothstar/member/repository/AddressRepository.java b/src/main/java/org/store/clothstar/member/repository/AddressRepository.java new file mode 100644 index 0000000..af83945 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/repository/AddressRepository.java @@ -0,0 +1,16 @@ +package org.store.clothstar.member.repository; + +import org.apache.ibatis.annotations.Mapper; +import org.store.clothstar.member.domain.Address; + +import java.util.List; +import java.util.Optional; + +@Mapper +public interface AddressRepository { + List

findMemberAllAddress(Long memberId); + + int save(Address address); + + Optional
findById(Long addressId); +} diff --git a/src/main/java/org/store/clothstar/member/repository/MemberRepository.java b/src/main/java/org/store/clothstar/member/repository/MemberRepository.java new file mode 100644 index 0000000..0b99b29 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/repository/MemberRepository.java @@ -0,0 +1,21 @@ +package org.store.clothstar.member.repository; + +import java.util.List; +import java.util.Optional; + +import org.apache.ibatis.annotations.Mapper; +import org.store.clothstar.member.domain.Member; + +@Mapper +public interface MemberRepository { + + List findAll(); + + Optional findById(Long memberId); + + Optional findByEmail(String email); + + int update(Member member); + + int save(Member member); +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/member/repository/SellerRepository.java b/src/main/java/org/store/clothstar/member/repository/SellerRepository.java new file mode 100644 index 0000000..b63bd46 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/repository/SellerRepository.java @@ -0,0 +1,13 @@ +package org.store.clothstar.member.repository; + +import org.apache.ibatis.annotations.Mapper; +import org.store.clothstar.member.domain.Seller; + +import java.util.Optional; + +@Mapper +public interface SellerRepository { + public int save(Seller seller); + + public Optional findById(Long memberId); +} diff --git a/src/main/java/org/store/clothstar/member/service/AddressService.java b/src/main/java/org/store/clothstar/member/service/AddressService.java new file mode 100644 index 0000000..6666248 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/service/AddressService.java @@ -0,0 +1,44 @@ +package org.store.clothstar.member.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.store.clothstar.common.dto.MessageDTO; +import org.store.clothstar.common.util.MessageDTOBuilder; +import org.store.clothstar.member.domain.Address; +import org.store.clothstar.member.dto.request.CreateAddressRequest; +import org.store.clothstar.member.dto.response.AddressResponse; +import org.store.clothstar.member.repository.AddressRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class AddressService { + private final AddressRepository addressInfoRepository; + + public List getMemberAllAddress(Long memberId) { + List
memberAddressList = addressInfoRepository.findMemberAllAddress(memberId); + + List memberAddressResponseList = memberAddressList.stream() + .map(AddressResponse::new) + .collect(Collectors.toList()); + + return memberAddressResponseList; + } + + public MessageDTO addrSave(Long memberId, CreateAddressRequest createAddressRequest) { + Address address = createAddressRequest.toAddress(memberId); + + int result = addressInfoRepository.save(address); + if (result == 0) { + throw new IllegalArgumentException("회원 배송지 주소가 저장되지 않았습니다."); + } + + return MessageDTOBuilder.buildMessage( + HttpStatus.OK.value(), + "addressId : " + address.getAddressId() + " 회원 배송지 주소가 정상적으로 저장 되었습니다." + ); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/member/service/MemberDetailsService.java b/src/main/java/org/store/clothstar/member/service/MemberDetailsService.java new file mode 100644 index 0000000..4882542 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/service/MemberDetailsService.java @@ -0,0 +1,27 @@ +package org.store.clothstar.member.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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 org.store.clothstar.member.domain.CustomUserDetails; +import org.store.clothstar.member.domain.Member; +import org.store.clothstar.member.repository.MemberRepository; + +@Service +@RequiredArgsConstructor +@Slf4j +public class MemberDetailsService implements UserDetailsService { + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + log.info("loadUserByUsername() 실행"); + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException(email + " not found")); + + return new CustomUserDetails(member); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/member/service/MemberService.java b/src/main/java/org/store/clothstar/member/service/MemberService.java new file mode 100644 index 0000000..17f6e66 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/service/MemberService.java @@ -0,0 +1,79 @@ +package org.store.clothstar.member.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.store.clothstar.common.dto.MessageDTO; +import org.store.clothstar.common.util.MessageDTOBuilder; +import org.store.clothstar.member.domain.Member; +import org.store.clothstar.member.dto.request.CreateMemberRequest; +import org.store.clothstar.member.dto.request.ModifyMemberRequest; +import org.store.clothstar.member.dto.response.MemberResponse; +import org.store.clothstar.member.repository.MemberRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class MemberService { + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + + public List getAllMember() { + List memberList = memberRepository.findAll(); + + List memberResponseList = memberList.stream() + .map(MemberResponse::new) + .collect(Collectors.toList()); + + return memberResponseList; + } + + public MemberResponse getMemberById(Long memberId) { + return memberRepository.findById(memberId) + .map(MemberResponse::new) + .orElseThrow(() -> new IllegalArgumentException("not found by memberId: " + memberId)); + } + + public MessageDTO emailCheck(String email) { + boolean emailExists = memberRepository.findByEmail(email).isPresent(); + + return MessageDTOBuilder.buildMessage( + HttpStatus.OK.value(), + (emailExists ? "이미 사용중인 이메일 입니다." : "사용 가능한 이메일 입니다."), + emailExists + ); + } + + + public MessageDTO modifyMember(Long memberId, ModifyMemberRequest modifyMemberRequest) { + Member member = modifyMemberRequest.toMember(memberId); + memberRepository.update(member); + + return MessageDTOBuilder.buildMessage( + HttpStatus.OK.value(), + "memberId : " + memberId + " 가 정상적으로 수정되었습니다.", + true + ); + } + + public MessageDTO signup(CreateMemberRequest createMemberDTO) { + String encryptedPassword = passwordEncoder.encode(createMemberDTO.getPassword()); + Member member = createMemberDTO.toMember(encryptedPassword); + + int result = memberRepository.save(member); + if (result == 0) { + throw new IllegalArgumentException("회원 가입이 되지 않았습니다."); + } + + return MessageDTOBuilder.buildMessage( + member.getMemberId(), + HttpStatus.OK.value(), + "memberId : " + member.getMemberId() + " 가 정상적으로 회원가입 되었습니다." + ); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/member/service/SellerService.java b/src/main/java/org/store/clothstar/member/service/SellerService.java new file mode 100644 index 0000000..a3b8d74 --- /dev/null +++ b/src/main/java/org/store/clothstar/member/service/SellerService.java @@ -0,0 +1,37 @@ +package org.store.clothstar.member.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.store.clothstar.common.dto.MessageDTO; +import org.store.clothstar.common.util.MessageDTOBuilder; +import org.store.clothstar.member.domain.Seller; +import org.store.clothstar.member.dto.request.CreateSellerRequest; +import org.store.clothstar.member.dto.response.SellerResponse; +import org.store.clothstar.member.repository.SellerRepository; + +@Service +@RequiredArgsConstructor +public class SellerService { + private final SellerRepository sellerRepository; + + public SellerResponse getSellerById(Long memberId) { + return sellerRepository.findById(memberId) + .map(SellerResponse::new) + .orElseThrow(() -> new IllegalArgumentException("not found by memberId: " + memberId)); + } + + public MessageDTO sellerSave(Long memberId, CreateSellerRequest createSellerRequest) { + Seller seller = createSellerRequest.toSeller(memberId); + + int result = sellerRepository.save(seller); + if (result == 0) { + throw new IllegalArgumentException("판매자 가입이 되지 않았습니다."); + } + + return MessageDTOBuilder.buildMessage( + HttpStatus.OK.value(), + "memberId : " + seller.getMemberId() + " 가 판매자 가입이 정상적으로 되었습니다." + ); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/order/controller/OrderController.java b/src/main/java/org/store/clothstar/order/controller/OrderController.java new file mode 100644 index 0000000..b49b7f5 --- /dev/null +++ b/src/main/java/org/store/clothstar/order/controller/OrderController.java @@ -0,0 +1,66 @@ +package org.store.clothstar.order.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.store.clothstar.common.dto.MessageDTO; +import org.store.clothstar.order.dto.reponse.OrderResponse; +import org.store.clothstar.order.dto.request.OrderRequestWrapper; +import org.store.clothstar.order.service.OrderApplicationService; +import org.store.clothstar.order.service.OrderService; +import org.store.clothstar.order.utils.URIBuilder; + +import java.net.URI; + +@Tag(name = "Order", description = "주문(Order) 정보 관리에 대한 API 입니다.") +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/orders") +public class OrderController { + + private final OrderService orderService; + private final OrderApplicationService orderApplicationService; + + @Operation(summary = "단일 주문 조회", description = "단일 주문의 정보를 조회한다.") + @GetMapping("/{orderId}") + public ResponseEntity getOrder(@PathVariable Long orderId) { + + OrderResponse orderResponse = orderService.getOrder(orderId); + + return ResponseEntity.ok().body(orderResponse); + } + + @Operation(summary = "주문 생성", description = "단일 주문을 생성한다.") + @PostMapping + public ResponseEntity saveOrder( + @RequestBody @Validated OrderRequestWrapper orderRequestWrapper) { + + Long orderId = orderApplicationService.saveOrderWithTransaction(orderRequestWrapper); + + URI location = URIBuilder.buildURI(orderId); + + return ResponseEntity.created(location).build(); + } + + @Operation(summary = "구매 확정", description = "구매자가 구매 확정 시, 주문상태가 '구매확정'으로 변경된다.") + @PatchMapping("/{orderId}") + public ResponseEntity deliveredToConfirmOrder(@PathVariable Long orderId) { + + orderService.deliveredToConfirmOrder(orderId); + + return ResponseEntity.ok() + .body(new MessageDTO( + HttpStatus.OK.value(), + "주문이 정상적으로 구매 확정 되었습니다.", + null)); + } +} + + + + + diff --git a/src/main/java/org/store/clothstar/order/controller/OrderSellerController.java b/src/main/java/org/store/clothstar/order/controller/OrderSellerController.java new file mode 100644 index 0000000..ea16ecb --- /dev/null +++ b/src/main/java/org/store/clothstar/order/controller/OrderSellerController.java @@ -0,0 +1,43 @@ +package org.store.clothstar.order.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.store.clothstar.common.dto.MessageDTO; +import org.store.clothstar.order.dto.reponse.OrderResponse; +import org.store.clothstar.order.dto.request.OrderSellerRequest; +import org.store.clothstar.order.service.OrderSellerService; + +import java.util.List; + +@Tag(name = "OrderSeller", description = "판매자(OrderSeller)의 주문 정보 관리에 대한 API 입니다.") +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/seller/orders") +public class OrderSellerController { + + private final OrderSellerService orderSellerService; + + @Operation(summary = "(판매자) WAITING 주문 리스트 조회", description = "(판매자) 주문상태가 '승인대기'인 주문 리스트를 조회한다.") + @GetMapping + public ResponseEntity> getWaitingOrder() { + + List orderResponseList = orderSellerService.getWaitingOrder(); + + return ResponseEntity.ok(orderResponseList); + } + + @Operation(summary = "(판매자) 주문 승인 또는 취소", description = "(판매자) 주문을 승인 또는 취소한다.") + @PatchMapping("/{orderId}") + public ResponseEntity cancelOrApproveOrder( + @PathVariable Long orderId, + @RequestBody @Validated OrderSellerRequest orderSellerRequest) { + + MessageDTO messageDTO = orderSellerService.cancelOrApproveOrder(orderId, orderSellerRequest); + + return ResponseEntity.ok().body(messageDTO); + } +} diff --git a/src/main/java/org/store/clothstar/order/domain/Order.java b/src/main/java/org/store/clothstar/order/domain/Order.java new file mode 100644 index 0000000..b057724 --- /dev/null +++ b/src/main/java/org/store/clothstar/order/domain/Order.java @@ -0,0 +1,31 @@ +package org.store.clothstar.order.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.store.clothstar.order.domain.type.PaymentMethod; +import org.store.clothstar.order.domain.type.Status; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class Order { + private Long orderId; + private Long memberId; + private Long addressId; + private LocalDateTime createdAt; // 주문 생성일 + private Status status; // 주문 상태 + private int totalShippingPrice; // 총 배송비 + private int totalProductsPrice; // 총 상품 금액 + private PaymentMethod paymentMethod; // 결제 수단 + private int totalPaymentPrice; // 총 결제 금액 + + public void updatePrices(int totalProductsPrice, int totalPaymentPrice) { + this.totalProductsPrice = totalProductsPrice; + this.totalPaymentPrice = totalPaymentPrice; + } +} diff --git a/src/main/java/org/store/clothstar/order/domain/type/ApprovalStatus.java b/src/main/java/org/store/clothstar/order/domain/type/ApprovalStatus.java new file mode 100644 index 0000000..dd50c0f --- /dev/null +++ b/src/main/java/org/store/clothstar/order/domain/type/ApprovalStatus.java @@ -0,0 +1,6 @@ +package org.store.clothstar.order.domain.type; + +public enum ApprovalStatus { + APPROVE, + CANCEL +} diff --git a/src/main/java/org/store/clothstar/order/domain/type/PaymentMethod.java b/src/main/java/org/store/clothstar/order/domain/type/PaymentMethod.java new file mode 100644 index 0000000..f20a5c9 --- /dev/null +++ b/src/main/java/org/store/clothstar/order/domain/type/PaymentMethod.java @@ -0,0 +1,7 @@ +package org.store.clothstar.order.domain.type; + +public enum PaymentMethod { + CARD, + KAKAOPAY, + NAVERPAY +} diff --git a/src/main/java/org/store/clothstar/order/domain/type/Status.java b/src/main/java/org/store/clothstar/order/domain/type/Status.java new file mode 100644 index 0000000..088a9bd --- /dev/null +++ b/src/main/java/org/store/clothstar/order/domain/type/Status.java @@ -0,0 +1,9 @@ +package org.store.clothstar.order.domain.type; + +public enum Status { + WAITING, + APPROVE, + DELIVERED, + CONFIRM, + CANCEL +} diff --git a/src/main/java/org/store/clothstar/order/dto/reponse/OrderResponse.java b/src/main/java/org/store/clothstar/order/dto/reponse/OrderResponse.java new file mode 100644 index 0000000..1aa1064 --- /dev/null +++ b/src/main/java/org/store/clothstar/order/dto/reponse/OrderResponse.java @@ -0,0 +1,58 @@ +package org.store.clothstar.order.dto.reponse; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import org.store.clothstar.order.domain.Order; +import org.store.clothstar.order.domain.type.PaymentMethod; +import org.store.clothstar.order.domain.type.Status; + +import java.time.LocalDate; + +@Getter +@Builder +@Schema(description = "주문 조회용 Response") +public class OrderResponse { + + @Schema(description = "주문 id", example = "1") + private Long orderId; + + @Schema(description = "회원 id", example = "1") + private Long memberId; + + @Schema(description = "배송지 id", example = "1") + private Long addressId; + + @Schema(description = "주문 생성 날짜", example = "2024-05-15") + private LocalDate createdAt; + + @Schema(description = "주문 상태", example = "WAITING") + private Status status; + + @Schema(description = "총 배송비", example = "3000") + private int totalShippingPrice; + + @Schema(description = "총 상품 금액", example = "15000") + private int totalProductsPrice; + + @Schema(description = "결제 수단", example = "CARD") + private PaymentMethod paymentMethod; + + @Schema(description = "총 결제 금액", example = "18000") + private int totalPaymentPrice; + + + public static OrderResponse fromOrder(Order order) { + return OrderResponse.builder() + .orderId(order.getOrderId()) + .memberId(order.getMemberId()) + .addressId(order.getAddressId()) + .createdAt(order.getCreatedAt().toLocalDate()) + .status(order.getStatus()) + .totalShippingPrice(order.getTotalShippingPrice()) + .totalProductsPrice(order.getTotalProductsPrice()) + .paymentMethod(order.getPaymentMethod()) + .totalPaymentPrice(order.getTotalPaymentPrice()) + .build(); + } +} diff --git a/src/main/java/org/store/clothstar/order/dto/request/CreateOrderRequest.java b/src/main/java/org/store/clothstar/order/dto/request/CreateOrderRequest.java new file mode 100644 index 0000000..7923cbe --- /dev/null +++ b/src/main/java/org/store/clothstar/order/dto/request/CreateOrderRequest.java @@ -0,0 +1,51 @@ +package org.store.clothstar.order.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.store.clothstar.member.domain.Address; +import org.store.clothstar.member.domain.Member; +import org.store.clothstar.order.domain.Order; +import org.store.clothstar.order.domain.type.PaymentMethod; +import org.store.clothstar.order.domain.type.Status; +import org.store.clothstar.order.utils.GenerateOrderId; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "주문 저장용 Request") +public class CreateOrderRequest { + + @Schema(description = "결제 수단", nullable = false) + @NotNull(message = "결제 수단은 비어있을 수 없습니다.") + private PaymentMethod paymentMethod; + + @Schema(description = "회원 번호", nullable = false) + @NotNull(message = "회원 번호는 비어있을 수 없습니다.") + private Long memberId; + + @Schema(description = "배송지 번호", nullable = false) + @NotNull(message = "배송지 번호는 비어있을 수 없습니다.") + private Long addressId; + + + public Order toOrder(Member member, Address address) { + return Order.builder() + .orderId(GenerateOrderId.generateOrderId()) + .memberId(member.getMemberId()) + .addressId(address.getAddressId()) + .createdAt(LocalDateTime.now()) + .status(Status.WAITING) + .totalShippingPrice(3000) + .totalProductsPrice(0) + .paymentMethod(paymentMethod) + .totalPaymentPrice(0) + .build(); + } +} diff --git a/src/main/java/org/store/clothstar/order/dto/request/OrderRequestWrapper.java b/src/main/java/org/store/clothstar/order/dto/request/OrderRequestWrapper.java new file mode 100644 index 0000000..c191485 --- /dev/null +++ b/src/main/java/org/store/clothstar/order/dto/request/OrderRequestWrapper.java @@ -0,0 +1,16 @@ +package org.store.clothstar.order.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.store.clothstar.orderDetail.dto.request.CreateOrderDetailRequest; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class OrderRequestWrapper { + private CreateOrderRequest createOrderRequest; + private CreateOrderDetailRequest createOrderDetailRequest; +} diff --git a/src/main/java/org/store/clothstar/order/dto/request/OrderSellerRequest.java b/src/main/java/org/store/clothstar/order/dto/request/OrderSellerRequest.java new file mode 100644 index 0000000..765d968 --- /dev/null +++ b/src/main/java/org/store/clothstar/order/dto/request/OrderSellerRequest.java @@ -0,0 +1,22 @@ +package org.store.clothstar.order.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.store.clothstar.order.domain.type.ApprovalStatus; + +import javax.validation.constraints.NotNull; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Schema(description = "(판매자)주문 수정용 Request") +public class OrderSellerRequest { + + @Schema(description = "요청 주문 상태(승인 or 취소)", nullable = false) + @NotNull(message = "요청할 주문 상태를 입력해주세요.") + private ApprovalStatus approvalStatus; +} diff --git a/src/main/java/org/store/clothstar/order/orderREADME.md b/src/main/java/org/store/clothstar/order/orderREADME.md new file mode 100644 index 0000000..39d53ed --- /dev/null +++ b/src/main/java/org/store/clothstar/order/orderREADME.md @@ -0,0 +1,75 @@ +# order 패키지 README + +## 패키지 개요 + +이 패키지는 주문에 대한 기능을 구현하기 위한 패키지이다(v1). + +### 주문 조회 설계안 + +1. 주문 조회 + 1-1. 조회되는 주문 정보 + - 주문번호, 회원번호, 배송지번호, 주문생성일, 주문상태, 총 배송비, 총 상품금액, 결제수단, 총 결제금액 +2. 구매 확정 시(구매자가 구매 확정을 하는 경우) + - 주문상태가 '구매확정'으로 변경됨 + +### 주문 & 주문상세 생성 설계안 + +1. 주문 생성 + 1-1. 생성되는 주문 정보 + - 주문번호, 회원번호, 배송지번호, 주문생성일, 주문상태, 총 배송비, 총 상품금액, 결제수단, 총 결제금액 + + * 총 배송비 = 3000원으로 고정 + * 총 상품금액 = 0원으로 고정 + * 총 결제금액 = 0원으로 고정 + * 결제수단 종류 + - [ 신용/체크카드, 네이버페이, 카카오페이, 무통장 입금 ] + * 주문상태 단계 + - [ 승인대기(WAITING) -> 주문승인(APPROVE) -> 배송완료(DELIVERED) -> 구매확정(CONFIRM) ] + - [ 승인대기(WAITING) -> 주문취소(CANCEL) ] +2. 주문상세 생성 + 2-1. 생성되는 주문상세 정보: + 주문상세번호, 주문번호, 상품번호, 상품옵션번호, 구매수량, 고정가격, 상품 종류 하나당 총 가격, 상품명, 재고, 옵션명, 추가비용, 브랜드명 + - 주문 유효성 검사: 구매 수량이 재고보다 크면 주문이 생성되지 않음 + - 상품 재고 차감: 주문상세 생성 시, 구매 수량만큼 상품 재고 차감 + - 주문과 주문상세 생성은 하나의 트랜잭션으로 이루어짐 + + * 총 상품금액 = 주문의 총 상품금액 + 주문상세의, 상품 종류 하나당 총 금액 + * 총 결제금액 = 주문의 총 상품금액 + 주문상세의, 상품 종류 하나당 총 금액 + 주문의 총 배송비 +3. 주문상세 추가 + - 처음 주문과 주문상세가 생성되면, 이후에는 주문에 주문상세를 추가하는 형식으로 진행됨 + +### (판매자) 주문 관리 설계안 + +1. 주문 리스트 조회: 주문 상태가 [승인대기]인 주문 리스트 조회 +2. 주문 승인 또는 취소 + - 주문 승인 시, 주문 상태가 [승인대기] 에서 [주문승인] 으로 변경됨 + - 주문 취소 시, 주문 상태가 [승인대기] 에서 [주문취소] 로 변경됨 + +### API 디자인 + +- 주문 조회: GET /v1/orders/{orderId} + +- 구매 확정: PATCH /v1/orders/{orderId} + * 프로세스 + 1. 유효성검사: 주문상태가 [배송완료]인지 확인 + 2. 유효시, 주문상태를 [구매확정]으로 변경 + +- 주문 생성: POST /v1/orders + * 프로세스 + 1. 주문 생성 + 2. 주문상세 생성 + * 주문-주문상세 생성은 하나의 트랜잭션임 + +- 주문상세 추가: POST /v1/orderdetails + * 프로세스 + 1. 유효성검사: 주문 수량이 상품 재고보다 클 경우, 주문이 생성되지 않음 + 2. 주문 정보 업데이트: 주문상세 추가에 따른, 주문의 총 상품 금액과 총 결제 금액 업데이트 + 3. 주문 수량만큼 상품 재고 차감 + +- (판매자) 주문 관리 : + - 승인대기 주문 리스트 조회: GET /v1/seller/orders + - 주문 승인 및 취소: PATCH /v1/seller/orders/{orderId} + * 프로세스 + 1. 유효성 검사: 주문상태가 [승인대기]인지 확인 + 2-1. 유효시, 'APPROVE'가 요청되면 주문상태를 [주문승인]으로 변경 + 2-2. 유효시, 'CANCEL'이 요청되면 주문상태를 [주문취소]로 변경 diff --git a/src/main/java/org/store/clothstar/order/repository/OrderRepository.java b/src/main/java/org/store/clothstar/order/repository/OrderRepository.java new file mode 100644 index 0000000..982e268 --- /dev/null +++ b/src/main/java/org/store/clothstar/order/repository/OrderRepository.java @@ -0,0 +1,18 @@ +package org.store.clothstar.order.repository; + +import org.apache.ibatis.annotations.Mapper; +import org.store.clothstar.order.domain.Order; + +import java.util.Optional; + +@Mapper +public interface OrderRepository { + + Optional getOrder(Long orderId); + + int saveOrder(Order order); + + void deliveredToConfirmOrder(Long orderId); + + void updateOrderPrices(Order order); +} diff --git a/src/main/java/org/store/clothstar/order/repository/OrderSellerRepository.java b/src/main/java/org/store/clothstar/order/repository/OrderSellerRepository.java new file mode 100644 index 0000000..a825e82 --- /dev/null +++ b/src/main/java/org/store/clothstar/order/repository/OrderSellerRepository.java @@ -0,0 +1,16 @@ +package org.store.clothstar.order.repository; + +import org.apache.ibatis.annotations.Mapper; +import org.store.clothstar.order.domain.Order; + +import java.util.List; + +@Mapper +public interface OrderSellerRepository { + + List SelectWaitingOrders(); + + void approveOrder(Long orderId); + + void cancelOrder(Long orderId); +} diff --git a/src/main/java/org/store/clothstar/order/service/OrderApplicationService.java b/src/main/java/org/store/clothstar/order/service/OrderApplicationService.java new file mode 100644 index 0000000..2b226e4 --- /dev/null +++ b/src/main/java/org/store/clothstar/order/service/OrderApplicationService.java @@ -0,0 +1,24 @@ +package org.store.clothstar.order.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.store.clothstar.order.dto.request.OrderRequestWrapper; +import org.store.clothstar.orderDetail.service.OrderDetailService; + +@Service +@RequiredArgsConstructor +public class OrderApplicationService { + private final OrderService orderService; + private final OrderDetailService orderDetailService; + + @Transactional + public Long saveOrderWithTransaction(OrderRequestWrapper orderRequestWrapper) { + + Long orderId = orderService.saveOrder(orderRequestWrapper.getCreateOrderRequest()); + + orderDetailService.saveOrderDetailWithOrder(orderRequestWrapper.getCreateOrderDetailRequest(), orderId); + + return orderId; + } +} diff --git a/src/main/java/org/store/clothstar/order/service/OrderSellerService.java b/src/main/java/org/store/clothstar/order/service/OrderSellerService.java new file mode 100644 index 0000000..663fb52 --- /dev/null +++ b/src/main/java/org/store/clothstar/order/service/OrderSellerService.java @@ -0,0 +1,66 @@ +package org.store.clothstar.order.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; +import org.store.clothstar.common.dto.MessageDTO; +import org.store.clothstar.order.domain.type.ApprovalStatus; +import org.store.clothstar.order.domain.type.Status; +import org.store.clothstar.order.dto.reponse.OrderResponse; +import org.store.clothstar.order.dto.request.OrderSellerRequest; +import org.store.clothstar.order.repository.OrderRepository; +import org.store.clothstar.order.repository.OrderSellerRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class OrderSellerService { + + private final OrderRepository orderRepository; + private final OrderSellerRepository orderSellerRepository; + + @Transactional(readOnly = true) + public List getWaitingOrder() { + + return orderSellerRepository.SelectWaitingOrders().stream() + .map(OrderResponse::fromOrder) + .collect(Collectors.toList()); + } + + @Transactional + public MessageDTO cancelOrApproveOrder(Long orderId, OrderSellerRequest orderSellerRequest) { + + // 주문 유효성 검사 + orderRepository.getOrder(orderId) + .filter(o -> o.getStatus() == Status.WAITING) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "주문이 존재하지 않거나 상태가 'WAITING'이 아니어서 처리할 수 없습니다.")); + + return processOrder(orderId, orderSellerRequest); + } + + // 주문 처리 + @Transactional + public MessageDTO processOrder(Long orderId, OrderSellerRequest orderSellerRequest) { + + MessageDTO messageDTO = null; + + if (orderSellerRequest.getApprovalStatus() == ApprovalStatus.APPROVE) { + orderSellerRepository.approveOrder(orderId); + messageDTO = new MessageDTO(HttpStatus.OK.value(), "주문이 정상적으로 승인 되었습니다.", null); + } else if (orderSellerRequest.getApprovalStatus() == ApprovalStatus.CANCEL) { + orderSellerRepository.cancelOrder(orderId); + messageDTO = new MessageDTO(HttpStatus.OK.value(), "주문이 정상적으로 취소 되었습니다.", null); + } + + orderRepository.getOrder(orderId) + .map(OrderResponse::fromOrder) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "처리 후 주문 정보를 찾을 수 없습니다.")); + + return messageDTO; + } +} + diff --git a/src/main/java/org/store/clothstar/order/service/OrderService.java b/src/main/java/org/store/clothstar/order/service/OrderService.java new file mode 100644 index 0000000..26e64ed --- /dev/null +++ b/src/main/java/org/store/clothstar/order/service/OrderService.java @@ -0,0 +1,65 @@ +package org.store.clothstar.order.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; +import org.store.clothstar.member.domain.Address; +import org.store.clothstar.member.domain.Member; +import org.store.clothstar.member.repository.AddressRepository; +import org.store.clothstar.member.repository.MemberRepository; +import org.store.clothstar.order.domain.Order; +import org.store.clothstar.order.domain.type.Status; +import org.store.clothstar.order.dto.reponse.OrderResponse; +import org.store.clothstar.order.dto.request.CreateOrderRequest; +import org.store.clothstar.order.repository.OrderRepository; +import org.store.clothstar.orderDetail.service.OrderDetailService; + +@Slf4j +@Service +@RequiredArgsConstructor +public class OrderService { + + private final OrderRepository orderRepository; + private final MemberRepository memberRepository; + private final AddressRepository addressRepository; + private final OrderDetailService orderDetailService; + + @Transactional(readOnly = true) + public OrderResponse getOrder(Long orderId) { + + return orderRepository.getOrder(orderId) + .map(OrderResponse::fromOrder) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "존재하지 않는 주문번호입니다.")); + } + + @Transactional + public Long saveOrder(CreateOrderRequest createOrderRequest) { + + Member member = memberRepository.findById(createOrderRequest.getMemberId()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "회원 정보를 찾을 수 없습니다.")); + + Address address = addressRepository.findById(createOrderRequest.getAddressId()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "배송지 정보를 찾을 수 없습니다.")); + + Order order = createOrderRequest.toOrder(member, address); + orderRepository.saveOrder(order); + + return order.getOrderId(); + } + + @Transactional + public void deliveredToConfirmOrder(Long orderId) { + + Order order = orderRepository.getOrder(orderId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "주문 정보를 찾을 수 없습니다.")); + + if (order.getStatus() != Status.DELIVERED) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "주문 상태가 '배송완료'가 아니기 때문에 주문확정이 불가능합니다."); + } + + orderRepository.deliveredToConfirmOrder(orderId); + } +} diff --git a/src/main/java/org/store/clothstar/order/utils/GenerateOrderId.java b/src/main/java/org/store/clothstar/order/utils/GenerateOrderId.java new file mode 100644 index 0000000..9957671 --- /dev/null +++ b/src/main/java/org/store/clothstar/order/utils/GenerateOrderId.java @@ -0,0 +1,36 @@ +package org.store.clothstar.order.utils; + +import java.security.SecureRandom; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class GenerateOrderId { + + private static final String DATE_FORMAT = "yyyyMMdd"; + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT); + private static final int RANDOM_NUMBER_LENGTH = 7; + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + // 주문생성날짜와 랜덤숫자를 이용하여, unique한 주문번호를 생성한다. + public static Long generateOrderId() { + String datePrefix = getDatePrefix(); + String randomDigits = generateRandomDigits(); + + return Long.parseLong(datePrefix + randomDigits); + } + + // 특정 날짜 형식으로 바꾼 현재 날짜를 얻는다. + public static String getDatePrefix() { + return LocalDate.now().format(DATE_TIME_FORMATTER); + } + + // 특정 개수의 String타입 랜덤 숫자를 생성한다. + public static String generateRandomDigits() { + + return IntStream.range(0, RANDOM_NUMBER_LENGTH) + .mapToObj(i -> String.valueOf(SECURE_RANDOM.nextInt(10))) + .collect(Collectors.joining()); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/order/utils/URIBuilder.java b/src/main/java/org/store/clothstar/order/utils/URIBuilder.java new file mode 100644 index 0000000..cb93cde --- /dev/null +++ b/src/main/java/org/store/clothstar/order/utils/URIBuilder.java @@ -0,0 +1,16 @@ +package org.store.clothstar.order.utils; + +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; + +public class URIBuilder { + + public static URI buildURI(Long id) { + return ServletUriComponentsBuilder + .fromCurrentRequest() // 현재 요청의 URI를 사용 + .path("/{id}") // 경로 변수 추가 + .buildAndExpand(id) // {/id} 자리에 실제 id 값을 삽입 + .toUri(); + } +} diff --git a/src/main/java/org/store/clothstar/orderDetail/controller/OrderDetailController.java b/src/main/java/org/store/clothstar/orderDetail/controller/OrderDetailController.java new file mode 100644 index 0000000..5f67d24 --- /dev/null +++ b/src/main/java/org/store/clothstar/orderDetail/controller/OrderDetailController.java @@ -0,0 +1,37 @@ +package org.store.clothstar.orderDetail.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +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 org.store.clothstar.order.utils.URIBuilder; +import org.store.clothstar.orderDetail.dto.request.AddOrderDetailRequest; +import org.store.clothstar.orderDetail.service.OrderDetailService; + +import java.net.URI; + +@Tag(name = "OrderDetail", description = "주문 내 개별 상품에 대한 옵션, 수량 등을 나타내는, 주문상세(OrderDetail) 정보 관리에 대한 API 입니다.") +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/orderdetails") +public class OrderDetailController { + + private final OrderDetailService orderdetailService; + + @Operation(summary = "주문상세 추가 저장", description = "개별 상품에 대한 주문상세(상품명, 가격, 개수...)를 특정 주문에 추가 저장한다.") + @PostMapping + public ResponseEntity addOrderDetail( + @RequestBody @Validated AddOrderDetailRequest addOrderDetailRequest) { + + Long orderDetailId = orderdetailService.addOrderDetail(addOrderDetailRequest); + + URI location = URIBuilder.buildURI(orderDetailId); + + return ResponseEntity.created(location).build(); + } +} diff --git a/src/main/java/org/store/clothstar/orderDetail/domain/OrderDetail.java b/src/main/java/org/store/clothstar/orderDetail/domain/OrderDetail.java new file mode 100644 index 0000000..9fca5d1 --- /dev/null +++ b/src/main/java/org/store/clothstar/orderDetail/domain/OrderDetail.java @@ -0,0 +1,31 @@ +package org.store.clothstar.orderDetail.domain; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Builder +public class OrderDetail { + + // 주문 상세 정보 + private Long orderDetailId; + private Long orderId; + private Long productLineId; + private Long productId; + private int quantity; + private int fixedPrice; // 고정된 상품 가격 ( 주문 당시 가격 ) + private int oneKindTotalPrice; // 상품 종류 하나당 총 가격 + + // 상품 정보 + private String name; // 상품명 + + // 상품 옵션 정보 + private Long stock; // 옵션 상품 재고 + private String optionName; + private int extraCharge; + + // 판매자 정보 + private String brandName; +} diff --git a/src/main/java/org/store/clothstar/orderDetail/dto/request/AddOrderDetailRequest.java b/src/main/java/org/store/clothstar/orderDetail/dto/request/AddOrderDetailRequest.java new file mode 100644 index 0000000..35f1c5d --- /dev/null +++ b/src/main/java/org/store/clothstar/orderDetail/dto/request/AddOrderDetailRequest.java @@ -0,0 +1,56 @@ +package org.store.clothstar.orderDetail.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.store.clothstar.order.domain.Order; +import org.store.clothstar.orderDetail.domain.OrderDetail; +import org.store.clothstar.product.domain.Product; +import org.store.clothstar.productLine.domain.ProductLine; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Schema(description = "주문 상세 추가용 Request") +public class AddOrderDetailRequest { + + @Schema(description = "주문 번호", nullable = false) + @NotNull(message = "주문 번호는 비어있을 수 없습니다.") + private Long orderId; + + @Schema(description = "상품 번호", nullable = false) + @NotNull(message = "상품 번호는 비어있을 수 없습니다.") + private Long productLineId; + + @Schema(description = "상품 옵션 번호", nullable = false) + @NotNull(message = "상품 옵션 번호는 비어있을 수 없습니다.") + private Long productId; + + @Schema(description = "상품 수량", nullable = false) + @NotNull(message = "상품 수량은 비어있을 수 없습니다.") + @Positive(message = "상품 수량은 0보다 커야 합니다.") + private int quantity; + + + public OrderDetail toOrderDetail(Order order, ProductLine productLine, Product product) { + return OrderDetail.builder() + .orderId(order.getOrderId()) + .productLineId(productLine.getProductLineId()) + .productId(product.getProductId()) + .quantity(quantity) + .fixedPrice(productLine.getPrice()) + .oneKindTotalPrice(quantity * productLine.getPrice()) + .name(productLine.getName()) + .stock(product.getStock()) + .optionName(product.getName()) + .extraCharge(product.getExtraCharge()) + .brandName(productLine.getBrandName()) + .build(); + } +} diff --git a/src/main/java/org/store/clothstar/orderDetail/dto/request/CreateOrderDetailRequest.java b/src/main/java/org/store/clothstar/orderDetail/dto/request/CreateOrderDetailRequest.java new file mode 100644 index 0000000..c32e042 --- /dev/null +++ b/src/main/java/org/store/clothstar/orderDetail/dto/request/CreateOrderDetailRequest.java @@ -0,0 +1,51 @@ +package org.store.clothstar.orderDetail.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.store.clothstar.orderDetail.domain.OrderDetail; +import org.store.clothstar.product.domain.Product; +import org.store.clothstar.productLine.domain.ProductLine; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Schema(description = "주문 상세 저장용 Request") +public class CreateOrderDetailRequest { + + @Schema(description = "상품 번호", nullable = false) + @NotNull(message = "상품 번호는 비어있을 수 없습니다.") + private Long productLineId; + + @Schema(description = "상품 옵션 번호", nullable = false) + @NotNull(message = "상품 옵션 번호는 비어있을 수 없습니다.") + private Long productId; + + @Schema(description = "상품 수량", nullable = false) + @NotNull(message = "상품 수량은 비어있을 수 없습니다.") + @Positive(message = "상품 수량은 0보다 커야 합니다.") + private int quantity; + + + public OrderDetail toOrderDetail(long orderId, ProductLine productLine, Product product) { + return OrderDetail.builder() + .orderId(orderId) + .productLineId(productLine.getProductLineId()) + .productId(product.getProductId()) + .quantity(quantity) + .fixedPrice(productLine.getPrice()) + .oneKindTotalPrice(quantity * productLine.getPrice()) + .name(productLine.getName()) + .stock(product.getStock()) + .optionName(product.getName()) + .extraCharge(product.getExtraCharge()) + .brandName(productLine.getBrandName()) + .build(); + } +} diff --git a/src/main/java/org/store/clothstar/orderDetail/dto/response/OrderDetailResponse.java b/src/main/java/org/store/clothstar/orderDetail/dto/response/OrderDetailResponse.java new file mode 100644 index 0000000..6bff614 --- /dev/null +++ b/src/main/java/org/store/clothstar/orderDetail/dto/response/OrderDetailResponse.java @@ -0,0 +1,66 @@ +package org.store.clothstar.orderDetail.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; +import org.store.clothstar.orderDetail.domain.OrderDetail; + +@Getter +@Builder +@Schema(description = "주문 상세 조회용 Response") +public class OrderDetailResponse { + + @Schema(description = "주문 상세 번호", example = "1") + private Long orderDetailId; + + @Schema(description = "주문 번호", example = "1") + private Long orderId; + + @Schema(description = "상품 번호", example = "1") + private Long productLineId; + + @Schema(description = "상품 옵션 번호", example = "1") + private Long productId; + + @Schema(description = "상품 수량", example = "2") + private int quantity; + + @Schema(description = "고정 가격", example = "15000") + private int fixedPrice; + + @Schema(description = "상품 종류 하나당 총 가격", example = "30000") + private int oneKindTotalPrice; + + @Schema(description = "상품 이름", example = "나이키 반팔티") + private String name; + + @Schema(description = "옵션 상품 재고", example = "30") + private Long stock; + + @Schema(description = "옵션 이름", example = "검정") + private String optionName; + + @Schema(description = "옵션 추가 비용", example = "0") + private int extraCharge; + + @Schema(description = "브랜드 이름", example = "나이키") + private String brandName; + + + public static OrderDetailResponse fromOrderDetail(OrderDetail orderDetail) { + return OrderDetailResponse.builder() + .orderDetailId(orderDetail.getOrderDetailId()) + .orderId(orderDetail.getOrderId()) + .productLineId(orderDetail.getProductLineId()) + .productId(orderDetail.getProductId()) + .quantity(orderDetail.getQuantity()) + .fixedPrice(orderDetail.getFixedPrice()) + .oneKindTotalPrice(orderDetail.getOneKindTotalPrice()) + .name(orderDetail.getName()) + .stock(orderDetail.getStock()) + .optionName(orderDetail.getOptionName()) + .extraCharge(orderDetail.getExtraCharge()) + .brandName(orderDetail.getBrandName()) + .build(); + } +} diff --git a/src/main/java/org/store/clothstar/orderDetail/repository/OrderDetailRepository.java b/src/main/java/org/store/clothstar/orderDetail/repository/OrderDetailRepository.java new file mode 100644 index 0000000..30d689e --- /dev/null +++ b/src/main/java/org/store/clothstar/orderDetail/repository/OrderDetailRepository.java @@ -0,0 +1,10 @@ +package org.store.clothstar.orderDetail.repository; + +import org.apache.ibatis.annotations.Mapper; +import org.store.clothstar.orderDetail.domain.OrderDetail; + +@Mapper +public interface OrderDetailRepository { + + void saveOrderDetail(OrderDetail orderdetail); +} diff --git a/src/main/java/org/store/clothstar/orderDetail/service/OrderDetailService.java b/src/main/java/org/store/clothstar/orderDetail/service/OrderDetailService.java new file mode 100644 index 0000000..6fb3a43 --- /dev/null +++ b/src/main/java/org/store/clothstar/orderDetail/service/OrderDetailService.java @@ -0,0 +1,98 @@ +package org.store.clothstar.orderDetail.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; +import org.store.clothstar.order.domain.Order; +import org.store.clothstar.order.repository.OrderRepository; +import org.store.clothstar.orderDetail.domain.OrderDetail; +import org.store.clothstar.orderDetail.dto.request.AddOrderDetailRequest; +import org.store.clothstar.orderDetail.dto.request.CreateOrderDetailRequest; +import org.store.clothstar.orderDetail.repository.OrderDetailRepository; +import org.store.clothstar.product.domain.Product; +import org.store.clothstar.product.repository.ProductRepository; +import org.store.clothstar.productLine.domain.ProductLine; +import org.store.clothstar.productLine.repository.ProductLineRepository; + +@Slf4j +@Service +@RequiredArgsConstructor +public class OrderDetailService { + private final OrderDetailRepository orderDetailRepository; + private final OrderRepository orderRepository; + private final ProductRepository productRepository; + private final ProductLineRepository productLineRepository; + + // 주문 생성시 같이 호출되는 주문 상세 생성 메서드 - 하나의 트랜잭션으로 묶임 + @Transactional + public void saveOrderDetailWithOrder(CreateOrderDetailRequest createOrderDetailRequest, long orderId) { + + Order order = orderRepository.getOrder(orderId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "상품 옵션 정보를 찾을 수 없습니다.")); + + ProductLine productLine = productLineRepository.selectByProductLineId(createOrderDetailRequest.getProductLineId()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "상품 옵션 정보를 찾을 수 없습니다.")); + + Product product = productRepository.selectByProductId(createOrderDetailRequest.getProductId()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "상품 정보를 찾을 수 없습니다.")); + + // 주문상세 생성 유효성 검사: 주문 수량이 상품 재고보다 클 경우, 주문이 생성되지 않는다. + if (createOrderDetailRequest.getQuantity() > product.getStock()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "주문 개수가 재고보다 더 많습니다."); + } + + OrderDetail orderDetail = createOrderDetailRequest.toOrderDetail(orderId, productLine, product); + orderDetailRepository.saveOrderDetail(orderDetail); + + // 주문 정보 업데이트: 주문 상세 생성에 따른, 총 상품 금액과 총 결제 금액 업데이트 + int newTotalProductsPrice = order.getTotalProductsPrice() + orderDetail.getOneKindTotalPrice(); + int newTotalPaymentPrice = + order.getTotalProductsPrice() + order.getTotalShippingPrice() + orderDetail.getOneKindTotalPrice(); + + order.updatePrices(newTotalProductsPrice, newTotalPaymentPrice); + orderRepository.updateOrderPrices(order); + + // 주문 수량만큼 상품 재고 차감 + updateProductStock(product, orderDetail.getQuantity()); + } + + // 주문 상세 추가 생성 + @Transactional + public Long addOrderDetail(AddOrderDetailRequest addOrderDetailRequest) { + + Order order = orderRepository.getOrder(addOrderDetailRequest.getOrderId()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "주문 정보를 찾을 수 없습니다.")); + + ProductLine productLine = productLineRepository.selectByProductLineId(addOrderDetailRequest.getProductLineId()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "상품 옵션 정보를 찾을 수 없습니다.")); + + Product product = productRepository.selectByProductId(addOrderDetailRequest.getProductId()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "상품 정보를 찾을 수 없습니다.")); + + if (addOrderDetailRequest.getQuantity() > product.getStock()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "주문 개수가 재고보다 더 많습니다."); + } + + OrderDetail orderDetail = addOrderDetailRequest.toOrderDetail(order, productLine, product); + orderDetailRepository.saveOrderDetail(orderDetail); + + int newTotalProductsPrice = order.getTotalProductsPrice() + orderDetail.getOneKindTotalPrice(); + int newTotalPaymentPrice = + order.getTotalProductsPrice() + order.getTotalShippingPrice() + orderDetail.getOneKindTotalPrice(); + order.updatePrices(newTotalProductsPrice, newTotalPaymentPrice); + orderRepository.updateOrderPrices(order); + + updateProductStock(product, orderDetail.getQuantity()); + + return orderDetail.getOrderDetailId(); + } + + void updateProductStock(Product product, int quantity) { + long updatedStock = product.getStock() - quantity; + product.updateStock(updatedStock); + productRepository.updateProduct(product); + } +} diff --git a/src/main/java/org/store/clothstar/product/controller/ProductController.java b/src/main/java/org/store/clothstar/product/controller/ProductController.java new file mode 100644 index 0000000..2faf8f3 --- /dev/null +++ b/src/main/java/org/store/clothstar/product/controller/ProductController.java @@ -0,0 +1,72 @@ +package org.store.clothstar.product.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.store.clothstar.common.dto.MessageDTO; +import org.store.clothstar.common.util.URIBuilder; +import org.store.clothstar.product.dto.request.CreateProductRequest; +import org.store.clothstar.product.dto.request.UpdateProductRequest; +import org.store.clothstar.product.dto.response.ProductResponse; +import org.store.clothstar.product.repository.ProductRepository; +import org.store.clothstar.product.service.ProductService; + +import java.net.URI; + +@Tag(name = "Products", description = "Products(상품 옵션) 관련 API 입니다.") +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/products") +public class ProductController { + + private final ProductService productService; + private final ProductRepository productRepository; + + /* + @Operation(summary = "전체 상품 옵션 조회", description = "상품 Id의 모든 상품 옵션을 조회한다.") + @GetMapping + public ResponseEntity> getAllProducts() { + List productResponses = productService.getAllProduct(); + return ResponseEntity.ok().body(productResponses); + } + */ + + @Operation(summary = "상품 옵션 상세 조회", description = "productId로 상품 옵션 한개를 상세 조회한다.") + @GetMapping("/{productId}") + public ResponseEntity getProduct(@PathVariable Long productId) { + ProductResponse productResponse = productService.getProduct(productId); + return ResponseEntity.ok().body(productResponse); + } + + + @Operation(summary = "상품 옵션 등록", description = "상품 옵션 이름, 추가금액, 재고 수를 입력하여 상품을 신규 등록한다.") + @PostMapping + public ResponseEntity createProduct(@Validated @RequestBody CreateProductRequest createProductRequest) { + Long productId = productService.createProduct(createProductRequest); + URI location = URIBuilder.buildURI(productId); + + return ResponseEntity.created(location).build(); + } + + @Operation(summary = "상품 옵션 수정", description = "상품 옵션 이름, 추가금액, 재고 수를 입력하여 상품 옵션 정보를 수정한다.") + @PutMapping("/{productId}") + public ResponseEntity updateProduct( + @PathVariable Long productId, + @Validated @RequestBody UpdateProductRequest updateProductRequest) { + + productService.updateProduct(productId, updateProductRequest); + + return ResponseEntity.ok().body(new MessageDTO(HttpStatus.OK.value(), "Product updated successfully", null)); + } + + @Operation(summary = "상품 옵션 삭제", description = "상품 옵션 id로 상품 옵션을 삭제한다.") + @DeleteMapping("/{productId}") + public ResponseEntity deleteProduct(@PathVariable Long productId) { + productService.deleteProduct(productId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/org/store/clothstar/product/domain/Product.java b/src/main/java/org/store/clothstar/product/domain/Product.java new file mode 100644 index 0000000..3e173ab --- /dev/null +++ b/src/main/java/org/store/clothstar/product/domain/Product.java @@ -0,0 +1,27 @@ +package org.store.clothstar.product.domain; + +import lombok.*; +import org.store.clothstar.product.dto.request.UpdateProductRequest; + +@Builder +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class Product { + private Long productId; + private Long productLineId; + private String name; + private int extraCharge; + private Long stock; + + public void updateOption(UpdateProductRequest updateProductRequest) { + this.name = updateProductRequest.getName(); + this.extraCharge = updateProductRequest.getExtraCharge(); + this.stock = updateProductRequest.getStock(); + } + + public void updateStock(long stock) { + this.stock = stock; + } +} diff --git a/src/main/java/org/store/clothstar/product/dto/request/CreateProductRequest.java b/src/main/java/org/store/clothstar/product/dto/request/CreateProductRequest.java new file mode 100644 index 0000000..c636738 --- /dev/null +++ b/src/main/java/org/store/clothstar/product/dto/request/CreateProductRequest.java @@ -0,0 +1,42 @@ +package org.store.clothstar.product.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.store.clothstar.product.domain.Product; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.PositiveOrZero; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CreateProductRequest { + + @Schema(description = "상품(productLine) id", nullable = false) + private Long productLineId; + + @Schema(description = "상품 옵션 이름", nullable = false) + @NotBlank(message = "상품 옵션 이름을 입력해주세요.") + private String name; + + @Schema(description = "상품 옵션 추가 금액") + @PositiveOrZero(message = "추가 금액은 0포함 양수만 입력할 수 있습니다.") + private int extraCharge; + + @Schema(description = "상품 옵션 재고", nullable = false) + @PositiveOrZero(message = "0이상 양수를 입력해주세요") + private Long stock; + + public Product toProduct() { + return Product.builder() + .productLineId(productLineId) + .name(name) + .extraCharge(extraCharge) + .stock(stock) + .build(); + } +} diff --git a/src/main/java/org/store/clothstar/product/dto/request/UpdateProductRequest.java b/src/main/java/org/store/clothstar/product/dto/request/UpdateProductRequest.java new file mode 100644 index 0000000..81c250c --- /dev/null +++ b/src/main/java/org/store/clothstar/product/dto/request/UpdateProductRequest.java @@ -0,0 +1,38 @@ +package org.store.clothstar.product.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.store.clothstar.product.domain.Product; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.PositiveOrZero; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class UpdateProductRequest { + + @Schema(description = "상품 이름", nullable = false) + @NotBlank(message = "상품 이름을 입력해주세요.") + private String name; + + @Schema(description = "상품 추가 가격") + @PositiveOrZero(message = "추가 가격은 0포함 양수만 입력할 수 있습니다.") + private int extraCharge; + + @Schema(description = "상품 이름", nullable = false) + @PositiveOrZero(message = "0포함 양수를 입력해주세요") + private Long stock; + + public Product toProduct() { + return Product.builder() + .name(name) + .extraCharge(extraCharge) + .stock(stock) + .build(); + } +} diff --git a/src/main/java/org/store/clothstar/product/dto/response/ProductResponse.java b/src/main/java/org/store/clothstar/product/dto/response/ProductResponse.java new file mode 100644 index 0000000..82de436 --- /dev/null +++ b/src/main/java/org/store/clothstar/product/dto/response/ProductResponse.java @@ -0,0 +1,25 @@ +package org.store.clothstar.product.dto.response; + +import lombok.Builder; +import lombok.Getter; +import org.store.clothstar.product.domain.Product; + +@Getter +@Builder +public class ProductResponse { + private Long productId; + private Long productLineId; + private String name; + private int extraCharge; + private Long stock; + + public static ProductResponse from(Product product) { + return ProductResponse.builder() + .productId(product.getProductId()) + .productLineId(product.getProductLineId()) + .name(product.getName()) + .extraCharge(product.getExtraCharge()) + .stock(product.getStock()) + .build(); + } +} diff --git a/src/main/java/org/store/clothstar/product/repository/ProductRepository.java b/src/main/java/org/store/clothstar/product/repository/ProductRepository.java new file mode 100644 index 0000000..4676199 --- /dev/null +++ b/src/main/java/org/store/clothstar/product/repository/ProductRepository.java @@ -0,0 +1,25 @@ +package org.store.clothstar.product.repository; + +import org.apache.ibatis.annotations.Mapper; +import org.store.clothstar.product.domain.Product; +import org.store.clothstar.productLine.dto.response.ProductLineWithProductsResponse; + +import java.util.List; +import java.util.Optional; + +@Mapper +public interface ProductRepository { + + List selectAllProducts(); + + Optional selectByProductId(Long productId); + + Optional selectProductLineWithOptions(Long productId); + + int save(Product product); + + int updateProduct(Product product); + + int deleteProduct(Long productId); + +} diff --git a/src/main/java/org/store/clothstar/product/service/ProductService.java b/src/main/java/org/store/clothstar/product/service/ProductService.java new file mode 100644 index 0000000..13c3dc2 --- /dev/null +++ b/src/main/java/org/store/clothstar/product/service/ProductService.java @@ -0,0 +1,68 @@ +package org.store.clothstar.product.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.server.ResponseStatusException; +import org.store.clothstar.product.domain.Product; +import org.store.clothstar.product.dto.request.CreateProductRequest; +import org.store.clothstar.product.dto.request.UpdateProductRequest; +import org.store.clothstar.product.dto.response.ProductResponse; +import org.store.clothstar.product.repository.ProductRepository; + +@Service +@RequiredArgsConstructor +public class ProductService { + private final ProductRepository productRepository; + + /* + @Transactional(readOnly = true) + public List getAllProduct() { + return productRepository.selectAllProducts().stream() + .map(ProductResponse::from) + .collect(Collectors.toList()); + } + */ + + @Transactional(readOnly = true) + public ProductResponse getProduct(Long productId) { + return productRepository.selectByProductId(productId) + .map(ProductResponse::from) + .orElseThrow(() -> new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "productId :" + productId + "인 상품 옵션 정보를 찾을 수 없습니다.")); + } + + @Transactional + public Long createProduct(@Validated @RequestBody CreateProductRequest createProductRequest) { + Product product = createProductRequest.toProduct(); + productRepository.save(product); + + return product.getProductLineId(); + } + + @Transactional + public void updateProduct(Long productId, UpdateProductRequest updateProductRequest) { + Product product = productRepository.selectByProductId(productId) + .orElseThrow(() -> new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "productId :" + productId + "인 상품 옵션 정보를 찾을 수 없습니다.")); + + product.updateOption(updateProductRequest); + + productRepository.updateProduct(product); + } + + @Transactional + public void deleteProduct(Long productId) { + Product product = productRepository.selectByProductId(productId) + .orElseThrow(() -> new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "productId :" + productId + "인 상품 옵션 정보를 찾을 수 없습니다.")); + + productRepository.deleteProduct(productId); + } +} diff --git a/src/main/java/org/store/clothstar/productLine/controller/ProductLineController.java b/src/main/java/org/store/clothstar/productLine/controller/ProductLineController.java new file mode 100644 index 0000000..14baa69 --- /dev/null +++ b/src/main/java/org/store/clothstar/productLine/controller/ProductLineController.java @@ -0,0 +1,68 @@ +package org.store.clothstar.productLine.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.store.clothstar.common.dto.MessageDTO; +import org.store.clothstar.common.util.URIBuilder; +import org.store.clothstar.productLine.dto.request.CreateProductLineRequest; +import org.store.clothstar.productLine.dto.request.UpdateProductLineRequest; +import org.store.clothstar.productLine.dto.response.ProductLineResponse; +import org.store.clothstar.productLine.dto.response.ProductLineWithProductsResponse; +import org.store.clothstar.productLine.service.ProductLineService; + +import java.net.URI; +import java.util.List; + +@Tag(name = "ProductLines", description = "ProductLine 관련 API 입니다.") +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/productLines") +public class ProductLineController { + + private final ProductLineService productLineService; + + @Operation(summary = "전체 상품 조회", description = "삭제되지 않은 모든 상품을 조회한다.") + @GetMapping + public ResponseEntity> getAllProductLines() { + List productLineResponses = productLineService.getAllProductLines(); + return ResponseEntity.ok().body(productLineResponses); + } + + @Operation(summary = "상품 상세 조회", description = "productLineId로 상품과 하위 옵션들을 상세 조회한다.") + @GetMapping("/{productLineId}") + public ResponseEntity getProductLine(@PathVariable Long productLineId) { + ProductLineWithProductsResponse productLineWithProducts = productLineService.getProductLineWithProducts(productLineId); + return ResponseEntity.ok().body(productLineWithProducts); + } + + @Operation(summary = "상품 등록", description = "카테고리 아이디, 상품 이름, 내용, 가격, 상태를 입력하여 상품을 신규 등록한다.") + @PostMapping + public ResponseEntity createProductLine(@Validated @RequestBody CreateProductLineRequest createProductLineRequest) { + Long productLineId = productLineService.createProductLine(createProductLineRequest); + URI location = URIBuilder.buildURI(productLineId); + + return ResponseEntity.created(location).build(); + } + + @Operation(summary = "상품 수정", description = "상품 이름, 가격, 재고, 상태를 입력하여 상품 정보를 수정한다.") + @PutMapping("/{productLineId}") + public ResponseEntity updateProductLine( + @PathVariable Long productLineId, + @Validated @RequestBody UpdateProductLineRequest updateProductLineRequest) { + + productLineService.updateProductLine(productLineId, updateProductLineRequest); + + return ResponseEntity.ok().body(new MessageDTO(HttpStatus.OK.value(), "ProductLine updated successfully", null)); + } + + @DeleteMapping("/{productLineId}") + public ResponseEntity deleteProductLine(@PathVariable Long productLineId) { + productLineService.setDeletedAt(productLineId); + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/productLine/domain/ProductLine.java b/src/main/java/org/store/clothstar/productLine/domain/ProductLine.java new file mode 100644 index 0000000..d2ad84c --- /dev/null +++ b/src/main/java/org/store/clothstar/productLine/domain/ProductLine.java @@ -0,0 +1,45 @@ +package org.store.clothstar.productLine.domain; + +import lombok.*; +import org.store.clothstar.productLine.domain.type.ProductLineStatus; +import org.store.clothstar.productLine.dto.request.UpdateProductLineRequest; + +import java.time.LocalDateTime; + +@Builder +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class ProductLine { + private Long productLineId; + private Long memberId; + private Long categoryId; + private String name; + private String content; + private int price; + private Long totalStock; + private ProductLineStatus status; + private Long saleCount; + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; + private LocalDateTime deletedAt; + private String brandName; + private String biz_no; + + public void updateProductLine(UpdateProductLineRequest updateProductLineRequest) { + this.name = updateProductLineRequest.getName(); + this.content = updateProductLineRequest.getContent(); + this.price = updateProductLineRequest.getPrice(); + this.status = updateProductLineRequest.getStatus(); + this.modifiedAt = LocalDateTime.now(); + } + + public void changeProductStatus(ProductLineStatus productLineStatus) { + this.status = productLineStatus; + } + + public void setDeletedAt() { + this.deletedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/productLine/domain/type/ProductLineStatus.java b/src/main/java/org/store/clothstar/productLine/domain/type/ProductLineStatus.java new file mode 100644 index 0000000..4a7a6d9 --- /dev/null +++ b/src/main/java/org/store/clothstar/productLine/domain/type/ProductLineStatus.java @@ -0,0 +1,13 @@ +package org.store.clothstar.productLine.domain.type; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum ProductLineStatus { + COMING_SOON, + FOR_SALE, + ON_SALE, + SOLD_OUT, + HIDDEN, + DISCONTINUED +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/productLine/dto/request/CreateProductLineRequest.java b/src/main/java/org/store/clothstar/productLine/dto/request/CreateProductLineRequest.java new file mode 100644 index 0000000..3ac5cfc --- /dev/null +++ b/src/main/java/org/store/clothstar/productLine/dto/request/CreateProductLineRequest.java @@ -0,0 +1,56 @@ +package org.store.clothstar.productLine.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.store.clothstar.productLine.domain.ProductLine; +import org.store.clothstar.productLine.domain.type.ProductLineStatus; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Positive; +import javax.validation.constraints.Size; +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CreateProductLineRequest { + @Schema(description = "카테고리 아이디", nullable = false) + @Positive(message = "카테고리 id는 0보다 큰 양수입니다.") + private Long categoryId; + + @Schema(description = "상품 이름", nullable = false) + @NotBlank(message = "상품 이름을 입력해주세요.") + @Size(max = 30) + private String name; + + @Schema(description = "상품 설명", nullable = false) + @NotBlank(message = "상품 설명을 입력해주세요.") + private String content; + + @Schema(description = "상품 가격", nullable = false) + @Positive(message = "상품 가격은 0보다 커야 합니다.") + private int price; + + @Schema(description = "상품 상태", nullable = false) + @Builder.Default + private ProductLineStatus status = ProductLineStatus.COMING_SOON; + + + public ProductLine toProductLine(Long memberId) { + return ProductLine.builder() + .memberId(memberId) + .categoryId(categoryId) + .name(name) + .content(content) + .price(price) + .totalStock(0L) + .status(status) + .createdAt(LocalDateTime.now()) + .saleCount(0L) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/productLine/dto/request/UpdateProductLineRequest.java b/src/main/java/org/store/clothstar/productLine/dto/request/UpdateProductLineRequest.java new file mode 100644 index 0000000..c7c956b --- /dev/null +++ b/src/main/java/org/store/clothstar/productLine/dto/request/UpdateProductLineRequest.java @@ -0,0 +1,36 @@ +package org.store.clothstar.productLine.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.store.clothstar.productLine.domain.type.ProductLineStatus; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Positive; +import javax.validation.constraints.Size; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class UpdateProductLineRequest { + @Schema(description = "상품 이름", nullable = false) + @NotBlank(message = "상품 이름을 입력해주세요.") + @Size(max = 30) + private String name; + + @Schema(description = "상품 설명", nullable = false) + @NotBlank(message = "상품 설명을 입력해주세요.") + private String content; + + @Schema(description = "상품 가격", nullable = false) + @Positive(message = "상품 가격은 0보다 커야 합니다.") + private int price; + + @Schema(description = "상품 상태", nullable = false) + @Builder.Default + private ProductLineStatus status = ProductLineStatus.COMING_SOON; + +} diff --git a/src/main/java/org/store/clothstar/productLine/dto/response/ProductLineDetailResponse.java b/src/main/java/org/store/clothstar/productLine/dto/response/ProductLineDetailResponse.java new file mode 100644 index 0000000..85474da --- /dev/null +++ b/src/main/java/org/store/clothstar/productLine/dto/response/ProductLineDetailResponse.java @@ -0,0 +1,42 @@ +package org.store.clothstar.productLine.dto.response; + +import lombok.Builder; +import lombok.Getter; +import org.store.clothstar.productLine.domain.ProductLine; +import org.store.clothstar.productLine.domain.type.ProductLineStatus; + +import java.time.LocalDateTime; + +@Getter +@Builder +public class ProductLineDetailResponse { + private Long productId; + private String name; + private String brandName; + private String content; + private int price; + private Long totalStock; + private Long saleCount; + private ProductLineStatus productLineStatus; + private String biz_no; + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; + private LocalDateTime deletedAt; + + public static ProductLineDetailResponse from(ProductLine productLine) { + return ProductLineDetailResponse.builder() + .productId(productLine.getProductLineId()) + .name(productLine.getName()) + .content(productLine.getContent()) + .brandName(productLine.getBrandName()) + .price(productLine.getPrice()) + .totalStock(productLine.getTotalStock()) + .saleCount(productLine.getSaleCount()) + .productLineStatus(productLine.getStatus()) + .biz_no(productLine.getBiz_no()) + .createdAt(productLine.getCreatedAt()) + .modifiedAt(productLine.getModifiedAt()) + .deletedAt(productLine.getDeletedAt()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/productLine/dto/response/ProductLineResponse.java b/src/main/java/org/store/clothstar/productLine/dto/response/ProductLineResponse.java new file mode 100644 index 0000000..19ffa91 --- /dev/null +++ b/src/main/java/org/store/clothstar/productLine/dto/response/ProductLineResponse.java @@ -0,0 +1,32 @@ +package org.store.clothstar.productLine.dto.response; + +import lombok.Builder; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.store.clothstar.productLine.domain.ProductLine; +import org.store.clothstar.productLine.domain.type.ProductLineStatus; + +@Getter +@Builder +@Slf4j +public class ProductLineResponse { + private Long productLineId; + private String brandName; + private String name; + private String content; + private int price; + // private Long totalStock; + private ProductLineStatus productLineStatus; + + public static ProductLineResponse from(ProductLine productLine) { + return ProductLineResponse.builder() + .productLineId(productLine.getProductLineId()) + .brandName(productLine.getBrandName()) + .name(productLine.getName()) + .content(productLine.getContent()) + .price(productLine.getPrice()) +// .totalStock(productLine.getTotalStock()) + .productLineStatus(productLine.getStatus()) + .build(); + } +} diff --git a/src/main/java/org/store/clothstar/productLine/dto/response/ProductLineWithProductsResponse.java b/src/main/java/org/store/clothstar/productLine/dto/response/ProductLineWithProductsResponse.java new file mode 100644 index 0000000..592a1ac --- /dev/null +++ b/src/main/java/org/store/clothstar/productLine/dto/response/ProductLineWithProductsResponse.java @@ -0,0 +1,31 @@ +package org.store.clothstar.productLine.dto.response; + +import lombok.*; +import org.store.clothstar.product.domain.Product; +import org.store.clothstar.productLine.domain.type.ProductLineStatus; + +import java.time.LocalDateTime; +import java.util.List; + +@Builder +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class ProductLineWithProductsResponse { + private Long productLineId; + private Long memberId; + private Long categoryId; + private String name; + private String content; + private int price; + private Long totalStock; + private ProductLineStatus status; + private Long saleCount; + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; + private LocalDateTime deletedAt; + private String brandName; + private String biz_no; + private List productList; +} diff --git a/src/main/java/org/store/clothstar/productLine/productLineREADME.md b/src/main/java/org/store/clothstar/productLine/productLineREADME.md new file mode 100644 index 0000000..951863b --- /dev/null +++ b/src/main/java/org/store/clothstar/productLine/productLineREADME.md @@ -0,0 +1,118 @@ +# productLine 패키지 README + +## 패키지 개요 + +이 패키지는 상품에 대한 기능을 구현하기 위한 패키지이다. + +### 상품 생성 및 등록 설계안 + +1. 판매자 Role 유효성 검사 + - 판매자의 Role로 로그인된 상태에서만 상품 생성 가능 +2. 상품 정보 입력 + - 상품명 + - 카테고리 + - 가격 + - 상품 대표 이미지 + - 상품 이미지 List + - 상품 상태 : [준비중 → 판매중 / 할인중 → 품절 → 숨김 → 단종 → 삭제] + 1. 준비중 - `Coming Soon` : 곧 판매될 예정이지만, 아직 판매가 시작되지 않은 상태입니다. + 2. 판매중 - `ForSale` : 고객이 구매할 수 있는 상태 + 3. 할인중 - `OnSale` : 판매중의 하위 상태 + 4. 품절 - `SoldOut` : 재고가 없어서 현재 구매할 수 없는 상태 + 5. 숨김 - `Hidden` : 고객에게 검색이나 카테고리 등에 노출되지 않도록 숨겨진 상태 + 6. 단종 - `Discontinued` : 재고가 없으며, 더 이상 판매되지 않는 상태 + 7. 삭제 - `deleted` : 판매자가 해당 상품을 삭제한 상태 + - 재고 + - 등록일시 + - 수정일시 +3. 상품 판매 + - 상품 상태가 `판매중`인 경우만 판매 가능한 상태 + +### (판매자) 등록 상품 리스트 조회 설계안 + +1. 판매자 Role 유효성 검사 + - 판매자의 Role로 로그인된 상태에서만 판매중인 상품 리스트 조회 가능 + +2. 등록 상품 리스트 확인 + - 등록 한 상품들에 대한 리스트 조회 + - 정렬 옵션 : [최신등록순/판매량순/상품 상태별] + +### (판매자) 등록 상품 상세 조회 / 수정 / 삭제 설계안 + +1. 등록 상품 상세 조회 + - 해당 상품 식별자 ID로 상품 상세 조회 + - 상품 상태가 4. 품절까지인 상품들에 대해 조회 가능 +2. 등록 상품 수정 + - 해당 상품 식별자 ID로 상품 수정 +3. 등록 상품 삭제 + - 재고가 0이 아닐 경우 경고 알림 + - 등록 상품 삭제 시 `isDeleted` 필드값을 `true` 로 변경, 더이상 리스트에서 조회 불가능 + +### (사용자) 상품 리스트 조회 + +1. 카테고리별 조회 + +- 1차 카테고리 : 남자, 여자, 랭킹 상품, 신상품 +- 2차 카테고리 : 아우터, 상의, 하의, 이너, 신발, 가방 + +2. 조건 검색 + +- 상품명이나 브랜드를 입력하여 검색가능 +- 품절상품 보기 옵션 선택가능 +- 정렬 옵션 가능 + [정렬 옵션] + - default: 신상품 순 + - 신상품순/낮은 가격/높은 가격/후기 많은 순/판매순/추천순 + +3. 하나의 상품에 상품 이미지, 이름, 브랜드, 가격, 할인율, 멤버십 가격, 별점, 후기 개수 출력 + +4. 페이징 출력 + +### API 디자인 + +- (판매자) 상품 등록: **POST** `/v1/productLines` + - 프로세스 + 1. 상품 정보 입력 + - 필수 입력 : 상품명, 대표 이미지, 가격, 재고, 상품 상세 설명, 카테고리 + - 추가 입력 : 상세 이미지 + 2. 재고 + - 재고가 0일 경우 상품 상태 `품절`로 변경 (PATCH), 판매자에게 재고 0 알림 전송 + - 주문 취소, 환불 상품에 대해 재고 추가로 들어오는 경우 재고 수량 변경 (PATCH) + +- (판매자) 등록 상품 리스트 조회: **GET** `/v1/productLines/?page={번호}&size={페이지당 상품 개수}` + - 프로세스 + 1. 판매자 Role 유효성 검사 + - 판매자의 Role로 로그인된 상태에서만 판매중인 상품 리스트 조회 + 2. 등록 리스트 정렬 옵션 + - 정렬 옵션 : [최신등록순/판매량순/상품 상태별] + +- (판매자) 상품 상세 조회: **GET** `/v1/productLines/{productId}` + - 프로세스 + 1. 주문 조회 + - 주문 상세 내역: 주문일자, 주문번호, 배송 진행 상태(+ 택배사 이름, 운송장 번호 - 판매자가 배송 보내면 입력됨), 주문 상품 정보(상품 이름, 브랜드 이름, 가격, 수량) + - 구매자 정보: 구매자 이름, 이메일 주소, 연락처 + 2. 구매 확정시(구매자가 구매 확정을 하는 경우) + - 주문상태가 구매 확정이 됨(PATCH) + - 주문상태: [ 주문 승인 -> 배송 완료 -> 구매 확정 ] + - 상태의 변경 과정을 validation 한다(주문 승인에서 바로 구매 확정으로 넘어가지 않도록) +- (판매자)상품 수정 : **PUT** `/v1/productLines/{productId}` + 1. 상품 수정 조건 + - 판매자 본인이 등록한상품에 대해서 수정 가능 + 2. 상품 수정 + - 상품명, 설명, 가격, 사진 등 상품 정보 수정 + +- (판매자)상품 취소 : **PATCH** `/v1/productLines/{productId}` + - 프로세스 + 1. 삭제 조건 + - 판매자 본인이 등록한상품에 대해서 삭제 가능 + - 상품이 판매중이 아닐 경우에만 삭제 가능 (판매중인 상품은 판매 중단 처리 필요) + 2. 상품 삭제 + - 판매중이 아닌 상태로 변경 + - 상품 삭제 + +- (구매자) 상품 리스트, 상세 조회 : + 상품 리스트 조회: **GET** /v1/productLines/?page={번호}&size={페이지당 상품 개수}&searchType={검색유형}&searchValue={검색값} + 상품 상세 조회 : **GET** /v1/productLines/{productId} + 카테고리별 조회: **GET** /v1/productLines/{category}?page={번호}&size={페이지당 게시글 개수} + - 프로세스 + 1. diff --git a/src/main/java/org/store/clothstar/productLine/repository/ProductLineRepository.java b/src/main/java/org/store/clothstar/productLine/repository/ProductLineRepository.java new file mode 100644 index 0000000..d911452 --- /dev/null +++ b/src/main/java/org/store/clothstar/productLine/repository/ProductLineRepository.java @@ -0,0 +1,24 @@ +package org.store.clothstar.productLine.repository; + +import org.apache.ibatis.annotations.Mapper; +import org.store.clothstar.productLine.domain.ProductLine; +import org.store.clothstar.productLine.dto.response.ProductLineWithProductsResponse; + +import java.util.List; +import java.util.Optional; + +@Mapper +public interface ProductLineRepository { + + List selectAllProductLinesNotDeleted(); + + Optional selectByProductLineId(Long productId); + + Optional selectProductLineWithOptions(Long productId); + + int save(ProductLine productLine); + + int updateProductLine(ProductLine productLine); + + int setDeletedAt(ProductLine productLine); +} \ No newline at end of file diff --git a/src/main/java/org/store/clothstar/productLine/service/ProductLineService.java b/src/main/java/org/store/clothstar/productLine/service/ProductLineService.java new file mode 100644 index 0000000..e226eb6 --- /dev/null +++ b/src/main/java/org/store/clothstar/productLine/service/ProductLineService.java @@ -0,0 +1,91 @@ +package org.store.clothstar.productLine.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.server.ResponseStatusException; +import org.store.clothstar.product.domain.Product; +import org.store.clothstar.productLine.domain.ProductLine; +import org.store.clothstar.productLine.dto.request.CreateProductLineRequest; +import org.store.clothstar.productLine.dto.request.UpdateProductLineRequest; +import org.store.clothstar.productLine.dto.response.ProductLineResponse; +import org.store.clothstar.productLine.dto.response.ProductLineWithProductsResponse; +import org.store.clothstar.productLine.repository.ProductLineRepository; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ProductLineService { + + private final ProductLineRepository productLineRepository; + + @Transactional(readOnly = true) + public List getAllProductLines() { + return productLineRepository.selectAllProductLinesNotDeleted().stream() + .map(ProductLineResponse::from) + .collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public Optional getProductLine(Long productLineId) { + return productLineRepository.selectByProductLineId(productLineId) + .map(ProductLineResponse::from); + } + + @Transactional(readOnly = true) + public ProductLineWithProductsResponse getProductLineWithProducts(Long productLineId) { + ProductLineWithProductsResponse productLineWithProducts = productLineRepository.selectProductLineWithOptions(productLineId) + .orElseThrow(() -> new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "productLineId :" + productLineId + "인 상품 및 옵션 정보를 찾을 수 없습니다.")); + List productList = productLineWithProducts.getProductList(); + + Long totalStock = 0L; + for (Product product : productList) { + totalStock += product.getStock(); + } + + productLineWithProducts.setTotalStock(totalStock); + + return productLineWithProducts; + } + + @Transactional + public Long createProductLine(CreateProductLineRequest createProductLineRequest) { + Long memberId = 1L; + ProductLine product = createProductLineRequest.toProductLine(memberId); + productLineRepository.save(product); + return product.getProductLineId(); + } + + @Transactional + public void updateProductLine(Long productLineId, UpdateProductLineRequest updateProductLineRequest) { + ProductLine productLine = productLineRepository.selectByProductLineId(productLineId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "상품 정보를 찾을 수 없습니다.")); + + productLine.updateProductLine(updateProductLineRequest); + + productLineRepository.updateProductLine(productLine); + } + + @Transactional + public void setDeletedAt(Long productId) { + ProductLine productLine = productLineRepository.selectByProductLineId(productId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "상품 정보를 찾을 수 없습니다.")); + + productLine.setDeletedAt(); + + ProductLine prodcutLine = productLineRepository.selectByProductLineId(productId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "상품 정보를 찾을 수 없습니다.")); + + prodcutLine.setDeletedAt(); + + productLineRepository.setDeletedAt(prodcutLine); + } +} \ No newline at end of file diff --git a/src/main/resources/application-db.yml b/src/main/resources/application-db.yml new file mode 100644 index 0000000..0cdec52 --- /dev/null +++ b/src/main/resources/application-db.yml @@ -0,0 +1,63 @@ +#default 공통설정 +# jpa: +# show-sql: true +# properties: +# jdbc: +# time_zone: Asia/Seoul +# hibernate: +# format_sql: true +# defer-datasource-initialization: true +sql: + init: + mode: always + +mybatis: + mapper-locations: classpath:/mappers/**.xml + config-location: classpath:/config/mybatis-config.xml + +logging: + level: + 'org.springframework.jdbc': debug + 'org.store.clothstar': debug + +jwt: + secret_key: Y2xvdGhzaG9wcGluZ21hbGxjbG90aHN0YXJjbG90aHNob3BwaW5nbWFsbGNsb3Roc3RhcmNsb3Roc2hvcHBpbmdtYWxsY2xvdGhzdGFyY2xvdGhzaG9wcGluZ21hbGxjbG90aHN0YXIK + accessTokenValidTimeMillis: 120000 + refreshTokenValidTimeMillis: 1200000 + +--- # local +spring: + config: + activate: + on-profile: "db-local" + # jpa: + # show-sql: true + # database-platform: H2 + # hibernate: + # ddl-auto: create + datasource: + url: jdbc:h2:mem:localdb + h2: + console: + enabled: true + +--- #dev +spring: + sql: + init: + platform: mysql + config: + activate: + on-profile: "db-dev" + thymeleaf: + cache: false + # jpa: + # database-platform: org.hibernate.dialect.MySQLDialect + # hibernate: + # ddl-auto: create + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://clothstar-v82.cpmu4ewuelxk.ap-northeast-2.rds.amazonaws.com/clothstar82 + username: admin + password: clothstar010101 + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7776386..0495a04 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,5 +1,19 @@ spring: - datasource: - url: jdbc:h2:tcp://localhost/~/test - driver-class-name: org.h2.Driver - username: sa \ No newline at end of file + profiles: + active: + - local + group: + local: + - db-local + dev: + - db-dev + include: + - db + +springdoc: + default-consumes-media-type: application/json # 소비 미디어 타입 + default-produces-media-type: application/json # 생산 미디어 타입 + swagger-ui: + operations-sorter: method # operations 정렬 방식은 HTTP Method 순 + tags-sorter: alpha # tag 정렬 방식은 알파벳 순 + path: "swagger.html" # http://localhost:8080/swagger.html로 접속 가능 diff --git a/src/main/resources/config/mybatis-config.xml b/src/main/resources/config/mybatis-config.xml new file mode 100644 index 0000000..6d9d435 --- /dev/null +++ b/src/main/resources/config/mybatis-config.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mappers/Address.xml b/src/main/resources/mappers/Address.xml new file mode 100644 index 0000000..b4a9d00 --- /dev/null +++ b/src/main/resources/mappers/Address.xml @@ -0,0 +1,23 @@ + + + + + + + + + + insert into address ( + member_id, receiver_name, zip_no, address_basic, address_detail, tel_no, delivery_request, default_address + ) values ( + #{memberId}, #{receiverName}, #{zipNo}, #{addressBasic}, #{addressDetail}, #{telNo}, #{deliveryRequest}, + #{defaultAddress}); + + \ No newline at end of file diff --git a/src/main/resources/mappers/Member.xml b/src/main/resources/mappers/Member.xml new file mode 100644 index 0000000..8c6cfaa --- /dev/null +++ b/src/main/resources/mappers/Member.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + update member + set role = #{role}, modified_at = #{modifiedAt} + where member_id = #{memberId} + + + + + insert into member + (email, password, name, tel_no, total_payment_price, point, role, grade, created_at) + values + (#{email}, #{password}, #{name}, #{telNo}, #{totalPaymentPrice}, #{point}, #{role}, #{grade}, #{createdAt}) + + \ No newline at end of file diff --git a/src/main/resources/mappers/Order.xml b/src/main/resources/mappers/Order.xml new file mode 100644 index 0000000..b4dfe6f --- /dev/null +++ b/src/main/resources/mappers/Order.xml @@ -0,0 +1,31 @@ + + + + + + + + INSERT INTO orders( order_id, member_id, address_id, total_shipping_price, created_at, + status, total_products_price, payment_method, total_payment_price ) + VALUES( #{orderId}, #{memberId}, #{addressId}, #{totalShippingPrice}, #{createdAt}, + #{status}, #{totalProductsPrice}, #{paymentMethod}, #{totalPaymentPrice} ) + + + + UPDATE orders + SET status = 'CONFIRM' + WHERE order_id=#{orderId} + + + + + UPDATE orders + SET total_products_price = #{totalProductsPrice}, + total_payment_price = #{totalPaymentPrice} + WHERE order_id = #{orderId} + + \ No newline at end of file diff --git a/src/main/resources/mappers/OrderDetail.xml b/src/main/resources/mappers/OrderDetail.xml new file mode 100644 index 0000000..82d4e3e --- /dev/null +++ b/src/main/resources/mappers/OrderDetail.xml @@ -0,0 +1,17 @@ + + + + + + + INSERT INTO order_detail( order_detail_id, product_line_id, order_id, product_id, quantity, fixed_price, + onekind_total_price, name, stock, option_name, brand_name + ) + VALUES( #{orderDetailId}, #{productLineId}, #{orderId}, #{productId}, #{quantity}, #{fixedPrice}, + #{oneKindTotalPrice}, #{name}, #{stock}, #{optionName}, #{brandName} ) + + + \ No newline at end of file diff --git a/src/main/resources/mappers/OrderSeller.xml b/src/main/resources/mappers/OrderSeller.xml new file mode 100644 index 0000000..cbbea21 --- /dev/null +++ b/src/main/resources/mappers/OrderSeller.xml @@ -0,0 +1,22 @@ + + + + + + + + UPDATE orders + SET status = 'APPROVE' + WHERE order_id=#{orderId} + + + + UPDATE orders + SET status = 'CANCEL' + WHERE order_id=#{orderId} + + \ No newline at end of file diff --git a/src/main/resources/mappers/ProductLineMapper.xml b/src/main/resources/mappers/ProductLineMapper.xml new file mode 100644 index 0000000..f7b0d44 --- /dev/null +++ b/src/main/resources/mappers/ProductLineMapper.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO product_line(member_id, category_id, name, content, price, total_stock, sale_count, status, created_at) + VALUES (#{memberId}, #{categoryId}, #{name}, #{content}, #{price}, #{totalStock}, #{saleCount}, #{status}, #{createdAt}) + + + update product_line set + name = #{name}, + content = #{content}, + price = #{price}, + modified_at = #{modifiedAt} + where product_line_id = #{productLineId} + + + update product_line set + deleted_at = #{deletedAt} + where product_line_id = #{productLineId} + + \ No newline at end of file diff --git a/src/main/resources/mappers/ProductMapper.xml b/src/main/resources/mappers/ProductMapper.xml new file mode 100644 index 0000000..978acd4 --- /dev/null +++ b/src/main/resources/mappers/ProductMapper.xml @@ -0,0 +1,37 @@ + + + + + + + + INSERT INTO product(product_line_id, name, extra_charge, stock) + VALUES (#{productLineId}, #{name}, #{extraCharge}, #{stock}) + + + update product set + name = #{name}, + extra_charge = #{extraCharge}, + stock = #{stock} + where product_id = #{productId} + + + delete from product + where product_id = #{productId} + + + \ No newline at end of file diff --git a/src/main/resources/mappers/Seller.xml b/src/main/resources/mappers/Seller.xml new file mode 100644 index 0000000..4e0d83e --- /dev/null +++ b/src/main/resources/mappers/Seller.xml @@ -0,0 +1,15 @@ + + + + + + + + insert into seller(member_id, brand_name, biz_no, total_sell_price, created_at) + values (#{memberId}, #{brandName}, #{bizNo}, #{totalSellPrice}, #{createdAt}); + + \ No newline at end of file diff --git a/src/main/resources/mappers/categoryMapper.xml b/src/main/resources/mappers/categoryMapper.xml new file mode 100644 index 0000000..ba86b51 --- /dev/null +++ b/src/main/resources/mappers/categoryMapper.xml @@ -0,0 +1,29 @@ + + + + + + + + INSERT INTO category(category_id, category_type) + VALUES (#{categoryId}, #{categoryType}) + + + update category set + category_type = #{categoryType} + where category_id = #{categoryId} + + \ No newline at end of file diff --git a/src/main/resources/sql/common.sql b/src/main/resources/sql/common.sql new file mode 100644 index 0000000..f6df244 --- /dev/null +++ b/src/main/resources/sql/common.sql @@ -0,0 +1 @@ +CREATE DATABASE dev_clothstar default CHARACTER SET UTF8; \ No newline at end of file diff --git a/src/main/resources/sql/member.sql b/src/main/resources/sql/member.sql new file mode 100644 index 0000000..d74b370 --- /dev/null +++ b/src/main/resources/sql/member.sql @@ -0,0 +1,103 @@ +CREATE TABLE `member` +( + `member_id` BIGINT NOT NULL auto_increment, + `email` varchar(255) NOT NULL, + `password` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `tel_no` varchar(255) NOT NULL, + `total_payment_price` INT NULL, + `point` INT NULL, + `role` varchar(100) NOT NULL COMMENT 'ADMIN, SELLER, USER', + `grade` varchar(100) NOT NULL COMMENT 'BRONZE, SILVER, GOLD, PLATINUM, DIAMOND', + `created_at` timestamp NOT NULL, + `modified_at` timestamp NULL, + `deleted_at` timestamp NULL, + + CONSTRAINT PK_member PRIMARY KEY (member_id), + CONSTRAINT UK_member_email UNIQUE (email) +); + +DROP TABLE IF EXISTS `member`; + +DROP TABLE IF EXISTS `address`; + +CREATE TABLE `address` +( + `address_id` BIGINT NOT NULL auto_increment, + `member_id` BIGINT NOT NULL, + `receiver_name` varchar(255) NULL, + `zip_no` varchar(255) NOT NULL, + `address_basic` varchar(255) NOT NULL, + `address_detail` varchar(255) NOT NULL, + `tel_no` varchar(255) NOT NULL, + `delivery_request` varchar(255) NULL, + `default_address` boolean NOT NULL DEFAULT 0, + + CONSTRAINT PK_ADDRESS PRIMARY KEY (address_id) +); + +DROP TABLE IF EXISTS `seller`; + +CREATE TABLE `seller` +( + `member_id` BIGINT NOT NULL, + `brand_name` varchar(255) NOT NULL, + `biz_no` varchar(255) NULL, + `total_sell_price` int NULL, + `created_at` timestamp NOT NULL, + + CONSTRAINT PK_seller PRIMARY KEY (member_id) +); + +select * +from member; +select * +from address; +select * +from seller; + +select * +from information_schema.table_constraints +where constraint_schema = 'dev_clothstar'; + +select * +from information_schema.TABLES +where TABLE_SCHEMA = 'dev_clothstar'; + +select * +from address; +select * +from seller; +select * +from productLine; +select * +from member; + +select * +from information_schema.table_constraints +where constraint_schema = 'dev_clothstar' + and CONSTRAINT_TYPE = 'FOREIGN KEY'; + +alter table `productLine` + drop foreign key `FK_seller_TO_product_1`; + +alter table `seller` + drop foreign key `FK_member_TO_seller_1`; + +ALTER TABLE `seller` + ADD CONSTRAINT `FK_member_TO_seller_1` FOREIGN KEY (`member_id`) + REFERENCES `member` (`member_id`); + +ALTER TABLE `productLine` + ADD CONSTRAINT `FK_seller_TO_product_1` FOREIGN KEY (`member_id`) + REFERENCES `seller` (`member_id`); + +INSERT INTO clothstar.seller (member_id, brand_name, biz_no, total_sell_price, created_at) +VALUES (3, '내셔널지오그래픽키즈 제주점', '232-05-02861', 3000000, '2024-03-29 03:59:07'); + +select * +from address +where member_id = 1; + +select * +from member; \ No newline at end of file diff --git a/src/main/resources/sql/order_detail.sql b/src/main/resources/sql/order_detail.sql new file mode 100644 index 0000000..415e113 --- /dev/null +++ b/src/main/resources/sql/order_detail.sql @@ -0,0 +1,39 @@ +DROP TABLE IF EXISTS `order_detail`; + +CREATE TABLE `order_detail` +( + `order_detail_id` BIGINT NOT NULL AUTO_INCREMENT, + `product_line_id` BIGINT NOT NULL, + `order_id` BIGINT NOT NULL, + `product_id` BIGINT NOT NULL, + `quantity` int NOT NULL, + `fixed_price` int NOT NULL, + `onekind_total_price` int NOT NULL, + `name` VARCHAR(255) NOT NULL, + `stock` VARCHAR(255) NOT NULL, + `option_name` VARCHAR(255) NOT NULL, + `brand_name` VARCHAR(255) NOT NULL, + + PRIMARY KEY (`order_detail_id`) +); + +select * +from order_detail; + +ALTER TABLE `order_detail` + ADD CONSTRAINT `FK_orders_TO_orderDetail_1` FOREIGN KEY (`order_id`) + REFERENCES `orders` (`order_id`); + +ALTER TABLE `order_detail` + ADD CONSTRAINT `FK_ProductLine_TO_orderDetail_1` FOREIGN KEY (`product_line_id`) + REFERENCES product_line (`product_line_id`); + +ALTER TABLE order_detail + DROP FOREIGN KEY `FK_ProductLine_TO_orderDetail_1`; + +ALTER TABLE `order_detail` + ADD CONSTRAINT `FK_Product_TO_orderDetail_1` FOREIGN KEY (`product_id`) + REFERENCES `product` (`product_id`); + +ALTER TABLE order_detail + DROP FOREIGN KEY `FK_Product_TO_orderDetail_1`; \ No newline at end of file diff --git a/src/main/resources/sql/orders.sql b/src/main/resources/sql/orders.sql new file mode 100644 index 0000000..5eab62a --- /dev/null +++ b/src/main/resources/sql/orders.sql @@ -0,0 +1,63 @@ +DROP TABLE IF EXISTS orders; + +CREATE TABLE orders +( + `order_id` bigint NOT NULL, + `member_id` bigint NOT NULL, + `address_id` bigint NOT NULL, + `created_at` timestamp NOT NULL, + `status` varchar(255) NOT NULL, + `total_shipping_price` int NOT NULL, + `total_products_price` int NOT NULL, + `payment_method` varchar(255) NOT NULL, + `total_payment_price` int NOT NULL, + + PRIMARY KEY (`order_id`) +); + +ALTER TABLE orders + DROP PRIMARY KEY; + +select * +from orders; +select * +from member; +select * +from address; +select * +from order_detail; +select * +from product_line; +select * +from product; + +ALTER TABLE `orders` + ADD CONSTRAINT `FK_member_TO_orders_1` FOREIGN KEY (`member_id`) + REFERENCES `member` (`member_id`); + +ALTER TABLE orders + DROP FOREIGN KEY `FK_member_TO_orders_1`; + +ALTER TABLE `orders` + ADD CONSTRAINT `FK_address_TO_orders_1` FOREIGN KEY (`address_id`) + REFERENCES `address` (`address_id`); + +ALTER TABLE orders + DROP FOREIGN KEY `FK_address_TO_orders_1`; + +SHOW CREATE TABLE orders; +SHOW CREATE TABLE address; +SHOW CREATE TABLE member; + +show index from orders; + +drop index FK_member_TO_orders_1 on orders; + +DELETE +FROM orders; + +INSERT INTO orders (order_id, member_id, address_id, created_at, status, total_shipping_price, total_products_price, + payment_method, total_payment_price) +VALUES ('14241232', '242', '334', CURRENT_TIMESTAMP, 'WAITING', '3000', '50000', 'CARD', '53000'); + + diff --git a/src/main/resources/sql/product-category-option.sql b/src/main/resources/sql/product-category-option.sql new file mode 100644 index 0000000..75d810f --- /dev/null +++ b/src/main/resources/sql/product-category-option.sql @@ -0,0 +1,61 @@ +DROP TABLE IF EXISTS product; +DROP TABLE IF EXISTS product_line; +DROP TABLE IF EXISTS category; + + +CREATE TABLE `category` ( + `category_id` BIGINT NOT NULL AUTO_INCREMENT, + `category_type` VARCHAR(20) NOT NULL, + PRIMARY KEY (`category_id`) +); + +CREATE TABLE `productLine` ( + `product_line_id` BIGINT NOT NULL AUTO_INCREMENT, + `member_id` BIGINT NOT NULL, + `category_id` BIGINT NOT NULL, + `name` VARCHAR(30) NOT NULL, + `content` TEXT NULL NOT NULL, + `price` INT NOT NULL, + `total_stock` INT NOT NULL, + `status` VARCHAR(20) NOT NULL COMMENT '준비중, 판매중, 할인중, 품절. 숨김, 단종', + `sale_count` BIGINT NOT NULL, + `created_at` TIMESTAMP NOT NULL, + `modified_at` TIMESTAMP, + `deleted_at` TIMESTAMP, + PRIMARY KEY (`product_line_id`), + FOREIGN KEY (`member_id`) REFERENCES seller (`member_id`), + FOREIGN KEY (`category_id`) REFERENCES `category` (`category_id`) +); + + +CREATE TABLE `product` ( + `product_id` BIGINT NOT NULL AUTO_INCREMENT, + `product_line_id` BIGINT NOT NULL, + `name` VARCHAR(20) NOT NULL, + `value` VARCHAR(20), + `stock` BIGINT, + PRIMARY KEY (`product_id`), + FOREIGN KEY (`product_line_id`) REFERENCES `productLine` (`product_line_id`) +); + + +select * from seller; +select * from member; +select * from product_line; +select * from product; +select * from category; + + +INSERT INTO category (category_type) VALUES ('상의'); + +alter table product_line modify column sale_count bigint after status; +ALTER TABLE product_line ADD content TEXT AFTER name; +SELECT * FROM category WHERE category_id = 2; +ALTER TABLE product_line + MODIFY COLUMN total_stock BIGINT; + +ALTER TABLE product + MODIFY COLUMN value INT; + +ALTER TABLE product + CHANGE COLUMN extra_fee extra_charge INT; diff --git a/src/main/resources/static/js/index.js b/src/main/resources/static/js/index.js new file mode 100644 index 0000000..67568ba --- /dev/null +++ b/src/main/resources/static/js/index.js @@ -0,0 +1,120 @@ +const userPageButton = document.getElementById("userPage-btn"); +const sellerPageButton = document.getElementById("sellerPage-btn"); +const adminPageButton = document.getElementById("adminPage-btn"); +const logoutButton = document.getElementById("logout-btn"); +const refreshButton = document.getElementById("refresh-btn"); + +if (userPageButton) { + userPageButton.addEventListener("click", (event) => { + const accessToken = localStorage.getItem("ACCESS_TOKEN"); + let headersObj = new Object(); + headersObj["Content-Type"] = "application/json"; + + if (accessToken) { + headersObj["Authorization"] = accessToken; + } + + fetch(`/user`, { + method: "GET", + headers: headersObj, + }).then((res) => { + if (!res.ok) { + throw new Error(`${res.status}`); + } + return res.json(); + }).then((res) => { + alert(JSON.stringify(res)); + location.href = "/userPage"; + }).catch((error) => { + alert(error); + }); + }) +} + +if (sellerPageButton) { + sellerPageButton.addEventListener("click", (event) => { + const accessToken = localStorage.getItem("ACCESS_TOKEN"); + let headersObj = new Object(); + headersObj["Content-Type"] = "application/json"; + + if (accessToken) { + headersObj["Authorization"] = accessToken; + } + + fetch(`/seller`, { + method: "GET", + headers: headersObj, + }).then((res) => { + if (!res.ok) { + throw new Error(`${res.status}`); + } + return res.json(); + }).then((res) => { + alert(JSON.stringify(res)); + location.href = "/sellerPage"; + }).catch((error) => { + alert(error); + }); + }) +} + + +if (adminPageButton) { + adminPageButton.addEventListener("click", (event) => { + const accessToken = localStorage.getItem("ACCESS_TOKEN"); + let headersObj = new Object(); + headersObj["Content-Type"] = "application/json"; + + if (accessToken) { + headersObj["Authorization"] = accessToken; + } + + fetch(`/admin`, { + method: "GET", + headers: headersObj, + }).then((res) => { + if (!res.ok) { + throw new Error(`${res.status}`); + } + return res.json(); + }).then((res) => { + alert(JSON.stringify(res)); + location.href = "/adminPage"; + }).catch((error) => { + alert(error); + }); + }) +} + +if (logoutButton) { + logoutButton.addEventListener("click", (event) => { + localStorage.removeItem("ACCESS_TOKEN"); + location.replace("/login"); + }) +} + +if (refreshButton) { + console.log("refreshButton"); + refreshButton.addEventListener("click", (event) => { + fetch(`/v1/access`, { + method: "POST", + }).then((res) => { + if (res.ok) { + const token = res.headers.get("Authorization"); + localStorage.setItem("ACCESS_TOKEN", token); + } else { + throw new Error(`${res.status}`); + } + + return res.json(); + }).then((res) => { + if (res.success) { + alert(res.message); + } else { + alert(res.message); + } + }).catch((error) => { + alert(error); + }); + }) +} \ No newline at end of file diff --git a/src/main/resources/static/js/login.js b/src/main/resources/static/js/login.js new file mode 100644 index 0000000..cc878a8 --- /dev/null +++ b/src/main/resources/static/js/login.js @@ -0,0 +1,24 @@ +const loginButton = document.getElementById("login-btn"); + +if (loginButton) { + loginButton.addEventListener("click", (event) => { + fetch("/v1/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: document.getElementById("email").value, + password: document.getElementById("password").value, + }), + }).then((res) => { + res.headers.forEach(console.log); + alert(res.headers.get("Authorization")); + const token = res.headers.get("Authorization"); + localStorage.setItem("ACCESS_TOKEN", token); + location.replace("/"); + }).catch(() => { + alert("ajax 호출 에러") + }); + }) +} \ No newline at end of file diff --git a/src/main/resources/static/js/signup.js b/src/main/resources/static/js/signup.js new file mode 100644 index 0000000..c0aeae7 --- /dev/null +++ b/src/main/resources/static/js/signup.js @@ -0,0 +1,51 @@ +const createButton = document.getElementById("create-btn"); + +if (createButton) { + createButton.addEventListener("click", (event) => { + fetch("/v1/members", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: document.getElementById("email").value, + password: document.getElementById("password").value, + name: document.getElementById("name").value, + telNo: document.getElementById("telNo").value, + }), + }).then((res) => res.json()) + .then((res) => { + if (res.success) { + alert(res.message); + location.replace("/login"); + } else { + alert(res.message); + } + }).catch(() => { + alert("ajax 호출 에러") + }); + }) +} + +const emailCheck = () => { + const emailValue = document.getElementById("email").value; + + if (emailValue) { + fetch(`/v1/members/email/${emailValue}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }).then((res) => { + return res.json() + }).then((res) => { + if (res.success) { + alert(res.message); + } else { + alert(res.message); + } + }).catch(() => { + console.log("catch"); + }); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/adminPage.html b/src/main/resources/templates/adminPage.html new file mode 100644 index 0000000..faaec2e --- /dev/null +++ b/src/main/resources/templates/adminPage.html @@ -0,0 +1,11 @@ + + + + + admin 페이지 + + + +admin page + + \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..719012f --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,30 @@ + + + + + 메인 페이지 + + + + +
+
+
+
+

Main Page

+
+ + + + +
+
+ +
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..082f240 --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,45 @@ + + + + + 로그인 + + + + + +
+
+
+
+

LOGIN

+

서비스를 사용하려면 로그인을 해주세요!

+ +
+ +
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/sellerPage.html b/src/main/resources/templates/sellerPage.html new file mode 100644 index 0000000..abaeafb --- /dev/null +++ b/src/main/resources/templates/sellerPage.html @@ -0,0 +1,11 @@ + + + + + seller 페이지 + + + +seller page + + \ No newline at end of file diff --git a/src/main/resources/templates/signup.html b/src/main/resources/templates/signup.html new file mode 100644 index 0000000..eaeaa26 --- /dev/null +++ b/src/main/resources/templates/signup.html @@ -0,0 +1,51 @@ + + + + + 회원 가입 + + + + + +
+
+
+
+

SIGN UP

+

서비스 사용을 위한 회원 가입

+ +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/userPage.html b/src/main/resources/templates/userPage.html new file mode 100644 index 0000000..0e4c5ed --- /dev/null +++ b/src/main/resources/templates/userPage.html @@ -0,0 +1,11 @@ + + + + + user 페이지 + + + +user page + + \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/category/service/CategoryServiceTest.java b/src/test/java/org/store/clothstar/category/service/CategoryServiceTest.java new file mode 100644 index 0000000..773374d --- /dev/null +++ b/src/test/java/org/store/clothstar/category/service/CategoryServiceTest.java @@ -0,0 +1,144 @@ +package org.store.clothstar.category.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.BDDMockito; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; +import org.store.clothstar.category.domain.Category; +import org.store.clothstar.category.dto.request.CreateCategoryRequest; +import org.store.clothstar.category.dto.response.CategoryDetailResponse; +import org.store.clothstar.category.dto.response.CategoryResponse; +import org.store.clothstar.category.repository.CategoryRepository; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; + +@DisplayName("비즈니스 로직 - categoryTest") +@ActiveProfiles("dev") +@ExtendWith(MockitoExtension.class) +class CategoryServiceTest { + + @InjectMocks + private CategoryService categoryService; + + @Mock + private CategoryRepository categoryRepository; + + @DisplayName("카테고리 리스트 조회에 성공한다.") + @Test + void givenCategories_whenGetAllCategories_thenGetAllCategories() { + //given + List categories = new ArrayList<>(); + Category category1 = Category.builder() + .categoryId(1L) + .categoryType("OUTER") + .build(); + Category category2 = Category.builder() + .categoryId(2L) + .categoryType("TOP") + .build(); + Category category3 = Category.builder() + .categoryId(3L) + .categoryType("PANTS") + .build(); + Category category4 = Category.builder() + .categoryId(4L) + .categoryType("SKIRT") + .build(); + Category category5 = Category.builder() + .categoryId(5L) + .categoryType("BAG") + .build(); + Category category6 = Category.builder() + .categoryId(6L) + .categoryType("HEADWEAR") + .build(); + + categories.add(category1); + categories.add(category2); + categories.add(category3); + categories.add(category4); + categories.add(category5); + categories.add(category6); + + BDDMockito.given(categoryRepository.selectAllCategory()).willReturn(categories); + + // when + List response = categoryService.getAllCategories(); + + // then + Mockito.verify(categoryRepository, Mockito.times(1)) + .selectAllCategory(); + assertThat(response).isNotNull(); + assertThat(response.size()).isEqualTo(6); + assertThat(response.get(0).getCategoryType()).isEqualTo("OUTER"); + assertThat(response.get(1).getCategoryType()).isEqualTo("TOP"); + assertThat(response.get(2).getCategoryType()).isEqualTo("PANTS"); + assertThat(response.get(3).getCategoryType()).isEqualTo("SKIRT"); + assertThat(response.get(4).getCategoryType()).isEqualTo("BAG"); + assertThat(response.get(5).getCategoryType()).isEqualTo("HEADWEAR"); + } + + @DisplayName("category_id로 카테고리 단건 조회에 성공한다.") + @Test + void givenCategoryId_whenCategoryId_thenCategoryReturned() { + // given + Long categoryId = 1L; + + Category category = Category.builder() + .categoryId(1L) + .categoryType("OUTER") + .build(); + + BDDMockito.given(categoryRepository.selectCategoryById(anyLong())).willReturn(category); + + // when + CategoryDetailResponse response = categoryService.getCategory(categoryId); + + // then + Mockito.verify(categoryRepository, Mockito.times(1)) + .selectCategoryById(anyLong()); + assertThat(response).isNotNull(); + assertThat(response.getCategoryId()).isEqualTo(1L); + assertThat(response.getCategoryType()).isEqualTo("OUTER"); + } + + @DisplayName("유효한 CreateCategoryRequest 가 들어오면 카테고리 생성에 성공한다.") + @Test + void givenValidCreateCategoryRequest_whenCreateCategory_thenCategoryCreated() { + // given + CreateCategoryRequest createCategoryRequest = CreateCategoryRequest.builder() + .categoryType("OUTER") + .build(); + + BDDMockito.given(categoryRepository.save(Mockito.any(Category.class))).willReturn(1); + + // when + Long categoryId = categoryService.createCategory(createCategoryRequest); + + // then + Mockito.verify(categoryRepository, Mockito.times(1)) + .save(Mockito.any(Category.class)); + } + + @DisplayName("중복된 카테고리 타입을 생성하려고 시도할 경우, 카테고리 생성에 실패한다.") + @Test + void givenDuplicateCategoryType_whenCreateCategory_thenFailToCreateCategory() { + + } + + @Test + void updateCategory() { + // given + String duplicateCategoryType = "OUTER"; + } + +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/common/config/SecurityConfigurationUnitTest.java b/src/test/java/org/store/clothstar/common/config/SecurityConfigurationUnitTest.java new file mode 100644 index 0000000..b605999 --- /dev/null +++ b/src/test/java/org/store/clothstar/common/config/SecurityConfigurationUnitTest.java @@ -0,0 +1,127 @@ +package org.store.clothstar.common.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +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.security.test.context.support.WithAnonymousUser; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("dev") +class SecurityConfigurationUnitTest { + @Autowired + MockMvc mockMvc; + + @Autowired + private WebApplicationContext context; + + @BeforeEach + public void setup() { + mockMvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + } + + @DisplayName("인덱스 페이지는 권한 없이 사용이 가능한지 테스트") + @Test + void indexPageAuthorityTest() throws Exception { + //given + final String url = "/"; + + //when && then + mockMvc.perform(get(url).with(anonymous())) + .andExpect(status().isOk()); + } + + @DisplayName("인증된 사용자는 User 페이지 사용이 가능한지 테스트") + @Test + @WithMockUser + void userPageAuthorityTest_authenticatedUser() throws Exception { + //given + final String url = "/userPage"; + + //when && then + mockMvc + .perform(get(url)) + .andExpect(status().isOk()); + } + + @DisplayName("비인증 사용자는 User 페이지 사용이 불가능한지 테스트") + @Test + @WithAnonymousUser + void userPageAuthorityTest_anonymousUser() throws Exception { + //given + final String url = "/userPage"; + + //when && then + mockMvc + .perform(get(url)) + .andExpect(status().isForbidden()); + } + + @DisplayName("판매자는 판매자 페이지 사용이 가능한지 테스트") + @Test + @WithMockUser(roles = "SELLER") + void sellerPageAuthorityTest_sellerUser() throws Exception { + //given + final String url = "/sellerPage"; + + //when && then + mockMvc + .perform(get(url)) + .andExpect(status().isOk()); + } + + @DisplayName("비인증 사용자는 판매자 페이지 사용이 불가능한지 테스트") + @Test + @WithAnonymousUser + void sellerPageAuthorityTest_anonymousUser() throws Exception { + //given + final String url = "/sellerPage"; + + //when && then + mockMvc + .perform(get(url)) + .andExpect(status().isForbidden()); + } + + @DisplayName("Admin 권한 사용자는 admin 페이지 사용이 가능한지 테스트") + @Test + @WithMockUser(roles = "ADMIN") + void adminPageAuthorityTest_adminUser() throws Exception { + //given + final String url = "/adminPage"; + + //when && then + mockMvc + .perform(get(url)) + .andExpect(status().isOk()); + } + + @DisplayName("비인증 사용자는 admin 페이지 사용이 불가능 한지 테스트") + @Test + @WithAnonymousUser + void adminPageAuthorityTest_anonymousUser() throws Exception { + //given + final String url = "/adminPage"; + + //when && then + mockMvc + .perform(get(url)) + .andExpect(status().isForbidden()); + } +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/common/config/jwt/JwtUnitTest.java b/src/test/java/org/store/clothstar/common/config/jwt/JwtUnitTest.java new file mode 100644 index 0000000..5c7bf49 --- /dev/null +++ b/src/test/java/org/store/clothstar/common/config/jwt/JwtUnitTest.java @@ -0,0 +1,83 @@ +package org.store.clothstar.common.config.jwt; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +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.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import org.store.clothstar.member.domain.Member; +import org.store.clothstar.member.domain.MemberGrade; +import org.store.clothstar.member.domain.MemberRole; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("dev") +@Transactional +class JwtUnitTest { + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private JwtService jwtService; + + @DisplayName("access 토큰이 생성되는지 확인") + @Test + void createAccessTokenTest() { + //given + Member member = getMember(); + + //when + String refreshToken = jwtUtil.createAccessToken(getMember()); + String tokenType = jwtUtil.getTokenType(refreshToken); + + //then + Assertions.assertThat(refreshToken).isNotNull(); + Assertions.assertThat(tokenType).isEqualTo("ACCESS_TOKEN"); + } + + @DisplayName("refresh 토큰이 생성되는지 확인") + @Test + void createRefreshTokenTest() { + //given + Member member = getMember(); + + //when + String refreshToken = jwtUtil.createRefreshToken(getMember()); + String tokenType = jwtUtil.getTokenType(refreshToken); + + //then + Assertions.assertThat(refreshToken).isNotNull(); + Assertions.assertThat(tokenType).isEqualTo("REFRESH_TOKEN"); + } + + @DisplayName("refresh 토큰으로 access 토큰이 생성되는지 확인") + @Test + void createAccessTokenByRefreshTokenTest() { + //given + final String refreshToken = jwtUtil.createRefreshToken(getMember()); + String refreshTokenType = jwtUtil.getTokenType(refreshToken); + + //when + final String accessToken = jwtService.getAccessTokenByRefreshToken(refreshToken); + String accessTokenType = jwtUtil.getTokenType(accessToken); + + //then + Assertions.assertThat(refreshToken).isNotNull(); + Assertions.assertThat(refreshTokenType).isEqualTo("REFRESH_TOKEN"); + Assertions.assertThat(accessTokenType).isEqualTo("ACCESS_TOKEN"); + } + + private Member getMember() { + return Member.builder() + .memberId(1L) + .email("test@test.com") + .password("test") + .telNo("010-1234-1234") + .role(MemberRole.USER) + .grade(MemberGrade.BRONZE) + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/member/controller/AddressControllerIntegrationTest.java b/src/test/java/org/store/clothstar/member/controller/AddressControllerIntegrationTest.java new file mode 100644 index 0000000..ba5278b --- /dev/null +++ b/src/test/java/org/store/clothstar/member/controller/AddressControllerIntegrationTest.java @@ -0,0 +1,64 @@ +package org.store.clothstar.member.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +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.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; +import org.store.clothstar.member.dto.request.CreateAddressRequest; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("dev") +@Transactional +class AddressControllerIntegrationTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + final Long memberId = 1L; + + @DisplayName("회원 배송지 저장 통합 테스트") + @Test + void saveMemberAddrTest() throws Exception { + //given + final String url = "/v1/members/" + memberId + "/address"; + CreateAddressRequest createAddressRequest = getCreateAddressRequest(); + final String requestBody = objectMapper.writeValueAsString(createAddressRequest); + + //when + ResultActions result = mockMvc.perform(post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody) + ); + + //then + result.andExpect(status().isCreated()); + } + + private CreateAddressRequest getCreateAddressRequest() { + final String receiverName = "현수"; + final String zipNo = "18292"; + final String addressBasic = "서울시 노원구 공릉동"; + final String addressDetail = "양지빌라"; + final String telNo = "019-1222-2311"; + final String deliveryRequest = "문앞에 놓고 가주세요"; + final boolean defaultAddress = true; + + CreateAddressRequest createAddressRequest = new CreateAddressRequest( + receiverName, zipNo, addressBasic, addressDetail, telNo, deliveryRequest, defaultAddress + ); + return createAddressRequest; + } +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/member/controller/MemberAndSellerControllerIntegrationTest.java b/src/test/java/org/store/clothstar/member/controller/MemberAndSellerControllerIntegrationTest.java new file mode 100644 index 0000000..c9c1e10 --- /dev/null +++ b/src/test/java/org/store/clothstar/member/controller/MemberAndSellerControllerIntegrationTest.java @@ -0,0 +1,92 @@ +package org.store.clothstar.member.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +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.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; +import org.store.clothstar.member.dto.request.CreateMemberRequest; +import org.store.clothstar.member.dto.request.CreateSellerRequest; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("dev") +@Transactional +class MemberAndSellerControllerIntegrationTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @DisplayName("회원가입, 판매자 신청 통합 테스트") + @Test + void signUpAndSellerTest() throws Exception { + //given + CreateMemberRequest createMemberRequest = getCreateMemberRequest(); + final String signUpUrl = "/v1/members"; + final String requestBody = objectMapper.writeValueAsString(createMemberRequest); + + //when + ResultActions actions = mockMvc.perform(post(signUpUrl) + .contentType(MediaType.APPLICATION_JSON) + .content(requestBody)); + + //then + actions.andExpect(status().isCreated()) + .andDo(print()); + + String responseBody = actions.andReturn().getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(responseBody); + Long memberId = jsonNode.get("id").asLong(); + + //given + final String sellerUrl = "/v1/sellers/" + memberId; + CreateSellerRequest createSellerRequest = getCreateSellerRequest(memberId); + final String sellerRequestBody = objectMapper.writeValueAsString(createSellerRequest); + + //when + ResultActions sellerActions = mockMvc.perform(post(sellerUrl) + .contentType(MediaType.APPLICATION_JSON) + .content(sellerRequestBody)); + + //then + sellerActions.andDo(print()) + .andExpect(status().isCreated()); + } + + private CreateMemberRequest getCreateMemberRequest() { + String email = "test11@naver.com"; + String password = "testl122sff"; + String name = "name"; + String telNo = "010-1234-1245"; + + CreateMemberRequest createMemberRequest = new CreateMemberRequest( + email, password, name, telNo + ); + + return createMemberRequest; + } + + private CreateSellerRequest getCreateSellerRequest(Long memberId) { + String brandName = "나이키"; + String bizNo = "102-13-13122"; + + CreateSellerRequest createSellerRequest = new CreateSellerRequest( + brandName, bizNo + ); + + return createSellerRequest; + } +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/member/service/AddressServiceMockUnitTest.java b/src/test/java/org/store/clothstar/member/service/AddressServiceMockUnitTest.java new file mode 100644 index 0000000..70194d0 --- /dev/null +++ b/src/test/java/org/store/clothstar/member/service/AddressServiceMockUnitTest.java @@ -0,0 +1,47 @@ +package org.store.clothstar.member.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.store.clothstar.member.domain.Address; +import org.store.clothstar.member.dto.response.AddressResponse; +import org.store.clothstar.member.repository.AddressRepository; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +class AddressServiceMockUnitTest { + @Mock + AddressRepository addressRepository; + + @InjectMocks + AddressService addressService; + + private Long memberId = 1L; + + @DisplayName("회원 배송지 조회 단위 테스트") + @Test + void getMemberAddrUnitTest() { + //given + given(addressRepository.findMemberAllAddress(anyLong())).willReturn(getAddressList()); + + //when + List memberAddressResponseList = addressService.getMemberAllAddress(memberId); + + //then + verify(addressRepository, times(1)).findMemberAllAddress((anyLong())); + assertThat(memberAddressResponseList.size()).isEqualTo(2); + } + + private List
getAddressList() { + Address address = mock(Address.class); + return List.of(address, address); + } +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/member/service/MemberServiceMockUnitTest.java b/src/test/java/org/store/clothstar/member/service/MemberServiceMockUnitTest.java new file mode 100644 index 0000000..fa3b864 --- /dev/null +++ b/src/test/java/org/store/clothstar/member/service/MemberServiceMockUnitTest.java @@ -0,0 +1,75 @@ +package org.store.clothstar.member.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.store.clothstar.common.dto.MessageDTO; +import org.store.clothstar.member.domain.Member; +import org.store.clothstar.member.dto.response.MemberResponse; +import org.store.clothstar.member.repository.MemberRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class MemberServiceMockUnitTest { + @Mock + MemberRepository memberRepository; + + @InjectMocks + MemberService memberService; + + @DisplayName("회원아이디로 회원 조회 테스트") + @Test + void getMemberTest() { + //given + Member member = mock(Member.class); + given(memberRepository.findById(anyLong())).willReturn(Optional.of(member)); + when(member.getMemberId()).thenReturn(1L); + + //when + MemberResponse memberResponse = memberService.getMemberById(1L); + + //then + verify(memberRepository, times(1)) + .findById(anyLong()); + + assertThat(memberResponse.getMemberId()).isEqualTo(member.getMemberId()); + } + + @DisplayName("이메일이 중복된 경우의 단위 테스트") + @Test + void duplicateEmailCheckTest() { + //given + String email = "test@test.com"; + Member member = mock(Member.class); + given(memberRepository.findByEmail(anyString())).willReturn(Optional.of(member)); + + //when + MessageDTO message = memberService.emailCheck(email); + + //then + assertThat(message.getMessage()).isEqualTo("이미 사용중인 이메일 입니다."); + } + + @DisplayName("이메일이 중복되지 않은 경우의 단위 테스트") + @Test + void duplicateEmailCheckTest2() { + //given + String email = "test@test.com"; + given(memberRepository.findByEmail(anyString())).willReturn(Optional.empty()); + + //when + MessageDTO message = memberService.emailCheck(email); + + //then + assertThat(message.getMessage()).isEqualTo("사용 가능한 이메일 입니다."); + } +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/member/service/MemberServiceUnitTest.java b/src/test/java/org/store/clothstar/member/service/MemberServiceUnitTest.java new file mode 100644 index 0000000..1388ffd --- /dev/null +++ b/src/test/java/org/store/clothstar/member/service/MemberServiceUnitTest.java @@ -0,0 +1,35 @@ +package org.store.clothstar.member.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.store.clothstar.common.dto.MessageDTO; +import org.store.clothstar.member.domain.MemberRole; +import org.store.clothstar.member.dto.request.ModifyMemberRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles("dev") +public class MemberServiceUnitTest { + + @Autowired + MemberService memberService; + + @DisplayName("회원 권한 수정 단위 테스트") + @Test + void modifyMemberAuthUnitTest() { + //given + Long memberId = 4L; + String modifyField = "role"; + ModifyMemberRequest modifyMemberRequest = new ModifyMemberRequest(MemberRole.SELLER); + + //when + MessageDTO messageDTO = memberService.modifyMember(memberId, modifyMemberRequest); + + //then + assertThat(messageDTO.isSuccess()).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/member/service/SellerServiceMockUnitTest.java b/src/test/java/org/store/clothstar/member/service/SellerServiceMockUnitTest.java new file mode 100644 index 0000000..674bbe5 --- /dev/null +++ b/src/test/java/org/store/clothstar/member/service/SellerServiceMockUnitTest.java @@ -0,0 +1,42 @@ +package org.store.clothstar.member.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.store.clothstar.member.domain.Seller; +import org.store.clothstar.member.dto.response.SellerResponse; +import org.store.clothstar.member.repository.SellerRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.*; + +@ExtendWith(MockitoExtension.class) +class SellerServiceMockUnitTest { + @Mock + SellerRepository sellerRepository; + + @InjectMocks + SellerService sellerService; + + @DisplayName("판매회원 조회 단위 테스트") + @Test + void getSellerUnitTest() { + //given + Long memberId = 1L; + Seller seller = mock(Seller.class); + given(sellerRepository.findById(anyLong())).willReturn(Optional.of(seller)); + when(seller.getBizNo()).thenReturn("102-121-23323"); + + //when + SellerResponse sellerResponse = sellerService.getSellerById(memberId); + + //then + verify(sellerRepository, times(1)).findById(anyLong()); + assertThat(sellerResponse.getBizNo()).isEqualTo(seller.getBizNo()); + } +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/order/dto/CreateOrderRequestTest.java b/src/test/java/org/store/clothstar/order/dto/CreateOrderRequestTest.java new file mode 100644 index 0000000..7eafc49 --- /dev/null +++ b/src/test/java/org/store/clothstar/order/dto/CreateOrderRequestTest.java @@ -0,0 +1,40 @@ +package org.store.clothstar.order.dto; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.store.clothstar.member.domain.Address; +import org.store.clothstar.member.domain.Member; +import org.store.clothstar.order.domain.Order; +import org.store.clothstar.order.domain.type.PaymentMethod; +import org.store.clothstar.order.dto.request.CreateOrderRequest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +class CreateOrderRequestTest { + + @Test + void toOrder() { + //given + Member member = mock(Member.class); + Address address = mock(Address.class); + CreateOrderRequest request = CreateOrderRequest.builder() + .paymentMethod(PaymentMethod.CARD) + .memberId(member.getMemberId()) + .addressId(address.getAddressId()) + .build(); + given(member.getMemberId()).willReturn(1L); + given(address.getAddressId()).willReturn(1L); + + //when + Order order = request.toOrder(member, address); + + //then + assertEquals(request.getPaymentMethod(), order.getPaymentMethod()); + assertNotNull(order.getOrderId()); + } +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/order/service/OrderSellerServiceTest.java b/src/test/java/org/store/clothstar/order/service/OrderSellerServiceTest.java new file mode 100644 index 0000000..8884068 --- /dev/null +++ b/src/test/java/org/store/clothstar/order/service/OrderSellerServiceTest.java @@ -0,0 +1,175 @@ +package org.store.clothstar.order.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.server.ResponseStatusException; +import org.store.clothstar.order.domain.Order; +import org.store.clothstar.order.domain.type.ApprovalStatus; +import org.store.clothstar.order.domain.type.PaymentMethod; +import org.store.clothstar.order.domain.type.Status; +import org.store.clothstar.order.dto.reponse.OrderResponse; +import org.store.clothstar.order.dto.request.OrderSellerRequest; +import org.store.clothstar.order.repository.OrderRepository; +import org.store.clothstar.order.repository.OrderSellerRepository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.BDDMockito.*; + +@Nested +@ExtendWith(MockitoExtension.class) +class OrderSellerServiceTest { + + @InjectMocks + private OrderSellerService orderSellerService; + + @Mock + private OrderSellerRepository orderSellerRepository; + + @Mock + private OrderRepository orderRepository; + + @Test + @DisplayName("getWaitingOrders 테스트") + void getWaitingOrder_test() { + //given + Order order1 = mock(Order.class); + given(order1.getCreatedAt()).willReturn(LocalDateTime.now()); + given(order1.getTotalShippingPrice()).willReturn(1000); + + Order order2 = mock(Order.class); + given(order2.getCreatedAt()).willReturn(LocalDateTime.now()); + + Order order3 = mock(Order.class); + given(order3.getCreatedAt()).willReturn(LocalDateTime.now()); + + List orders = List.of(order1, order2, order3); + given(orderSellerRepository.SelectWaitingOrders()).willReturn(orders); + + //when + List response = orderSellerService.getWaitingOrder(); + + //then + then(orderSellerRepository).should(times(1)).SelectWaitingOrders(); + assertThat(response).isNotNull().hasSize(3); + assertThat(response.get(0).getTotalShippingPrice()).isEqualTo(1000); + } + + // 판매자 주문상태 수정(승인/취소) 테스트 + @Test + @DisplayName("cancelOrApproveOrder: 주문상태 승인 메서드 호출 테스트") + void approveOrder_verify_test() { + + // given + Order order = Order.builder() + .orderId(1L) + .memberId(1L) + .addressId(1L) + .createdAt(LocalDateTime.now()) + .status(Status.WAITING) + .totalShippingPrice(3000) + .totalProductsPrice(0) + .paymentMethod(PaymentMethod.CARD) + .totalPaymentPrice(0) + .build(); + + OrderSellerRequest orderSellerRequest = OrderSellerRequest.builder() + .approvalStatus(ApprovalStatus.APPROVE) + .build(); + + Long orderId = order.getOrderId(); + + given(orderRepository.getOrder(orderId)).willReturn(Optional.of(order)); + + //when + orderSellerService.cancelOrApproveOrder(orderId, orderSellerRequest); + + //then + then(orderSellerRepository).should().approveOrder(orderId); + } + + @Test + @DisplayName("cancelOrApproveOrder: 주문상태 취소 메서드 호출 테스트") + void cancelOrApproveOrder_verify_test() { + + // given + Order order = Order.builder() + .orderId(1L) + .memberId(1L) + .addressId(1L) + .createdAt(LocalDateTime.now()) + .status(Status.WAITING) + .totalShippingPrice(3000) + .totalProductsPrice(0) + .paymentMethod(PaymentMethod.CARD) + .totalPaymentPrice(0) + .build(); + + OrderSellerRequest orderSellerRequest = OrderSellerRequest.builder() + .approvalStatus(ApprovalStatus.CANCEL) + .build(); + + Long orderId = order.getOrderId(); + + given(orderRepository.getOrder(orderId)).willReturn(Optional.of(order)); + + //when + orderSellerService.cancelOrApproveOrder(orderId, orderSellerRequest); + + //then + then(orderSellerRepository).should().cancelOrder(orderId); + } + + @Test + @DisplayName("cancelOrApproveOrder: 메서드 호출 테스트") + void cancelOrder_verify_test() { + //given + Long orderId = 1L; + Order mockOrder = mock(Order.class); + mock(OrderResponse.class); + OrderSellerRequest mockOrderSellerRequest = mock(OrderSellerRequest.class); + + given(orderRepository.getOrder(orderId)).willReturn(Optional.of(mockOrder)); + given(mockOrder.getStatus()).willReturn(Status.WAITING); + given(mockOrder.getCreatedAt()).willReturn(LocalDateTime.now()); + given(mockOrderSellerRequest.getApprovalStatus()).willReturn(ApprovalStatus.APPROVE); + + //when + orderSellerService.cancelOrApproveOrder(orderId, mockOrderSellerRequest); + + //then + then(orderRepository).should(times(2)).getOrder(orderId); + then(orderSellerRepository).should().approveOrder(orderId); + } + + @Test + @DisplayName("cancelOrApproveOrder - 주문상태가 WAITING이 아닐 때 예외처리 테스트") + void cancelOrApproveOrder_NotWAITING_exception_test() { + + //given + Long orderId = 1L; + Order mockOrder = mock(Order.class); + OrderSellerRequest mockOrderSellerRequest = mock(OrderSellerRequest.class); + + given(orderRepository.getOrder(orderId)).willReturn(Optional.of(mockOrder)); + given(mockOrder.getStatus()).willReturn(Status.DELIVERED); + + //when + ResponseStatusException thrown = assertThrows(ResponseStatusException.class, () -> { + orderSellerService.cancelOrApproveOrder(orderId, mockOrderSellerRequest); + }); + + //then + assertEquals("400 BAD_REQUEST \"주문이 존재하지 않거나 상태가 'WAITING'이 아니어서 처리할 수 없습니다.\"", thrown.getMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/order/service/OrderServiceTest.java b/src/test/java/org/store/clothstar/order/service/OrderServiceTest.java new file mode 100644 index 0000000..545f043 --- /dev/null +++ b/src/test/java/org/store/clothstar/order/service/OrderServiceTest.java @@ -0,0 +1,194 @@ +package org.store.clothstar.order.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.server.ResponseStatusException; +import org.store.clothstar.member.domain.Address; +import org.store.clothstar.member.domain.Member; +import org.store.clothstar.member.repository.AddressRepository; +import org.store.clothstar.member.repository.MemberRepository; +import org.store.clothstar.order.domain.Order; +import org.store.clothstar.order.domain.type.Status; +import org.store.clothstar.order.dto.reponse.OrderResponse; +import org.store.clothstar.order.dto.request.CreateOrderRequest; +import org.store.clothstar.order.dto.request.OrderRequestWrapper; +import org.store.clothstar.order.repository.OrderRepository; +import org.store.clothstar.orderDetail.service.OrderDetailService; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.BDDMockito.*; + +@ExtendWith(MockitoExtension.class) +class OrderServiceTest { + + @InjectMocks + private OrderService orderService; + + @Mock + private OrderRepository orderRepository; + + @Mock + private MemberRepository memberRepository; + + @Mock + private AddressRepository addressRepository; + + @Mock + private OrderDetailService orderDetailService; + + @Test + @DisplayName("getOrder 주문 조회 테스트") + void getOrder_test() { + //given + Order order = mock(Order.class); + given(order.getOrderId()).willReturn(1L); + given(order.getCreatedAt()).willReturn(LocalDateTime.now()); + + given(orderRepository.getOrder(order.getOrderId())).willReturn(Optional.of(order)); + + //when + OrderResponse orderResponse = orderService.getOrder(order.getOrderId()); + + //then + assertThat(orderResponse.getOrderId()).isEqualTo(order.getOrderId()); + } + + @Test + @DisplayName("getOrder 메서드 호출 테스트") + void getOrder_verify_test() { + //given + Long orderId = 1L; + Order mockOrder = mock(Order.class); + mock(OrderResponse.class); + + given(orderRepository.getOrder(orderId)).willReturn(Optional.of(mockOrder)); + given(mockOrder.getCreatedAt()).willReturn(LocalDateTime.now()); + given(mockOrder.getStatus()).willReturn(Status.DELIVERED); + + //when + orderService.getOrder(orderId); + + //then + then(orderRepository).should().getOrder(orderId); + } + + @Test + @DisplayName("saveOrder 메서드 반환값 테스트") + void saveOrder_test() { + //given + Order order = mock(Order.class); + OrderRequestWrapper orderRequestWrapper = mock(OrderRequestWrapper.class); + CreateOrderRequest createOrderRequest = mock(CreateOrderRequest.class); + Member mockmember = mock(Member.class); + Address mockAddress = mock(Address.class); + + given(order.getOrderId()).willReturn(1L); + + given(orderRequestWrapper.getCreateOrderRequest()).willReturn(createOrderRequest); + + given(createOrderRequest.getMemberId()).willReturn(1L); + given(createOrderRequest.getAddressId()).willReturn(2L); + + given(memberRepository.findById(1L)).willReturn(Optional.of(mockmember)); + given(addressRepository.findById(2L)).willReturn(Optional.of(mockAddress)); + given(createOrderRequest.toOrder(mockmember, mockAddress)).willReturn(order); + + //when + Long orderId = orderService.saveOrder(orderRequestWrapper.getCreateOrderRequest()); + + //then + assertThat(orderId).isEqualTo(1L); + } + + @Test + @DisplayName("saveOrder 메서드 호출 테스트") + void saveOrder_verify_test() { + //given + Order order = mock(Order.class); + OrderRequestWrapper orderRequestWrapper = mock(OrderRequestWrapper.class); + CreateOrderRequest createOrderRequest = mock(CreateOrderRequest.class); + Member mockmember = mock(Member.class); + Address mockAddress = mock(Address.class); + + given(orderRequestWrapper.getCreateOrderRequest()).willReturn(createOrderRequest); + given(createOrderRequest.getMemberId()).willReturn(1L); + given(createOrderRequest.getAddressId()).willReturn(2L); + + given(memberRepository.findById(createOrderRequest.getMemberId())).willReturn(Optional.of(mockmember)); + given(addressRepository.findById(createOrderRequest.getAddressId())).willReturn(Optional.of(mockAddress)); + given(createOrderRequest.toOrder(mockmember, mockAddress)).willReturn(order); + + //when + orderService.saveOrder(orderRequestWrapper.getCreateOrderRequest()); + + //then + then(memberRepository).should(times(1)).findById(createOrderRequest.getMemberId()); + then(addressRepository).should(times(1)).findById(createOrderRequest.getAddressId()); + then(orderRepository).should(times(1)).saveOrder(order); + verify(order).getOrderId(); + } + + @Test + @DisplayName("deliveredToConfirmOrder 메서드 호출 테스트") + void deliveredToConfirmOrder_verify() { + //given + Long orderId = 1L; + Order order = mock(Order.class); + mock(OrderResponse.class); + + given(orderRepository.getOrder(1L)).willReturn(Optional.of(order)); + given(order.getStatus()).willReturn(Status.DELIVERED); + + //when + orderService.deliveredToConfirmOrder(orderId); + + //then + then(orderRepository).should(times(1)).getOrder(orderId); + then(orderRepository).should().deliveredToConfirmOrder(orderId); + } + + @Test + @DisplayName("deliveredToConfirmOrder 성공 테스트") + void deliveredToConfirmOrder_success_test() { + //given + Long orderId = 1L; + Order mockOrder = mock(Order.class); + + given(mockOrder.getStatus()).willReturn(Status.DELIVERED); + given(orderRepository.getOrder(orderId)).willReturn(Optional.of(mockOrder)); + + //when + orderService.deliveredToConfirmOrder(orderId); + + //then + then(orderRepository).should().deliveredToConfirmOrder(orderId); + } + + @Test + @DisplayName("deliveredToConfirmOrder 실패 테스트") + void deliveredToConfirmOrder_fail_test() { + //given + Long orderId = 1L; + Order mockOrder = mock(Order.class); + + given(mockOrder.getStatus()).willReturn(Status.APPROVE); + given(orderRepository.getOrder(orderId)).willReturn(Optional.of(mockOrder)); + + //when + ResponseStatusException thrown = assertThrows(ResponseStatusException.class, () -> { + orderService.deliveredToConfirmOrder(orderId); + }); + + //then + assertEquals("400 BAD_REQUEST \"주문 상태가 '배송완료'가 아니기 때문에 주문확정이 불가능합니다.\"", thrown.getMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/orderDetail/service/OrderDetailServiceTest.java b/src/test/java/org/store/clothstar/orderDetail/service/OrderDetailServiceTest.java new file mode 100644 index 0000000..6e77f35 --- /dev/null +++ b/src/test/java/org/store/clothstar/orderDetail/service/OrderDetailServiceTest.java @@ -0,0 +1,176 @@ +package org.store.clothstar.orderDetail.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.server.ResponseStatusException; +import org.store.clothstar.order.domain.Order; +import org.store.clothstar.order.repository.OrderRepository; +import org.store.clothstar.orderDetail.domain.OrderDetail; +import org.store.clothstar.orderDetail.dto.request.AddOrderDetailRequest; +import org.store.clothstar.orderDetail.dto.request.CreateOrderDetailRequest; +import org.store.clothstar.orderDetail.repository.OrderDetailRepository; +import org.store.clothstar.product.domain.Product; +import org.store.clothstar.product.repository.ProductRepository; +import org.store.clothstar.productLine.domain.ProductLine; +import org.store.clothstar.productLine.repository.ProductLineRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.BDDMockito.*; + +@ExtendWith(MockitoExtension.class) +class OrderDetailServiceTest { + + @InjectMocks + private OrderDetailService orderDetailService; + + @Mock + private OrderRepository orderRepository; + @Mock + private ProductLineRepository productLineRepository; + @Mock + private ProductRepository productRepository; + @Mock + private OrderDetailRepository orderDetailRepository; + + @DisplayName("saveOrderDetailWithOrder 메서드 호출 테스트") + @Test + void saveOrderDetailWithOrder_verify_test() { + //given + long orderId = 1L; + CreateOrderDetailRequest mockRequest = mock(CreateOrderDetailRequest.class); + OrderDetail mockOrderDetail = mock(OrderDetail.class); + ProductLine mockProductLine = mock(ProductLine.class); + Product mockProduct = mock(Product.class); + Order mockOrder = mock(Order.class); + + given(orderRepository.getOrder(orderId)).willReturn(Optional.of(mockOrder)); + given(productLineRepository.selectByProductLineId(mockRequest.getProductLineId())).willReturn(Optional.of(mockProductLine)); + given(productRepository.selectByProductId(mockRequest.getProductId())).willReturn(Optional.of(mockProduct)); + given(mockRequest.toOrderDetail(orderId, mockProductLine, mockProduct)).willReturn(mockOrderDetail); + + //when + orderDetailService.saveOrderDetailWithOrder(mockRequest, orderId); + + //then + then(orderRepository).should().getOrder(orderId); + then(productLineRepository).should().selectByProductLineId(mockRequest.getProductLineId()); + then(productRepository).should().selectByProductId(mockRequest.getProductId()); + } + + @DisplayName("addOrderDetail 주문 상세 추가 생성 테스트") + @Test + void addOrderDetail_test() { + //given + OrderDetail orderDetail = OrderDetail.builder() + .orderDetailId(1L) + .orderId(1L) + .productLineId(1L) + .productId(1L) + .quantity(1) + .fixedPrice(3000) + .oneKindTotalPrice(3000) + .name("워셔블 케이블 반팔 니트 세트") + .stock(30L) + .optionName("아이보리") + .extraCharge(0) + .brandName("수아레") + .build(); + + AddOrderDetailRequest mockRequest = mock(AddOrderDetailRequest.class); + ProductLine mockProductLine = mock(ProductLine.class); + Product mockProduct = mock(Product.class); + Order mockOrder = mock(Order.class); + + given(orderRepository.getOrder(mockRequest.getOrderId())).willReturn(Optional.of(mockOrder)); + given(productLineRepository.selectByProductLineId(mockRequest.getProductLineId())).willReturn(Optional.of(mockProductLine)); + given(productRepository.selectByProductId(mockRequest.getProductId())).willReturn(Optional.of(mockProduct)); + given(mockRequest.toOrderDetail(mockOrder, mockProductLine, mockProduct)).willReturn(orderDetail); + + //when + Long orderDetailResponse = orderDetailService.addOrderDetail(mockRequest); + + //then + assertThat(orderDetailResponse).isEqualTo(1L); + + } + + @DisplayName("addOrderDetail 메서드 호출 테스트") + @Test + void addOrderDetail_verify_test() { + //given + AddOrderDetailRequest mockRequest = mock(AddOrderDetailRequest.class); + OrderDetail mockOrderDetail = mock(OrderDetail.class); + ProductLine mockProductLine = mock(ProductLine.class); + Product mockProduct = mock(Product.class); + Order mockOrder = mock(Order.class); + + given(orderRepository.getOrder(mockRequest.getOrderId())).willReturn(Optional.of(mockOrder)); + given(productLineRepository.selectByProductLineId(mockRequest.getProductLineId())).willReturn(Optional.of(mockProductLine)); + given(productRepository.selectByProductId(mockRequest.getProductId())).willReturn(Optional.of(mockProduct)); + given(mockRequest.toOrderDetail(mockOrder, mockProductLine, mockProduct)).willReturn(mockOrderDetail); + + //when + orderDetailService.addOrderDetail(mockRequest); + + //then + then(orderRepository).should().getOrder(mockRequest.getOrderId()); + then(productLineRepository).should().selectByProductLineId(mockRequest.getProductLineId()); + then(productRepository).should().selectByProductId(mockRequest.getProductId()); + } + + @DisplayName("addOrderDetail - 주문 유효성 검사 예외처리 테스트") + @Test + void getOrderDetail_quantityZero_exception_test() { + //given + AddOrderDetailRequest mockRequest = mock(AddOrderDetailRequest.class); + Order mockOrder = mock(Order.class); + ProductLine mockProductLine = mock(ProductLine.class); + Product mockProduct = mock(Product.class); + + given(orderRepository.getOrder(mockRequest.getOrderId())).willReturn(Optional.of(mockOrder)); + given(productLineRepository.selectByProductLineId(mockRequest.getProductLineId())).willReturn(Optional.of(mockProductLine)); + given(productRepository.selectByProductId(mockRequest.getProductId())).willReturn(Optional.of(mockProduct)); + given(mockRequest.getQuantity()).willReturn(10); + given(mockProduct.getStock()).willReturn(1L); + + //when + ResponseStatusException thrown = assertThrows(ResponseStatusException.class, () -> { + orderDetailService.addOrderDetail(mockRequest); + }); + + //then + assertEquals("400 BAD_REQUEST \"주문 개수가 재고보다 더 많습니다.\"", thrown.getMessage()); + } + + @Test + @DisplayName("주문생성시, 상품재고를 차감하는 메서드(updateProduct())가 호출되는지 테스트") + void product_stock_subtract() { + //given + AddOrderDetailRequest mockRequest = mock(AddOrderDetailRequest.class); + Order mockOrder = mock(Order.class); + ProductLine mockProductLine = mock(ProductLine.class); + Product mockProduct = mock(Product.class); + OrderDetail mockOrderDetail = mock(OrderDetail.class); + + given(orderRepository.getOrder(mockRequest.getOrderId())).willReturn(Optional.of(mockOrder)); + given(productLineRepository.selectByProductLineId(mockRequest.getProductLineId())).willReturn(Optional.of(mockProductLine)); + given(productRepository.selectByProductId(mockRequest.getProductId())).willReturn(Optional.of(mockProduct)); + given(mockRequest.getQuantity()).willReturn(1); + given(mockProduct.getStock()).willReturn(10L); + given(mockRequest.toOrderDetail(mockOrder, mockProductLine, mockProduct)).willReturn(mockOrderDetail); + + //when + orderDetailService.addOrderDetail(mockRequest); + + //then + verify(productRepository).updateProduct(mockProduct); + } +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/product/controller/ProductControllerIntegrationTest.java b/src/test/java/org/store/clothstar/product/controller/ProductControllerIntegrationTest.java new file mode 100644 index 0000000..97fd057 --- /dev/null +++ b/src/test/java/org/store/clothstar/product/controller/ProductControllerIntegrationTest.java @@ -0,0 +1,22 @@ +package org.store.clothstar.product.controller; + +import org.junit.jupiter.api.Test; + +class ProductControllerIntegrationTest { + + @Test + void getProduct() { + } + + @Test + void createProduct() { + } + + @Test + void updateProduct() { + } + + @Test + void deleteProduct() { + } +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/product/service/ProductServiceTest.java b/src/test/java/org/store/clothstar/product/service/ProductServiceTest.java new file mode 100644 index 0000000..fa3784e --- /dev/null +++ b/src/test/java/org/store/clothstar/product/service/ProductServiceTest.java @@ -0,0 +1,142 @@ +package org.store.clothstar.product.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; +import org.store.clothstar.product.domain.Product; +import org.store.clothstar.product.dto.request.CreateProductRequest; +import org.store.clothstar.product.dto.request.UpdateProductRequest; +import org.store.clothstar.product.dto.response.ProductResponse; +import org.store.clothstar.product.repository.ProductRepository; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@DisplayName("비즈니스 로직 - Product") +@ActiveProfiles("dev") +@ExtendWith(MockitoExtension.class) +class ProductServiceTest { + + @InjectMocks + private ProductService productService; + + @Mock + private ProductRepository productRepository; + + @DisplayName("product_id로 상품 옵션 단건 조회에 성공한다.") + @Test + public void givenProductId_whenGetProductById_thenProductReturned() { + // given + Long productId = 1L; + + Product product = Product.builder() + .productId(1L) + .productLineId(1L) + .name("곰돌이 블랙") + .extraCharge(1000) + .stock(30L) + .build(); + + given(productRepository.selectByProductId(anyLong())).willReturn(Optional.ofNullable(product)); + + // when + ProductResponse response = productService.getProduct(productId); + + // then + assertThat(response).isNotNull(); + assertThat(response.getProductId()).isEqualTo(1L); + assertThat(response.getProductLineId()).isEqualTo(1L); + assertThat(response.getExtraCharge()).isEqualTo(1000); + assertThat(response.getStock()).isEqualTo(30L); + } + + @DisplayName("유효한 createProductRequest 가 들어오면, product_line_id로 1:N 관계의 상품 옵션 생성에 성공한다.") + @Test + void givenCreateProductRequest_whenCreateProduct_thenCreatedProductReturned() { + + Long productLineId = 1L; + + CreateProductRequest createProductRequest = CreateProductRequest.builder() + .productLineId(productLineId) + .name("곰돌이 블랙") + .extraCharge(1000) + .stock(200L) + .build(); + + given(productRepository.save(any(Product.class))).willReturn(1); + + // when + Long createdProductId = productService.createProduct(createProductRequest); + + // then + verify(productRepository, times(1)) + .save(any(Product.class)); + assertThat(createdProductId).isNotNull(); + } + + @DisplayName("유효한 productId와 UpdateProductRequest 가 들어오면 product 수정에 성공한다.") + @Test + void givenValidProductIdWithUpdateProductRequest_whenUpdateProduct_thenUpdateProductSuccess() { + Long productId = 1L; + + Product product = Product.builder() + .productId(1L) + .productLineId(1L) + .name("곰돌이 블랙") + .extraCharge(1000) + .stock(30L) + .build(); + + UpdateProductRequest updateProductRequest = UpdateProductRequest.builder() + .name("곰돌이 블랙진") + .extraCharge(1000) + .stock(180L) + .build(); + + given(productRepository.selectByProductId(anyLong())).willReturn(Optional.ofNullable(product)); + given(productRepository.updateProduct(any(Product.class))).willReturn(1); + + // when + productService.updateProduct(productId, updateProductRequest); + + // then + verify(productRepository, times(1)) + .selectByProductId(anyLong()); + verify(productRepository, times(1)) + .updateProduct(any(Product.class)); + } + + @DisplayName("해당 productId의 product 가 존재하면 삭제에 성공한다.") + @Test + void deleteProduct() { + Long productId = 1L; + + Product product = Product.builder() + .productId(1L) + .productLineId(1L) + .name("곰돌이 블랙") + .extraCharge(1000) + .stock(30L) + .build(); + + given(productRepository.selectByProductId(anyLong())).willReturn(Optional.ofNullable(product)); + given(productRepository.deleteProduct(anyLong())).willReturn(1); + + // when + productService.deleteProduct(productId); + + // then + verify(productRepository, times(1)) + .selectByProductId(anyLong()); + verify(productRepository, times(1)) + .deleteProduct(anyLong()); + } +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/productLine/controller/ProductLineControllerIntegrationTest.java b/src/test/java/org/store/clothstar/productLine/controller/ProductLineControllerIntegrationTest.java new file mode 100644 index 0000000..566e6c4 --- /dev/null +++ b/src/test/java/org/store/clothstar/productLine/controller/ProductLineControllerIntegrationTest.java @@ -0,0 +1,49 @@ +package org.store.clothstar.productLine.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("dev") +public class ProductLineControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + final Long productId = 3L; + +// @DisplayName("상품 상세 조회 테스트") +// @Test +// void givenProducts_whenGetProducsList_thenGetProductsWhereDeletedAtIsNull() throws Exception { +// // given +// final String url = "/v1/products/" + productId; +// +// //when +// ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.get(url) +// .accept(MediaType.APPLICATION_JSON)); +// +// //then +// resultActions +// .andExpect(MockMvcResultMatchers.status().isOk()) +// .andDo(print()) +// .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("내셔널지오그래픽 곰돌이 후드?????????티")) +// .andExpect(MockMvcResultMatchers.jsonPath("$.brandName").value("내셔널지오그래픽키즈 제주점")) +// .andExpect(MockMvcResultMatchers.jsonPath("$.content").value("많이 사주세용~")) +// .andExpect(MockMvcResultMatchers.jsonPath("$.price").value(69000)) +// .andExpect(MockMvcResultMatchers.jsonPath("$.totalStock").value(0)) +// .andExpect(MockMvcResultMatchers.jsonPath("$.saleCount").value(0)) +// .andExpect(MockMvcResultMatchers.jsonPath("$.productLineStatus").value("COMING_SOON")) +// .andExpect(MockMvcResultMatchers.jsonPath("$.biz_no").value("232-05-02861")) +// .andExpect(MockMvcResultMatchers.jsonPath("$.createdAt").value("2024-04-04T23:07:30")) +// .andExpect(MockMvcResultMatchers.jsonPath("$.modifiedAt").value(Matchers.nullValue())) +// .andExpect(MockMvcResultMatchers.jsonPath("$.deletedAt").value(Matchers.nullValue())); +// } +} diff --git a/src/test/java/org/store/clothstar/productLine/repository/ProductLineRepositoryTest.java b/src/test/java/org/store/clothstar/productLine/repository/ProductLineRepositoryTest.java new file mode 100644 index 0000000..52453bc --- /dev/null +++ b/src/test/java/org/store/clothstar/productLine/repository/ProductLineRepositoryTest.java @@ -0,0 +1,16 @@ +package org.store.clothstar.productLine.repository; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +@DisplayName("ProductLineRepository 테스트") +@ExtendWith(MockitoExtension.class) +class ProductLineRepositoryTest { + + @InjectMocks + private ProductLineRepository productLineRepository; + + +} \ No newline at end of file diff --git a/src/test/java/org/store/clothstar/productLine/service/ProductLineServiceTest.java b/src/test/java/org/store/clothstar/productLine/service/ProductLineServiceTest.java new file mode 100644 index 0000000..9a5515a --- /dev/null +++ b/src/test/java/org/store/clothstar/productLine/service/ProductLineServiceTest.java @@ -0,0 +1,263 @@ +package org.store.clothstar.productLine.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; +import org.store.clothstar.product.domain.Product; +import org.store.clothstar.productLine.domain.ProductLine; +import org.store.clothstar.productLine.domain.type.ProductLineStatus; +import org.store.clothstar.productLine.dto.request.CreateProductLineRequest; +import org.store.clothstar.productLine.dto.request.UpdateProductLineRequest; +import org.store.clothstar.productLine.dto.response.ProductLineResponse; +import org.store.clothstar.productLine.dto.response.ProductLineWithProductsResponse; +import org.store.clothstar.productLine.repository.ProductLineRepository; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.*; + +@DisplayName("비즈니스 로직 - ProductLine") +@ActiveProfiles("dev") +@ExtendWith(MockitoExtension.class) +class ProductLineServiceTest { + + @InjectMocks + private ProductLineService productLineService; + + @Mock + private ProductLineRepository productLineRepository; + + @DisplayName("상품 리스트 조회에 성공한다.") + @Test + public void givenProductLines_whenGetProductLineList_thenGetProductLines() { + // given + ProductLine productLine1 = mock(ProductLine.class); + ProductLine productLine2 = mock(ProductLine.class); + ProductLine productLine3 = mock(ProductLine.class); + + when(productLine1.getName()).thenReturn("오구 키링"); + when(productLine1.getPrice()).thenReturn(13000); + when(productLine1.getStatus()).thenReturn(ProductLineStatus.COMING_SOON); + + when(productLine2.getName()).thenReturn("오구 바디 필로우"); + when(productLine2.getPrice()).thenReturn(57000); + when(productLine2.getStatus()).thenReturn(ProductLineStatus.FOR_SALE); + + when(productLine3.getName()).thenReturn("오구 볼펜"); + when(productLine3.getPrice()).thenReturn(7900); + when(productLine3.getStatus()).thenReturn(ProductLineStatus.SOLD_OUT); + + List productLines = List.of(productLine1, productLine2, productLine3); + when(productLineRepository.selectAllProductLinesNotDeleted()).thenReturn(productLines); + + // when + List response = productLineService.getAllProductLines(); + + // then + verify(productLineRepository, times(1)) + .selectAllProductLinesNotDeleted(); + assertThat(response).isNotNull(); + assertThat(response.size()).isEqualTo(3); + assertThat(response.get(0).getName()).isEqualTo("오구 키링"); + assertThat(response.get(0).getPrice()).isEqualTo(13000); +// assertThat(response.get(0).getTotalStock()).isEqualTo(20); + assertThat(response.get(0).getProductLineStatus()).isEqualTo(ProductLineStatus.COMING_SOON); + } + + @DisplayName("product_line_id로 상품 단건 조회에 성공한다.") + @Test + public void givenProductLineId_whenGetProductLineById_thenProductLineReturned() { + // given + ProductLine productLine = mock(ProductLine.class); + when(productLine.getBrandName()).thenReturn("내셔널지오그래픽키즈 제주점"); + when(productLine.getName()).thenReturn("내셔널지오그래픽 곰돌이 후드티"); + when(productLine.getContent()).thenReturn("귀여운 곰돌이가 그려진 후드티에요!"); + when(productLine.getPrice()).thenReturn(69000); + when(productLine.getStatus()).thenReturn(ProductLineStatus.ON_SALE); + + given(productLineRepository.selectByProductLineId(anyLong())).willReturn(Optional.ofNullable(productLine)); + + // when + Optional response = productLineService.getProductLine(productLine.getProductLineId()); + + // then + assertThat(response).isPresent(); // Optional이 비어있지 않은지 확인 + + // Optional이 비어있지 않은 경우에만 값을 가져와서 검증 + response.ifPresent(productLineResponse -> { + assertThat(productLineResponse.getBrandName()).isEqualTo("내셔널지오그래픽키즈 제주점"); + assertThat(productLineResponse.getName()).isEqualTo("내셔널지오그래픽 곰돌이 후드티"); + assertThat(productLineResponse.getContent()).isEqualTo("귀여운 곰돌이가 그려진 후드티에요!"); + assertThat(productLineResponse.getPrice()).isEqualTo(69000); + assertThat(productLineResponse.getProductLineStatus()).isEqualTo(ProductLineStatus.ON_SALE); + }); + + } + + @DisplayName("상품 id와 상품과 1:N 관계에 있는 상품 옵션 리스트를 조회한다.") + @Test + public void givenProductLineId_whenGetProductLineWithProducts_thenProductLineWithProducts() { + // given + Long productLineId = 1L; + Product product1 = Product.builder() + .productId(1L) + .productLineId(1L) + .name("블랙") + .extraCharge(0) + .stock(30L) + .build(); + + Product product2 = Product.builder() + .productId(2L) + .productLineId(1L) + .name("화이트") + .extraCharge(1000) + .stock(30L) + .build(); + + Product product3 = Product.builder() + .productId(3L) + .productLineId(1L) + .name("네이비") + .extraCharge(1000) + .stock(30L) + .build(); + + List productList = new ArrayList<>(); + productList.add(product1); + productList.add(product2); + productList.add(product3); + + ProductLineWithProductsResponse productLineWithProductsResponse = ProductLineWithProductsResponse.builder() + .productLineId(1L) + .memberId(1L) + .categoryId(2L) + .name("내셔널지오그래픽 곰돌이 후드티") + .content("귀여운 곰돌이가 그려진 후드티에요!") + .price(69000) + .status(ProductLineStatus.ON_SALE) + .createdAt(LocalDateTime.now()) + .modifiedAt(null) + .deletedAt(null) + .brandName("내셔널지오그래픽키즈 제주점") + .biz_no("232-05-02861") + .productList(productList) + .build(); + + given(productLineRepository.selectProductLineWithOptions(anyLong())).willReturn(Optional.ofNullable(productLineWithProductsResponse)); + + // when + ProductLineWithProductsResponse response = productLineService.getProductLineWithProducts(productLineId); + + // then + assertThat(response).isNotNull(); + assertThat(response.getProductLineId()).isEqualTo(1L); + assertThat(response.getTotalStock()).isEqualTo(90L); + } + + @DisplayName("유효한 상품 생성 Request가 들어오면 상품 생성에 성공한다.") + @Test + public void givenValidCreateProductLineRequest_whenCreateProductLine_thenProductLineCreated() { + // given + Long productLineId = 1L; + CreateProductLineRequest createProductLineRequest = CreateProductLineRequest.builder() + .categoryId(1L) + .name("데님 자켓") + .content("봄에 입기 딱 좋은 데님 소재의 청자켓이에요!") + .price(19000) + .status(ProductLineStatus.ON_SALE) + .build(); + + given(productLineRepository.save(any(ProductLine.class))).willReturn(1); + + // when + Long responseProductLineId = productLineService.createProductLine(createProductLineRequest); + + // then + verify(productLineRepository, times(1)) + .save(any(ProductLine.class)); + } + + @DisplayName("유효한 UpdateProductLineRequest가 들어오면 상품 수정에 성공한다.") + @Test + public void givenValidUpdateProductLineRequest_whenUpdateProductLine_thenProductLineUpdated() { + // given + Long productLineId = 1L; + UpdateProductLineRequest updateProductLineRequest = UpdateProductLineRequest.builder() + .name("워싱 데님 데님 자켓 ") + .content("봄에 입기 딱 좋은 데님 소재의 워싱 빈티지 청자켓이에요! ") + .price(19000) + .status(ProductLineStatus.ON_SALE) + .build(); + + ProductLine productLine = ProductLine.builder() + .productLineId(productLineId) + .memberId(1L) + .categoryId(1L) + .name("데님 자켓") + .price(19000) + .totalStock(50L) + .status(ProductLineStatus.ON_SALE) + .createdAt(LocalDateTime.now()) + .modifiedAt(null) + .deletedAt(null) + .brandName("내셔널지오그래픽키즈 제주점") + .biz_no("232-05-02861") + .build(); + + given(productLineRepository.selectByProductLineId(anyLong())).willReturn(Optional.ofNullable(productLine)); + given(productLineRepository.updateProductLine(any(ProductLine.class))).willReturn(1); + + // when + productLineService.updateProductLine(productLineId, updateProductLineRequest); + + // then + verify(productLineRepository, times(1)) + .selectByProductLineId(anyLong()); + verify(productLineRepository, times(1)) + .updateProductLine(any(ProductLine.class)); + } + + @DisplayName("유효한 UpdateProductLineRequest가 들어오면 상품 수정에 성공한다.") + @Test + public void givenProductLineId_whenDeleteProducctLine_thenSetDeletedAt() { + // given + Long productLineId = 1L; + + ProductLine productLine = ProductLine.builder() + .productLineId(productLineId) + .memberId(1L) + .categoryId(1L) + .name("데님 자켓") + .price(19000) + .totalStock(50L) + .status(ProductLineStatus.ON_SALE) + .createdAt(LocalDateTime.now()) + .modifiedAt(null) + .deletedAt(null) + .brandName("내셔널지오그래픽키즈 제주점") + .biz_no("232-05-02861") + .build(); + + given(productLineRepository.selectByProductLineId(anyLong())).willReturn(Optional.ofNullable(productLine)); + given(productLineRepository.setDeletedAt(any(ProductLine.class))).willReturn(1); + + // when + productLineService.setDeletedAt(productLineId); + + // then + verify(productLineRepository, times(2)) + .selectByProductLineId(anyLong()); + verify(productLineRepository, times(1)) + .setDeletedAt(any(ProductLine.class)); + } +} \ No newline at end of file